143 lines
18 KiB
Python
143 lines
18 KiB
Python
|
|
"""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)
|