From 5cfd3d9bd396a205a7335f803a9753dbfaa33abd Mon Sep 17 00:00:00 2001 From: shunli Date: Mon, 10 Aug 2026 12:50:06 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D(08-10-session-fallback-failu?= =?UTF-8?q?re-classification):=20=E5=AE=9A=E6=80=A7=E5=B9=B6=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E4=BC=9A=E8=AF=9D=E5=A4=B9=E5=85=B7=E5=9B=9E=E8=90=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../spec/hyb-oa/frontend-react-vite-design.md | 82 ++++++++++++++++ .../check.jsonl | 1 + .../design.md | 76 +++++++++++++++ .../implement.jsonl | 1 + .../implement.md | 46 +++++++++ .../prd.md | 93 +++++++++++++++++++ .../research/execution-evidence.md | 74 +++++++++++++++ .../research/initial-evidence.md | 27 ++++++ .../task.json | 26 ++++++ client/tests/e2e/global-search.spec.ts | 2 + .../r103-cross-page-ui-consistency.spec.ts | 2 + 11 files changed, 430 insertions(+) create mode 100644 .trellis/tasks/08-10-session-fallback-failure-classification/check.jsonl create mode 100644 .trellis/tasks/08-10-session-fallback-failure-classification/design.md create mode 100644 .trellis/tasks/08-10-session-fallback-failure-classification/implement.jsonl create mode 100644 .trellis/tasks/08-10-session-fallback-failure-classification/implement.md create mode 100644 .trellis/tasks/08-10-session-fallback-failure-classification/prd.md create mode 100644 .trellis/tasks/08-10-session-fallback-failure-classification/research/execution-evidence.md create mode 100644 .trellis/tasks/08-10-session-fallback-failure-classification/research/initial-evidence.md create mode 100644 .trellis/tasks/08-10-session-fallback-failure-classification/task.json diff --git a/.trellis/spec/hyb-oa/frontend-react-vite-design.md b/.trellis/spec/hyb-oa/frontend-react-vite-design.md index 462c8a1b..e88f362f 100644 --- a/.trellis/spec/hyb-oa/frontend-react-vite-design.md +++ b/.trellis/spec/hyb-oa/frontend-react-vite-design.md @@ -105,6 +105,88 @@ and verification contracts. Do not copy broad aesthetic guidance from with `AUTH_005`. - Run `cd client && npm run typecheck && npm run build` for frontend changes. +## Scenario: Playwright API Mocks Against A Production PWA Build + +### 1. Scope / Trigger + +- Trigger: an E2E spec uses `page.route` or `browserContext.route` to mock + authentication or business APIs and may run against `client/dist`, Vite + preview or another production-built PWA runner. +- Purpose: keep the route fixture as the only network fact source after full + navigation, reload or repeated page bootstrap. + +### 2. Signatures + +- Spec-level context contract: `test.use({ serviceWorkers: "block" })`. +- Production-build witness: single worker, zero retries, same business + assertions and full-navigation sequence as the original spec. + +### 3. Contracts + +- A spec that depends on Playwright route mocks must block Service Workers when + it can run a PWA build. Do not rely on Service Worker installation or takeover + timing to keep requests visible to `page.route`. +- Vite dev success is not equivalent evidence: the generated PWA worker is a + production-build behavior and may be absent in dev. +- Blocking the worker is a fixture boundary, not an authentication bypass. The + spec must retain session hint, refresh, `/api/me`, full navigation and + business assertions. +- Tests whose purpose is offline/PWA behavior may allow Service Workers, but + they must not also treat page-level API mocks as a complete network witness. + +### 4. Validation & Error Matrix + +- Dev passes, production build stalls during session bootstrap -> inspect the + Service Worker policy before changing authentication code. +- `serviceWorkers=allow` and a mocked API request becomes incomplete after a + later navigation -> fixture/harness candidate; prove causality with the + single-variable blocked control. +- Blocked control still reproduces a real refresh failure -> do not classify as + fixture-only; continue with loopback real-backend session evidence. +- A fix deletes navigation/session assertions or adds retry -> invalid fix. + +### 5. Good/Base/Bad Cases + +- Good: a mock-heavy ledger spec blocks Service Workers and passes the same + repeated full-navigation sequence against the production build. +- Base: a focused PWA offline spec allows the worker and uses a network setup + designed for worker-owned requests. +- Bad: accept Vite dev green as proof that a production-built PWA runner has no + session problem, or modify `AuthContext` to accommodate a route-fixture race. + +### 6. Tests Required + +- Reproduce once against the production build with single worker and zero + retries; record only safe request path/status and cookie presence. +- Add the blocked-worker control without changing navigation or assertions, + then rerun the historically failing titles and the complete target spec. +- Run an independent frontend bootstrap refresh test and a loopback backend + refresh/session regression before declaring the issue fixture-only. + +### 7. Wrong vs Correct + +#### Wrong + +```ts +import { test } from "@playwright/test"; + +test.beforeEach(async ({ page }) => { + await page.route("**/api/**", mockApi); +}); +``` + +#### Correct + +```ts +import { test } from "@playwright/test"; + +test.use({ serviceWorkers: "block" }); + +test.beforeEach(async ({ page }) => { + await page.route("**/api/**", mockApi); +}); +``` + ## Scenario: UTC Date-Time Display In The Company Time Zone ### 1. Scope / Trigger diff --git a/.trellis/tasks/08-10-session-fallback-failure-classification/check.jsonl b/.trellis/tasks/08-10-session-fallback-failure-classification/check.jsonl new file mode 100644 index 00000000..9dd3234a --- /dev/null +++ b/.trellis/tasks/08-10-session-fallback-failure-classification/check.jsonl @@ -0,0 +1 @@ +{"_example": "Fill with {\"file\": \"\", \"reason\": \"\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."} diff --git a/.trellis/tasks/08-10-session-fallback-failure-classification/design.md b/.trellis/tasks/08-10-session-fallback-failure-classification/design.md new file mode 100644 index 00000000..7ad4c25d --- /dev/null +++ b/.trellis/tasks/08-10-session-fallback-failure-classification/design.md @@ -0,0 +1,76 @@ +# 会话回落失败定性:诊断设计 + +## 1. 问题模型 + +表面症状是业务页面断言超时,实际决策点在认证 bootstrap: + +```text +mock login 成功 + -> 内存 access token 允许首次 SPA 跳转 + -> 完整 page.goto / reload 重建 AuthProvider + -> 有 has_session hint ? + 否 -> 不请求 refresh -> 未认证 ready -> /login + 是 -> POST /api/auth/refresh -> GET /api/me -> 保持目标路由 +``` + +另一个独立真实路径是 Vite proxy 的 Host/Origin 一致性:即使 cookie 齐全,proxy 错误改写 Host 也可能让后端 refresh 返回 `AUTH_005`。因此最终 URL 不能区分“无 hint”“mock refresh 失败”“真实 refresh 被拒绝”。 + +## 2. 诊断观测合同 + +每个可复现 witness 只记录以下安全字段: + +- `testTitle`、`step`、`pathname`; +- cookie 名称与布尔存在性,不记录值; +- `login|refresh|me` 的调用计数、HTTP status、安全错误码; +- browser origin、proxy target 类别(loopback/非 loopback),不记录凭据或连接串; +- runner 参数、worker 数、是否原样/控制变量。 + +证据写入当前 task `research/` 或 `implement.md`,不复制 trace 中的 cookie、header、payload。原始 trace 仅留在本地临时测试输出,敏感检查前不得提交。 + +## 3. 实验顺序 + +### A. 原样基线 + +1. 用两份目标 spec、单 worker、无 retry 运行一次。 +2. 逐条对账历史 8 条;若同类失败存在,选择最短 global-search 二次 portal 用例作为主 witness,再以一个 R103 完整导航用例交叉验证。 +3. 若结果漂移,使用相同 runner 对目标标题重复执行;不更改代码来制造复现。 + +### B. Mock fixture 因果实验 + +1. 在 login 后、第一次完整导航前取 `context.cookies()` 的名称/布尔值。 +2. 记录完整导航后是否调用 refresh 和 me。 +3. 控制组显式安装与真实前端合同一致的 session hint,但保持业务 API mock、断言和导航不变。 +4. 若缺 hint -> 无 refresh -> `/login`,而只增加 hint 后 refresh/me -> 原路由,则形成 fixture 因果链。 + +### C. 真实前端与后端对照 + +1. 运行已有 session-hint bootstrap refresh Playwright 用例,验证 AuthContext 的受控路径。 +2. 运行后端 refresh rotation focused tests,验证服务端基本合同。 +3. 若前两项不足以排除集成问题,在 loopback/local-regression target gate 后,以真实本地 login cookie 做一次同源完整导航 witness;检查 Vite browser origin、request Host/Origin 与 refresh 状态。 + +## 4. 决策树 + +```text +原样失败可复现? + 否 -> 重复/逐条仍不能复现 -> INCONCLUSIVE / PAUSED + 是 -> fixture-only hint 控制是否消除? + 否 -> 检查 refresh/me ledger 与真实 session 对照 + 是 -> 真实 session 对照通过? + 是 -> FIXTURE + 否 -> MIXED + 真实同源 session 仍回落? + 是 -> PRODUCT(或与 fixture 缺口并存时 MIXED) +``` + +分类必须显式写出反证。例如 FIXTURE 结论同时说明:真实 session 对照未复现、后端 rotation 测试通过、runner 无 Host rewrite;不能只写“加 cookie 后绿了”。 + +## 5. 修改与停止边界 + +- Phase A/B 可添加最小 test-side ledger 或控制变量,不修改 `client/src/auth/**`、`client/src/api/http.ts`、Vite proxy 或 `server/src/routes/auth.js`。 +- 分类为 FIXTURE 后,如修法显然局部且不弱化断言,可在同任务追加 test-only 修复;先把分类证据写入任务,再改测试。 +- 分类为 PRODUCT/MIXED 时停止。后续任务必须覆盖认证代码、真实 PostgreSQL/session race、浏览器完整导航,并在实现后启动只读 reviewer。 +- 任何测试通过若来自删除 `page.goto`、删除登录态断言、增加 retry 或把 `/login` 视为正常,均为无效结果。 + +## 6. Rollback + +本任务的诊断探针和 fixture 控制均是本地测试侧改动。若未形成稳定证据,恢复探针但保留任务记录。任务不触碰生产、schema 或业务数据;真实本地会话 witness 仅在 target gate 后运行并按测试合同清理。 diff --git a/.trellis/tasks/08-10-session-fallback-failure-classification/implement.jsonl b/.trellis/tasks/08-10-session-fallback-failure-classification/implement.jsonl new file mode 100644 index 00000000..9dd3234a --- /dev/null +++ b/.trellis/tasks/08-10-session-fallback-failure-classification/implement.jsonl @@ -0,0 +1 @@ +{"_example": "Fill with {\"file\": \"\", \"reason\": \"\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."} diff --git a/.trellis/tasks/08-10-session-fallback-failure-classification/implement.md b/.trellis/tasks/08-10-session-fallback-failure-classification/implement.md new file mode 100644 index 00000000..07b805b8 --- /dev/null +++ b/.trellis/tasks/08-10-session-fallback-failure-classification/implement.md @@ -0,0 +1,46 @@ +# 会话回落失败定性:执行计划 + +## Phase A:原样复现与逐条账本 + +- [x] 记录 HEAD、工作区、Node/Playwright 版本、runner base origin 与 worker/retry 参数;不输出 secret 或完整连接信息。 +- [x] 原样运行两份目标 spec(单 worker、无 retry),逐条对账历史 8 条。 +- [x] 选择最短 global-search witness 与一个 R103 witness,必要时 `--repeat-each` 验证稳定性和顺序依赖。— Vite dev 历史 8 标题重复三次为 24/24;production preview 聚焦 R103 再次失败并保留 trace。 +- [x] 在修改任何 fixture 前冻结基线结论与失败阶段。— 见 `research/execution-evidence.md`。 + +## Phase B:三面证据与分类检查点 + +- [x] 以 test-side 安全 ledger 记录 cookie 名称/存在性、login/refresh/me 计数与状态、pathname 转移。— trace 白名单账本证明 hint 存在并触发 refresh,未记录值/header/body。 +- [x] 做单变量控制;不得改业务断言、导航次数或认证产品代码。— session hint 已由 trace 反证,改用 production build 唯一新增变量 Service Worker;仅设置 `serviceWorkers: "block"` 后历史 8 条为 8/8 PASS。 +- [x] 运行已有前端 bootstrap refresh 对照和后端 refresh rotation focused tests。— 前端 1/1、后端 route 30/30 PASS。 +- [x] 如仍不能排除真实集成问题,先确认 loopback/local-regression target,再运行最小真实本地 session 完整导航 witness。— 仓库现成 `auth-security-local` 强制 loopback/临时库,真实 PostgreSQL/HTTP 认证回归 9/9 PASS,refresh/reset race 通过,临时库已清理;前端完整导航由独立 bootstrap Playwright 覆盖。 +- [x] 按 PRD R3 写出 FIXTURE / PRODUCT / MIXED / INCONCLUSIVE-PAUSED 唯一分类、支持证据和反证。— 分类检查点为 `FIXTURE`,见执行证据。 + +## Phase C:分类后的分叉 + +- [x] FIXTURE:先写分类检查点;可选实施最小 test-only 修复并复跑 8 条,测试变绿仅作支持证据,不替代分类。— 检查点已先写入,再将两处 context 设置认定为永久 test-only 修复。 +- [ ] PRODUCT/MIXED:停止,不修改认证产品代码;提出独立认证缺陷 task、风险和 reviewer 要求。 +- [ ] INCONCLUSIVE/PAUSED:记录缺失证据和恢复条件,不把未复现写成 PASS。 +- [x] 所有分支均保留“不得以删除断言作为默认修法”,核对目标测试数和关键完整导航没有减少。— 两份 spec 仍为 15 tests;历史 8 标题及导航未删减,retry 仍为 0。 + +## Phase D:验证与收口 + +- [x] 若有 test code diff:`cd client && npm run typecheck`。— PASS。 +- [x] 两份目标 spec 和相关 auth 对照按最终分类复跑;不使用 retry 掩盖结果。— production-built preview 两份目标 15/15,前端 bootstrap 1/1,后端 route 30/30,真实本地 PostgreSQL/HTTP 9/9;均零 retry。 +- [x] task/process/governance/sensitive/`git diff --check`。— 全部 PASS;敏感扫描 0 finding。 +- [x] 评估是否需要更新前端/认证 spec;仅沉淀经实验确认的稳定合同。— 已在前端 spec 增加 production PWA build 下 route mock 必须阻断 Service Worker 的可执行合同、反例与测试要求。 +- [x] 中文 commit 引用 `08-10-session-fallback-failure-classification`;不推送、不部署。— 用户已确认 commit 计划。 + +## Risky Files / Stop Points + +- `client/tests/e2e/r103-cross-page-ui-consistency.spec.ts`、`global-search.spec.ts`:可以加安全 ledger/fixture 控制,但禁止删除业务断言或完整导航。 +- `client/src/auth/AuthContext.tsx`、`client/src/auth/authService.ts`、`client/src/api/http.ts`、`server/src/routes/auth.js`:本任务中只读;一旦需要修改即按 PRODUCT/MIXED 停止。 +- Vite proxy/config:只读核对;若发现 Host/Origin 漂移,不在本任务顺手修。 +- 本地真实 session witness:必须先通过 loopback/local-regression target gate;门禁失败即停止。 + +## Expected Evidence + +- 历史 8 条逐项映射表。 +- 至少一个失败的安全 request/cookie/navigation ledger。 +- session-hint 单变量控制结果。 +- 前端 bootstrap、后端 rotation、必要时真实本地集成对照。 +- 唯一分类与后续分叉,不以“8 条是否全绿”代替。 diff --git a/.trellis/tasks/08-10-session-fallback-failure-classification/prd.md b/.trellis/tasks/08-10-session-fallback-failure-classification/prd.md new file mode 100644 index 00000000..21b70e7d --- /dev/null +++ b/.trellis/tasks/08-10-session-fallback-failure-classification/prd.md @@ -0,0 +1,93 @@ +# 会话回落失败定性 + +## Goal + +在与 2026-08-07 历史记录一致的本地 Playwright runner 下,稳定复现并定性 8 条“完整导航或二次返回后落到登录页”的失败,给出可审计结论:测试 fixture/session mock 问题、真实会话行为问题、二者混合,或证据仍不足而保持 PAUSED。任务价值是消除唯一尚不明确且可能影响真实用户登录态的 Residual,不以让 8 条测试变绿作为终点。 + +## Confirmed Facts + +- 父任务 Residual Register(`.trellis/tasks/archive/2026-08/08-05-product-interaction-consistency/prd.md:76-81`)记录:`r103-cross-page-ui-consistency.spec.ts` 7 条、`global-search.spec.ts` 1 条失败,统一症状为多次完整导航或二次返回后落到登录页;现有证据不能排除真实会话续期行为。 +- 历史同 runner 命令为 `cd client && npx playwright test tests/e2e/procurement-flow.spec.ts tests/e2e/assets-flow.spec.ts tests/e2e/r103-cross-page-ui-consistency.spec.ts tests/e2e/pc-detail-mobile-visual-wave3.spec.ts tests/e2e/global-search.spec.ts --workers=1 --reporter=list`;其中目标两份 spec 当时分别为 `4 passed / 7 failed` 与 `3 passed / 1 failed`。 +- 两份目标 spec 都拦截 `/api/auth/login`、`/api/auth/refresh` 与 `/api/me`,因此它们自身不能直接证明真实后端 refresh rotation 正常或异常。 +- `AuthContext` 在完整页面重建时先检查可读的 `has_session=1`;无 session hint 时不会请求 refresh,而是按未认证状态完成 bootstrap。真实后端登录和 refresh 会同时写 refresh cookie 与 `has_session` cookie。 +- 现有前端规约还记录另一条真实会话风险:本地 Vite proxy 若把浏览器 Host 从 `127.0.0.1:5173` 改写成后端 Host,refresh 的 Origin/Referer 校验会返回 `AUTH_005`,完整刷新后丢失会话。因此诊断必须同时取证 cookie/hint、refresh 请求与 runner origin/host,不能只看最终 URL。 +- 仓库已有独立证据面:`login-portal-ux.spec.ts` 覆盖带 session hint 的 bootstrap refresh 重试;`auth.login.test.js` 覆盖后端 refresh rotation;它们可作为对照,但不能单独替代目标 runner 的复现。 + +## Scope + +- 两份目标 Playwright spec 的原样复现、逐条失败账本和安全诊断 ledger。 +- mock session fixture、真实前端 bootstrap、真实本地后端 session 三个证据面的对照实验。 +- 基于明确判据输出 FIXTURE、PRODUCT、MIXED 或 INCONCLUSIVE/PAUSED 分类。 +- 分类为 FIXTURE 后可选的最小 test-only 修复;任何认证产品改动、生产连接或部署均不在本任务范围。 + +## Requirements + +### R1 同 runner 稳定复现 + +- 先用历史命令的同一目标 spec、`--workers=1 --reporter=list` 复现;不得先改 fixture、断言、认证实现、Vite proxy 或 runner 配置。 +- 至少执行一次两份目标 spec 的原样组合;若结果不稳定,再对失败标题逐一、重复执行以区分顺序依赖、完整导航依赖和偶发 runner 状态。 +- 记录测试名、阶段、最终 pathname、cookie 名称/是否存在、是否发出 login/refresh/me 请求、响应状态与安全错误码。不得记录 cookie/token 值、密码、完整账号或原始认证 payload。 + +### R2 证据隔离与分类实验 + +- 建立三个彼此独立的证据面: + 1. **mock fixture 面**:目标 spec 的 login 响应后浏览器是否真的持有 `has_session`,完整导航时是否触发 mock refresh/me; + 2. **真实前端 bootstrap 面**:显式存在 session hint 时 AuthContext 是否按设计请求 refresh 并保持路由; + 3. **真实后端 session 面**:本地真实登录产生的 cookies、同源 refresh rotation 和完整导航是否成立,且 Vite proxy host/origin 符合规约。 +- 对照实验只允许使用 synthetic fixture 或经 target gate 确认为回环/本地回归库的账号与会话;禁止连接生产或复用生产凭据。 +- 为证明因果,可做最小的 test-side 控制变量(例如显式安装 session hint、记录安全 request ledger);在分类完成前不得修改产品认证代码。临时探针如不形成稳定回归价值,应在结论前移除。 + +### R3 分类判据 + +- **FIXTURE**:原样失败可复现;证据显示 mock 登录未建立 AuthContext 完整导航所需的浏览器 session contract,最小 fixture-only 控制变量消除同一失败;独立真实 bootstrap/backend 对照通过。 +- **PRODUCT**:使用真实本地 session contract、正确同源 runner 与无 mock 的 refresh/me 路径仍可复现相同会话回落,或真实认证对照出现同一根因。立即停止本任务内修复,登记独立认证缺陷任务;后续实现必须只读 reviewer。 +- **MIXED**:fixture 合同确有缺口,同时真实 session 对照也失败。分别登记测试债与认证缺陷,不在本任务合并修复。 +- **INCONCLUSIVE / PAUSED**:无法稳定复现、关键证据面不可用或 target gate 不通过。不得把未复现解释为 PASS,也不得猜测根因。 + +### R4 修复边界 + +- 任务终点是分类结论和证据链,不是测试全绿。 +- 若结论为 FIXTURE,可在分类检查点之后选择同任务内提交最小 test-only 修复,但它不是完成 AC 的必要条件;不得改变产品认证实现、弱化会话语义或扩大 mock 的默认放行。 +- 若结论为 PRODUCT 或 MIXED,必须在任何认证代码修改前停止并请求创建独立任务与实施批准。 +- **不得以删除断言作为默认修法。** 不得删除目标用例、移除完整导航、跳过 refresh/session 检查、增加 retry 掩盖失败,或把登录页当作可接受结果。 + +## Acceptance Criteria + +- [x] AC1:在历史同 runner 参数下记录两份目标 spec 的原样结果,并能把 8 条历史失败逐条映射为当前 PASS/FAIL/未复现,不用套件总数代替逐条证据。 +- [x] AC2:至少一个可复现失败拥有脱敏 request/cookie/navigation ledger,能回答 session hint 是否存在、refresh/me 是否发出、在哪一步进入 `/login`。 +- [x] AC3:完成 mock fixture、真实前端 bootstrap、真实本地后端 session 三个证据面的对照;任何未执行项均有明确阻塞原因,不以另一层 PASS 顶替。 +- [x] AC4:按 R3 输出且仅输出一个最终分类:FIXTURE、PRODUCT、MIXED 或 INCONCLUSIVE/PAUSED,并列出支持证据和反证。— 唯一分类 `FIXTURE`。 +- [x] AC5:若为 PRODUCT/MIXED,认证产品代码保持未修改并停在独立任务批准前;若为 FIXTURE 且同任务修复,diff 仅限 test fixture/harness,先有分类检查点,且不得删除或弱化断言。 +- [x] AC6:原样保留并落实“不得以删除断言作为默认修法”;目标测试数量、关键完整导航与会话语义无静默减少。 +- [x] AC7:所有自动化、typecheck(若 test code 有改动)、task/process/governance/sensitive 与 `git diff --check` 按范围通过;结果明确为本地诊断,未连接生产、未部署。 + +## Out of Scope + +- 未完成分类前修复 `AuthContext`、refresh rotation、cookie、Vite proxy 或后端认证路由。 +- 生产登录、生产 session 查询、生产配置、部署、公网 smoke 或真实用户账号操作。 +- 合同 UI 验收、员工负向验收、管理员手册附件承诺,以及其他 Playwright 失败。 +- 为追求套件全绿而重写 8 条业务断言或扩大通用 mock 成功面。 + +## Risks and Deferred Items + +- 当前源码证据更偏向 fixture/session hint 缺口,但这只是待验证假设,不是结论。 +- 真实本地 session 对照会创建/轮换本地测试会话;执行前必须 fail-closed 确认回环主机和本地回归库,结束后按既有测试清理合同处理。 +- 若最终需要认证产品修复,风险等级上升为生产登录态缺陷,必须新建独立任务、补真实数据库/浏览器回归并启动只读 reviewer。 +- 管理员手册的合同附件承诺应在首条真实合同出现前单独修订,不并入本任务。 + +## Test Plan + +- 原样复现:`cd client && npx playwright test tests/e2e/r103-cross-page-ui-consistency.spec.ts tests/e2e/global-search.spec.ts --workers=1 --reporter=list` +- 必要时按历史失败标题 `--grep` 单跑并 `--repeat-each` 验证稳定性;不以 retry 作为通过证据。 +- 前端对照:聚焦 `login-portal-ux.spec.ts` 中 bootstrap refresh 用例及新增/临时安全 ledger。 +- 后端对照:`cd server && npm test -- tests/unit/routes/auth.login.test.js`;若需真实本地 session,先运行本地 target gate,再执行最小浏览器/API witness。 +- 若修改测试:`cd client && npm run typecheck`,再运行两份目标 spec 与相关 auth 对照。 +- `python3 ./.trellis/scripts/task.py validate 08-10-session-fallback-failure-classification` +- `node scripts/qa/check-process-contract.mjs --baseline --json` +- `node scripts/qa/check-trellis-governance.mjs --json` +- `node scripts/qa/check-sensitive-content.mjs` +- `git diff --check` + +## Open Questions + +- 无。分类目标、四种停止状态、生产边界和修复分叉均已明确;技术未知由本任务实验回答。 diff --git a/.trellis/tasks/08-10-session-fallback-failure-classification/research/execution-evidence.md b/.trellis/tasks/08-10-session-fallback-failure-classification/research/execution-evidence.md new file mode 100644 index 00000000..83a1379f --- /dev/null +++ b/.trellis/tasks/08-10-session-fallback-failure-classification/research/execution-evidence.md @@ -0,0 +1,74 @@ +# 执行证据:生产构建 runner 会话回落 + +## 环境与原样基线 + +- 候选:`5518e974a2e85f0036b8f7bbf71f86de0370b6cb`;Node `v22.22.2`;Playwright `1.58.2`。 +- runner:`http://127.0.0.1:3000`,单 worker、零 retry。本地 Vite dev 两份目标 spec 为 `15 passed`,历史 8 个标题重复 3 次为 `24 passed`;该结果不等价于历史 `SERVE_CLIENT_DIST=true` 的生产构建 runner。 +- 对当前 `dist` 使用 Vite preview 复现生产构建/PWA 行为,两份目标 spec 为 `11 passed / 4 failed`。其中 3 条是历史会话回落标题,另 1 条是资产长表单 toast 遮挡点击,属于本任务外的新失败,不计入 8 条会话分类。 + +## 历史 8 条逐项映射(原样生产构建 runner) + +| 历史标题 | 当前结果 | 说明 | +| --- | --- | --- | +| 七个主要业务台账标题区保持一主操作加至多一个更多,列表工具不常驻 | FAIL | 后续完整导航时停在“正在加载会话...” | +| 采购页使用安全格式上传并通过认证 Blob 下载 | PASS | 本轮未复现历史回落 | +| 采购与资产列表、详情各自只请求职责内数据并恢复 URL 筛选 | PASS | 本轮未复现历史回落 | +| 采购与资产长表单详情对 403/404 使用同一安全错误态 | PASS | 本轮未复现历史回落 | +| 采购长表单切回记录列表时支持取消保持与确认放弃 | PASS | 本轮未复现历史回落 | +| 共享未保存保护覆盖浏览器返回取消、确认与刷新拦截 | FAIL | 接受 reload 后停在“正在加载会话...” | +| /projects /assets /reimbursements 在中等宽度下操作列可达且页面不横溢 | FAIL | 后续完整导航时停在“正在加载会话...” | +| E2E 全局搜索:采购与资产忽略旧 href 并使用实体 canonical 路径 | PASS | 第二次回到 portal 的历史回落本轮未复现 | + +## 聚焦 trace 安全账本 + +聚焦复跑“七个主要业务台账标题区”仍失败。原始 trace 只保留在 gitignored 测试输出中;下列账本仅从中提取白名单字段: + +- browser origin:loopback `http://127.0.0.1:3000`;context `serviceWorkers=allow`。 +- 导航序列:`/login` → `/purchase-requests` → `/contracts-registry` → `/projects` → `/assets` → `/payment-batches`。 +- login:`POST /api/auth/login -> 200`;随后 `GET /api/me -> 200`。 +- 前四次完整业务导航均发出 `POST /api/auth/refresh -> 200`,并各自完成 `GET /api/me -> 200`。 +- 最后一次完整导航仍发出 `POST /api/auth/refresh -> 200`,但其后的 `GET /api/me` 未完成;页面保持“正在加载会话...”,业务标题断言超时。 +- 因每次完整导航都触发 refresh,`has_session` 缺失假设被本 witness 反证;当前控制变量应转向生产构建才启用的 PWA Service Worker,而不是重复安装 session hint。 + +## 下一控制变量 + +两份目标 spec 当前允许 Service Worker,而仓库现有认证、权限、合同和关键流程 E2E 普遍显式使用 `serviceWorkers: "block"`。下一步仅在 test context 增加该设置,保持 mock、业务断言、导航次数和产品认证代码不变;若同一生产构建 runner 消除历史回落,再结合独立前端 bootstrap 与后端 rotation 对照完成分类。 + +## 单变量控制结果 + +- 仅为两份目标 spec 增加 `test.use({ serviceWorkers: "block" })`;未改 mock 响应、业务断言、完整导航、retry、PWA 产品实现或认证产品代码。 +- 同一 production-built preview、单 worker、零 retry 下,历史 8 个标题由原样基线的 3 FAIL / 5 PASS 变为 `8 passed`。 +- 该设置与这些 spec 的测试边界一致:用例以 `page.route` 合成认证和业务 API;PWA Service Worker 属于被测页面之外的网络代理,会让 mock 拦截在构建产物下随安装/接管时机变化。阻断它不是弱化会话语义,而是让 API mock 成为该测试唯一网络事实源。 + +## 独立认证对照 + +- 真实前端 bootstrap:`login-portal-ux.spec.ts` 的“Portal bootstrap refresh 首次瞬时失败后重试成功仍保留登录态”在同一 preview runner 下 `1 passed`。该用例显式安装 session hint,第一次 refresh 返回瞬时 500、后续重试成功,最终保持 `/portal`。 +- 后端 focused route:`auth.login.test.js` 为 `30 passed`,包含登录建立 session、refresh 前锁定并重查账号、密码变更/重置撤销 session 等合同。 +- 真实本地 PostgreSQL/HTTP:`run-web-gates.sh auth-security-local` 的 target gate 强制 loopback API、loopback PostgreSQL 与 `hyb_oa_auth_*` 临时库;实际自动避开已占用端口,应用 65 条迁移、写入合成种子并完成 `9/9` 认证安全回归,含 refresh/reset race,最终 `AUTH-REGRESSION PASS FULL`。cleanup 后同前缀临时数据库计数为 0。 +- 全过程未连接生产、未使用生产凭据、未部署。 + +## 分类检查点(先于永久修法) + +**唯一分类:FIXTURE。** + +支持证据: + +1. 原样 production-built/PWA runner 可重复复现同类会话 bootstrap 卡住;Vite dev 不复现,说明 runner 差异是必要条件。 +2. trace 反证 session hint 缺失:失败前每次完整导航都发出 refresh;最后一次 refresh 仍为 200,卡点是其后 `/api/me` 未完成。 +3. 单一 test-context 变量 `serviceWorkers: "block"` 在保持 8 条业务断言与完整导航不变时消除全部历史回落。 +4. 真实前端 bootstrap、后端 route 合同和 loopback 真实 PostgreSQL/HTTP 认证回归均通过,未出现同一会话回落根因。 + +反证与边界: + +- 本轮没有证据支持 PRODUCT:认证产品代码未修改,真实后端 refresh/reset 竞争通过。 +- 本轮没有证据支持 MIXED:fixture 控制消除历史 8 条失败,独立产品对照均通过。 +- 资产长表单 toast 遮挡是本轮新观察的非会话失败,不改变本分类,也不在本任务顺手修复。 + +基于该检查点,允许把两处 `serviceWorkers: "block"` 保留为最小 test-only 修复;不得删除断言、完整导航或增加 retry。 + +## 修法后的完整目标回归 + +- 同一 production-built preview、单 worker、零 retry 完整运行两份目标 spec:`15 passed / 0 failed / 0 skipped`。 +- 历史 8 条全部包含在该结果中;两份文件测试总数仍为 15,关键完整导航未减少。 +- 原样基线出现过的资产长表单 toast 遮挡在完整复跑中通过,按一次性非会话观察保留在基线说明中,不新登记本任务 residual。 +- 聚焦诊断 raw trace 已从本地 HTML report 数据目录删除;任务只保留本文件中的白名单账本。 diff --git a/.trellis/tasks/08-10-session-fallback-failure-classification/research/initial-evidence.md b/.trellis/tasks/08-10-session-fallback-failure-classification/research/initial-evidence.md new file mode 100644 index 00000000..4e03a7ec --- /dev/null +++ b/.trellis/tasks/08-10-session-fallback-failure-classification/research/initial-evidence.md @@ -0,0 +1,27 @@ +# 初始证据:8 条会话回落失败 + +## 历史事实 + +- Residual 来源:`08-05-product-interaction-consistency` 父任务 PRD 的 Residual Register。 +- 历史 runner:两份目标 spec 与其他三份回归同进程、`workers=1`、list reporter。 +- 历史结果:R103 `4 passed / 7 failed`;global-search `3 passed / 1 failed`。 +- 失败共同点:登录后发生完整 `page.goto`、reload 或第二次回到 portal,业务断言前进入 `/login`。 + +## 源码链路 + +1. 两份目标 spec 都 mock login/refresh/me;mock login 声明写 `has_session=1`。 +2. 登录后的 access token 只在 React AuthProvider 内存中;完整页面重建不会保留它。 +3. AuthProvider bootstrap 先检查 `document.cookie` 中的 `has_session=1`。无 hint 时不会请求 refresh,直接完成未认证状态。 +4. 真实后端 login/refresh 同时设置 refresh cookie 与 session hint;refresh 还要求 device fingerprint 与 Origin/Host 合同。 +5. 现有 `login-portal-ux` 用例在 bootstrap refresh 场景中显式用 init script 安装 session hint;这提示目标 fixture 的 cookie 是否真正进入 browser context 是首要待证假设,但尚不能据此下结论。 + +## 待验证假设 + +- H1(fixture):`route.fulfill` 声明的 `set-cookie` 没有在目标 runner 中形成完整导航可见的 session hint,导致 AuthContext 不发 refresh。 +- H2(fixture/order):catch-all route、重复完整导航或 runner 顺序使 mock refresh/me 某次未命中。 +- H3(product/integration):真实 local refresh 因 device fingerprint、Origin/Host 或 rotation 行为失败,mock 症状只是碰巧相同。 +- H4(mixed):目标 fixture 缺 hint,同时真实集成路径也存在 Host/refresh 问题。 + +## 规划结论 + +不能从源码静态检查直接选择 H1。必须先原样复现,再通过 cookie/request ledger、session-hint 单变量控制和独立真实 session 对照定性。 diff --git a/.trellis/tasks/08-10-session-fallback-failure-classification/task.json b/.trellis/tasks/08-10-session-fallback-failure-classification/task.json new file mode 100644 index 00000000..c02d4ab6 --- /dev/null +++ b/.trellis/tasks/08-10-session-fallback-failure-classification/task.json @@ -0,0 +1,26 @@ +{ + "id": "session-fallback-failure-classification", + "name": "session-fallback-failure-classification", + "title": "会话回落失败定性", + "description": "在历史同 runner 下复现并定性 8 条会话回落失败,区分 fixture/session mock 与真实会话行为;任务终点是分类结论,不在未定性时修改认证代码。", + "status": "in_progress", + "dev_type": null, + "scope": "client Playwright session fixture diagnostics, AuthContext/auth route read-only comparison, Trellis evidence only; no production", + "package": null, + "priority": "P2", + "creator": "shunli", + "assignee": "shunli", + "createdAt": "2026-08-10", + "completedAt": null, + "branch": null, + "base_branch": "main", + "worktree_path": null, + "commit": null, + "pr_url": null, + "subtasks": [], + "children": [], + "parent": null, + "relatedFiles": [], + "notes": "继承 08-05-product-interaction-consistency Residual Register;不得以删除断言作为默认修法。", + "meta": {} +} \ No newline at end of file diff --git a/client/tests/e2e/global-search.spec.ts b/client/tests/e2e/global-search.spec.ts index 37ff7b53..681d820a 100644 --- a/client/tests/e2e/global-search.spec.ts +++ b/client/tests/e2e/global-search.spec.ts @@ -2,6 +2,8 @@ import { test, expect, type Page } from "@playwright/test"; import { ADMIN, login } from "./helpers/auth"; +test.use({ serviceWorkers: "block" }); + function envelope(data: T) { return { requestId: "mock-request", diff --git a/client/tests/e2e/r103-cross-page-ui-consistency.spec.ts b/client/tests/e2e/r103-cross-page-ui-consistency.spec.ts index 55b543ad..f1bcb8dc 100644 --- a/client/tests/e2e/r103-cross-page-ui-consistency.spec.ts +++ b/client/tests/e2e/r103-cross-page-ui-consistency.spec.ts @@ -2,6 +2,8 @@ import { mkdirSync } from "node:fs"; import path from "node:path"; import { expect, test, type Page, type Route } from "@playwright/test"; +test.use({ serviceWorkers: "block" }); + function envelope(data: unknown) { return { requestId: "mock-r103-ui-consistency", data }; }