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 <cursoragent@cursor.com>
This commit is contained in:
@@ -5162,3 +5162,20 @@
|
|||||||
- 相关记录:BUG-009、BUG-073、BUG-202、BUG-273
|
- 相关记录:BUG-009、BUG-073、BUG-202、BUG-273
|
||||||
- 复发自:BUG-202(服务端公共日盘已修好,首页无分钟入口后来又把 entrypoint 丢掉,普通 session 仍走整题拒绝;校正还被当成功能门)
|
- 复发自:BUG-202(服务端公共日盘已修好,首页无分钟入口后来又把 entrypoint 丢掉,普通 session 仍走整题拒绝;校正还被当成功能门)
|
||||||
- 修复版本:9958e00a
|
- 修复版本: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
|
||||||
|
|
||||||
|
|||||||
@@ -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 resolved = await usage;/);
|
||||||
assert.match(consultRoute, /const actualUsage = await usagePayload\(usage\);[\s\S]*p_actual_usage: actualUsage/);
|
assert.match(consultRoute, /const actualUsage = await usagePayload\(usage\);[\s\S]*p_actual_usage: actualUsage/);
|
||||||
assert.match(consultRoute, /function mergeUsage\(usages: Promise<Usage>\[\]\): Promise<Usage> \{[\s\S]*Promise\.all\(usages\)/);
|
assert.match(consultRoute, /function mergeUsage\(usages: Promise<Usage>\[\]\): Promise<Usage> \{[\s\S]*Promise\.all\(usages\)/);
|
||||||
assert.equal(consultRoute.match(/usages\.push\(result\.totalUsage\)/g)?.length, 2);
|
assert.equal(consultRoute.match(/usages\.push\(result\.totalUsage\)/g)?.length, 3);
|
||||||
// Two agentic paths with an answer retry each, plus a contract retry on the one
|
// Three agentic first streams: public/general, declared-window, and natal.
|
||||||
// path that requires a calculation: every one of those three model calls spends
|
// Window and natal each have a contract retry plus an answer retry; general
|
||||||
// tokens and must be metered. The no-birth-time path has no contract left to
|
// has only an answer retry. Every one of those model calls spends tokens.
|
||||||
// repair now that its method is bound before the run starts.
|
assert.equal(consultRoute.match(/usages\.push\(retried\.totalUsage\)/g)?.length, 5);
|
||||||
assert.equal(consultRoute.match(/usages\.push\(retried\.totalUsage\)/g)?.length, 3);
|
assert.equal(consultRoute.match(/const retryForAnswer = async \(\) => \{/g)?.length, 3);
|
||||||
assert.equal(consultRoute.match(/const retryForAnswer = async \(\) => \{/g)?.length, 2);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("standard consultation forwards its stable reservation request as the usage event key", async () => {
|
test("standard consultation forwards its stable reservation request as the usage event key", async () => {
|
||||||
|
|||||||
@@ -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.
|
// Given: the pre-flight guard runs before send() clears the composer draft.
|
||||||
assert.match(creditGuard, /router\.push\(membershipHref\("insufficient-credits"\)\)/);
|
assert.match(creditGuard, /router\.push\(membershipHref\("insufficient-credits"\)\)/);
|
||||||
assert.doesNotMatch(creditGuard, /window\.location/);
|
assert.doesNotMatch(creditGuard, /window\.location/);
|
||||||
assert.ok(sendSource.indexOf('router.push(membershipHref("insufficient-credits"))')
|
const creditPush = sendSource.indexOf('router.push(membershipHref("insufficient-credits"))');
|
||||||
< sendSource.indexOf(' setDraft("");'));
|
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);
|
assert.equal(pageSource.match(/router\.push\(membershipHref\("insufficient-credits"\)\)/g)?.length, 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ test("every external draft writer keeps working through the page-owned setters",
|
|||||||
assert.match(saveOnboardingName, /setDraft\(""\)/);
|
assert.match(saveOnboardingName, /setDraft\(""\)/);
|
||||||
assert.match(stopRestore, /setDraft\(pending\.question\);\n\s*setDraftTheme\(pending\.theme\);\n\s*setDraftEntrypoint\(pending\.entrypoint\);/);
|
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(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\)/);
|
assert.match(pageSource, /void send\(composerDraftSnapshot\(\), draftTheme\.current \?\? undefined, draftEntrypoint\.current\)/);
|
||||||
|
|
||||||
// And: opening a rectification case still empties the composer it replaces.
|
// And: opening a rectification case still empties the composer it replaces.
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ test("reserves usage and binds the owned consultation session atomically", () =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("persists transformed assistant metadata before atomically settling usage", () => {
|
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.equal(consultRoute.match(/onComplete: \(rawTransformedText\) => settle\(\(\) => completeResponse\(/g)?.length, 2);
|
||||||
assert.match(
|
assert.match(
|
||||||
consultRoute,
|
consultRoute,
|
||||||
@@ -40,11 +40,14 @@ test("persists partial transformed output when the upstream stream errors", () =
|
|||||||
2,
|
2,
|
||||||
);
|
);
|
||||||
assert.equal(consultRoute.match(/const settleErrored = \(emitted: boolean, output: string\) => settle\(/g)?.length, 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: \(\) => settle\(cancel\)/g)?.length, 2);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
consultRoute.match(/onCancel: \(\) => settleRun\(cancel, "cancelled"\)/g)?.length,
|
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.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.doesNotMatch(consultRoute, /const AGENT_TIMEOUT_MS =/);
|
||||||
assert.match(agentic, /const agentAbortSignal = AbortSignal\.timeout\(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.doesNotMatch(agentic, /abortSignal: request\.signal/);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
agentic.match(/onError: \(error\) => settleRun\(\s*cancel,[\s\S]*?toAgentObservabilityErrorCode\(error\),\s*\)/g)?.length,
|
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) ?? [];
|
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/);
|
for (const block of onErrorBlocks) assert.doesNotMatch(block, /completeResponse|completed_partial/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user