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