"""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)