From cb410fbdeaa87528aaaab1f0dbffeb0240a9a32e Mon Sep 17 00:00:00 2001 From: leefer <2298888111@qq.com> Date: Fri, 23 Oct 2026 13:36:32 +0800 Subject: [PATCH] test: package HEL-806 evidence and verify genuine empty state Co-authored-by: multica-agent --- .../2026-10-23-hel806-same-state-audit.md | 2 +- e2e/hel806.cjs | 5 +- scripts/report-hel806.py | 48 ++++++++++++++----- 3 files changed, 41 insertions(+), 14 deletions(-) diff --git a/docs/changelogs/2026-10-23-hel806-same-state-audit.md b/docs/changelogs/2026-10-23-hel806-same-state-audit.md index ae20873..c0f75a9 100644 --- a/docs/changelogs/2026-10-23-hel806-same-state-audit.md +++ b/docs/changelogs/2026-10-23-hel806-same-state-audit.md @@ -6,7 +6,7 @@ The approved HTML remains read-only, SHA256 `0473d80d22b90194375f3566c1b8ca30dbd - `StudioDesk.svelte`, `Artboard.svelte`: replace the extra 12px gap with the original stage padding (12px 16px 8px, top 8px when height <=800), keep the 40px action row for empty as well as populated states, and clip media to its theme radius (8px night/day, 4px grade, 14px bluehour). Values have one source in tokens; no overlay stylesheet or duplicate component. - `e2e/hel806.cjs`: real disposable HTTP data, fake ComfyUI/model only, four themes × three viewports. Original reference, runtime content map and actual screenshots are distinct. Reference changes use existing templates/content/state; no reference CSS/layout changes. Missing empty states, non-storyboard stage content, real edit/compare/validation dialogs and organization-only pages are explicitly marked. The sample's static parameter list is not substituted for actual workflow declarations. - `e2e/hel806-toast.mjs`, `hel806-toast-browser.cjs`: deterministic expiry/pause/sticky/cleanup regression plus full/reduced motion, project footer clearance and real explicit confirmation. -- `scripts/report-hel806.py`: measured values, self-contained themed galleries and parallel images. Screenshot delivery uses lossless WebP; decoded RGB equality is checked against source PNGs. Manifests retain both hashes. Screenshot counts and DOM measurements are evidence, never automatic visual approval. +- `scripts/report-hel806.py`: measured values, self-contained themed galleries and parallel images. Screenshot delivery uses lossless WebP; decoded RGB equality is checked against source PNGs. Manifests retain both hashes. Identical bitmap bytes are deduplicated inside self-contained upload parts; media-files.json preserves the logical-name mapping, and every state stays with its three viewport sizes. Screenshot counts and DOM measurements are evidence, never automatic visual approval. Reproduce from a frozen build: install the repository's existing dependencies, run frontend lint/check/test/build, then set existing `PLAYWRIGHT_MODULE_PATH`, browser library path and `HEL732_PYTHON` for this machine. `HEL733_TESTS=e2e/hel732.cjs,e2e/hel729.cjs,e2e/hel742.cjs,e2e/hel733-live.cjs,e2e/hel732-visual.cjs,e2e/hel806.cjs,e2e/hel806-toast-browser.cjs HEL733_SKIP_SHOTS=1 python3 e2e/run-hel733.py`. The original HTML is downloaded through the CLI to `../materials` and the B4 reference copy to `../evidence/reference`. All required results are collected before exit; the runner closes its exact child processes and removes the disposable database. Run `node e2e/hel806-toast.mjs` separately. The report script reads `../evidence/hel806-final/results.json`. diff --git a/e2e/hel806.cjs b/e2e/hel806.cjs index 9b6504f..1bef294 100644 --- a/e2e/hel806.cjs +++ b/e2e/hel806.cjs @@ -37,6 +37,9 @@ const metrics=(p,sels)=>p.evaluate(sels=>Object.fromEntries(Object.entries(sels) if(process.env.HEL806_SEED_TASKS==='1'){ const seeded=[];for(const mode of ['ok','fail']){fs.writeFileSync('e2e/fake_mode.json',JSON.stringify({mode,delay_sec:0,unique_filename:true,image_file:path.resolve('calliope-web/static/refract/coast.jpg')}));const created=await api('/api/playground/generate',{workflow_id:f.dual,source:'studio-image',input_values:{'1':'HEL806 isolated '+mode,'2':'模糊','3':42}});let job;for(let i=0;i<200;i++){job=await api('/api/jobs/'+created.job.id);if(!['running','pending'].includes(job.status))break;await wait(150)}assert.equal(job.status,mode==='ok'?'done':'failed');seeded.push(job)}fs.writeFileSync(OUT+'/seed-jobs.json',JSON.stringify(seeded,null,2)); } + if(process.env.HEL806_REAL_EMPTY==='1'){ + assert.equal((await api('/api/jobs')).length,0,'empty-state run must start without jobs');const owned=await api('/api/projects');for(const project of owned){const r=await fetch(API+'/api/projects/'+project.id,{method:'DELETE'});assert(r.ok,'delete only disposable fixture project')};const after={projects:await api('/api/projects'),jobs:await api('/api/jobs')};assert.equal(after.projects.length,0);assert.equal(after.jobs.length,0);fs.writeFileSync(OUT+'/real-empty.json',JSON.stringify({removedFixtureIds:owned.map(p=>p.id),after},null,2)); + } const sample=http.createServer((req,res)=>{res.setHeader('Content-Type','text/html;charset=utf-8');res.end(fs.readFileSync('../materials/夜片场-多主题交互样本.html'))});await new Promise(r=>sample.listen(0,'127.0.0.1',r)); const browser=await chromium.launch({args:['--no-sandbox','--disable-dev-shm-usage']}); try{ @@ -55,7 +58,7 @@ const metrics=(p,sels)=>p.evaluate(sels=>Object.fromEntries(Object.entries(sels) {name:'video-output',ref:'create',run:()=>generate(videoWf.id,'video'),creative:true}, {name:'image-failed',ref:'create',run:async()=>{await creative(f.dual);await p.locator('#desk-prompt').fill('HEL806 fake failure');fs.writeFileSync('e2e/fake_mode.json',JSON.stringify({mode:'fail',delay_sec:0}));await p.locator('.gen').click();await p.locator('.stage-empty').filter({hasText:'失败'}).waitFor({timeout:45000})},creative:true}, {name:'studio-real',ref:'studio',run:()=>goto('/'),basis:'approved-cover-exception'}, - {name:'studio-empty',ref:'studio',run:()=>emptyRoute('/','**/api/projects',[]),missing:'原件没有无项目空态'}, + {name:'studio-empty',ref:'studio',run:async()=>{if(process.env.HEL806_REAL_EMPTY==='1')await goto('/');else{await p.route('**/api/jobs',r=>r.fulfill({json:[]}));await emptyRoute('/','**/api/projects',[])}},missing:'原件没有无项目空态'}, {name:'image-text-empty',ref:'create',run:async()=>{await p.route('**/api/playground/jobs',r=>r.fulfill({json:[]}));await creative(f.dual)},creative:true}, {name:'image-long-prompt',ref:'create',run:async()=>{await creative(f.dual);await p.locator('#desk-prompt').fill('海边女孩、晨光、纸船与灯塔,保持人物和空间连续。'.repeat(70));const btn=p.getByRole('button',{name:'展开提示词',exact:true});if(await btn.count())await btn.click()},creative:true}, {name:'image-edit-upload',ref:'create',run:async()=>{await creative(edit.id);await upload()},creative:true}, diff --git a/scripts/report-hel806.py b/scripts/report-hel806.py index aaf7163..dfdb603 100644 --- a/scripts/report-hel806.py +++ b/scripts/report-hel806.py @@ -44,18 +44,42 @@ with (REPORT/'key-values.csv').open('w') as f: values=lambda o:[round(o[k],4) for k in ['x','y','width','height']] av,bv=values(a),values(b);w.writerow([r['name'],r['theme'],f"{r['width']}x{r['height']}",key,av,bv,[round(x-y,4)for x,y in zip(av,bv)],r['basis']]) (REPORT/'capture-manifest.json').write_text(json.dumps({'source_png_to_lossless_webp':manifest,'states':len({r['name']for r in rows}),'groups':len(rows),'errors':data['errors'],'blocked_external_origins':sorted(set(data['blocked']))},indent=2)) -# A theme archive is self-contained and opens without a running server. +# Self-contained parts keep every state/three sizes together and each upload under 40 MB. +archives=[] for theme in ['night','day','grade','bluehour']: theme_rows=[r for r in rows if r['theme']==theme] - intro='HEL806 '+theme+'

HEL806 '+theme+'

原图只读。mapped只映射内容;missing-reference 表示原件缺此状态。并排图左为映射参考,右为隔离实图,不代表全页通过。原始数值在results.json。

' - chunks=[intro] - for r in theme_rows: - stem=f"{r['name']}-{theme}-{r['width']}" - chunks.append('
'+html.escape(stem+' · '+r['basis']+' · '+str(r.get('missing','')))+''+f'未改原图 · 内容映射参考 · 实图
') - with zipfile.ZipFile(REPORT/f'HEL806-{theme}.zip','w',zipfile.ZIP_DEFLATED,compresslevel=3) as z: - z.writestr('index.html','\n'.join(chunks));z.writestr('results.json',json.dumps(theme_rows,ensure_ascii=False,indent=2)) - for r in theme_rows: + b4files=[p for p in (ROOT/'e2e/shots/hel732').glob(f'*-{theme}-*.png') if p.name.startswith(('home-','task-'))] + b4size=sum(p.stat().st_size for p in b4files) + chunks=[];current=[];objects={};weight=b4size + def files_for(group): + return [OUT/'shots'/f"{r['name']}-{theme}-{r['width']}-{suffix}.webp" for r in group for suffix in ['original','mapped','actual','pair']] + for name in dict.fromkeys(r['name'] for r in theme_rows): + group=[r for r in theme_rows if r['name']==name] + candidates={hashlib.sha256(p.read_bytes()).hexdigest():p for p in files_for(group)} + extra=sum(p.stat().st_size for h,p in candidates.items() if h not in objects) + if current and weight+extra>40_000_000: + chunks.append(current);current=[];objects={};weight=0 + extra=sum(p.stat().st_size for p in candidates.values()) + current+=group;objects.update(candidates);weight+=extra + if current:chunks.append(current) + for part,part_rows in enumerate(chunks,1): + media={};objects={} + for p in files_for(part_rows): + h=hashlib.sha256(p.read_bytes()).hexdigest();media[p.name]='assets/'+h+'.webp';objects[h]=p + doc=['HEL806 '+theme+'

HEL806 '+theme+' / '+str(part)+'

原图只读;mapped只映射内容。missing-reference 是缺依据;并排左参考、右隔离实图,不代表全页通过。此分包可独立解压阅读。B4 gallery 位于本主题 part1 的 B4-index.html

'] + for r in part_rows: stem=f"{r['name']}-{theme}-{r['width']}" - for suffix in ['original','mapped','actual','pair']: - p=OUT/'shots'/f'{stem}-{suffix}.webp';z.write(p,'shots/'+p.name) -print(json.dumps({'groups':len(rows),'states':len({r['name']for r in rows}),'archives':{p.name:p.stat().st_size for p in REPORT.glob('*.zip')}},indent=2)) + link=lambda suffix:media[stem+'-'+suffix+'.webp'] + doc.append('
'+html.escape(stem+' · '+r['basis']+' · '+str(r.get('missing','')))+''+f'未改原图 · 内容映射参考 · 实图
') + target=REPORT/f'HEL806-{theme}-part{part}.zip' + with zipfile.ZipFile(target,'w',zipfile.ZIP_DEFLATED,compresslevel=3) as z: + z.writestr('index.html','\n'.join(doc));z.writestr('results.json',json.dumps(part_rows,ensure_ascii=False,indent=2));z.writestr('media-files.json',json.dumps(media,indent=2)) + for h,p in objects.items():z.write(p,'assets/'+h+'.webp') + if part==1: + b4doc=['

B4 '+theme+'

沿用已批准内容/hero映射。原PNG保持不变;不把获批例外扩大解释。

'] + for p in sorted(b4files):z.write(p,'b4/'+p.name);b4doc.append('

'+html.escape(p.name)+'

') + z.writestr('B4-index.html','\n'.join(b4doc)) + assert target.stat().st_size<45_000_000 + archives.append({'name':target.name,'bytes':target.stat().st_size,'groups':len(part_rows)}) +(REPORT/'archives.json').write_text(json.dumps(archives,indent=2)) +print(json.dumps({'groups':len(rows),'states':len({r['name']for r in rows}),'archives':archives},indent=2))