test: qualify built demo images through mobile and upload flows

This commit is contained in:
archipelago
2026-10-05 16:48:52 -04:00
parent 157c9ec055
commit a902cc84fe
2 changed files with 101 additions and 5 deletions
+30 -5
View File
@@ -14,8 +14,9 @@ whenever human acceptance is needed.
- Alpha backend: isolated suite **1,681 passed**, zero failed, four explicit
ignores; separate container runtime suite **82 passed**. Optimized binary
SHA256 `a5a6cfc7dcab011963a6f18dc570446b6fa624180d206a108fab27504f4fd41d`
is deployed on dev, yaya and Framework with private rollback backups.
SHA256 `560aa6006cd9ef8be95b1f7831cf3b53854e911622b50022bb4402ce0f8b010a`
is deployed on dev, yaya and Shorty with private rollback backups. Framework
retains the preceding A5 candidate; its earlier acceptance remains recorded.
- Frontend: **1,222 passed across 150 files**; production build and real mobile/
desktop media/menu checks pass. Dev serves index SHA256
`c18b24024a78789fe65c74c5ce27efe2125ae869016ab65e33c5a2680b543f17`.
@@ -45,9 +46,10 @@ whenever human acceptance is needed.
acceptance; retain the completed
fresh/nested disposable, public staging issuance/forced renewal and actual
yaya state-preservation checks.
- [ ] **Shorty NPM:** shop certificate12/Force SSL are operator accepted and
independently verified; qualify and apply the manual-route migration. Preserve live
management containment and current public app routing.
- [ ] **Shorty NPM:** shop certificate12/Force SSL and manual-route migration pass. Final
corrected backend restart, 302 continuous denial probes and the external
32-case security matrix pass. Remaining: packaged boot/OTA acceptance.
Preserve management containment and current public app routing.
- [ ] **Security:** verify final deployed/booted artifacts against public raw IP,
unknown Host/SNI, forged forwarding headers, IPv4/IPv6, assets/RPC/WS;
preserve private access, ACME issuance/renewal and public app TLS/WSS.
@@ -759,3 +761,26 @@ The release wrapper sets this path and the release harness includes the test.
Accepted companion54 APK/metadata replace the stale copies in the packaging
staging directory; exact SHA remains ceb58a7d…fab1a1. Final ISO and OTA package
acceptance/signatures remain pending.
### Final demo images and exact-node follow-up
Shorty final backend restart follow-up passes the external32-case management
denial matrix and trusted public TLS/WSS/official Angor browser fixture. Evidence:
`/tmp/archy-190-final-shorty-public-security-after-restart.log` and
`/tmp/archy-190-final-shorty-angor-browser.log`. This is the known recovered
project, not all35-project discovery. Signed catalog479f6193 is privately active
on dev, yaya and Shorty; no public catalog publication has occurred.
Built demo images pass isolated real-image qualification: optional upstream DNS
failure returns502 for that service while the main demo remains200; fresh AIUI
provenance, backend healthcheck, four upload protocol/recovery cases, and normal
mobile intro/login/dashboard pass. The test uses a temporary container-only DNS
file; host DNS and the live public demo are untouched. Test:
`tests/lifecycle/demo-images.py`; evidence:
`/tmp/archy-190-demo-images-acceptance-final-2.log`. Earlier failure in the added
DNS test was an incorrect assumption about Podman's generated resolver, repaired
by explicitly mounting the disposable resolver fixture.
Qualified web image169e59da is built from157c9ec0; backend2722fa29 frome6e46a14.
Public demo deployment remains scheduled after release with rollback. RC2 raw
ISO assembly is running; no boot/install or final OTA pass is claimed yet.
+71
View File
@@ -0,0 +1,71 @@
#!/usr/bin/env python3
"""Opt-in built-demo acceptance; disposable containers, no production data mounts."""
import pathlib,subprocess,tempfile,uuid,json,time,urllib.request,os
if os.environ.get('ARCHY_ALLOW_DISPOSABLE_CONTAINERS') != '1':
raise SystemExit('Set ARCHY_ALLOW_DISPOSABLE_CONTAINERS=1 for isolated demo tests')
root=pathlib.Path(__file__).resolve().parents[2]
web_image=os.environ['ARCHY_DEMO_WEB_IMAGE']
backend_image=os.environ['ARCHY_DEMO_BACKEND_IMAGE']
source_revision=os.environ['ARCHY_DEMO_SOURCE_REVISION']
assert len(source_revision)==40 and all(c in '0123456789abcdef' for c in source_revision)
uid=uuid.uuid4().hex[:10];net='archy-release-demo-'+uid;backend=net+'-backend';web=net+'-web'
def run(*args):return subprocess.check_output(args,text=True,stderr=subprocess.STDOUT,timeout=60).strip()
dns_fixture=tempfile.TemporaryDirectory(prefix='archy-demo-dns-')
resolv=pathlib.Path(dns_fixture.name)/'resolv.conf'
resolv.write_text('nameserver 127.0.0.1\n')
try:
run('podman','network','create',net)
run('podman','run','-d','--name',backend,'--network',net,'--network-alias','neode-backend','--memory','1g','-p','127.0.0.1::5959','-e','DEMO=1','-e','DEMO_FILE_QUOTA_BYTES=8388608',backend_image)
backend_config=json.loads(run('podman','inspect','--type','container',backend))[0]
backend_ip=next(iter(backend_config['NetworkSettings']['Networks'].values()))['IPAddress']
run('podman','run','-d','--name',web,'--dns','none','-v',str(resolv)+':/etc/resolv.conf:ro','--add-host','neode-backend:'+backend_ip,'-p','127.0.0.1::2101','--network',net,'--memory','256m','-p','127.0.0.1::80',web_image)
front='http://'+run('podman','port',web,'80/tcp');back='http://'+run('podman','port',backend,'5959/tcp')
end=time.monotonic()+120
while time.monotonic()<end:
try:
with urllib.request.urlopen(back+'/health',timeout=3) as r:assert r.status==200
with urllib.request.urlopen(front+'/',timeout=3) as r:assert r.status==200
break
except Exception:
if run('podman','inspect','--type','container','--format','{{.State.Status}}',web)=='exited':
print(run('podman','logs','--tail','12',web),flush=True)
raise RuntimeError('Demo web exited before readiness')
time.sleep(2)
else:raise RuntimeError('Demo image health timeout')
print('PASS actual demo web/backend images ready on private loopback ports',flush=True)
assert 'nameserver 127.0.0.1' in run('podman','exec',web,'cat','/etc/resolv.conf')
optional='http://'+run('podman','port',web,'2101/tcp')
try:
urllib.request.urlopen(optional,timeout=10)
raise AssertionError('Expected unavailable optional upstream with DNS disabled')
except urllib.error.HTTPError as error:assert error.code==502
with urllib.request.urlopen(front+'/',timeout=5) as response:assert response.status==200
print('PASS unavailable optional DNS returns502 while the demo stays200',flush=True)
with urllib.request.urlopen(front+'/aiui/BUILD-INFO',timeout=5) as r:assert r.read().decode().startswith('commit='+source_revision+'\n')
config=json.loads(run('podman','inspect','--type','container',backend))[0]
assert config['Config']['Healthcheck']['Test']
assert not config.get('Mounts'),'Demo must have no production socket/data mounts'
print('PASS current AIUI provenance, backend healthcheck and no host-data mounts',flush=True)
original=(root/'neode-ui/scripts/demo-upload-test.mjs').read_text()
original=original.replace("const root = new URL('../', import.meta.url)","const root = new URL("+json.dumps((root/'neode-ui').as_uri()+'/')+")")
start=original.index('before(async () => {');end=original.index('after(async () => {',start)
original=original[:start]+"before(async () => { origin = "+json.dumps(front)+" })\n"+original[end:]
original=original.replace('const response = await fetch(`${origin}/health`)', 'const response = await fetch('+json.dumps(back+'/health')+')')
# Keep this file beside the source so Node resolves the project's TypeScript.
test=root/'neode-ui/scripts'/('.demo-image-acceptance-'+uid+'.mjs')
test.write_text(original)
try:subprocess.run(['node','--test',str(test)],cwd=root,check=True,timeout=180)
finally:test.unlink(missing_ok=True)
print('PASS all production upload protocol cases through the built nginx image',flush=True)
(pathlib.Path('/tmp/archy-190-demo-image-ports.json')).write_text(json.dumps({'front':front,'back':back,'web':web,'backend':backend}))
# Browser smoke, using the real public-demo login page.
script=r'''
const {createRequire}=require('node:module');const req=createRequire(process.cwd()+'/neode-ui/package.json');const {chromium}=req('@playwright/test');
(async()=>{const browser=await chromium.launch({headless:true});try {const page=await browser.newPage({viewport:{width:390,height:844}});await page.goto(process.env.DEMO_IMAGE_URL,{waitUntil:'domcontentloaded'});await page.locator('.tap-to-start-content').click({timeout:30000});await page.getByRole('button',{name:'Skip Intro',exact:true}).click();await page.waitForURL('**/onboarding/intro',{timeout:45000});await page.screenshot({path:'/tmp/archy-190-demo-entry.png',fullPage:true});console.log('Demo entry route:',new URL(page.url()).pathname);await page.getByRole('button',{name:'Enter the demo →',exact:true}).click();const password=page.locator('input[type=password]').first();await password.waitFor({timeout:30000});await password.fill('entertoexit');await password.press('Enter');await page.getByText('My Apps',{exact:true}).first().waitFor({timeout:45000});if(await password.isVisible())throw Error('Demo login did not reach dashboard');await page.screenshot({path:'/tmp/archy-190-demo-mobile.png',fullPage:true});console.log('PASS built demo mobile login and dashboard');}finally{await browser.close()}})().catch(e=>{console.error(e);process.exit(1)});
'''
subprocess.run(['node','-e',script],cwd=root,env=os.environ|{'DEMO_IMAGE_URL':front},check=True,timeout=120)
finally:
for name in [web,backend]:subprocess.run(['podman','rm','-f',name],capture_output=True)
subprocess.run(['podman','network','rm',net],capture_output=True)
dns_fixture.cleanup()
print('Disposable demo containers and network removed',flush=True)