fix(frontend): recover rectification and consultation retries
Independent Staging Quality Gate / validate (push) Successful in 14m1s
Independent Staging Quality Gate / publish (push) Successful in 9m58s

This commit is contained in:
Jesse_Chen
2026-08-16 13:13:49 +08:00
parent 8ddcef4cf1
commit 9d8570640d
6 changed files with 124 additions and 12 deletions
@@ -159,6 +159,54 @@ test("domain plan enforces the raw plan upper bound and one input mode", async (
assert.equal(calls, 0);
});
test("invalid model input does not poison a later valid contract retry", async () => {
let calls = 0;
const state = createConsultationRuntimeState();
const tool = createConsultationTools({
userId: "u", sessionId: "s", requestId: "r-retry", consultationMode: "verified_chart",
serverChart, state,
runWorkflow: async (input) => { calls += 1; return workflow(input.theme); },
})["run-jyotish-consultation"];
const context = { observe: { span: async (_n: string, fn: () => Promise<unknown>) => fn(), log() {} } } as never;
await assert.rejects(
tool.execute!({ question: "先给出错误参数", domains: ["career"], theme: "career" }, context),
/invalid_consultation_domain_plan/,
);
assert.equal(calls, 0);
assert.equal(state.consultationToolCallCount, 0);
const result = await tool.execute!({ question: "改用合法参数", theme: "timing" }, context) as { domains: string[] };
assert.equal(calls, 1);
assert.deepEqual(result.domains, ["timing"]);
assert.equal(state.consultationToolCallCount, 1);
assert.equal(state.consultationToolCompleted, true);
});
test("a rejected workflow promise is cleared before a later tool call", async () => {
let calls = 0;
const state = createConsultationRuntimeState();
const tool = createConsultationTools({
userId: "u", sessionId: "s", requestId: "r-rejected-promise", consultationMode: "verified_chart",
serverChart, state,
runWorkflow: async (input) => {
calls += 1;
if (calls === 1) throw new Error("workflow_temporarily_failed");
return workflow(input.theme);
},
})["run-jyotish-consultation"];
const context = { observe: { span: async (_n: string, fn: () => Promise<unknown>) => fn(), log() {} } } as never;
await assert.rejects(
tool.execute!({ question: "第一次计算", theme: "career" }, context),
/workflow_temporarily_failed/,
);
const result = await tool.execute!({ question: "重新计算", theme: "timing" }, context) as { domains: string[] };
assert.equal(calls, 2);
assert.deepEqual(result.domains, ["timing"]);
});
test("personal Agent exposes the Jyotish Skill and named server tool", async () => {
const state = createConsultationRuntimeState();
const agent = getJyotishAgent({
@@ -102,6 +102,30 @@ test("server profile failures pause automatic rectification resume", () => {
);
});
test("homepage rectification open failures always surface a visible recovery message", () => {
const opening = page.slice(
page.indexOf("async function openRectificationCase"),
page.indexOf("async function openRectificationFromHomepage"),
);
assert.match(
opening,
/code === "profile_incomplete"[\s\S]*?handleRectificationProfileIncomplete\(\)/,
);
assert.match(
opening,
/code === "invalid_open_request"[\s\S]*?setRectificationError\(/,
);
assert.match(
opening,
/catch \{[\s\S]*?setRectificationError\("生时校正会话暂时无法打开,请稍后重试。"\)/,
);
assert.match(
page,
/rectificationError && !rectificationSurfaceOpen && \([\s\S]*?role="alert"[\s\S]*?rectificationErrorMessage/,
);
});
test("account rehydration normalizes persisted ISO birth dates before completeness checks", () => {
const profileReader = page.slice(
page.indexOf("function readProfile"),