- Implement `current-operating-systems-ui.test.py` to validate the functionality of current operating systems in the UI, including tests for source and states, computer changes, responsive design, error handling, and version saving. - Create `workplace-add-asset.test.py` to test the addition of assets to workstations, covering scenarios such as conflict resolution, responsive design on mobile, error handling during asset creation, and HTTP guards for asset management.
178 lines
17 KiB
Python
178 lines
17 KiB
Python
"""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 or context['options'].company_applications:
|
|
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)
|