metis-desk/scripts/global-software-ui-audit.py

143 lines
18 KiB
Python
Raw Normal View History

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
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']
globals_ = []
for index in range(50):
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'}}}, expected=201)['resource']
globals_.append(product)
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'}}}, expected=201)['resource']
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();")
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();")
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;}}});")
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)