2026-10-24 23:19:17 +00:00
""" R2.4: software global mediante el MCP y la instancia desechable de visual-audit.py. """
import json
2026-10-27 10:39:53 +00:00
import base64
import runpy
2026-10-24 23:19:17 +00:00
import os
import secrets
import signal
import time
import uuid
def creation_id ( ) :
value = f ' { int ( time . time ( ) * 1000 ) : 012x } ' + ' 7 ' + secrets . token_hex ( 2 ) [ 1 : ] + ' a ' + secrets . token_hex ( 4 ) [ 1 : ] + secrets . token_hex ( 4 )
return str ( uuid . UUID ( value ) )
def verify ( context , fixture , admin_password ) :
api = context [ ' api ' ]
api . resource_contract = ' 2 '
api . request ( ' /auth/login ' , { ' email ' : ' setup@audit.example.test ' , ' password ' : admin_password } )
company = fixture [ ' company ' ]
asset = fixture [ ' softwareAssets ' ] [ 0 ]
workstation = fixture [ ' softwareWorkstations ' ] [ 0 ]
catalog = api . request ( ' /resource-catalog/ ' + company ) [ ' items ' ]
definitions = { item [ ' builtinKey ' ] : item for item in catalog if item . get ( ' builtinKey ' ) }
template = api . request ( ' /resource-global-applications/template ' ) [ ' specification ' ] [ ' templateId ' ]
provider = definitions [ ' field:provider ' ] [ ' id ' ]
2026-10-27 10:39:53 +00:00
icon_id = definitions [ ' field:icon ' ] [ ' id ' ]
image = " data:image/png;base64, " + base64 . b64encode ( ( context [ " ROOT " ] / " apps/server/data/resource-icons/windows-10.png " ) . read_bytes ( ) ) . decode ( " ascii " )
assert_icons = runpy . run_path ( str ( context [ " ROOT " ] / " scripts/resource-image-ui-checks.py " ) ) [ " assert_list_icons " ]
2026-10-24 23:19:17 +00:00
globals_ = [ ]
for index in range ( 50 ) :
2026-10-27 10:39:53 +00:00
product = api . request ( ' /resource-global-applications ' , { ' creationId ' : creation_id ( ) , ' values ' : { ' name ' : ' Chrome sintético — Global reutilizable ' if index == 0 else f ' Producto global Ágil { index : 02 } — Nombre extenso de pruebas ' , ' templateId ' : template , ' active ' : index % 5 != 4 , ' notes ' : ' Gestión privada global sintética ' , ' characteristics ' : { provider : ' Fábrica sintética ' , icon_id : image } } } , expected = 201 ) [ ' resource ' ]
2026-10-24 23:19:17 +00:00
globals_ . append ( product )
2026-10-27 10:39:53 +00:00
local = api . request ( ' /resources/ ' + company , { ' values ' : { ' name ' : ' Chrome sintético — Edición de la compañía ' , ' templateId ' : template , ' active ' : True , ' notes ' : ' ' , ' characteristics ' : { provider : ' Equipo interno ' , icon_id : image } } } , expected = 201 ) [ ' resource ' ]
2026-10-24 23:19:17 +00:00
license_ = api . request ( ' /resources/ ' + company , { ' values ' : { ' name ' : ' Licencia sintética R2.4 local ' , ' templateId ' : definitions [ ' template:software_license ' ] [ ' id ' ] , ' active ' : True , ' notes ' : ' ' , ' characteristics ' : { } } } , expected = 201 ) [ ' resource ' ]
locals_ = [ item for item in api . request ( ' /resources/ ' + company ) [ ' resources ' ] if item [ ' values ' ] [ ' templateId ' ] == template and item [ ' values ' ] [ ' active ' ] ]
api . request ( ' /auth/logout ' , { } )
tool , browser , capture = context [ ' tool ' ] , context [ ' browser ' ] , context [ ' capture ' ]
directory , origin = context [ ' directory ' ] , context [ ' origin ' ]
asset_url = origin + f ' /companies/ { company } /assets/ { asset } '
workstation_url = origin + f ' /companies/ { company } /workstations/ { workstation } '
screenshots , checks = [ ] , [ ]
global_id = globals_ [ 0 ] [ ' id ' ]
def code ( client , source ) :
print ( ' R2.4 MCP: ' + source [ : 110 ] , flush = True )
try :
return tool ( client , ' browser_run_code_unsafe ' , { ' code ' : ' async (page) => { ' + source + ' } ' } )
except RuntimeError :
capture ( client , directory , ' r24-failure ' )
raise
def login ( client ) :
tool ( client , ' browser_navigate ' , { ' url ' : origin + ' /login ' } )
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 ' : fixture [ ' email ' ] } ,
{ ' name ' : ' Contraseña ' , ' type ' : ' textbox ' , ' target ' : ' input[type=password] ' , ' value ' : fixture [ ' 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.4: usuario ordinario, alta desde activo y selector mixto real… ' , flush = True )
tool ( client , ' browser_resize ' , { ' width ' : 1440 , ' height ' : 1000 } )
login ( client )
tool ( client , ' browser_navigate ' , { ' url ' : asset_url } )
code ( client , " await page.getByRole( ' button ' , { name: ' Añadir instalación ' ,exact:true}).waitFor(); await page.getByRole( ' button ' , { name: ' Añadir instalación ' ,exact:true}).click(); await page.getByRole( ' dialog ' , { name: ' Añadir instalación ' ,exact:true}).getByRole( ' button ' , { name: ' Software ' ,exact:true}).waitFor(); " )
code ( client , " await page.getByRole( ' dialog ' , { name: ' Añadir instalación ' ,exact:true}).getByRole( ' button ' , { name: ' Software ' ,exact:true}).click(); await page.getByRole( ' dialog ' , { name: ' Seleccionar software ' ,exact:true}).getByRole( ' textbox ' , { name: ' Buscar por nombre o proveedor ' ,exact:true}).fill( ' Chrome sintético ' ); await page.getByRole( ' cell ' , { name: ' Chrome sintético — Global reutilizable ' ,exact:true}).waitFor(); await page.getByRole( ' cell ' , { name: ' Chrome sintético — Edición de la compañía ' ,exact:true}).waitFor(); " )
2026-10-27 10:39:53 +00:00
assert_icons ( client , code , ' dialog img,[role=dialog] img ' , 2 )
checks . append ( ' mixed-picker-png-icons ' )
2026-10-24 23:19:17 +00:00
screenshots . append ( capture ( client , directory , ' r24-01-mixed-search ' ) )
checks . extend ( [ ' ordinary-login ' , ' mixed-global-local ' , ' search-with-accents ' , ' origin-visible ' ] )
code ( client , " await page.getByRole( ' row ' ).filter( { hasText: ' Chrome sintético — Global reutilizable ' }).click(); await page.getByRole( ' dialog ' , { name: ' Seleccionar software ' ,exact:true}).getByRole( ' textbox ' , { name: ' Buscar por nombre o proveedor ' ,exact:true}).fill( ' OTRA-BUSQUEDA-R24 ' ); await page.getByText( ' No hay coincidencias. Cambia la búsqueda o vuelve al inicio. ' , { exact:true}).waitFor(); await page.getByRole( ' dialog ' , { name: ' Seleccionar software ' ,exact:true}).getByRole( ' button ' , { name: ' Seleccionar ' ,exact:true}).click(); await page.getByRole( ' dialog ' , { name: ' Añadir instalación ' ,exact:true}).getByRole( ' textbox ' , { name: ' Versión ' ,exact:true}).fill( ' 123.4 ' ); await page.getByRole( ' combobox ' , { name: ' Estado ' ,exact:true}).click(); await page.getByRole( ' option ' , { name: ' Configurado ' ,exact:true}).click(); await page.getByRole( ' combobox ' , { name: ' Licencia vinculada ' ,exact:true}).click(); await page.getByRole( ' option ' , { name: ' Licencia sintética R2.4 local ' ,exact:true}).click(); " )
screenshots . append ( capture ( client , directory , ' r24-02-installation-form ' ) )
code ( client , " await page.getByRole( ' dialog ' , { name: ' Añadir instalación ' ,exact:true}).getByRole( ' button ' , { name: ' Guardar ' ,exact:true}).click(); await page.getByRole( ' dialog ' , { name: ' Añadir instalación ' ,exact:true}).waitFor( { state: ' hidden ' }); await page.getByRole( ' link ' , { name: ' Chrome sintético — Global reutilizable Global ' ,exact:true}).waitFor(); " )
2026-10-27 10:39:53 +00:00
assert_icons ( client , code )
checks . append ( ' installation-png-icon ' )
2026-10-24 23:19:17 +00:00
checks . extend ( [ ' selection-retained-across-search ' , ' global-uuid-selected ' , ' version-123.4 ' , ' local-license-selected ' , ' save-installation ' ] )
# Reabrir por la acción explícita de su fila: el producto permanece fijo y resoluble.
code ( client , " await page.getByRole( ' row ' ).filter( { hasText: ' Chrome sintético — Global reutilizable ' }).getByRole( ' button ' , { name: ' Editar ' ,exact:true}).click(); await page.getByRole( ' dialog ' , { name: ' Editar instalación ' ,exact:true}).getByRole( ' button ' , { name: ' Software ' ,exact:true}).waitFor(); if(!await page.getByRole( ' dialog ' , { name: ' Editar instalación ' ,exact:true}).getByRole( ' button ' , { name: ' Software ' ,exact:true}).isDisabled())throw Error( ' Destino editable ' ); if(await page.getByRole( ' dialog ' , { name: ' Editar instalación ' ,exact:true}).getByRole( ' textbox ' , { name: ' Versión ' ,exact:true}).inputValue()!== ' 123.4 ' )throw Error( ' Versión perdida ' ); await page.getByRole( ' dialog ' , { name: ' Editar instalación ' ,exact:true}).getByRole( ' button ' , { name: ' Cancelar ' ,exact:true}).click(); " )
checks . append ( ' reopen-fixed-product ' )
tool ( client , ' browser_navigate ' , { ' url ' : workstation_url } )
code ( client , " await page.getByRole( ' link ' ).filter( { hasText: ' Chrome sintético — Global reutilizable ' }).first().waitFor(); if(!await page.getByText( ' 123.4 ' , { exact:false}).count())throw Error( ' Versión ausente en puesto ' ); if(!await page.getByText( ' Licencia sintética R2.4 local ' , { exact:false}).count())throw Error( ' Licencia ausente en puesto ' ); " )
screenshots . append ( capture ( client , directory , ' r24-03-workstation ' ) )
checks . append ( ' workstation-main-computer ' )
api . request ( ' /auth/login ' , { ' email ' : ' setup@audit.example.test ' , ' password ' : admin_password } )
api . request ( f ' /resource-global-applications/ { global_id } /status ' , { ' active ' : False } )
api . request ( ' /auth/logout ' , { } )
tool ( client , ' browser_navigate ' , { ' url ' : asset_url } )
code ( client , " await page.getByRole( ' link ' ).filter( { hasText: ' Chrome sintético — Global reutilizable ' }).first().waitFor(); await page.getByText( ' Revisar: retirada pendiente, equipo o software no disponibles, o licencia vinculada inactiva o caducada. ' , { exact:true}).waitFor(); await page.getByRole( ' button ' , { name: ' Añadir instalación ' ,exact:true}).click(); await page.getByRole( ' dialog ' , { name: ' Añadir instalación ' ,exact:true}).getByRole( ' button ' , { name: ' Software ' ,exact:true}).click(); await page.getByRole( ' dialog ' , { name: ' Seleccionar software ' ,exact:true}).getByRole( ' textbox ' , { name: ' Buscar por nombre o proveedor ' ,exact:true}).fill( ' Chrome sintético ' ); await page.getByRole( ' cell ' , { name: ' Chrome sintético — Edición de la compañía ' ,exact:true}).waitFor(); if(await page.getByRole( ' dialog ' , { name: ' Seleccionar software ' ,exact:true}).getByRole( ' cell ' , { name: ' Chrome sintético — Global reutilizable ' ,exact:true}).count())throw Error( ' Inactivo candidato ' ); " )
checks . extend ( [ ' inactive-used-visible ' , ' needs-review-derived ' , ' inactive-excluded-from-candidates ' ] )
code ( client , " await page.getByRole( ' dialog ' , { name: ' Seleccionar software ' ,exact:true}).getByRole( ' textbox ' , { name: ' Buscar por nombre o proveedor ' ,exact:true}).fill( ' SIN-COINCIDENCIAS-R24 ' ); await page.getByText( ' No hay coincidencias. Cambia la búsqueda o vuelve al inicio. ' , { exact:true}).waitFor(); " )
screenshots . append ( capture ( client , directory , ' r24-04-no-results ' ) )
code ( client , " await page.getByRole( ' dialog ' , { name: ' Seleccionar software ' ,exact:true}).getByRole( ' textbox ' , { name: ' Buscar por nombre o proveedor ' ,exact:true}).fill( ' ' ); await page.getByRole( ' button ' , { name: ' Siguiente ' ,exact:true}).last().click(); await page.getByRole( ' button ' , { name: ' Anterior ' ,exact:true}).last().waitFor(); await page.keyboard.press( ' Escape ' ); await page.getByRole( ' dialog ' , { name: ' Seleccionar software ' ,exact:true}).waitFor( { state: ' hidden ' }); if(await page.evaluate(()=>document.activeElement?.getAttribute( ' aria-label ' ))!== ' Software ' )throw Error( ' Foco del selector ' ); await page.keyboard.press( ' Escape ' ); await page.getByRole( ' dialog ' , { name: ' Añadir instalación ' ,exact:true}).waitFor( { state: ' hidden ' }); " )
checks . extend ( [ ' empty-recovery ' , ' cursor-pagination ' , ' keyboard-escape ' , ' picker-focus-return ' ] )
# Fallo de transporte controlado del GET; el alta y todas las escrituras son reales.
code ( client , " page.r24Pattern= ' **/api/resource-application-candidates/*?* ' ; await page.route(page.r24Pattern,r=>r.fulfill( { status:500,contentType: ' application/json ' ,json: { code: ' INTERNAL_ERROR ' }})); await page.getByRole( ' button ' , { name: ' Añadir instalación ' ,exact:true}).click(); await page.getByRole( ' dialog ' , { name: ' Añadir instalación ' ,exact:true}).getByRole( ' button ' , { name: ' Software ' ,exact:true}).click(); await page.getByText( ' No se han podido consultar los candidatos. Reintenta la búsqueda. ' , { exact:true}).waitFor(); " )
screenshots . append ( capture ( client , directory , ' r24-05-candidate-error ' ) )
code ( client , " await page.unroute(page.r24Pattern); await page.getByRole( ' button ' , { name: ' Reintentar ' ,exact:true}).click(); await page.getByRole( ' dialog ' , { name: ' Seleccionar software ' ,exact:true}).getByRole( ' radio ' ).first().waitFor(); await page.keyboard.press( ' Escape ' ); await page.keyboard.press( ' Escape ' ); " )
checks . extend ( [ ' partial-candidate-error ' , ' retry ' ] )
# Densidad persistida: 25 productos globales y 25 particulares, todos en el ordenador principal.
api . request ( ' /auth/login ' , { ' email ' : ' setup@audit.example.test ' , ' password ' : admin_password } )
selected = [ item for item in globals_ [ 1 : ] if item [ ' values ' ] [ ' active ' ] ] [ : 25 ] + locals_ [ : 25 ]
for index , product in enumerate ( selected ) :
api . request ( ' /software-installations/ ' + company , { ' values ' : { ' assetId ' : asset , ' resourceId ' : product [ ' id ' ] , ' licenseResourceId ' : None , ' version ' : f ' Edición { index + 1 } ' , ' state ' : ' configured ' if index % 3 else ' pending ' , ' notes ' : ' Registro operativo sintético ' } } , expected = 201 )
api . request ( ' /auth/logout ' , { } )
tool ( client , ' browser_navigate ' , { ' url ' : asset_url } )
code ( client , " await page.getByRole( ' button ' , { name: ' Añadir instalación ' ,exact:true}).waitFor(); await page.getByRole( ' cell ' , { name:/^Edición /}).first().waitFor(); " )
for width in [ 1600 , 1440 , 1280 , 1024 , 768 , 390 ] :
tool ( client , ' browser_resize ' , { ' width ' : width , ' height ' : 1000 if width > 768 else 844 } )
code ( client , " await page.getByRole( ' heading ' , { name: ' Software del ordenador ' ,exact:true}).evaluate(el=> { el.scrollIntoView( { block: ' start ' }); for(let parent=el.parentElement;parent;parent=parent.parentElement) { if(parent.scrollHeight>parent.clientHeight&&[ ' auto ' , ' scroll ' ].includes(getComputedStyle(parent).overflowY)) { parent.scrollTop-=140;break;}}}); if(await page.evaluate(()=>document.documentElement.scrollWidth>innerWidth+1))throw Error( ' Desbordamiento de página ' ); " )
screenshots . append ( capture ( client , directory , f ' r24-asset- { width } ' ) )
if width == 390 :
code ( client , " await page.getByRole( ' button ' , { name: ' Añadir instalación ' ,exact:true}).click(); await page.getByRole( ' dialog ' , { name: ' Añadir instalación ' ,exact:true}).getByRole( ' button ' , { name: ' Software ' ,exact:true}).click(); await page.getByRole( ' dialog ' , { name: ' Seleccionar software ' ,exact:true}).getByRole( ' radio ' ).first().waitFor(); if(await page.evaluate(()=>document.documentElement.scrollWidth>innerWidth+1))throw Error( ' Desbordamiento del selector móvil ' ); " )
screenshots . append ( capture ( client , directory , ' r24-mobile-picker ' ) )
code ( client , " await page.keyboard.press( ' Escape ' ); await page.keyboard.press( ' Escape ' ); " )
checks . append ( ' density-51-installations ' )
tool ( client , ' browser_navigate ' , { ' url ' : workstation_url } )
code ( client , " await page.getByRole( ' link ' ).filter( { hasText: ' Chrome sintético — Global reutilizable ' }).first().waitFor(); " )
code ( client , " await page.getByRole( ' button ' , { name: ' Gestionar software ' ,exact:true}).evaluate(el=> { el.scrollIntoView( { block: ' start ' }); for(let parent=el.parentElement;parent;parent=parent.parentElement) { if(parent.scrollHeight>parent.clientHeight&&[ ' auto ' , ' scroll ' ].includes(getComputedStyle(parent).overflowY)) { parent.scrollTop-=140;break;}}}); " )
2026-10-27 10:39:53 +00:00
assert_icons ( client , code )
checks . append ( ' workstation-png-icon ' )
2026-10-24 23:19:17 +00:00
screenshots . append ( capture ( client , directory , ' r24-workstation-390 ' ) )
# Apagado real de gates en la misma instalación temporal; el lector debe seguir funcionando.
backend = context [ ' backend ' ]
os . killpg ( backend . pid , signal . SIGTERM )
backend . wait ( timeout = 10 )
closed = { * * context [ ' active_environment ' ] , ' RESOURCE_GLOBAL_SOFTWARE_ENABLED ' : ' false ' , ' RESOURCE_GLOBAL_SOFTWARE_CANDIDATES_ENABLED ' : ' false ' }
with context [ ' child_process ' ] ( [ ' node ' , ' --import ' , ' tsx ' , ' apps/server/src/index.ts ' ] , closed , directory / ' api-software-closed.log ' ) as restarted :
context [ ' wait_for ' ] ( api . ready , restarted )
tool ( client , ' browser_resize ' , { ' width ' : 1440 , ' height ' : 1000 } )
tool ( client , ' browser_navigate ' , { ' url ' : asset_url } )
code ( client , " await page.getByRole( ' button ' , { name: ' Añadir instalación ' ,exact:true}).waitFor(); await page.getByRole( ' region ' , { name: ' Software del ordenador ' ,exact:true}).getByRole( ' textbox ' ).fill( ' Chrome sintético ' ); await page.getByRole( ' link ' ).filter( { hasText: ' Chrome sintético — Global reutilizable ' }).first().waitFor(); await page.getByRole( ' button ' , { name: ' Añadir instalación ' ,exact:true}).click(); await page.getByRole( ' dialog ' , { name: ' Añadir instalación ' ,exact:true}).getByRole( ' combobox ' , { name: ' Software ' ,exact:true}).waitFor(); await page.getByRole( ' combobox ' , { name: ' Software ' ,exact:true}).click(); await page.getByRole( ' option ' ).filter( { hasText: ' Chrome sintético — Edición de la compañía ' }).click(); await page.getByRole( ' dialog ' , { name: ' Añadir instalación ' ,exact:true}).getByRole( ' button ' , { name: ' Guardar ' ,exact:true}).click(); await page.getByRole( ' dialog ' , { name: ' Añadir instalación ' ,exact:true}).waitFor( { state: ' hidden ' }); " )
screenshots . append ( capture ( client , directory , ' r24-06-gate-closed-local ' ) )
checks . extend ( [ ' gate-closed-global-read ' , ' gate-closed-legacy-selector ' , ' gate-closed-local-save ' ] )
api . request ( ' /auth/login ' , { ' email ' : ' setup@audit.example.test ' , ' password ' : admin_password } )
previous = next ( item for item in api . request ( ' /assets/ ' + company ) [ ' workstations ' ] if item [ ' id ' ] == workstation ) [ ' values ' ]
replacement = { * * previous , ' computerAssetId ' : fixture [ ' asset ' ] , ' assetIds ' : list ( dict . fromkeys ( [ * previous [ ' assetIds ' ] , fixture [ ' asset ' ] ] ) ) }
api . request ( ' /assets/ ' + company + ' /workstations/ ' + workstation + ' /update ' , { ' values ' : replacement } )
tool ( client , ' browser_navigate ' , { ' url ' : workstation_url } )
code ( client , " await page.getByText( ' No hay instalaciones registradas. ' , { exact:true}).waitFor(); if(await page.getByRole( ' link ' ).filter( { hasText: ' Chrome sintético — Global reutilizable ' }).count())throw Error( ' Instalación trasladada al ordenador nuevo ' ); " )
preserved = api . request ( ' /software-installations/ ' + company ) [ ' configurations ' ]
original = next ( item for item in preserved if item [ ' values ' ] [ ' resourceId ' ] == global_id )
if original [ ' values ' ] [ ' assetId ' ] != asset :
raise RuntimeError ( ' Una instalación cambió de ordenador indebidamente. ' )
api . request ( ' /assets/ ' + company + ' /workstations/ ' + workstation + ' /update ' , { ' values ' : previous } )
api . request ( ' /auth/logout ' , { } )
checks . append ( ' main-computer-change-does-not-transfer-installations ' )
report = { ' checks ' : checks , ' screenshots ' : screenshots , ' companies ' : 15 , ' principal ' : { ' people ' : 50 , ' assets ' : 51 , ' profiles ' : 50 , ' workstations ' : 50 , ' localApplicationsAtLeast ' : 50 , ' softwareInstallations ' : 52 } , ' globalApplications ' : 50 , ' targetComputer ' : asset , ' workstation ' : workstation , ' database ' : ' metis_desk_test ' , ' mcp ' : ' Docker stdio fijado en .codex/config.toml ' , ' credentialsSaved ' : False }
( directory / ' r24-report.json ' ) . write_text ( json . dumps ( report , ensure_ascii = False , indent = 2 ) + ' \n ' )
print ( ' R2.4 recorrido verificado. Evidencia: ' + str ( directory . relative_to ( context [ ' ROOT ' ] ) ) , flush = True )