metis-desk/scripts/global-operating-systems-ui-audit.py
David d2bf856c97 feat(global-operating-systems): implement global operating systems management features
- Add softwareReferenceHref helper function for generating resource links.
- Create useGlobalResourceEditor hook for managing global application resources.
- Implement useGlobalResourceForm hook for handling global resource forms.
- Introduce GlobalSoftwareCatalogProvider for context management of global applications.
- Develop GlobalOperatingSystemsPage to render global operating systems using the existing GlobalApplicationsPage.
- Add operating system form identity helpers for managing OS-specific fields.
- Create operating system specification labels for better localization.
- Implement OperatingSystemIdentityReview component for reviewing OS identities.
- Add OperatingSystemProductFields component for displaying OS product details.
- Write tests for resource characteristic control to ensure proper translation and value handling.
- Create a Python script for auditing global operating systems UI interactions.
- Add tests for expanded workstation profiles to ensure proper functionality and responsiveness.
2026-10-25 20:39:06 +01:00

184 lines
20 KiB
Python

"""SO globales: recorrido HTTP real y Playwright MCP en instalación desechable."""
import json
import base64
import hashlib
import time
import uuid
def verify(context, fixture, admin_password):
api, tool, capture = context['api'], context['tool'], context['capture']
origin, directory = context['origin'], context['directory']
checks, screenshots, measurements = [], [], []
base = '/catalogs/global-operating-systems'
def code(client, source):
print(' SO MCP: ' + source[:90], flush=True)
try:
return tool(client, 'browser_run_code_unsafe', {'code': 'async(page)=>{' + source + '}'})
except RuntimeError:
shot(client, 'failure')
diagnostic = context['result_json'](tool(client, 'browser_run_code_unsafe', {'code': "async(page)=>({alerts:await page.locator('[role=alert]').allTextContents(),dialogs:await page.getByRole('dialog').allTextContents(),buttons:await page.getByRole('button').evaluateAll(items=>items.filter(x=>x.getBoundingClientRect().width).map(x=>({text:x.innerText,disabled:x.disabled})))})"}))
measurements.append({'failure': diagnostic})
raise
def login(client, email, password):
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': email},
{'name': 'Contraseña', 'type': 'textbox', 'target': 'input[type=password]', 'value': password}]})
tool(client, 'browser_click', {'target': 'button[type=submit]'})
code(client, "await page.locator('[data-slot=workspace]').waitFor({timeout:30000});")
def shot(client, name):
screenshots.append(capture(client, directory, 'global-os-' + name))
def report():
data = {'checks': checks, 'screenshots': screenshots, 'measurements': measurements, 'database': 'metis_desk_test', 'companies': 15,
'principal': {'people': 50, 'assetsAtLeast': 50, 'resourcesAtLeast': 50, 'profiles': 50, 'workstations': 50},
'mcp': 'Docker stdio de .codex/config.toml', 'productionEnabled': False}
(directory / 'global-operating-systems-report.json').write_text(json.dumps(data, ensure_ascii=False, indent=2) + '\n')
with context['browser'](context['config']) as client:
try:
login(client, 'setup@audit.example.test', admin_password)
tool(client, 'browser_resize', {'width': 1280, 'height': 1000})
tool(client, 'browser_navigate', {'url': origin + base})
code(client, "await page.getByText('Todavía no hay sistemas operativos globales.',{exact:false}).waitFor();")
checks.append('empty-catalog')
shot(client, 'empty')
code(client, "await page.getByRole('button',{name:'Nuevo sistema operativo global',exact:true}).click(); await page.getByRole('dialog').getByRole('textbox',{name:'Nombre',exact:true}).fill('Windows 11 Pro'); await page.getByRole('textbox',{name:'Fabricante / proveedor',exact:true}).fill('Microsoft'); await page.getByRole('textbox',{name:'Familia',exact:true}).fill('Windows'); await page.getByRole('textbox',{name:'Versión principal del producto',exact:true}).fill('11'); await page.getByRole('combobox',{name:'Estado de edición',exact:true}).click(); await page.getByRole('option',{name:'Edición conocida',exact:true}).click(); await page.getByRole('textbox',{name:'Edición',exact:true}).fill('Pro');")
code(client, "if(!await page.getByRole('button',{name:'Guardar',exact:true}).isDisabled())throw Error('Revisión obligatoria'); await page.getByRole('button',{name:'Comprobar identidad en el catálogo',exact:true}).click(); await page.getByText('No hay otra identidad equivalente en el catálogo.',{exact:false}).waitFor(); await page.getByRole('checkbox',{name:'He revisado la identidad y el catálogo; este producto es distinto.',exact:true}).check(); await page.getByRole('textbox',{name:'Edición',exact:true}).fill('Home'); if(!await page.getByRole('button',{name:'Guardar',exact:true}).isDisabled())throw Error('Revisión obsoleta'); await page.getByRole('textbox',{name:'Edición',exact:true}).fill('Pro');")
checks.extend(['identity-fields', 'known-edition', 'review-required', 'identity-review-invalidation'])
shot(client, 'create-review')
code(client, "await page.getByRole('button',{name:'Guardar',exact:true}).click(); await page.getByRole('heading',{name:'Windows 11 Pro',exact:true}).waitFor(); await page.getByRole('heading',{name:'Historial',exact:true}).waitFor();")
product_url = context['result_json'](code(client, 'return page.url();'))
product_id = product_url.split('/')[-1]
checks.extend(['create-in-browser', 'history'])
shot(client, 'created-detail')
code(client, "await page.getByRole('button',{name:'Editar sistema operativo global',exact:true}).click(); await page.getByRole('textbox',{name:'Versión principal del producto',exact:true}).fill('10'); await page.getByRole('textbox',{name:'Nombre',exact:true}).fill('Windows 10 Pro'); await page.getByRole('button',{name:'Comprobar identidad en el catálogo',exact:true}).click(); await page.getByRole('checkbox',{name:'He revisado la identidad y el catálogo; este producto es distinto.',exact:true}).check(); await page.getByRole('button',{name:'Guardar',exact:true}).click(); await page.getByText('Este producto ya se publicó.',{exact:false}).waitFor();")
checks.append('server-rejects-product-transformation-form-preserved')
shot(client, 'identity-error')
code(client, "await page.getByRole('button',{name:'Cancelar',exact:true}).click(); await page.getByRole('dialog').waitFor({state:'hidden'}); if(!await page.getByRole('button',{name:'Editar sistema operativo global',exact:true}).evaluate(el=>el===document.activeElement))throw Error('Retorno de foco');")
checks.append('dialog-focus-return')
api.request('/auth/login', {'email': 'setup@audit.example.test', 'password': admin_password})
rc = api.request('/resource-catalog/global')['items']
ids = {item['builtinKey']: item['id'] for item in rc if item.get('builtinKey')}
identity_keys = {'provider': 'provider', 'family': 'os_family', 'productVersion': 'os_product_version', 'editionState': 'os_edition_state', 'edition': 'software_edition'}
for index in range(50):
identity = {'provider': 'Microsoft', 'family': 'Windows', 'productVersion': str(100 + index), 'editionState': 'known', 'edition': 'Pro'}
creation = str(uuid.UUID(int=(int(time.time()*1000)<<80) | (0x7<<76) | (0x2<<62) | (uuid.uuid4().int & ((1<<62)-1))))
api.request('/resource-global-operating-systems', {'creationId': creation, 'reviewedIdentity': identity,
'values': {'name': f'Windows {100+index} Pro — Producto sintético Álvarez', 'templateId': ids['template:os'], 'active': True, 'notes': '', 'characteristics': {ids['field:'+identity_keys[key]]: value for key, value in identity.items()}}}, expected=201)
company = fixture['company']
local = api.request('/resources/' + company, {'values': {'name': 'Windows 11 Pro', 'templateId': ids['template:os'], 'active': True, 'notes': '', 'characteristics': {}}}, expected=201)['resource']
for resource_id in [product_id, local['id']]:
api.request('/software-installations/' + company, {'values': {'assetId': fixture['softwareAssets'][0], 'resourceId': resource_id, 'licenseResourceId': None, 'version': '24H2', 'state': 'configured', 'notes': ''}}, expected=201)
api.request('/auth/logout', {})
tool(client, 'browser_navigate', {'url': origin + base})
code(client, "await page.getByRole('link',{name:'Windows 11 Pro',exact:true}).waitFor(); await page.getByRole('button',{name:'Siguiente',exact:true}).click(); await page.getByRole('button',{name:'Anterior',exact:true}).click(); await page.getByRole('textbox',{name:'Buscar por nombre o proveedor',exact:true}).fill('SIN-COINCIDENCIAS-SO'); await page.getByText('No hay sistemas operativos que coincidan.',{exact:false}).waitFor();")
checks.extend(['51-global-products', 'cursor-pagination', 'search-empty-recovery'])
shot(client, 'no-results')
code(client, "await page.getByRole('textbox',{name:'Buscar por nombre o proveedor',exact:true}).fill(''); await page.getByRole('link',{name:'Windows 11 Pro',exact:true}).waitFor();")
for theme in ['light', 'dark']:
code(client, "await page.evaluate(theme=>{document.documentElement.classList.toggle('dark',theme==='dark');}," + json.dumps(theme) + ");")
for width in [320,390,768,1024,1280,1600]:
tool(client, 'browser_resize', {'width': width, 'height': 1000 if width > 768 else 844})
code(client, "if(await page.evaluate(()=>document.documentElement.scrollWidth>innerWidth+1))throw Error('Desbordamiento de página');")
shot(client, f'list-{theme}-{width}')
contrast = context['result_json'](code(client, """return await page.evaluate(()=>{
const canvas=document.createElement('canvas');canvas.width=canvas.height=1;const ctx=canvas.getContext('2d',{willReadFrequently:true});
const rgba=color=>{ctx.clearRect(0,0,1,1);ctx.fillStyle=color;ctx.fillRect(0,0,1,1);return [...ctx.getImageData(0,0,1,1).data].map((v,i)=>i===3?v/255:v)};
const over=(a,b)=>{const alpha=a[3]+b[3]*(1-a[3]);return [...a.slice(0,3).map((v,i)=>(v*a[3]+b[i]*b[3]*(1-a[3]))/alpha),alpha]};
const bg=e=>e?over(rgba(getComputedStyle(e).backgroundColor),bg(e.parentElement)):[255,255,255,1];
const lum=c=>c.slice(0,3).map(v=>v/255).map(v=>v<=.04045?v/12.92:((v+.055)/1.055)**2.4).reduce((sum,v,i)=>sum+v*[.2126,.7152,.0722][i],0);
return [...document.querySelectorAll('main h1,main p,main table a,main th')].filter(e=>e.getBoundingClientRect().width&&e.innerText.trim()).map(e=>{const a=lum(over(rgba(getComputedStyle(e).color),bg(e))),b=lum(bg(e));return {text:e.innerText.slice(0,80),ratio:(Math.max(a,b)+.05)/(Math.min(a,b)+.05)}});
});"""))
if not contrast or any(item['ratio'] < 4.5 for item in contrast):
raise RuntimeError('Contraste de texto insuficiente en ' + theme + ': ' + json.dumps(contrast))
measurements.append({'theme': theme, 'contrast': contrast})
checks.append('computed-text-contrast-' + theme)
checks.append('list-' + theme + '-320-390-768-1024-1280-1600')
tool(client, 'browser_resize', {'width': 390, 'height': 844})
code(client, "await page.getByRole('button',{name:'Nuevo sistema operativo global',exact:true}).click(); await page.getByRole('dialog').waitFor(); if(await page.evaluate(()=>document.documentElement.scrollWidth>innerWidth+1))throw Error('Desbordamiento de modal');")
shot(client, 'mobile-form')
code(client, "await page.keyboard.press('Escape'); await page.getByRole('dialog').waitFor({state:'hidden'});")
checks.append('mobile-form-keyboard-close')
tool(client, 'browser_resize', {'width': 1280, 'height': 1000})
code(client, "page.osPattern='**/api/resource-global-operating-systems?*'; await page.route(page.osPattern,r=>r.fulfill({status:500,contentType:'application/json',json:{code:'INTERNAL_ERROR'}})); await page.goto(" + json.dumps(origin+base) + "); await page.getByText('No se han podido cargar los sistemas operativos.',{exact:true}).waitFor();")
shot(client, 'list-error')
code(client, "await page.unroute(page.osPattern); await page.getByRole('button',{name:'Reintentar',exact:true}).click(); await page.getByRole('link',{name:'Windows 11 Pro',exact:true}).waitFor();")
checks.append('transport-error-retry')
# Una sola llamada conserva las barreras y sus callbacks en el mismo ámbito.
loading_capture = context['result_json'](code(client, """
const releases=[], pending=[];
const pattern='**/api/resource-global-operating-systems?*';
const hold=async route=>{const task=(async()=>{await new Promise(resolve=>releases.push(resolve));await route.continue()})();pending.push(task);await task};
await page.route(pattern,hold);
let screenshot;
try {
await page.goto(""" + json.dumps(origin+base) + """,{waitUntil:'domcontentloaded'});
await page.getByText('Cargando sistemas operativos…',{exact:true}).waitFor();
screenshot=(await page.screenshot({fullPage:false})).toString('base64');
} finally {
for(const release of releases)release();
await Promise.all(pending);
await page.unroute(pattern,hold);
}
await page.getByRole('link',{name:'Windows 11 Pro',exact:true}).waitFor();
return screenshot;
"""))
png = base64.b64decode(loading_capture, validate=True)
(directory/'global-os-list-loading.png').write_bytes(png)
screenshots.append({'file': 'global-os-list-loading.png', 'sha256': hashlib.sha256(png).hexdigest()})
checks.append('initial-loading')
code(client, "await page.getByRole('button',{name:'Nuevo sistema operativo global',exact:true}).click(); await page.getByRole('textbox',{name:'Nombre',exact:true}).fill('Borrador de SO'); await page.getByRole('checkbox',{name:'Publicar como producto activo',exact:true}).uncheck(); await page.getByRole('button',{name:'Guardar',exact:true}).click(); await page.getByRole('heading',{name:'Borrador de SO',exact:true}).waitFor();")
checks.append('inactive-draft-in-browser')
code(client, "await page.getByRole('button',{name:'Editar sistema operativo global',exact:true}).click(); await page.keyboard.press('Escape'); await page.getByRole('dialog').waitFor({state:'hidden'}); await page.getByLabel('Menú de usuario',{exact:true}).click(); await page.getByRole('combobox',{name:'Idioma',exact:true}).click(); await page.getByRole('option',{name:'English',exact:true}).click(); await page.getByRole('combobox',{name:'Language',exact:true}).waitFor(); await page.getByLabel('User menu',{exact:true}).click(); await page.getByRole('button',{name:'Edit global operating system',exact:true}).click(); await page.getByRole('textbox',{name:'Major product version',exact:true}).waitFor(); await page.getByRole('combobox',{name:'Edition status',exact:true}).click(); await page.getByRole('option',{name:'Edition not applicable',exact:true}).waitFor(); await page.keyboard.press('Escape');")
shot(client, 'english-form')
checks.append('english-form-labels-edition-options')
code(client, "await page.keyboard.press('Escape'); await page.getByRole('dialog').waitFor({state:'hidden'});")
finally:
report()
with context['browser'](context['config']) as client:
try:
login(client, fixture['email'], fixture['password'])
tool(client, 'browser_resize', {'width': 1280, 'height': 1000})
tool(client, 'browser_navigate', {'url': product_url})
code(client, "await page.getByRole('heading',{name:'Windows 11 Pro',exact:true}).waitFor(); await page.getByText('Versión principal del producto',{exact:true}).waitFor(); if(await page.getByRole('button',{name:'Editar sistema operativo global',exact:true}).count())throw Error('Administración ordinaria');")
checks.append('ordinary-public-projection')
shot(client, 'ordinary-detail')
asset_url = origin + '/companies/' + company + '/assets/' + fixture['softwareAssets'][0]
station_url = origin + '/companies/' + company + '/workstations/' + fixture['softwareWorkstations'][0]
for view, url in [('asset',asset_url),('workstation',station_url)]:
tool(client, 'browser_navigate', {'url': url})
code(client, "await page.locator('[data-current-operating-systems]').getByRole('link',{name:'Windows 11 Pro',exact:true}).first().waitFor(); if(!await page.locator('[data-current-operating-systems]').locator('a[href=\"" + base + '/' + product_id + "\"]').count())throw Error('Destino SO global');")
code(client, "await page.locator('[data-current-operating-systems]').locator('xpath=ancestor::section[1]').scrollIntoViewIfNeeded();")
shot(client, view + '-current-os')
checks.append(view + '-global-os-navigation')
global_href = base + '/' + product_id
if view == 'asset':
code(client, "const row=page.getByRole('row').filter({has:page.locator('a[href=\"" + global_href + "\"]')}); await row.getByRole('button',{name:'Acciones',exact:true}).click(); await page.getByRole('menuitem',{name:/Historial/}).click(); await page.getByRole('dialog',{name:/Historial/}).locator('a[href=\"" + global_href + "\"]').waitFor();")
shot(client, 'installation-history-global-link')
code(client, "await page.keyboard.press('Escape'); await page.getByRole('dialog').waitFor({state:'hidden'});")
checks.append('installation-panel-and-history-global-os-navigation')
else:
code(client, "await page.locator('[data-workplace-software] a[href=\"" + global_href + "\"]').waitFor(); await page.locator('[data-workplace-map]').scrollIntoViewIfNeeded();")
shot(client, 'workplace-map-global-os')
code(client, "await page.locator('[data-workplace-software] a[href=\"" + global_href + "\"]').click(); await page.getByRole('heading',{name:'Windows 11 Pro',exact:true}).waitFor(); if(new URL(page.url()).pathname!==" + json.dumps(global_href) + ")throw Error('Destino del mapa');")
checks.append('map-click-opens-global-os-detail')
tool(client, 'browser_navigate', {'url': station_url})
code(client, "await page.locator('[data-workplace-equipment=principal] a').click(); await page.getByRole('heading',{name:'AUDIT-R23-PORT-01',exact:true}).waitFor();")
checks.append('map-equipment-pointer-navigation')
tool(client, 'browser_navigate', {'url': asset_url})
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('textbox',{name:'Buscar por nombre o proveedor',exact:true}).fill('Windows 11 Pro'); await page.getByRole('dialog',{name:'Seleccionar software',exact:true}).getByRole('cell',{name:'Windows 11 Pro',exact:true}).first().waitFor(); if(await page.getByRole('dialog',{name:'Seleccionar software',exact:true}).getByRole('cell',{name:'Windows 11 Pro',exact:true}).count()!==2)throw Error('Coexistencia');")
shot(client, 'mixed-picker')
checks.append('mixed-picker-global-local-no-replacement')
code(client, "await page.keyboard.press('Escape'); await page.keyboard.press('Escape');")
finally:
report()
print('SO global: recorrido MCP verificado. Evidencias: ' + str(directory.relative_to(context['ROOT'])), flush=True)