"""Build the portable report from completed test evidence, without credentials.""" import html import json import re import zipfile from pathlib import Path import markdown from markdown.extensions.toc import slugify_unicode ROOT = Path(__file__).resolve().parents[1] REPORT = ROOT / 'reports/review-20260905' def main(): live = json.loads((REPORT / 'live/results.json').read_text(encoding='utf-8'))['stats'] front = json.loads((REPORT / 'frontend-e2e/results.json').read_text(encoding='utf-8'))['stats'] cleanup = json.loads((REPORT / 'cleanup.json').read_text(encoding='utf-8')) if any(value['unexpected'] or value['skipped'] or value['flaky'] for value in (live, front)): raise RuntimeError('Final browser evidence is not fully passing') if not all(cleanup['originalRowsUnchanged'].values()) or cleanup['remainingActiveTestUsers']: raise RuntimeError('Cleanup evidence is not passing') source = (REPORT / 'review-report.md').read_text(encoding='utf-8') source = source.replace('状态:最终浏览器复核中', '状态:审查与验证完成,4 项功能差异和 1 项素材选择待确认') summary = f'''| 验证 | 最终结果 | 证据与范围 | |---|---|---| | 后端全量测试 | 402/402 通过,0 失败、错误、跳过 | Gateway 1、Auth 86、Tran 315;含新增迟到事件回归 | | 后端构建 | 通过 | Java 17 / Maven;三个服务打包完成 | | 前端 Node 回归 | 25/25 通过 | 原 24 项加视频资产、候选与抓拍合同 | | 前端构建 | 通过 | vue-tsc、Vite、旧模型 iframe 校验 | | 真实服务 E2E(Edge) | {live['expected']}/4 通过,0 失败/跳过/重试 | {live['duration']/1000:.1f} 秒;开始时间 {live['startTime']}(UTC) | | 浏览器模拟接口回归 | {front['expected']}/15 通过,0 失败/跳过/重试 | {front['duration']/1000:.1f} 秒;编辑保存、状态组合、军工绿/暗色主题 | | 数据库迁移 | 通过 | 033、035;首次、重跑和指定中断点恢复;开发库检查值均为 0 | | 测试脚本类型检查、Git 空白检查 | 通过 | 两份专用 Playwright 测试与配置;API/Web/E2E 仓库 | 最终通过结果来自各自一次完整运行,未启用自动重试。补入三次完整场景加载与截图后,真实虚拟用例曾超过原 120 秒测试总预算,断言和评定已完成但截图收尾超时;保留 [该轮记录](diagnostics/live-resource-wait-timeout.json),随后将该用例预算调到 240 秒,并在其他浏览器回归结束后重新完整运行。单次资源就绪仍有 60 秒上限。 实装视频新增检查还发现本机 Playwright 自带 Chromium 不支持 H.264(`canPlayType` 返回空字符串);[失败轮次](diagnostics/live-chromium-h264-unsupported.json)与[Edge 播放探针](diagnostics/video-probe-msedge.json)保留。最终真实 E2E 使用已安装 Microsoft Edge,模拟接口回归继续使用 Chromium。另独立修复了实际素材与候选配置不匹配的 WEB-06;最终用例验证默认视频可播、片长覆盖末尾候选,并在结尾得到 12 条候选。测试开发阶段的权限夹具、请求拦截范围与 UI 选择器错误已修正,不计为产品缺陷。''' source = source.replace('', summary) cleared = ', '.join(f"{table.removeprefix('ut_teaching_')} {count}" for table, count in cleanup['deleted'].items() if count) source = source.replace('', f"最终清理时间:`{cleanup['time']}`。删除本轮临时任务及关联记录({cleared}),测试账号已停用/软删除,会话已撤销,临时管理员密码文件已移除。四张原始教学表的既有行逐行对照均未变化,剩余活动测试账号为 0。审计与软删除记录按系统机制保留。见 [最终清理记录](cleanup.json);较早轮次见 `diagnostics/cleanup-*.json`。") titles = { '01-login-three-roles': '真实服务:登录仅展示教员、学员、管理员', '02-virtual-formal-task-center': '真实服务:正式虚拟任务中心', '03-virtual-1920': '真实服务:1920 宽度任务中心', '04-teacher-content': '真实服务:教员训练编排列表已加载', '05-virtual-started-no-exam-overlay': '真实服务:开始训练、模型就绪,无错误考核遮罩', '06-virtual-all-steps-completed': '真实服务:8/8 步骤完成,真实模型已加载', '07-virtual-reviewed': '真实服务:提交评定后刷新', '08-physical-api-task-list': '真实服务:实装任务清单', '09-physical-offline-replay-boundary': '真实页面:离线视频就绪,并标明不推进正式进度、不计分', '10-physical-twelve-candidates': '真实页面:匹配视频到达末尾,12 条离线候选可达、等待复核', '01-saved-step-move': '模拟接口:移动已保存步骤后保持正确顺序', '01-saved-step-delete': '模拟接口:删除步骤后末步进度 100%', '01-saved-step-duplicate': '模拟接口:复制步骤后重新编号', '02-multichannel-virtual-save': '模拟接口:虚拟渠道配置保存与重开', '03-multichannel-confrontation-save': '模拟接口:对抗渠道配置保存与重开', '05-failed-before-retry': '模拟接口:失败步骤出现重试入口', '06-failed-after-retry': '模拟接口:重试后尝试 2 次、失败 1 次', '07-no-retry-SUBMITTED': '模拟接口:已提交不出现重试按钮', '07-no-retry-TERMINATED': '模拟接口:已终止不出现重试按钮', '08-visual-safety-military': '模拟接口:军工绿主题安全区,对比度 12.00', '08-visual-safety-dark': '模拟接口:暗色主题安全区,对比度 15.30', } gallery = [] count = 0 for folder, caption in [('live', '真实服务与页面'), ('frontend-e2e', '模拟接口浏览器回归')]: gallery.append(f'