"""Recorrido R2.3 con HTTP real y Playwright MCP del procedimiento visual aislado.""" import json import secrets import signal import shutil def run(context, verifier=None): api = context['api'] if context['options'].global_software or context['options'].global_profiles: api.resource_contract = '2' environment = {**context['environment'], 'RESOURCE_GLOBAL_CATALOG_ENABLED': 'false'} directory = context['directory'] child = context['child_process'] wait = context['wait_for'] command = context['command'] command(['pnpm', 'build'], cwd=context['ROOT'], env=environment) build_directory = directory / 'web-build' shutil.copytree(context['ROOT'] / 'apps/web/dist', build_directory) admin_password = secrets.token_urlsafe(32) with child(['node', '--import', 'tsx', 'apps/server/src/index.ts'], environment, directory / 'api-legacy.log') as backend: wait(api.ready, backend) fixture = context['prepare_fixture'](api, admin_password) api.request('/auth/login', {'email': 'setup@audit.example.test', 'password': admin_password}) company = fixture['company'] fixture["otherCompanies"] = [] for index in range(14): created_company = api.request('/companies', {'name': f'Compañía sintética aislada {index + 2} — Peña', 'active': True}, expected=201) fixture['otherCompanies'].append(created_company['company']['id']) catalog = api.request('/asset-catalog/' + company)['items'] laptop = next(item for item in catalog if item['values']['kind'] == 'template' and item['values']['name'] == 'Portátil') resource_catalog = api.request('/resource-catalog/' + company)['items'] application = next(item for item in resource_catalog if item.get('builtinKey') == 'template:application') fixture["configurationProfiles"] = [] fixture["softwareAssets"] = [] fixture["softwareWorkstations"] = [] for index in range(50): person = api.request('/companies/' + company + '/people', {'name': f'Persona Álvarez {index+1}', 'position': None, 'email': None, 'phone': None, 'active': True, 'isContact': False}, expected=201)['company']['people'][-1] asset = api.request('/assets/' + company, {'values': {'name': f'AUDIT-R23-PORT-{index+1:02}', 'templateId': laptop['id'], 'siteId': None, 'registeredOn': '2026-01-15', 'status': 'operational', 'notes': '', 'characteristics': {}}}, expected=201)['asset'] workstation = api.request('/assets/' + company + '/workstations', {'values': {'name': f'Puesto Álvarez {index+1}', 'active': True, 'siteId': None, 'personId': person['id'], 'computerAssetId': asset['id'], 'assetIds': [asset['id']]}}, expected=201) fixture['softwareAssets'].append(asset['id']) fixture['softwareWorkstations'].append(workstation['workstation']['id']) created_profile = api.request('/work-profiles/' + company, {'values': {'name': f'Perfil Álvarez {index+1}', 'personId': person['id'], 'assetId': asset['id'], 'accountId': None, 'active': True, 'notes': None}}, expected=201) fixture['configurationProfiles'].append(created_profile['profile']['id']) api.request('/resources/' + company, {'values': {'name': f'Aplicación particular Ágil {index+1}', 'templateId': application['id'], 'active': True, 'notes': 'Solo compañía sintética', 'characteristics': {}}}, expected=201) api.request('/auth/logout', {}) command(['node', '--import', 'tsx', 'apps/server/src/migrate-resource-scope.ts', '--apply', '--writes-paused'], cwd=context['ROOT'], env=environment) environment['RESOURCE_GLOBAL_CATALOG_ENABLED'] = 'true' if context['options'].global_software: environment['RESOURCE_GLOBAL_SOFTWARE_ENABLED'] = 'true' environment['RESOURCE_GLOBAL_SOFTWARE_CANDIDATES_ENABLED'] = 'true' if context['options'].application_coincidences: environment['RESOURCE_GLOBAL_PROFILE_ENABLED'] = 'true' environment['RESOURCE_GLOBAL_SOFTWARE_ENABLED'] = 'true' environment['RESOURCE_GLOBAL_SOFTWARE_CANDIDATES_ENABLED'] = 'true' if context['options'].global_profiles: environment['RESOURCE_GLOBAL_PROFILE_ENABLED'] = 'true' environment['RESOURCE_GLOBAL_SOFTWARE_ENABLED'] = 'false' environment['RESOURCE_GLOBAL_SOFTWARE_CANDIDATES_ENABLED'] = 'false' with child(['node', '--import', 'tsx', 'apps/server/src/index.ts'], environment, directory / 'api.log') as backend: wait(api.ready, backend) with child(['pnpm', '--config.verify-deps-before-run=false', '--filter', '@metis/web', 'exec', 'vite', 'preview', '--outDir', str(build_directory), '--host', '127.0.0.1', '--port', str(context['web_port']), '--strictPort'], environment, directory / 'vite.log') as frontend: web = context['TestApi'](context['origin'], context['origin']) wait(web.ready, frontend) (verifier or verify)({**context, "backend": backend, "active_environment": environment}, fixture, admin_password) if not context['options'].check: print('Auditoría R2.3 abierta: ' + context['origin'] + '/catalogs/global-applications', flush=True) signal.pause() def verify(context, fixture, admin_password): tool = context['tool'] browser = context['browser'] capture = context['capture'] result_json = context['result_json'] directory = context['directory'] origin = context['origin'] base = '/catalogs/global-applications' screenshots = [] checks = [] def code(client, source): print(' Comprobación MCP: ' + source[:90], flush=True) try: return tool(client, 'browser_run_code_unsafe', {'code': 'async (page) => { ' + source + ' }'}) except RuntimeError: capture(client, directory, 'r23-failure') raise def login(client, email, password): print(' Navegación al login real', flush=True) try: client.tool('browser_navigate', {'url': origin + '/login'}) except RuntimeError as error: # Esta página todavía no tiene credenciales ni sesión autenticada. print('Diagnóstico de navegación previa al login: ' + str(error)[:1400], flush=True) raise RuntimeError('La navegación inicial al login falló.') from None code(client, "await page.getByRole('button',{name:'Entrar',exact:true}).waitFor();") tool(client, 'browser_fill_form', {'fields': [ {'name': 'Correo electrónico', 'type': 'textbox', 'target': 'input[type=email]', 'value': email}, {'name': 'Contraseña', 'type': 'textbox', 'target': 'input[type=password]', 'value': password}]}) tool(client, 'browser_click', {'target': 'button[type=submit]'}) code(client, "await page.locator('[data-slot=workspace]').waitFor({timeout:30000});") with browser(context['config']) as client: print('R2.3 navegador: acceso real de administrador y catálogo vacío…', flush=True) tool(client, 'browser_resize', {'width': 1440, 'height': 1000}) login(client, 'setup@audit.example.test', admin_password) tool(client, 'browser_navigate', {'url': origin + base}) code(client, "await page.getByText('Todavía no hay aplicaciones globales.',{exact:false}).waitFor();") screenshots.append(capture(client, directory, 'r23-01-empty')) checks.append('empty') code(client, "await page.getByRole('button',{name:'Nueva aplicación global',exact:true}).click(); await page.getByRole('dialog').waitFor();") code(client, "if(await page.getByRole('dialog').getByRole('textbox',{name:'Nombre',exact:true}).evaluate(e=>e.checkValidity()))throw Error('Nombre obligatorio'); await page.getByRole('dialog').getByRole('textbox',{name:'Nombre',exact:true}).fill('Ágil — Producto de pruebas'); await page.getByRole('dialog').getByRole('checkbox').check();") screenshots.append(capture(client, directory, 'r23-02-create')) code(client, "await page.getByRole('dialog').getByRole('button',{name:'Guardar',exact:true}).click(); await page.getByRole('heading',{name:'Ágil — Producto de pruebas',exact:true}).waitFor(); await page.getByRole('heading',{name:'Historial',exact:true}).waitFor();") product_url = result_json(code(client, 'return page.url();')) checks.extend(['create', 'required-name', 'detail', 'history']) screenshots.append(capture(client, directory, 'r23-03-detail')) code(client, "await page.getByRole('button',{name:'Editar aplicación global',exact:true}).click(); await page.getByRole('dialog').getByRole('textbox',{name:'Nombre',exact:true}).fill('Ágil — Producto actualizado'); await page.getByRole('dialog').getByRole('button',{name:'Guardar',exact:true}).click(); await page.getByRole('heading',{name:'Ágil — Producto actualizado',exact:true}).waitFor();") code(client, "await page.getByRole('button',{name:'Desactivar',exact:true}).click(); await page.getByRole('button',{name:'Reactivar',exact:true}).waitFor();") checks.extend(['update', 'deactivate']) screenshots.append(capture(client, directory, 'r23-04-inactive')) # Los demás productos se preparan por el mismo HTTP autorizado, en la BD desechable. api = context['api'] api.request('/auth/login', {'email': 'setup@audit.example.test', 'password': admin_password}) specification = api.request('/resource-global-applications/template')['specification'] # UUID v7 sin dependencias: reloj de milisegundos y entropía de plataforma. import time import uuid for index in range(50): identity = f'{int(time.time()*1000):012x}' + '7' + secrets.token_hex(2)[1:] + 'a' + secrets.token_hex(4)[1:] + secrets.token_hex(4) api.request('/resource-global-applications', {'creationId': str(uuid.UUID(identity)), 'values': {'name': f'Aplicación global Álvarez {index+1:02} — Producto sintético de nombre largo', 'templateId': specification['templateId'], 'active': index % 5 != 0, 'notes': 'Nota administrativa privada', 'characteristics': {}}}, expected=201) api.request('/auth/logout', {}) tool(client, 'browser_navigate', {'url': origin + base}) code(client, "await page.getByRole('button',{name:'Siguiente',exact:true}).waitFor(); await page.getByRole('button',{name:'Siguiente',exact:true}).click(); await page.getByText('Página 2',{exact:true}).waitFor(); await page.getByRole('button',{name:'Anterior',exact:true}).click(); await page.getByText('Página 1',{exact:true}).waitFor();") code(client, "await page.getByRole('textbox',{name:'Buscar por nombre o proveedor',exact:true}).fill('Producto actualizado'); await page.getByRole('link',{name:'Ágil — Producto actualizado',exact:true}).waitFor(); await page.getByRole('link',{name:'Ágil — Producto actualizado',exact:true}).click(); await page.getByRole('heading',{name:'Ágil — Producto actualizado',exact:true}).waitFor(); await page.getByRole('link',{name:'Catálogo global de aplicaciones',exact:true}).last().click(); await page.getByRole('textbox',{name:'Buscar por nombre o proveedor',exact:true}).waitFor(); if(await page.getByRole('textbox',{name:'Buscar por nombre o proveedor',exact:true}).inputValue()!=='Producto actualizado')throw Error('Contexto perdido');") checks.extend(['volume-51-globals', 'server-pagination', 'search', 'return-context']) code(client, "await page.getByRole('combobox',{name:'Estado',exact:true}).click(); await page.getByRole('option',{name:'Inactivas',exact:true}).click(); await page.waitForURL(u=>u.searchParams.get('active')==='false'); await page.getByRole('link',{name:'Ágil — Producto actualizado',exact:true}).waitFor();") checks.append('inactive-filter') code(client, "await page.getByRole('button',{name:'Limpiar filtros',exact:true}).click(); await page.getByRole('button',{name:'Nueva aplicación global',exact:true}).click(); await page.getByRole('dialog').waitFor(); await page.keyboard.press('Escape'); await page.getByRole('dialog').waitFor({state:'hidden'}); if(await page.evaluate(()=>document.activeElement?.textContent)!=='Nueva aplicación global')throw Error('Foco no restaurado'); await page.keyboard.press('Tab'); if(!await page.evaluate(()=>document.activeElement!==document.body))throw Error('Teclado');") checks.extend(['escape', 'focus-return', 'keyboard']) for width in [1600, 1440, 1280, 1024, 768, 390]: tool(client, 'browser_resize', {'width': width, 'height': 1000 if width > 768 else 844}) code(client, "if(await page.evaluate(()=>document.documentElement.scrollWidth>innerWidth+1))throw Error('Desbordamiento horizontal');") screenshots.append(capture(client, directory, f'r23-list-{width}')) tool(client, 'browser_navigate', {'url': product_url}) code(client, "await page.getByRole('heading',{name:'Ágil — Producto actualizado',exact:true}).waitFor();") screenshots.append(capture(client, directory, 'r23-detail-390')) tool(client, 'browser_resize', {'width': 1440, 'height': 1000}) code(client, "await page.getByRole('button',{name:'Reactivar',exact:true}).click(); await page.getByRole('button',{name:'Desactivar',exact:true}).waitFor();") checks.append('reactivate') # Estados de transporte sintéticos; las escrituras anteriores usan API/SQL reales. tool(client, 'browser_navigate', {'url': origin + base}) code(client, "await page.getByRole('textbox',{name:'Buscar por nombre o proveedor',exact:true}).fill('INEXISTENTE-PRUEBA'); await page.getByText('No hay aplicaciones que coincidan.',{exact:false}).waitFor();") screenshots.append(capture(client, directory, 'r23-no-matches')) checks.append('no-matches') code(client, "page.r23Pattern='**/api/resource-global-applications?*'; await page.route(page.r23Pattern, r=>r.fulfill({status:500,contentType:'application/json',json:{code:'INTERNAL_ERROR'}})); await page.goto('" + origin + base + "'); await page.getByText('No se han podido cargar las aplicaciones.',{exact:true}).waitFor();") screenshots.append(capture(client, directory, 'r23-error')) code(client, "await page.unroute(page.r23Pattern); await page.getByRole('button',{name:'Reintentar',exact:true}).click(); await page.getByRole('link',{name:'Ágil — Producto actualizado',exact:true}).waitFor();") checks.extend(['transport-error', 'retry']) code(client, "page.r23Releases=[]; await page.route(page.r23Pattern, async r=>{await new Promise(resolve=>page.r23Releases.push(resolve)); await r.continue()}); await page.goto('" + origin + base + "',{waitUntil:'domcontentloaded'}); await page.getByText('Cargando aplicaciones…',{exact:true}).waitFor();") screenshots.append(capture(client, directory, 'r23-loading')) code(client, "for(const release of page.r23Releases)release(); await page.unroute(page.r23Pattern,{behavior:'wait'}); await page.getByRole('link',{name:'Ágil — Producto actualizado',exact:true}).waitFor();") checks.append('loading') with browser(context['config']) as client: print('R2.3 navegador: usuario ordinario y proyección mínima…', flush=True) login(client, fixture['email'], fixture['password']) tool(client, 'browser_navigate', {'url': origin + base}) code(client, "await page.getByRole('textbox',{name:'Buscar por nombre o proveedor',exact:true}).waitFor(); if(await page.getByRole('button',{name:'Nueva aplicación global',exact:true}).count())throw Error('Alta visible a ordinario'); if(await page.getByRole('button',{name:'Acciones',exact:true}).count())throw Error('Menú administrativo vacío para ordinario');") tool(client, 'browser_navigate', {'url': product_url}) code(client, "await page.getByRole('heading',{name:'Ágil — Producto actualizado',exact:true}).waitFor(); if(await page.getByRole('button',{name:'Editar aplicación global',exact:true}).count())throw Error('Edición visible a ordinario'); if(await page.getByRole('heading',{name:'Historial',exact:true}).count())throw Error('Historia visible a ordinario');") screenshots.append(capture(client, directory, 'r23-ordinary-detail')) checks.extend(['ordinary-read', 'ordinary-no-administration', 'ordinary-no-history']) report = {'checks': checks, 'screenshots': screenshots, 'companies': 15, 'principal': {'people': 50, 'assets': 51, 'profiles': 50, 'workstations': 50, 'localApplications': 50}, 'globalApplications': 51, 'database': 'metis_desk_test', 'mcp': 'Docker stdio fijado en .codex/config.toml', 'credentialsSaved': False} (directory / 'r23-report.json').write_text(json.dumps(report, ensure_ascii=False, indent=2) + '\n') print('Recorrido R2.3 verificado. Evidencias: ' + str(directory.relative_to(context['ROOT'])), flush=True)