Move-only (TASK-rectification-code-split-20260926 T3/T4). No behavior, receipt, billing, Skill or scoring change. - agent-run.ts: 1400 -> 135 lines; runV9AgentTurn 1046 -> 25 lines, no nested functions. It keeps the public types, the budget constants and the phase order: agent-run-prepare.ts (reliability, session, year re-ask, Skill identity, delivered guard, reserve, turn row / replay), agent-run-retry.ts (attempt loop), agent-run-attempt.ts (the old nested streamAttempt), agent-run-finish.ts (billing settle, receipts, interview, finalize), agent-run-support.ts (outcome types, retry classification, turn receipt writers), agent-run-messages.ts (buildAgentMessages / buildOpeningBrief, re-exported). - One token changed with the move: the attempt passes its own `previousErrorCode` argument to buildAgentMessages instead of reading the enclosing `lastAttemptError`; the loop passes that same value (clears the old unused-parameter warning). - Tests: whole-source contracts read tests/rectification-agent-run-surface.ts; behavioral runV9AgentTurn tests unchanged. agent-run growth caps added. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
159 lines
7.0 KiB
TypeScript
159 lines
7.0 KiB
TypeScript
import assert from "node:assert/strict";
|
||
import { readFileSync } from "node:fs";
|
||
import test from "node:test";
|
||
|
||
import { withSpokenPrompt } from "../src/lib/rectification-agentic/v9/spoken-prompt.ts";
|
||
import { projectCurrentQuestion } from "../src/lib/rectification-agentic/v9/turn-decision.ts";
|
||
import {
|
||
TARGETED_YEAR_PROMPT,
|
||
isYearStageQuestionId,
|
||
pendingTargetedYearDomain,
|
||
} from "../src/lib/rectification-agentic/v9/collection-question-pool.ts";
|
||
import {
|
||
collectFocusSchema,
|
||
isCollectFocusSchema,
|
||
persistServerOwnedFocus,
|
||
shouldResolveCollectFocusAfterEvidence,
|
||
} from "../src/lib/rectification-agentic/v9/server-focus.ts";
|
||
import { shouldHostReaskYearEntry, userMessageHasYear } from "../src/lib/rectification-agentic/v9/year-entry-host.ts";
|
||
import { TARGETED_COLLECT_OPTION_A } from "../src/lib/rectification-agentic/v9/choice-card.ts";
|
||
import { targetedCollectYearFollowup as yearFollowup } from "../src/lib/rectification-agentic/v9/method-followup.ts";
|
||
import {
|
||
CASE_ID,
|
||
FOCUS_ID,
|
||
USER_ID,
|
||
fakeAccounting,
|
||
} from "./rectification-v9-test-support.ts";
|
||
import { rectificationChatSurface } from "./rectification-chat-surface.ts";
|
||
import { rectificationAgentRunSurface } from "./rectification-agent-run-surface.ts";
|
||
|
||
const YEAR_Q = "collect:targeted:health_pressure:year";
|
||
const SERVER_PROMPT = TARGETED_YEAR_PROMPT;
|
||
const FOLLOWUP = "2019年3月这次手术,是你本人做的吗?";
|
||
|
||
test("year-stage set-focus overlay keeps the server prompt (BUG-908)", () => {
|
||
const schema = {
|
||
collect: true,
|
||
prompt: SERVER_PROMPT,
|
||
};
|
||
const next = withSpokenPrompt(schema, FOLLOWUP, YEAR_Q);
|
||
assert.equal(next.prompt, SERVER_PROMPT);
|
||
assert.equal(next.spoken_prompt, FOLLOWUP);
|
||
const question = projectCurrentQuestion({
|
||
id: FOCUS_ID,
|
||
questionId: YEAR_Q,
|
||
intent: "collect_method_evidence",
|
||
targetDomain: "health_pressure",
|
||
expectedAnswerSchema: next,
|
||
});
|
||
assert.equal(question?.prompt, SERVER_PROMPT);
|
||
assert.equal(question?.kind, "collect_spoken");
|
||
});
|
||
|
||
test("year-stage questions are typed collect, not an entry card (T0)", () => {
|
||
assert.equal(isYearStageQuestionId(YEAR_Q), true);
|
||
assert.equal(isYearStageQuestionId("collect:targeted:health_pressure"), false);
|
||
assert.equal(TARGETED_COLLECT_OPTION_A, "有,我来说");
|
||
// 原值: 读 rectification-agentic-chat.tsx 单文件。新值: rectificationChatSurface(容器 + 拆出的 hook / 参数函数 / 块,见 rectification-chat-surface.ts)。原因: TASK-rectification-code-split-20260926 只搬不改,整文件断言跟着代码走;切片断言另行改为调用函数。
|
||
const chat = rectificationChatSurface;
|
||
assert.doesNotMatch(chat, /EventDateEntryCard|event_date_entry/);
|
||
const followup = yearFollowup("health_pressure");
|
||
const schema = collectFocusSchema(followup);
|
||
assert.ok(isCollectFocusSchema(schema));
|
||
assert.equal(schema?.event_date_entry, undefined);
|
||
assert.equal(followup.spoken_prompt, TARGETED_YEAR_PROMPT);
|
||
});
|
||
|
||
test("batch and propose-evidence share the same collect-focus resolve (BUG-909)", () => {
|
||
const tools = readFileSync(new URL("../src/mastra/rectification-v9-tools.ts", import.meta.url), "utf8");
|
||
const first = tools.indexOf("await resolveActiveCollectFocusAfterEvidence(");
|
||
const second = tools.indexOf("await resolveActiveCollectFocusAfterEvidence(", first + 1);
|
||
assert.ok(first > 0 && second > first);
|
||
assert.equal(tools.indexOf("await resolveActiveCollectFocusAfterEvidence(", second + 1), -1);
|
||
});
|
||
|
||
test("dated evidence closes the year stage for that domain (BUG-909)", () => {
|
||
const topics = [{
|
||
questionId: "collect:targeted:health_pressure",
|
||
target_domain: "health_pressure",
|
||
status: "resolved",
|
||
intent: "collect_method_evidence",
|
||
}];
|
||
assert.equal(pendingTargetedYearDomain(topics, []), "health_pressure");
|
||
assert.equal(pendingTargetedYearDomain(topics, [{
|
||
status: "confirmed",
|
||
domain: "health",
|
||
datePrecision: "month",
|
||
occurredFrom: "2019-03-01",
|
||
occurredTo: "2019-03-01",
|
||
}]), null);
|
||
const schema = collectFocusSchema(yearFollowup("health_pressure"));
|
||
assert.equal(shouldResolveCollectFocusAfterEvidence({
|
||
intent: "collect_method_evidence",
|
||
questionId: YEAR_Q,
|
||
expectedAnswerSchema: schema ?? { collect: true, prompt: SERVER_PROMPT },
|
||
}), true);
|
||
});
|
||
|
||
test("persist year followup writes a collect schema that batch can resolve (BUG-909 a)", async () => {
|
||
const accounting = fakeAccounting({
|
||
set_agentic_rectification_conversation_focus: (_fn, args) => ({
|
||
focus: {
|
||
id: FOCUS_ID,
|
||
case_id: CASE_ID,
|
||
question_id: args.p_question_id,
|
||
intent: args.p_intent,
|
||
target_domain: args.p_target_domain,
|
||
expected_answer_schema: args.p_expected_answer_schema,
|
||
status: "active",
|
||
asked_at: "2026-09-16T00:00:00.000Z",
|
||
},
|
||
idempotent: false,
|
||
}),
|
||
});
|
||
const result = await persistServerOwnedFocus({
|
||
accounting: accounting.client,
|
||
userId: USER_ID,
|
||
caseId: CASE_ID,
|
||
activeFocus: null,
|
||
decisionReceipt: null,
|
||
followup: yearFollowup("health_pressure"),
|
||
});
|
||
assert.equal(result.status, "created");
|
||
assert.equal(isCollectFocusSchema(result.focus?.expectedAnswerSchema ?? null), true);
|
||
assert.equal(result.focus?.expectedAnswerSchema.prompt, TARGETED_YEAR_PROMPT);
|
||
assert.equal(result.focus?.expectedAnswerSchema.event_date_entry, undefined);
|
||
});
|
||
|
||
test("yearless short replies are held by the host pre-step (BUG-910)", () => {
|
||
assert.equal(userMessageHasYear("是"), false);
|
||
assert.equal(userMessageHasYear("不是本人"), false);
|
||
assert.equal(userMessageHasYear("2019年3月做过手术"), true);
|
||
assert.equal(userMessageHasYear("2020 年住院"), true);
|
||
const focus = {
|
||
questionId: YEAR_Q,
|
||
expectedAnswerSchema: { collect: true, prompt: SERVER_PROMPT },
|
||
};
|
||
assert.equal(shouldHostReaskYearEntry(focus, "是"), SERVER_PROMPT);
|
||
assert.equal(shouldHostReaskYearEntry(focus, "2019年3月做过手术"), null);
|
||
// 原值: 读 lib/rectification-agentic/v9/agent-run.ts 单文件。新值: rectificationAgentRunSurface(agent-run.ts + 拆出的 agent-run-*.ts,见 rectification-agent-run-surface.ts)。原因: TASK-rectification-code-split-20260926 只搬不改,整文件断言跟着代码走;切片断言另行改写。
|
||
const agentRun = rectificationAgentRunSurface;
|
||
const reliability = agentRun.indexOf("classifyDateReliabilityUtterance");
|
||
const yearHost = agentRun.indexOf("shouldHostReaskYearEntry");
|
||
const reserve = agentRun.indexOf("billing.reserve");
|
||
assert.ok(reliability > 0 && yearHost > reliability && reserve > yearHost);
|
||
});
|
||
|
||
test("standalone persisted question shares the assistant inset (BUG-911)", () => {
|
||
const styles = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8");
|
||
assert.match(
|
||
styles,
|
||
/\.rectification-message-question\.is-standalone \{[\s\S]*margin-inline-start: var\(--assistant-content-inset\)/,
|
||
);
|
||
assert.match(
|
||
styles,
|
||
/\.rectification-message-question\.is-standalone \.rectification-choice-card\.is-embedded \{[\s\S]*margin-inline-start: 0/,
|
||
);
|
||
assert.doesNotMatch(styles, /\.rectification-event-entry \{/);
|
||
});
|