"""New rc4 ASGI-backed browser fixtures with synthetic retained reports. Bootstrap5.3.6 is a declared fixture substitute; HTMX is not loaded. This does not qualify production HTTPS/proxy traffic or native Ansible/Windows Update behavior. No substituted browser asset is copied to the release static/vendor directory. """ from __future__ import annotations from dataclasses import replace from pathlib import Path import argparse import json import re import sys import tempfile import time ROOT=Path(__file__).resolve().parents[1] sys.path.insert(0,str(ROOT/'src')) from playwright.sync_api import sync_playwright from fastapi.testclient import TestClient from aim_webgui.app import create_app from aim_webgui.auth.service import Auth from aim_webgui.reports import persist from conftest import core_root as core_fixture, settings as settings_fixture from evidence_fixtures import declared, plan, result, job from patch_fixtures import patch_data, update_failure def main(): parser=argparse.ArgumentParser(description=__doc__) parser.add_argument('--out',type=Path,required=True) parser.add_argument('--bootstrap',type=Path,default=Path('/opt/imagemagick/share/doc/ImageMagick-7/www/assets/bootstrap.min.css')) args=parser.parse_args();args.out.mkdir(parents=True,exist_ok=True) records=[] with tempfile.TemporaryDirectory(prefix='aim-rc4-patch-browser-') as tmp: root=Path(tmp);core=core_fixture.__wrapped__(root);base=settings_fixture.__wrapped__(root,core) url=base.public_url settings=replace(base,public_url=url,execution_enabled=False,execution_transport_verified=False,credentials_enabled=False) a=Auth(settings);a.bootstrap() with a.store.transaction() as db: db.execute('UPDATE users SET must_change_password=0');uid=db.execute('SELECT id FROM users').fetchone()[0] d=declared('patch_summary_v1');paths={} scenarios={ 'next-wave':patch_data(), 'pending':patch_data(remaining_updates_known=True,reboot_performed=False,pending=[ {'name':'Synthetic security update','identifier':'fixture-pending','kb':['KB0000002']}]), 'failed-update':patch_data(complete=False,blocked_reason='install_not_allowed',failed_updates=[update_failure()]), 'reboot-deferred':patch_data(reboot_deferred=True,reboot_performed=False,reboot_required=True,reboot_required_after=True), } for name,content in scenarios.items(): p=plan(d,['win01.example']);p['playbook']='maintenance_patch_os';p['core_request']['playbook']=p['playbook'] failed=name=='failed-update' ident=job(a,uid,p,status='failed' if failed else 'successful') r=result(d,hosts=p['targets'],report_data=content,status='failed' if failed else 'succeeded') if failed: r.update(stage='execution',exit_code=2,error={'code':'playbook_failed','message':'Synthetic task failed','stage':'execution'}) r['targets'][0]['outcome']='failed';r['targets'][0]['counts']['failures']=1;r['counts']['failures']=1 r['target_summary'].update(successful=0,failed=1) with a.store.transaction() as db: small=persist(db,settings,ident,p,r) db.execute('UPDATE jobs SET core_result=?,finished_at=? WHERE id=?',(json.dumps(small),time.time(),ident)) paths[name]=f'/jobs/{ident}/reports' client=TestClient(create_app(settings),base_url=url) token,_=a.new_session(uid);client.cookies.set(settings.cookie_name,token) pages={} def bridge_read(source,path): response=client.get(path) return {'status':response.status_code,'text':response.text} def mount(page,path): if path not in pages: response=client.get(path);assert response.status_code==200,(path,response.text[:500]) content=response.text def css(match): name=match.group(1).split('?')[0] source=args.bootstrap if name.endswith('bootstrap.min.css') else ROOT/'src/aim_webgui'/name.lstrip('/') return '' def js(match): name=match.group(1).split('?')[0] if 'htmx.min.js' in name:return '' return '' content=re.sub(r']+href="(/static/[^"]+\.css(?:\?[^"]*)?)"[^>]*>',css,content) content=re.sub(r']+src="(/static/[^"]+\.js(?:\?[^"]*)?)"[^>]*>',js,content) fixture="""""" pages[path]=content.replace('',''+fixture) page.set_content(pages[path],wait_until='domcontentloaded') try: with sync_playwright() as pw: browser=pw.chromium.launch(executable_path='/usr/bin/chromium',args=['--no-sandbox','--disable-dev-shm-usage']) try: for width,height in ((320,750),(390,844),(760,900),(1440,1000),(740,390)): for theme in ('light','dark'): context=browser.new_context(viewport={'width':width,'height':height},locale='en-GB',timezone_id='Europe/Berlin',reduced_motion='reduce') page=context.new_page();page.expose_binding('fixtureRead',bridge_read);errors=[];page.on('pageerror',lambda exc:errors.append(str(exc))) for name,path in paths.items(): mount(page,path) page.locator('[data-theme-option='+theme+']:visible').click() assert page.evaluate('document.documentElement.scrollWidth<=innerWidth+1'),(name,width,height) assert page.locator('.patch-wave-panel').is_visible() if name=='next-wave': assert 'Another patch run needs review' in page.locator('.patch-wave-panel').inner_text() assert 'Final pending list not established' in page.locator('#report-pending').inner_text() assert '0 entries' not in page.locator('#report-pending').inner_text() elif name=='pending': assert 'final read-only discovery' in page.locator('#report-pending').inner_text() assert 'Synthetic security update' in page.locator('#report-pending').inner_text() elif name=='failed-update': assert '0x80240016' in page.locator('#report-failed_updates').inner_text() assert 'does not by itself prove a pending reboot' in page.locator('.patch-wave-panel').inner_text() page.locator('[data-report-json] summary').click() page.wait_for_function("document.querySelector('[data-json-status]').textContent.includes('Retained')") stored=json.loads(page.locator('[data-json-output]').inner_text()) assert stored==scenarios[name] or stored.get('data')==scenarios[name] if name=='next-wave' and width in(390,1440) and theme=='dark': page.locator('[data-report-json] summary').click();page.evaluate('window.scrollTo(0,0)') page.screenshot(path=str(args.out/f'patch-wave-{width}-dark.png'),full_page=True) records.append({'page':name,'width':width,'height':height,'theme':theme,'passed':True}) mount(page,'/plan?customer=example&playbook=maintenance_patch_os') page.locator('[data-theme-option='+theme+']:visible').click() option=page.locator('[name="option.os_patching_rescan_after_reboot"]') assert option.input_value()=='' and 'catalog hint: false' in option.inner_text() option.select_option('false') page.locator('[name="option.os_patching_reboot_delay_minutes"]').fill('10') page.locator('[name="option.os_patching_reboot_message"]').fill('Synthetic approved maintenance') assert page.evaluate('document.documentElement.scrollWidth<=innerWidth+1') records.append({'page':'catalog patch options','width':width,'height':height,'theme':theme,'passed':True}) assert not errors,errors context.close() finally:browser.close() finally: client.close() evidence={'cases':records,'count':len(records),'real_local_http_and_fetch':False,'asgi_bridge':True,'production_https_tested':False, 'bootstrap_substitute':'5.3.6 for pinned5.3.8','htmx_loaded':False,'native_updates_executed':False, 'source':'synthetic retained data; real public Core catalog/discovery; about:blank fetch bridge to ASGI TestClient'} (args.out/'results.json').write_text(json.dumps(evidence,indent=2)+'\n') print(len(records),'patch UI cases passed') if __name__=='__main__':main()