From 6a0394aa5713d0dd1e7d88f8496203362880b3a5 Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Fri, 21 Aug 2026 17:59:07 +0800 Subject: [PATCH] fix(web): update consult source contracts for the declared-window path BUG-341 added a third agentic consultation stream and renamed the homepage draft entrypoint, but the exact-count contracts still described two paths, so staging validate failed before publish. Co-authored-by: Cursor --- docs/BUG_HISTORY.md | 17 +++++++++++++++++ .../tests/application-billing-contract.test.ts | 13 ++++++------- .../tests/chat-navigation-a11y-contract.test.ts | 5 +++-- .../tests/composer-isolation-contract.test.ts | 2 +- .../tests/consultation-stream-recovery.test.ts | 15 +++++++++------ 5 files changed, 36 insertions(+), 16 deletions(-) diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 7a0edf12..f2325969 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -5162,3 +5162,20 @@ - 相关记录:BUG-009、BUG-073、BUG-202、BUG-273 - 复发自:BUG-202(服务端公共日盘已修好,首页无分钟入口后来又把 entrypoint 丢掉,普通 session 仍走整题拒绝;校正还被当成功能门) - 修复版本:9958e00a + +## BUG-342 | BUG-341 未更新咨询源码合同,staging quality gate 连续失败 + +- 状态:resolved +- 首次发现:2026-08-21 +- 最近更新:2026-08-21 +- 影响面:Gitea `backend-quality-gate` `validate`、`frontend/tests/application-billing-contract.test.ts`、`frontend/tests/consultation-stream-recovery.test.ts`、`frontend/tests/chat-navigation-a11y-contract.test.ts`、`frontend/tests/composer-isolation-contract.test.ts`;点选卡提交因此无法发布 +- 用户现象:向 `staging` 推送后质量门失败,镜像未发布,站点仍停在 `e7f4030e`。run `2026`(BUG-341)与 run `2027`(点选卡)均是 `validate` 失败、`publish` 跳过。前端测试摘要 `# tests 1871 / pass 1865 / fail 6`。 +- 触发条件:`npm test --prefix frontend`。BUG-341 增加声明窗口咨询路径并改了首页草稿变量后,精确计数合同仍按两条 agentic 路径与 `setDraftEntrypoint(entrypoint)` 断言。 +- 根因:咨询路由现有三条 agentic 首流(公共/百科、声明窗口、本命)及对应重试,`continueAfterDisconnect`、`abortSignal`、`settleRun` onError/onCancel 都变成 3/5。首页 `send()` 把草稿 entrypoint 收成 `consultEntrypoint`,且校正交接会在积分检查前清一次草稿。合同仍用 `send()` 里第一次 `setDraft("")` 当发送清草稿标记,切片失效。这是 BUG-308 同类:产品改了字面,源码合同没一起改。 +- 修复:把计量次数改成三条路径的实际次数;失败回填断言 `consultEntrypoint`;积分不足合同改为对照真正发送清草稿(`conversationAnchor.anchorToLatest()` 之后),不再误伤校正交接。 +- 验证:上述四份合同测试本地 35/35;此前失败的 6 项均通过。 +- 防复发:再增加咨询 agent 路径时必须同步 `usages.push`、`retryForAnswer`、`continueAfterDisconnect`、`abortSignal: agentAbortSignal` 与 `settleRun` onError/onCancel 的精确计数。切 `send()` 清草稿不得用文件里第一次 `setDraft("")`。失败回填的 entrypoint 变量名必须随 `send()` 一起改。 +- 相关记录:BUG-308、BUG-341 +- 复发自:BUG-308(源码合同锚点过期把无关提交打红);BUG-341(第三条咨询路径与草稿变量未改合同) +- 修复版本:pending + diff --git a/frontend/tests/application-billing-contract.test.ts b/frontend/tests/application-billing-contract.test.ts index ef4ebd1a..70b3c4f7 100644 --- a/frontend/tests/application-billing-contract.test.ts +++ b/frontend/tests/application-billing-contract.test.ts @@ -60,13 +60,12 @@ test("standard consultation awaits real usage before durable response settlement assert.match(consultRoute, /const resolved = await usage;/); assert.match(consultRoute, /const actualUsage = await usagePayload\(usage\);[\s\S]*p_actual_usage: actualUsage/); assert.match(consultRoute, /function mergeUsage\(usages: Promise\[\]\): Promise \{[\s\S]*Promise\.all\(usages\)/); - assert.equal(consultRoute.match(/usages\.push\(result\.totalUsage\)/g)?.length, 2); - // Two agentic paths with an answer retry each, plus a contract retry on the one - // path that requires a calculation: every one of those three model calls spends - // tokens and must be metered. The no-birth-time path has no contract left to - // repair now that its method is bound before the run starts. - assert.equal(consultRoute.match(/usages\.push\(retried\.totalUsage\)/g)?.length, 3); - assert.equal(consultRoute.match(/const retryForAnswer = async \(\) => \{/g)?.length, 2); + assert.equal(consultRoute.match(/usages\.push\(result\.totalUsage\)/g)?.length, 3); + // Three agentic first streams: public/general, declared-window, and natal. + // Window and natal each have a contract retry plus an answer retry; general + // has only an answer retry. Every one of those model calls spends tokens. + assert.equal(consultRoute.match(/usages\.push\(retried\.totalUsage\)/g)?.length, 5); + assert.equal(consultRoute.match(/const retryForAnswer = async \(\) => \{/g)?.length, 3); }); test("standard consultation forwards its stable reservation request as the usage event key", async () => { diff --git a/frontend/tests/chat-navigation-a11y-contract.test.ts b/frontend/tests/chat-navigation-a11y-contract.test.ts index 3cf5cdff..b731d7cb 100644 --- a/frontend/tests/chat-navigation-a11y-contract.test.ts +++ b/frontend/tests/chat-navigation-a11y-contract.test.ts @@ -49,8 +49,9 @@ test("both insufficient-credit paths are soft so the typed question is not throw // Given: the pre-flight guard runs before send() clears the composer draft. assert.match(creditGuard, /router\.push\(membershipHref\("insufficient-credits"\)\)/); assert.doesNotMatch(creditGuard, /window\.location/); - assert.ok(sendSource.indexOf('router.push(membershipHref("insufficient-credits"))') - < sendSource.indexOf(' setDraft("");')); + const creditPush = sendSource.indexOf('router.push(membershipHref("insufficient-credits"))'); + const sendClearsComposer = sendSource.indexOf("conversationAnchor.anchorToLatest();\n setDraft(\"\");"); + assert.ok(creditPush >= 0 && sendClearsComposer > creditPush); assert.equal(pageSource.match(/router\.push\(membershipHref\("insufficient-credits"\)\)/g)?.length, 2); }); diff --git a/frontend/tests/composer-isolation-contract.test.ts b/frontend/tests/composer-isolation-contract.test.ts index c067f45c..13faf4ea 100644 --- a/frontend/tests/composer-isolation-contract.test.ts +++ b/frontend/tests/composer-isolation-contract.test.ts @@ -101,7 +101,7 @@ test("every external draft writer keeps working through the page-owned setters", assert.match(saveOnboardingName, /setDraft\(""\)/); assert.match(stopRestore, /setDraft\(pending\.question\);\n\s*setDraftTheme\(pending\.theme\);\n\s*setDraftEntrypoint\(pending\.entrypoint\);/); assert.match(sendClear, /setDraft\(""\);\n\s*setDraftTheme\(null\);\n\s*setDraftEntrypoint\(null\);/); - assert.match(sendRestore, /setDraft\(originalQuestion\);\n\s*setDraftTheme\(theme\);\n\s*setDraftEntrypoint\(entrypoint\);/); + assert.match(sendRestore, /setDraft\(originalQuestion\);\n\s*setDraftTheme\(theme\);\n\s*setDraftEntrypoint\(consultEntrypoint\);/); assert.match(pageSource, /void send\(composerDraftSnapshot\(\), draftTheme\.current \?\? undefined, draftEntrypoint\.current\)/); // And: opening a rectification case still empties the composer it replaces. diff --git a/frontend/tests/consultation-stream-recovery.test.ts b/frontend/tests/consultation-stream-recovery.test.ts index 429e12e4..db12dd06 100644 --- a/frontend/tests/consultation-stream-recovery.test.ts +++ b/frontend/tests/consultation-stream-recovery.test.ts @@ -18,7 +18,7 @@ test("reserves usage and binds the owned consultation session atomically", () => }); test("persists transformed assistant metadata before atomically settling usage", () => { - assert.equal(consultRoute.match(/continueAfterDisconnect: true/g)?.length, 4); + assert.equal(consultRoute.match(/continueAfterDisconnect: true/g)?.length, 5); assert.equal(consultRoute.match(/onComplete: \(rawTransformedText\) => settle\(\(\) => completeResponse\(/g)?.length, 2); assert.match( consultRoute, @@ -40,11 +40,14 @@ test("persists partial transformed output when the upstream stream errors", () = 2, ); assert.equal(consultRoute.match(/const settleErrored = \(emitted: boolean, output: string\) => settle\(/g)?.length, 2); - assert.equal(consultRoute.match(/emitted[\s\S]*?\? \(\) => completeResponse\([\s\S]*?output,[\s\S]*?result\.totalUsage,[\s\S]*?: cancel,/g)?.length, 2); + assert.equal( + consultRoute.match(/emitted\s*\n\s*\? \(\) => completeResponse\(\n\s*output,\n\s*result\.totalUsage,/g)?.length, + 2, + ); assert.equal(consultRoute.match(/onCancel: \(\) => settle\(cancel\)/g)?.length, 2); assert.equal( consultRoute.match(/onCancel: \(\) => settleRun\(cancel, "cancelled"\)/g)?.length, - 2, + 3, ); }); @@ -58,14 +61,14 @@ test("Agentic failures always refund and detached execution uses a server-owned assert.match(consultRoute, /import \{\n AGENT_MAX_STEPS,\n AGENT_TIMEOUT_MS,[\s\S]*?\} from "@\/mastra\/consultation-tools";/); assert.doesNotMatch(consultRoute, /const AGENT_TIMEOUT_MS =/); assert.match(agentic, /const agentAbortSignal = AbortSignal\.timeout\(AGENT_TIMEOUT_MS\)/); - assert.equal(agentic.match(/abortSignal: agentAbortSignal/g)?.length, 2); + assert.equal(agentic.match(/abortSignal: agentAbortSignal/g)?.length, 3); assert.doesNotMatch(agentic, /abortSignal: request\.signal/); assert.equal( agentic.match(/onError: \(error\) => settleRun\(\s*cancel,[\s\S]*?toAgentObservabilityErrorCode\(error\),\s*\)/g)?.length, - 2, + 3, ); const onErrorBlocks = agentic.match(/onError:[\s\S]*?onCancel:/g) ?? []; - assert.equal(onErrorBlocks.length, 2); + assert.equal(onErrorBlocks.length, 3); for (const block of onErrorBlocks) assert.doesNotMatch(block, /completeResponse|completed_partial/); });