aim-web2.1.0rc9

This commit is contained in:
admin_rb
2026-09-22 19:23:17 +02:00
parent d095887d2e
commit 3dfc80b782
438 changed files with 31613 additions and 1510 deletions
@@ -0,0 +1,137 @@
"""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 '<style>'+source.read_text()+'</style>'
def js(match):
name=match.group(1).split('?')[0]
if 'htmx.min.js' in name:return ''
return '<script>'+(ROOT/'src/aim_webgui'/name.lstrip('/')).read_text()+'</script>'
content=re.sub(r'<link[^>]+href="(/static/[^"]+\.css(?:\?[^"]*)?)"[^>]*>',css,content)
content=re.sub(r'<script[^>]+src="(/static/[^"]+\.js(?:\?[^"]*)?)"[^>]*></script>',js,content)
fixture="""<script>window.fetch=async function(url,opts){const r=await window.fixtureRead(String(url));return {ok:r.status>=200&&r.status<300,status:r.status,json:async()=>JSON.parse(r.text),text:async()=>r.text};};</script>"""
pages[path]=content.replace('<head>','<head>'+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()