diff --git a/.github/workflows/ui-browser.yml b/.github/workflows/ui-browser.yml index f459c9d4..a0615d54 100644 --- a/.github/workflows/ui-browser.yml +++ b/.github/workflows/ui-browser.yml @@ -42,3 +42,13 @@ jobs: run: npm run test:web - name: Run blocking Chromium regression run: npm run test:browser + + - name: Upload Playwright failure artifacts + if: failure() + uses: actions/upload-artifact@v4 + with: + name: playwright-test-results + path: e2e/node_modules/.cache/playwright-test-results/ + if-no-files-found: warn + retention-days: 7 + include-hidden-files: true diff --git a/AGENTS.md b/AGENTS.md index f01a512f..4f66cd7d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ 过程中请使用中文和我沟通,但 console 里请优先使用英文。 -ui-evidence-exempt: 2026-10-08 PWRS_REPORTER_TOKEN 缺失且自建 runner 的 PWRS 访问与实际发布链路尚未验证 (#199) +浏览器验证政策:真实 Chromium 回归保持阻断;本开源仓不接维护者私有 PWRS 服务,也不要求 PWRS_REPORTER_TOKEN;失败证据由本仓 Browser regression workflow 上传 GitHub Actions artifact。 # Repository Guidelines ## Project Structure & Module Organization diff --git a/docs/sessions/261017-issue-cleanup/reviews/C-browser-verdict.md b/docs/sessions/261017-issue-cleanup/reviews/C-browser-verdict.md new file mode 100644 index 00000000..560a0ed8 --- /dev/null +++ b/docs/sessions/261017-issue-cleanup/reviews/C-browser-verdict.md @@ -0,0 +1,43 @@ + + +## [codex] 后续裁决 + +两条 P2 事实成立,但按校准后的验收契约接受不修、不阻断交付。第一条是末尾 console/network 汇总在业务断言通过后运行;更早的断言失败由原断言消息和 Playwright trace 取证,本次不承诺每个失败都输出末尾汇总。第二条是 job 超时、手动取消或 runner 终止不在“测试失败后上传 artifact”的承诺内。`failure-visibility: p2-only` 保持不变。 + +实证:[workflow run 39003528268](https://github.com/zlxlabs/VideoTranscriptAPI/actions/runs/39003528268) 以预期的 Chromium 404 失败结束,artifact `playwright-test-results`(ID `12090260702`)已实际下载。下载的 trace 在 Trace Viewer 显示失败测试动作及 `http://127.0.0.1:41277/__e2e__/diagnostic-404.png` 的 GET 404。咨询来源:Claude Opus,task `vta-261017-browser-review-scope`;采纳两条 P2 不阻断的结论,不采纳历史取消时序发生在 setup 阶段的猜测。 + +## Verdict + +审查结论:P2-only;阻断交付:是。真实 Chromium 门禁、正常失败路径的 trace 与截图保存、artifact 路径及长期政策均成立;不变式 1 的错误汇总会被前置断言短路,不变式 2 的 workflow 取消路径不会上传产物。 + +failure-visibility: p2-only + +## Findings + +### P2 — 前置断言失败时不会输出 console 与网络错误汇总(不变式 1) + +- 位置:[e2e/tests/local-upload-full-chain.spec.ts:229](../../../../e2e/tests/local-upload-full-chain.spec.ts#L229)、[e2e/tests/local-upload-full-chain.spec.ts:401](../../../../e2e/tests/local-upload-full-chain.spec.ts#L401)。 +- 触发:让 `POST /api/uploads` 返回 HTTP 500;第 229 行的 202 断言先抛错,末尾第 401–411 行构造并断言 `failureReport` 的代码不会运行。 +- 风险:该次 Chromium 回归仍失败,trace artifact 仍可提供底层事件,但 Actions 测试失败输出不会同时列出 console 与 network 两类错误,违反不变式 1 的报告要求。 +- 阻断理由:这是明确验收项;P2 来自诊断信息缺失,测试本身仍失败、没有静默放行。 + +### P2 — Job 到时限取消时 failure-only 上传步骤不会执行(不变式 2) + +- 位置:[.github/workflows/ui-browser.yml:14](../../../../.github/workflows/ui-browser.yml#L14)、[.github/workflows/ui-browser.yml:47](../../../../.github/workflows/ui-browser.yml#L47)。 +- 触发:job 的 15 分钟上限在 Chromium 回归仍运行时到期。真实 runner run `38715807063` 从 job 开始到 cancelled 约 15 分 25 秒,Chromium step 在结束前 16 秒才启动并被取消;当前固定 diff 保留同一个 job timeout,并新增 `if: failure()` 上传条件。 +- 证据与机制:GitHub 对 job timeout 的定义是自动取消;取消时会重评尚未执行的 step 条件。`failure()` 仅表示之前的 step 失败,而取消由 `cancelled()` 表示,因此该上传 step 不会因取消而执行。[GitHub workflow 条件](https://docs.github.com/en/actions/reference/workflows-and-actions/expressions)、[timeout-minutes](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax)、[取消流程](https://docs.github.com/en/enterprise-cloud@latest/actions/reference/workflows-and-actions/workflow-cancellation)。 +- 风险:job 以 cancelled 结束、门禁不会变绿,但该次 Chromium 现场的 trace、截图没有可下载 artifact。实际自托管 runner 已出现该时限形态;严重度 P2(证据丢失,不是静默放行),按不变式 2 阻断交付。 + +## 不变式核对 + +- 不变式 1:保留原业务断言;console、page error、请求失败与 HTTP 错误仍被观察,console 行带 location URL。前置断言绕过统一汇总见上述 finding。 +- 不变式 2:Playwright `outputDir` 与 workflow 上传路径均为 `e2e/node_modules/.cache/playwright-test-results`;失败截图、失败 trace、隐藏目录与 7 天保留均有配置。普通 step failure 会上传;job timeout/cancellation 会跳过当前 `failure()` 条件,见第二条 finding。官方配置说明支持全局 `use` 作为项目共享选项,trace 模式保留失败运行:[Playwright 配置](https://playwright.dev/docs/test-use-options)、[Playwright TestConfig](https://playwright.dev/docs/api/class-testconfig)。 +- 不变式 3:AGENTS.md 与浏览器测试文档声明本仓长期使用原生 Actions artifact,不接维护者私有 PWRS、不要求 token;真实 Chromium workflow 仍是阻断步骤。`include-hidden-files` 与 retention 输入符合上传 action 契约:[upload-artifact](https://github.com/actions/upload-artifact/blob/main/README.md)。 +- 不变式 4:没有新增重试、服务或宽泛上传路径;`retries: 0` 保留,上传仅指向 Playwright 的实际 `outputDir`。 + +## 验证与基线 + +- OCR 前置扫描:`status=reviewed`。工具标注 cancellation 风险为 High,本仓按真实用法重定为 P2:近期 50 次 workflow 中的取消 run `38715807063` 全 job 用时约 15 分 25 秒,且 Chromium step 正在运行,吻合 15 分钟 job 上限;该路径真实存在。若触发,门禁保持 cancelled/非绿,但产物缺失只影响诊断,故不是 P1。精确 H0 run `39002966220` 的 Chromium job 成功,运行约 3 分 51 秒。Playwright 的 180 秒单测超时覆盖普通用例挂起,但不能覆盖本次实测的 setup 耗时挤占整个 job 预算。 +- OCR 对“上传路径重复”意见被当前 config、npm 调用路径及精确 H0 成功运行证伪;设备字段顺序意见无当前行为证据,均未登记为 finding。 +- 任务卡所给主干基线不可用,故继承红未能判定;本次没有运行测试或触发新 CI,因此没有本卡新红。已读取精确 H0 的既有 workflow run:Vitest 与 Chromium 步骤成功,artifact 步骤按成功路径跳过。另将旧 run `38715807063` 的取消记录作为当前自托管 runner 的环境证据,不把它归为本卡新红。 +- 窄验证 `git diff --check` 通过;本 verdict 是本分支唯一新增文件。 diff --git a/docs/testing/local-upload-browser.md b/docs/testing/local-upload-browser.md index 09e74081..c06e0859 100644 --- a/docs/testing/local-upload-browser.md +++ b/docs/testing/local-upload-browser.md @@ -16,10 +16,12 @@ `/view/{token}?raw=...` 与 `?page=...` 是阅读页导出目的地,主路径测试通过真实链接点击断言。`/robots.txt`、`/sitemap.xml`、`/sw.js`、manifest 与 API 是资源/接口,不是额外页面;profile 路由是 API JSON,不是 UI。浏览器测试收集 console error、page error、request failed 及 HTTP 4xx/5xx,任一均使 case 失败。 +普通断言失败或 Playwright 用例超时、且 Chromium step 正常以失败结束时,failure-only 上传步骤会运行并上传 Playwright `outputDir`。末尾 console/network 汇总仅在业务断言通过后运行;更早失败时应结合原断言消息与 trace 取证。job 超时、手动取消或 runner 终止时不承诺执行 artifact 上传。 + ## 运行及 CI 首次运行:`uv sync`、根目录 `npm ci`、`npm --prefix e2e ci`,并安装 Chromium:`cd e2e && npx playwright install chromium`。之后 `npm run test:browser`;保留的 Vitest 契约层单独以 `npm run test:web` 运行。缺 Chromium、服务无法启动或断言不符均非零;无 skip/retry。单一 spec、`fullyParallel: false`、`workers: 1` 避免共享 fixture 并发污染。 `.github/workflows/ui-browser.yml` 对所有 PR `opened`、`synchronize`、`reopened`、`ready_for_review`、`converted_to_draft` 事件在 `[self-hosted, ci]` 执行 Chromium 与 Vitest;也可 `workflow_dispatch`。本仓没有 push workflow。`.github/workflows/gate.yml` 的 caller 已设置 `has_ui: true`;引用的 `zlxlabs/gate/.github/workflows/gate-v2.yml@v2` 在实际 tag 的 `workflow_call.inputs` 声明 `has_ui: boolean`(tag `v2` 当前解析为 commit `805856b9c6a501145521e8cae06000dc669af8c6`,已通过 GitHub API 读取实际 workflow_call schema 确认)。gate-hub 台账仍待 #1393,不能据本仓 caller 推断登记已生效。 -录屏证据不在本阻断 workflow。因无 `PWRS_REPORTER_TOKEN` 且 runner 发布通路未经验证,未接假 workflow;`AGENTS.md` 中有效期 90 天的 `ui-evidence-exempt:` 对应本仓接入待办 #199。该豁免只针对录屏,不豁免真实浏览器行为门禁。 +真实 Chromium 回归保持阻断。本开源仓不接维护者私有 PWRS 服务,也不要求 `PWRS_REPORTER_TOKEN`;失败时由 Browser regression workflow 上传 Playwright `outputDir`,包含失败 trace 与截图,artifact 保留 7 天。该政策取代 #199 的限时豁免和待接 PWRS 待办。 diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts index 5c998344..790a360c 100644 --- a/e2e/playwright.config.ts +++ b/e2e/playwright.config.ts @@ -16,6 +16,8 @@ export default defineConfig({ use: { baseURL, browserName: 'chromium', + screenshot: 'only-on-failure', + trace: 'retain-on-failure', ...devices['Desktop Chrome'], }, projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }], diff --git a/e2e/tests/local-upload-full-chain.spec.ts b/e2e/tests/local-upload-full-chain.spec.ts index 33a77d5c..4df6367d 100644 --- a/e2e/tests/local-upload-full-chain.spec.ts +++ b/e2e/tests/local-upload-full-chain.spec.ts @@ -176,9 +176,12 @@ test('Chromium uploads raw media through production API, worker, shared ASR/LLM, const browserErrors: string[] = []; const unexpectedNetwork: string[] = []; let observedResponseCount = 0; - page.on('pageerror', (error) => browserErrors.push(error.message)); + page.on('pageerror', (error) => browserErrors.push(`pageerror: ${error.message}`)); page.on('console', (message) => { - if (message.type() === 'error') browserErrors.push(message.text()); + if (message.type() === 'error') { + const url = message.location().url || '(unknown URL)'; + browserErrors.push(`console: ${message.text()} at ${url}`); + } }); page.on('requestfailed', (failed) => { const path = new URL(failed.url()).pathname; @@ -389,13 +392,21 @@ test('Chromium uploads raw media through production API, worker, shared ASR/LLM, const revoked = (await inspectRealState(request)).uploads[0]; expect(revoked.revoked_at).toBeTruthy(); expect(revoked.root_status).toBe('success'); - expect(browserErrors).toEqual([]); const expectedPageAborts = [ 'GET /api/audit/filter-options net::ERR_ABORTED', 'GET /api/audit/history net::ERR_ABORTED', ]; expect(observedResponseCount).toBeGreaterThan(0); + const unexpectedNetworkErrors = unexpectedNetwork.filter((entry) => !expectedPageAborts.includes(entry)); + const failureReport = [ + 'Browser failure diagnostics', + 'Console errors:', + ...(browserErrors.length ? browserErrors.map((entry) => ` ${entry}`) : [' none']), + 'Unexpected network errors:', + ...(unexpectedNetworkErrors.length ? unexpectedNetworkErrors.map((entry) => ` ${entry}`) : [' none']), + ].join('\n'); expect( - unexpectedNetwork.filter((entry) => !expectedPageAborts.includes(entry)), - ).toEqual([]); + { consoleErrors: browserErrors, networkErrors: unexpectedNetworkErrors }, + failureReport, + ).toEqual({ consoleErrors: [], networkErrors: [] }); });