2026-10-24 23:19:17 +00:00
""" 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 '
2026-10-25 19:39:06 +00:00
environment = { * * context [ ' environment ' ] , ' RESOURCE_GLOBAL_CATALOG_ENABLED ' : ' false ' , ' RESOURCE_GLOBAL_OS_CATALOG_ENABLED ' : ' false ' , ' RESOURCE_GLOBAL_OS_SOFTWARE_ENABLED ' : ' false ' }
2026-10-24 23:19:17 +00:00
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 '
2026-10-25 19:39:06 +00:00
if context [ ' options ' ] . global_operating_systems :
api . resource_contract = ' 3 '
environment [ ' RESOURCE_GLOBAL_OS_CATALOG_ENABLED ' ] = ' true '
environment [ ' RESOURCE_GLOBAL_OS_SOFTWARE_ENABLED ' ] = ' true '
2026-10-24 23:19:17 +00:00
if context [ ' options ' ] . global_software :
environment [ ' RESOURCE_GLOBAL_SOFTWARE_ENABLED ' ] = ' true '
environment [ ' RESOURCE_GLOBAL_SOFTWARE_CANDIDATES_ENABLED ' ] = ' true '
2026-10-25 12:46:06 +00:00
if context [ ' options ' ] . application_coincidences or context [ ' options ' ] . company_applications :
2026-10-24 23:19:17 +00:00
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 )