metis-desk/scripts/global-profile-applications-ui-audit.py
2026-10-25 01:19:17 +02:00

140 lines
17 KiB
Python

"""R2.5: aplicaciones por perfil mediante MCP y HTTP de una instalación desechable."""
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']
profile=fixture['configurationProfiles'][0]
asset=fixture['softwareAssets'][0]
workstation=fixture['softwareWorkstations'][0]
template=api.request('/resource-global-applications/template')['specification']['templateId']
products=[]
for index in range(50):
response=api.request('/resource-global-applications',{'creationId':creation_id(),'values':{'name':'Chrome sintético R2.5 — Global' if index==0 else f'Aplicación global Ágil de perfil {index:02} — Nombre largo','templateId':template,'active':index%5!=4,'notes':'Administración privada de prueba','characteristics':{}}},expected=201)
products.append(response['resource'])
local=api.request('/resources/'+company,{'values':{'name':'Chrome sintético R2.5 — Compañía','templateId':template,'active':True,'notes':'','characteristics':{}}},expected=201)['resource']
profiles=api.request('/work-profiles/'+company)['profiles']
second_person=next(item for item in profiles if item['id']==fixture['configurationProfiles'][1])['values']['personId']
second=api.request('/work-profiles/'+company,{'values':{'name':'Perfil compartido R2.5','personId':second_person,'assetId':asset,'accountId':None,'active':True,'notes':None}},expected=201)['profile']
global_id=products[0]['id']
common={'resourceId':global_id,'delivery':'web','version':None,'state':'configured','notes':'SOLO-OTRO-PERFIL-R25'}
api.request('/application-configurations/'+company,{'values':{**common,'profileId':second['id']}},expected=201)
other=fixture['otherCompanies'][0]
person=api.request('/companies/'+other+'/people',{'name':'Persona B sintética R2.5','active':True,'isContact':False,'position':None,'email':None,'phone':None},expected=201)['company']['people'][-1]
asset_template=next(item for item in api.request('/asset-catalog/'+other)['items'] if item['values']['kind']=='template' and item['values']['name']=='Portátil')
other_asset=api.request('/assets/'+other,{'values':{'name':'ORDENADOR-B-R25','templateId':asset_template['id'],'siteId':None,'registeredOn':'2026-01-01','status':'operational','notes':'','characteristics':{}}},expected=201)['asset']
api.request('/assets/'+other+'/workstations',{'values':{'name':'Puesto B R2.5','active':True,'siteId':None,'personId':person['id'],'computerAssetId':other_asset['id'],'assetIds':[other_asset['id']]}},expected=201)
other_profile=api.request('/work-profiles/'+other,{'values':{'name':'Perfil B R2.5','personId':person['id'],'assetId':other_asset['id'],'accountId':None,'active':True,'notes':None}},expected=201)['profile']
api.request('/application-configurations/'+other,{'values':{**common,'profileId':other_profile['id'],'delivery':'installed','version':'VERSION-B-R25','notes':'SOLO-COMPAÑIA-B-R25'}},expected=201)
api.request('/auth/logout',{})
tool,browser,capture=context['tool'],context['browser'],context['capture']
origin,directory=context['origin'],context['directory']
profile_url=origin+f'/companies/{company}/work-profiles/{profile}/application-configurations'
workstation_url=origin+f'/companies/{company}/workstations/{workstation}'
screenshots,checks=[],[]
def code(client,source):
print(' R2.5 MCP: '+source[:105],flush=True)
try:
return tool(client,'browser_run_code_unsafe',{'code':'async (page) => { '+source+' }'})
except RuntimeError:
capture(client,directory,'r25-failure')
raise
with browser(context['config']) as client:
tool(client,'browser_resize',{'width':1440,'height':1000})
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});")
tool(client,'browser_navigate',{'url':profile_url})
code(client,"await page.getByRole('button',{name:'Configurar aplicación',exact:true}).waitFor(); await page.getByRole('button',{name:'Configurar aplicación',exact:true}).click(); await page.getByRole('dialog',{name:'Configurar aplicación',exact:true}).getByRole('button',{name:'Aplicación',exact:true}).click(); await page.getByRole('dialog',{name:'Seleccionar aplicación',exact:true}).getByRole('textbox',{name:'Buscar por nombre o proveedor',exact:true}).fill('Chrome sintético R2.5'); await page.getByRole('cell',{name:'Chrome sintético R2.5 — Global',exact:true}).waitFor(); await page.getByRole('cell',{name:'Chrome sintético R2.5 — Compañía',exact:true}).waitFor();")
screenshots.append(capture(client,directory,'r25-01-mixed-selector'))
checks.extend(['ordinary-login','profile-fixed','mixed-search','origin-visible'])
code(client,"await page.getByRole('row').filter({hasText:'Chrome sintético R2.5 — Global'}).click(); await page.getByRole('dialog',{name:'Seleccionar aplicación',exact:true}).getByRole('textbox',{name:'Buscar por nombre o proveedor',exact:true}).fill('SIN-RESULTADOS-R25'); await page.getByText('No hay coincidencias. Cambia la búsqueda o vuelve al inicio.',{exact:true}).waitFor();")
screenshots.append(capture(client,directory,'r25-02-empty-selection-retained'))
code(client,"await page.getByRole('dialog',{name:'Seleccionar aplicación',exact:true}).getByRole('button',{name:'Seleccionar',exact:true}).click(); await page.getByRole('combobox',{name:'Forma de uso',exact:true}).click(); await page.getByRole('option',{name:'Instalada en el equipo',exact:true}).click(); await page.getByRole('textbox',{name:'Versión',exact:true}).fill('PERFIL-123.4');")
screenshots.append(capture(client,directory,'r25-03-form-delivery-version'))
code(client,"await page.getByRole('dialog',{name:'Configurar aplicación',exact:true}).getByRole('button',{name:'Guardar',exact:true}).click(); await page.getByRole('dialog',{name:'Configurar aplicación',exact:true}).waitFor({state:'hidden'}); await page.getByRole('row').filter({hasText:'Chrome sintético R2.5 — Global'}).waitFor(); if(await page.getByText('SOLO-OTRO-PERFIL-R25',{exact:true}).count())throw Error('Perfil ajeno visible');")
checks.extend(['global-uuid-selected','selection-retained-across-search','installed-delivery','configured-version','save-real-http'])
code(client,"await page.getByRole('row').filter({hasText:'Chrome sintético R2.5 — Global'}).getByRole('button',{name:'Editar',exact:true}).click(); if(!await page.getByRole('button',{name:'Aplicación',exact:true}).isDisabled())throw Error('Producto editable'); if(await page.getByRole('textbox',{name:'Versión',exact:true}).inputValue()!=='PERFIL-123.4')throw Error('Versión perdida'); await page.getByRole('button',{name:'Cancelar',exact:true}).click();")
checks.append('reopen-immutable-product')
tool(client,'browser_navigate',{'url':origin+f'/companies/{other}/work-profiles/{other_profile["id"]}/application-configurations'})
code(client,"await page.getByRole('cell',{name:'VERSION-B-R25',exact:true}).waitFor(); if(await page.getByRole('cell',{name:'PERFIL-123.4',exact:true}).count())throw Error('Versión de A visible en B');")
screenshots.append(capture(client,directory,'r25-company-b-independent'))
tool(client,'browser_navigate',{'url':origin+f'/companies/{company}/work-profiles/{second["id"]}/application-configurations'})
code(client,"await page.getByRole('cell',{name:'Acceso web',exact:true}).waitFor(); if(await page.getByRole('cell',{name:'PERFIL-123.4',exact:true}).count())throw Error('Versión del perfil primario visible en el segundo');")
screenshots.append(capture(client,directory,'r25-second-profile-web'))
checks.extend(['browser-company-b-version','browser-second-profile-web'])
tool(client,'browser_navigate',{'url':workstation_url})
code(client,"await page.getByRole('button',{name:'Ver configuraciones pendientes',exact:true}).waitFor(); if(await page.locator('[data-pending-count]').getAttribute('data-pending-count')!=='1')throw Error('Pendientes duplicados'); await page.getByRole('button',{name:'Ver configuraciones pendientes',exact:true}).click(); await page.getByRole('region',{name:'Configuraciones de Perfil Álvarez 1',exact:true}).getByRole('link',{name:'Chrome sintético R2.5 — Global',exact:true}).waitFor(); if(await page.getByText('SOLO-OTRO-PERFIL-R25',{exact:true}).count())throw Error('Mezcla entre perfiles del mismo ordenador');")
screenshots.append(capture(client,directory,'r25-04-workstation-expanded-pending'))
checks.extend(['workstation-profile-expansion','pending-count-one','pending-navigation','same-computer-profile-isolation'])
api.request('/auth/login',{'email':'setup@audit.example.test','password':admin_password})
primary=next(item for item in api.request('/application-configurations/'+company)['configurations'] if item['values']['profileId']==profile and item['values']['resourceId']==global_id)
if primary['values']['version']!='PERFIL-123.4' or primary['values']['delivery']!='installed':
raise RuntimeError('Configuración primaria inesperada.')
if not any(item['values']['version']=='VERSION-B-R25' for item in api.request('/application-configurations/'+other)['configurations']):
raise RuntimeError('Configuración independiente de B ausente.')
if api.request('/software-installations/'+company)['configurations']:
raise RuntimeError('Se generaron instalaciones automáticamente.')
api.request('/resource-global-applications/'+global_id+'/status',{'active':False})
api.request('/auth/logout',{})
checks.extend(['two-companies-independent','delivery-version-isolation','no-software-installation-side-effect'])
tool(client,'browser_navigate',{'url':profile_url})
code(client,"await page.getByRole('row').filter({hasText:'Chrome sintético R2.5 — Global'}).waitFor(); await page.getByText('Revisar: hay una retirada pendiente o el recurso, perfil, persona o equipo ya no está disponible.',{exact:true}).waitFor(); await page.getByRole('button',{name:'Configurar aplicación',exact:true}).click(); await page.getByRole('button',{name:'Aplicación',exact:true}).click(); await page.getByRole('textbox',{name:'Buscar por nombre o proveedor',exact:true}).fill('Chrome sintético R2.5'); await page.getByRole('cell',{name:'Chrome sintético R2.5 — Compañía',exact:true}).waitFor(); if(await page.getByRole('cell',{name:'Chrome sintético R2.5 — Global',exact:true}).count())throw Error('Inactivo candidato'); await page.keyboard.press('Escape'); if(await page.evaluate(()=>document.activeElement?.getAttribute('aria-label'))!=='Aplicación')throw Error('Foco perdido'); await page.keyboard.press('Escape');")
checks.extend(['inactive-existing-visible','needs-review-derived','inactive-excluded','keyboard-escape','picker-focus-return'])
code(client,"page.r25Pattern='**/api/resource-application-candidates/*?*'; await page.route(page.r25Pattern,r=>r.fulfill({status:500,contentType:'application/json',json:{code:'INTERNAL_ERROR'}})); await page.getByRole('button',{name:'Configurar aplicación',exact:true}).click(); await page.getByRole('button',{name:'Aplicación',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,'r25-05-partial-error'))
code(client,"await page.unroute(page.r25Pattern); await page.getByRole('button',{name:'Reintentar',exact:true}).click(); await page.getByRole('dialog',{name:'Seleccionar aplicación',exact:true}).getByRole('radio').first().waitFor(); await page.keyboard.press('Escape'); await page.keyboard.press('Escape');")
checks.extend(['candidate-error-keeps-profile','retry-real-query'])
api.request('/auth/login',{'email':'setup@audit.example.test','password':admin_password})
active=[item for item in products[1:] if item['values']['active']][:25]
locals_=[item for item in api.request('/resources/'+company)['resources'] if item['values']['templateId']==template and item['values']['active']][:25]
for index,product in enumerate(active+locals_):
api.request('/application-configurations/'+company,{'values':{'profileId':profile,'resourceId':product['id'],'delivery':'installed' if index%2 else 'web','version':f'Perfil-{index+1}' if index%2 else None,'state':'configured','notes':'Volumen sintético de configuraciones'}},expected=201)
api.request('/auth/logout',{})
tool(client,'browser_navigate',{'url':profile_url})
code(client,"await page.getByRole('cell',{name:'Perfil-2',exact:true}).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:'Aplicaciones del perfil',exact:true}).last().scrollIntoViewIfNeeded(); if(await page.evaluate(()=>document.documentElement.scrollWidth>innerWidth+1))throw Error('Desbordamiento de página');")
screenshots.append(capture(client,directory,f'r25-profile-{width}'))
if width==390:
code(client,"await page.getByRole('button',{name:'Configurar aplicación',exact:true}).click(); await page.getByRole('button',{name:'Aplicación',exact:true}).click(); await page.getByRole('dialog',{name:'Seleccionar aplicación',exact:true}).getByRole('radio').first().waitFor();")
screenshots.append(capture(client,directory,'r25-mobile-picker'))
code(client,"await page.keyboard.press('Escape'); await page.keyboard.press('Escape');")
checks.extend(['density-51-primary-configurations','six-viewports','mobile-picker'])
tool(client,'browser_navigate',{'url':workstation_url})
code(client,"await page.getByRole('button',{name:'Ver configuraciones pendientes',exact:true}).waitFor(); await page.getByRole('button',{name:'Ver configuraciones pendientes',exact:true}).click(); await page.getByRole('region',{name:'Configuraciones de Perfil Álvarez 1',exact:true}).getByRole('link',{name:'Chrome sintético R2.5 — Global',exact:true}).waitFor();")
screenshots.append(capture(client,directory,'r25-workstation-390'))
backend=context['backend'];os.killpg(backend.pid,signal.SIGTERM);backend.wait(timeout=10)
closed={**context['active_environment'],'RESOURCE_GLOBAL_PROFILE_ENABLED':'false'}
with context['child_process'](['node','--import','tsx','apps/server/src/index.ts'],closed,directory/'api-profiles-closed.log') as restarted:
context['wait_for'](api.ready,restarted)
tool(client,'browser_resize',{'width':1440,'height':1000})
tool(client,'browser_navigate',{'url':profile_url})
code(client,"await page.getByRole('button',{name:'Configurar aplicación',exact:true}).waitFor(); await page.getByRole('region',{name:'Aplicaciones del perfil',exact:true}).getByRole('textbox').fill('Chrome sintético R2.5'); await page.getByRole('row').filter({hasText:'Chrome sintético R2.5 — Global'}).waitFor(); await page.getByRole('button',{name:'Configurar aplicación',exact:true}).click(); await page.getByRole('dialog',{name:'Configurar aplicación',exact:true}).getByRole('combobox',{name:'Aplicación',exact:true}).waitFor(); await page.getByRole('combobox',{name:'Aplicación',exact:true}).click(); await page.getByRole('option',{name:'Chrome sintético R2.5 — Compañía',exact:true}).click();")
screenshots.append(capture(client,directory,'r25-06-gate-closed-local-form'))
code(client,"await page.getByRole('dialog',{name:'Configurar aplicación',exact:true}).getByRole('button',{name:'Guardar',exact:true}).click(); await page.getByRole('dialog',{name:'Configurar aplicación',exact:true}).waitFor({state:'hidden'});")
checks.extend(['gate-closed-local-selector','gate-closed-local-save','gate-closed-global-read'])
report={'checks':checks,'screenshots':screenshots,'companies':15,'principal':{'people':50,'assets':51,'profiles':51,'workstations':50,'resourcesAtLeast':50,'primaryProfileConfigurations':52},'globalApplications':50,'database':'metis_desk_test','mcp':'Docker stdio fijado en .codex/config.toml','credentialsSaved':False}
(directory/'r25-report.json').write_text(json.dumps(report,ensure_ascii=False,indent=2)+'\n')
print('R2.5 recorrido verificado. Evidencia: '+str(directory.relative_to(context['ROOT'])),flush=True)