- Evidence turns whose batch accepted items: the body is the server recap built from those items (two or fewer listed inline, more as a count); the model no longer restates them (both prompt sets updated). - The case snapshot carries each assistant turn's recorded evidence from the ledger (rejected/superseded excluded); the message shows it in a collapsed <details> list when more than two. - Chinese date labels read straight into the event phrase (2016年入学); ISO labels keep their space. - Assertions updated with original/new/reason notes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
571 lines
22 KiB
TypeScript
571 lines
22 KiB
TypeScript
import assert from "node:assert/strict";
|
||
import { existsSync, readFileSync } from "node:fs";
|
||
import { fileURLToPath } from "node:url";
|
||
import test from "node:test";
|
||
|
||
import {
|
||
publicCanAdopt,
|
||
publicDecisionFields,
|
||
decideRectification,
|
||
} from "../src/lib/rectification-agentic/core/rectification-decision.ts";
|
||
import {
|
||
canShowRectificationReadonlyRange,
|
||
canShowRectificationSelectionCards,
|
||
parseRectificationCandidateResult,
|
||
} from "../src/lib/rectification-candidate-result.ts";
|
||
import {
|
||
attachOfferResultToTurns,
|
||
attachQuestionsToTurns,
|
||
parseTurnQuestion,
|
||
} from "../src/lib/rectification-agentic/v9/turn-question.ts";
|
||
import {
|
||
buildMethodFollowupPlan,
|
||
collectQuestionDomain,
|
||
OPENING_COLLECT_DOMAIN,
|
||
parsePersistedFollowupQuestionId,
|
||
projectRectificationChoiceCard,
|
||
spokenCollectFallbackFollowup,
|
||
} from "../src/lib/rectification-agentic/v9/method-followup.ts";
|
||
import {
|
||
CHOICE_SKIP_QUESTION_LABEL,
|
||
CHOICE_STOP_LABEL,
|
||
parseRectificationChoiceCard,
|
||
} from "../src/lib/rectification-agentic/v9/choice-card.ts";
|
||
import { stableFollowupQuestionId } from "../src/lib/rectification-agentic/v9/server-focus.ts";
|
||
import { hasPersistedVedastroValidation } from "../src/lib/rectification-agentic/v9/confirmation-gate.ts";
|
||
import {
|
||
parseToolActivityDetail,
|
||
loadV9TurnReceipt,
|
||
} from "../src/lib/rectification-agentic/v9/tool-service.ts";
|
||
import {
|
||
RECTIFICATION_AGENT_ATTEMPT_TIMEOUT_MS,
|
||
RECTIFICATION_SLOW_STEP_MS,
|
||
RECTIFICATION_TIMEOUT_WARN_BEFORE_MS,
|
||
rectificationLiveProgressLabel,
|
||
} from "../src/lib/rectification-activity-labels.ts";
|
||
import { OPEN_ENGINE_CAPABILITY_CEILING, CASE_ID, FOCUS_ID, TURN_ID, USER_ID, fakeAccounting } from "./rectification-v9-test-support.ts";
|
||
import { rectificationChatSurface } from "./rectification-chat-surface.ts";
|
||
|
||
const preAdopt = JSON.parse(readFileSync(
|
||
new URL("./fixtures/rectification-case-bf7a8de6-pre-adopt.json", import.meta.url),
|
||
"utf8",
|
||
)) as Record<string, unknown>;
|
||
const postAdopt = JSON.parse(readFileSync(
|
||
new URL("./fixtures/rectification-case-bf7a8de6-post-adopt.json", import.meta.url),
|
||
"utf8",
|
||
)) as Record<string, unknown>;
|
||
|
||
const STYLE_OPTIONS = [
|
||
{ label: "明确发生且时间吻合", answer_class: "yes" as const },
|
||
{ label: "发生过但程度较弱", answer_class: "weak_yes" as const },
|
||
{ label: "明确没有发生", answer_class: "no" as const },
|
||
{ label: "这段记不清楚", answer_class: "unsure" as const },
|
||
];
|
||
|
||
const EDUCATION_VERIFY = {
|
||
year: 2016,
|
||
year_label: "2016 年前后",
|
||
domain: "education" as const,
|
||
event_family: "升学结果或学习环境出现明显变化",
|
||
source: "dasha_activation" as const,
|
||
tracks: ["vimshottari", "narayana"] as const,
|
||
tracks_agree: true,
|
||
unique_minute_claim: false as const,
|
||
user_meaning: "2016 年前后升学结果或学习环境出现明显变化",
|
||
role: "reverse_verify" as const,
|
||
information_gain: 0.4,
|
||
semantic_key: "education.2016",
|
||
style_options: STYLE_OPTIONS,
|
||
};
|
||
|
||
const VERIFY_COPY = {
|
||
prompt: "2016 年前后,升学结果或学习环境有没有明显变化?",
|
||
option_a: STYLE_OPTIONS[0].label,
|
||
option_b: STYLE_OPTIONS[1].label,
|
||
option_c: STYLE_OPTIONS[2].label,
|
||
option_d: STYLE_OPTIONS[3].label,
|
||
options: STYLE_OPTIONS.map((option, index) => ({
|
||
key: (["A", "B", "C", "D"] as const)[index]!,
|
||
label: option.label,
|
||
answer_class: option.answer_class,
|
||
})),
|
||
} as const;
|
||
|
||
function reverseVerifySchema() {
|
||
return {
|
||
prompt: VERIFY_COPY.prompt,
|
||
probe_year: 2016,
|
||
choice: VERIFY_COPY,
|
||
};
|
||
}
|
||
|
||
function collectWithInternalAdopt() {
|
||
return decideRectification({
|
||
methodCoverageAll: false,
|
||
datedMethodCollectOpen: true,
|
||
datedEventCount: 4,
|
||
datedDomainCount: 3,
|
||
trainingGateOpen: true,
|
||
snapshotCurrent: true,
|
||
candidateScores: [
|
||
{ time: "05:06", score: 14 },
|
||
{ time: "04:53", score: 13 },
|
||
{ time: "05:03", score: 13 },
|
||
],
|
||
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
|
||
confirmationAllowed: false,
|
||
inferenceCredibleRange: ["04:53", "05:06"],
|
||
});
|
||
}
|
||
|
||
test("public can_adopt is closed during collect even when internal capability is open", () => {
|
||
const decision = collectWithInternalAdopt();
|
||
// 原值: sessionOutcome=collect_evidence,公开 can_adopt=false
|
||
// 新值: 训练门开、无带年月探针 → adopt_representative,公开 can_adopt=true
|
||
// 原因: BUG-651 带年月池空即交付,剩余采集不得挡住结果卡
|
||
assert.equal(decision.sessionOutcome, "adopt_representative");
|
||
assert.equal(decision.canAdopt, true);
|
||
const fields = publicDecisionFields(decision);
|
||
assert.equal(fields.can_adopt, true);
|
||
assert.equal(fields.selection_allowed, decision.selectionAllowed);
|
||
assert.equal(publicCanAdopt(decision), true);
|
||
});
|
||
|
||
test("public can_adopt stays open only for adopt outcomes", () => {
|
||
assert.equal(publicCanAdopt({ canAdopt: true, sessionOutcome: "provisional_range_user_stopped" }), true);
|
||
assert.equal(publicCanAdopt({ canAdopt: true, sessionOutcome: "adopt_representative" }), true);
|
||
assert.equal(publicCanAdopt({ canAdopt: true, sessionOutcome: "awaiting_confirmation" }), true);
|
||
assert.equal(publicCanAdopt({ canAdopt: true, sessionOutcome: "validated_range" }), true);
|
||
assert.equal(publicCanAdopt({ canAdopt: true, sessionOutcome: "discriminate_candidates" }), false);
|
||
assert.equal(publicCanAdopt({ canAdopt: true, sessionOutcome: "validate_holdout" }), false);
|
||
assert.equal(publicCanAdopt({ canAdopt: false, sessionOutcome: "adopt_representative" }), false);
|
||
});
|
||
|
||
test("accept route 409s before the adopt RPC when public can_adopt is false", () => {
|
||
const accept = readFileSync(
|
||
new URL("../src/app/api/rectification/cases/[caseId]/candidates/accept/route.ts", import.meta.url),
|
||
"utf8",
|
||
);
|
||
const gate = accept.indexOf("overlaid.can_adopt !== true");
|
||
const rpc = accept.indexOf("accept_agentic_rectification_candidate_for_case_v2");
|
||
assert.ok(gate >= 0, "accept must gate on overlaid.can_adopt");
|
||
assert.ok(rpc > gate, "RPC must not run before the session_outcome gate");
|
||
assert.match(accept, /adoption_not_allowed/);
|
||
assert.match(accept, /session_outcome: fields\.session_outcome/);
|
||
});
|
||
|
||
test("pre-adopt snapshot does not show adopt cards and does show a readonly range", () => {
|
||
const result = parseRectificationCandidateResult(preAdopt);
|
||
assert.ok(result);
|
||
assert.equal(result.sessionOutcome, "collect_evidence");
|
||
assert.equal(result.canAdopt, true);
|
||
assert.equal(canShowRectificationSelectionCards(result), false);
|
||
assert.equal(canShowRectificationReadonlyRange(result), true);
|
||
assert.deepEqual(result.credibleRange, ["04:53", "05:06"]);
|
||
});
|
||
|
||
test("post-adopt snapshot keeps the offer on the pre-verify message", () => {
|
||
const result = parseRectificationCandidateResult(postAdopt);
|
||
assert.ok(result);
|
||
assert.equal(result.selectedTime, "05:06");
|
||
const turns = Array.isArray(postAdopt.turns) ? postAdopt.turns as Array<{
|
||
id: string;
|
||
role: string;
|
||
question?: unknown;
|
||
}> : [];
|
||
const withQuestions = turns.map((turn) => ({
|
||
...turn,
|
||
text: null,
|
||
question: parseTurnQuestion(turn.question),
|
||
}));
|
||
const attached = attachOfferResultToTurns(withQuestions, {
|
||
resultId: result.resultId,
|
||
canAdopt: true,
|
||
acceptedTime: "05:06",
|
||
});
|
||
assert.equal(attached[0]?.offer_result_id, result.resultId);
|
||
assert.equal(attached[1]?.offer_result_id, null);
|
||
assert.equal(attached[1]?.question?.kind, "reverse_verify");
|
||
});
|
||
|
||
test("accepted reverse_verify keep reuses the persisted question id", () => {
|
||
const questionId = "reverse_verify:education_style:score";
|
||
const plan = buildMethodFollowupPlan({
|
||
evidence: [{
|
||
status: "confirmed",
|
||
domain: "education",
|
||
datePrecision: "year",
|
||
occurredFrom: "2016-01-01",
|
||
occurredTo: null,
|
||
}],
|
||
accepted: true,
|
||
sessionOutcome: "adopt_representative",
|
||
eventProbes: [EDUCATION_VERIFY],
|
||
activeFocus: {
|
||
id: FOCUS_ID,
|
||
questionId,
|
||
intent: "reverse_verify",
|
||
targetDomain: "education",
|
||
targetKind: "education_milestone",
|
||
expectedAnswerSchema: reverseVerifySchema(),
|
||
},
|
||
});
|
||
const followup = plan.next_followup;
|
||
assert.equal(followup?.intent, "reverse_verify");
|
||
assert.equal(followup?.method_id, "reverse_verify");
|
||
assert.equal(followup?.ask_theme, "education_style");
|
||
assert.equal(stableFollowupQuestionId(followup!), questionId);
|
||
assert.match(followup?.user_prompt_hint ?? "", /当前排盘已采用该时间/);
|
||
const parsed = parsePersistedFollowupQuestionId(questionId);
|
||
assert.deepEqual(parsed, { method_id: "reverse_verify", ask_theme: "education_style", scoring: true });
|
||
|
||
const card = projectRectificationChoiceCard({
|
||
evidence: [{
|
||
status: "confirmed",
|
||
domain: "education",
|
||
datePrecision: "year",
|
||
occurredFrom: "2016-01-01",
|
||
occurredTo: null,
|
||
}],
|
||
accepted: true,
|
||
sessionOutcome: "adopt_representative",
|
||
eventProbes: [EDUCATION_VERIFY],
|
||
activeFocus: {
|
||
id: FOCUS_ID,
|
||
questionId,
|
||
intent: "reverse_verify",
|
||
targetDomain: "education",
|
||
targetKind: "education_milestone",
|
||
expectedAnswerSchema: reverseVerifySchema(),
|
||
},
|
||
});
|
||
assert.ok(card);
|
||
assert.equal(card.focus_id, FOCUS_ID);
|
||
assert.equal(card.question_id, questionId);
|
||
assert.equal(card.stop_label, CHOICE_SKIP_QUESTION_LABEL);
|
||
assert.ok(parseRectificationChoiceCard(card));
|
||
const again = projectRectificationChoiceCard({
|
||
evidence: [{
|
||
status: "confirmed",
|
||
domain: "education",
|
||
datePrecision: "year",
|
||
occurredFrom: "2016-01-01",
|
||
occurredTo: null,
|
||
}],
|
||
accepted: true,
|
||
sessionOutcome: "adopt_representative",
|
||
eventProbes: [EDUCATION_VERIFY],
|
||
activeFocus: {
|
||
id: FOCUS_ID,
|
||
questionId,
|
||
intent: "reverse_verify",
|
||
targetDomain: "education",
|
||
targetKind: "education_milestone",
|
||
expectedAnswerSchema: reverseVerifySchema(),
|
||
},
|
||
});
|
||
assert.equal(again?.question_id, card.question_id);
|
||
assert.equal(again?.focus_id, card.focus_id);
|
||
});
|
||
|
||
test("distinguish keep still uses probe: semantic_key ids", () => {
|
||
const questionId = "probe:education.2016";
|
||
const plan = buildMethodFollowupPlan({
|
||
evidence: [{
|
||
status: "confirmed",
|
||
domain: "career",
|
||
datePrecision: "year",
|
||
occurredFrom: "2020-01-01",
|
||
occurredTo: null,
|
||
}],
|
||
sessionOutcome: "discriminate_candidates",
|
||
eventProbes: [{
|
||
...EDUCATION_VERIFY,
|
||
role: "distinguish",
|
||
semantic_key: "education.2016",
|
||
candidate_split_hash: "education:2016:05:06|04:53",
|
||
candidate_ids: ["05:06", "04:53"],
|
||
expected_outcomes: [
|
||
{ answer_class: "yes", supports: ["05:06"], conflicts: ["04:53"] },
|
||
{ answer_class: "no", supports: ["04:53"], conflicts: ["05:06"] },
|
||
],
|
||
}],
|
||
activeFocus: {
|
||
id: FOCUS_ID,
|
||
questionId,
|
||
intent: "distinguish_candidates",
|
||
targetDomain: "education",
|
||
targetKind: "education_milestone",
|
||
expectedAnswerSchema: {
|
||
semantic_key: "education.2016",
|
||
candidate_split_hash: "education:2016:05:06|04:53",
|
||
probe_id: "education.2016",
|
||
choice: {
|
||
prompt: "2016 年前后,升学结果或学习环境有没有明显变化?",
|
||
option_a: STYLE_OPTIONS[0].label,
|
||
option_b: STYLE_OPTIONS[1].label,
|
||
option_c: STYLE_OPTIONS[2].label,
|
||
option_d: STYLE_OPTIONS[3].label,
|
||
},
|
||
},
|
||
},
|
||
});
|
||
assert.equal(plan.next_followup?.intent, "distinguish_candidates");
|
||
assert.equal(plan.next_followup?.semantic_key, "education.2016");
|
||
assert.equal(stableFollowupQuestionId(plan.next_followup!), questionId);
|
||
assert.equal(parsePersistedFollowupQuestionId(questionId), null);
|
||
});
|
||
|
||
test("post-adopt live question identity matches the last reverse_verify turn", () => {
|
||
const last = Array.isArray(postAdopt.turns)
|
||
? postAdopt.turns[postAdopt.turns.length - 1] as { question?: unknown }
|
||
: null;
|
||
const question = parseTurnQuestion(last?.question);
|
||
assert.equal(question?.kind, "reverse_verify");
|
||
assert.equal(question?.options?.length, 4);
|
||
const card = projectRectificationChoiceCard({
|
||
evidence: [{
|
||
status: "confirmed",
|
||
domain: "education",
|
||
datePrecision: "year",
|
||
occurredFrom: "2016-01-01",
|
||
occurredTo: null,
|
||
}],
|
||
accepted: true,
|
||
sessionOutcome: "adopt_representative",
|
||
eventProbes: [EDUCATION_VERIFY],
|
||
activeFocus: {
|
||
id: question?.focus_id,
|
||
questionId: question?.question_id,
|
||
intent: "reverse_verify",
|
||
targetDomain: "education",
|
||
targetKind: "education_milestone",
|
||
expectedAnswerSchema: reverseVerifySchema(),
|
||
},
|
||
});
|
||
assert.equal(card?.focus_id, question?.focus_id);
|
||
assert.equal(card?.question_id, question?.question_id);
|
||
});
|
||
|
||
test("opening collect domain is other rather than unknown", () => {
|
||
// 旧 education → 新 other → 保留 不得为 unknown、stable id 不含 unknown
|
||
assert.equal(OPENING_COLLECT_DOMAIN, "other");
|
||
assert.equal(collectQuestionDomain(null), "other");
|
||
assert.equal(collectQuestionDomain("unknown"), "other");
|
||
const fallback = spokenCollectFallbackFollowup({
|
||
method_id: "dasha_events",
|
||
intent: "collect_method_evidence",
|
||
ask_theme: "dated_event",
|
||
domain: null,
|
||
kind_hint: null,
|
||
user_prompt_hint: "collect",
|
||
must_not_label: false,
|
||
choice_frame: null,
|
||
source: "method_coverage",
|
||
});
|
||
assert.equal(fallback.domain, "other");
|
||
// 原值: collect:other:collect_method_evidence
|
||
// 新值: collect:other:collect_method_evidence
|
||
// 原因: 开场 domain 仍是 other,稳定 id 不得含 unknown
|
||
assert.equal(stableFollowupQuestionId(fallback), "collect:other:collect_method_evidence");
|
||
assert.doesNotMatch(stableFollowupQuestionId(fallback), /unknown/);
|
||
});
|
||
|
||
test("skipped collect questions stay attached to their asked turn", () => {
|
||
const attached = attachQuestionsToTurns(
|
||
[{ id: TURN_ID, role: "assistant", text: "记下了。", status: "completed" }],
|
||
[{
|
||
id: FOCUS_ID,
|
||
caseId: CASE_ID,
|
||
questionId: "collect:family:collect_method_evidence",
|
||
intent: "collect_method_evidence",
|
||
targetEvidenceId: null,
|
||
targetDomain: "family",
|
||
targetKind: null,
|
||
expectedAnswerSchema: {
|
||
collect: true,
|
||
prompt: "2021 年前后,家里有没有人住院、搬家或职责明显变化?",
|
||
},
|
||
status: "skipped",
|
||
askedAt: "2026-09-02T00:00:00.000Z",
|
||
resolvedAt: "2026-09-02T00:03:00.000Z",
|
||
askedTurnId: TURN_ID,
|
||
answerOption: "stop",
|
||
}],
|
||
);
|
||
assert.equal(attached[0]?.question?.status, "skipped");
|
||
assert.equal(attached[0]?.question?.kind, "collect_spoken");
|
||
});
|
||
|
||
test("chat owns adopt cards on the offering message and drops the list-end handoff", () => {
|
||
// 原值: 读 rectification-agentic-chat.tsx 单文件。新值: rectificationChatSurface(容器 + 拆出的 hook / 参数函数 / 块,见 rectification-chat-surface.ts)。原因: TASK-rectification-code-split-20260926 只搬不改,整文件断言跟着代码走;切片断言另行改为调用函数。
|
||
const chat = rectificationChatSurface;
|
||
assert.match(chat, /canShowRectificationSelectionCards/);
|
||
assert.match(chat, /canShowRectificationReadonlyRange/);
|
||
assert.match(chat, /candidateOffer/);
|
||
assert.match(chat, /!candidateResult\?\.selectedTime/);
|
||
// 原值: 采用后状态条 rectification-adopt-status + 改选
|
||
// 新值: 采用状态只在卡片行内;composer-wrap 无状态条
|
||
// 原因: BUG-595 决策 3
|
||
assert.doesNotMatch(chat, /rectification-adopt-status/);
|
||
assert.doesNotMatch(chat, /用这个时间看盘/);
|
||
// 原值: 容器源码含 CHOICE_STOP_LABEL、已跳过(已采用)、scoring: question.kind。
|
||
// 新值: 这些 token 随 choiceCardFromQuestion / afterAnswer 进了行组件。
|
||
// 原因: BUG-725。
|
||
const messageEntry = readFileSync(new URL("../src/components/rectification-message-entry.tsx", import.meta.url), "utf8");
|
||
assert.match(messageEntry, /CHOICE_STOP_LABEL/);
|
||
// 原值: 口述采集输入框上方必须有与点选卡同款停止按钮
|
||
// 新值: 停止只留选择题卡与生成中「停止回答」
|
||
// 原因: BUG-595 决策 2
|
||
const wrap = chat.slice(chat.indexOf("className=\"composer-wrap\""), chat.indexOf("className=\"composer-footer\""));
|
||
assert.doesNotMatch(wrap, /rectification-collect-stop/);
|
||
assert.match(
|
||
readFileSync(new URL("../src/components/rectification-choice-card.tsx", import.meta.url), "utf8"),
|
||
/stop_label !== CHOICE_STOP_LABEL/,
|
||
);
|
||
assert.match(messageEntry, /已跳过(已采用/);
|
||
assert.match(chat, /rectificationLiveProgressLabel/);
|
||
assert.doesNotMatch(chat, /rectification-consult-handoff/);
|
||
assert.match(chat, /kind === "reverse_verify"/);
|
||
assert.match(messageEntry, /scoring: question\.kind !== "reverse_verify"/);
|
||
});
|
||
|
||
test("choice cards use 这题跳过 for reverse_verify and 先这样 for collect", () => {
|
||
assert.equal(CHOICE_SKIP_QUESTION_LABEL, "这题跳过");
|
||
assert.equal(CHOICE_STOP_LABEL, "先这样,先看当前范围");
|
||
const choiceCard = readFileSync(
|
||
new URL("../src/lib/rectification-agentic/v9/choice-card.ts", import.meta.url),
|
||
"utf8",
|
||
);
|
||
assert.match(choiceCard, /CHOICE_SKIP_QUESTION_LABEL/);
|
||
const prompt = readFileSync(
|
||
new URL("../src/mastra/agentic-rectification.ts", import.meta.url),
|
||
"utf8",
|
||
);
|
||
assert.match(prompt, /case\.accepted_time 非空时,正文第一句要说明已按该时间采用/);
|
||
// 原值: /必须先用一句话承接用户本轮给出的事实(年份\+事件)/(模型写承接句)。
|
||
// 新值: /证据轮的复述由服务器写:「记下了 N 件事」/(承接由服务器按入账条目写)。
|
||
// 原因: BUG-1136(TASK-rectification-message-cleanup-20261001 D2)——复述改由服务器写,模型不再逐条复述;采用后首句说明规则不变。
|
||
assert.match(prompt, /证据轮的复述由服务器写:「记下了 N 件事」/);
|
||
const voice = readFileSync(new URL("../docs/VOICE.md", import.meta.url), "utf8");
|
||
assert.match(voice, /不要预告选项/);
|
||
});
|
||
|
||
test("set-focus failures persist the spoken-prompt reason on the receipt", () => {
|
||
const tools = readFileSync(
|
||
new URL("../src/mastra/rectification-v9-tools.ts", import.meta.url),
|
||
"utf8",
|
||
);
|
||
const persist = readFileSync(
|
||
new URL("../src/lib/rectification-agentic/v9/score-persist.ts", import.meta.url),
|
||
"utf8",
|
||
);
|
||
assert.match(tools, /resultFingerprint: JSON\.stringify\(\{ reason: spoken\.reason \}\)/);
|
||
assert.match(persist, /readVedastroMinuteSensitiveStatus\(decisionReceipt\) !== "passed"/);
|
||
const detail = parseToolActivityDetail({
|
||
error: "invalid_spoken_prompt",
|
||
result_fingerprint: JSON.stringify({ reason: "probe_mismatch" }),
|
||
});
|
||
assert.equal(detail?.reason, "probe_mismatch");
|
||
assert.equal(detail?.error, "invalid_spoken_prompt");
|
||
});
|
||
|
||
test("elapsed_ms serializes from the turn receipt and 45s copy follows the live tool", () => {
|
||
const migration = readFileSync(
|
||
new URL("../supabase/migrations/20260902020000_rectification_tool_activity_timing.sql", import.meta.url),
|
||
"utf8",
|
||
);
|
||
assert.match(migration, /'elapsed_ms'/);
|
||
assert.match(migration, /'started_at'/);
|
||
assert.equal(existsSync(fileURLToPath(new URL("../db/migrations/20260902020000_rectification_tool_activity_timing.sql", import.meta.url))), false);
|
||
assert.equal(RECTIFICATION_SLOW_STEP_MS, 45_000);
|
||
assert.equal(
|
||
rectificationLiveProgressLabel({
|
||
tool: "rectification-compare-candidates",
|
||
baseLabel: "正在重新对照盘面…",
|
||
stepStartedAt: 0,
|
||
runStartedAt: 0,
|
||
now: RECTIFICATION_SLOW_STEP_MS,
|
||
}),
|
||
// 原值: "引擎在比较候选,可能需要一两分钟" / 新值: "还在重新对照盘面,可能需要一两分钟" / 原因: BUG-1050 步骤句改大白话(慢步骤句从进行中句派生)
|
||
"还在重新对照盘面,可能需要一两分钟",
|
||
);
|
||
assert.equal(
|
||
rectificationLiveProgressLabel({
|
||
tool: "rectification-set-focus",
|
||
baseLabel: "正在准备下一个问题…",
|
||
stepStartedAt: 0,
|
||
runStartedAt: 0,
|
||
now: RECTIFICATION_SLOW_STEP_MS,
|
||
}),
|
||
"还在准备下一个问题,可能需要一两分钟",
|
||
);
|
||
assert.equal(
|
||
rectificationLiveProgressLabel({
|
||
tool: "rectification-read-case",
|
||
// 原值: "正在读取校正记录…" / 新值: "正在看你的资料…" / 原因: BUG-1050(本断言只看超时句,不看这行)
|
||
baseLabel: "正在看你的资料…",
|
||
stepStartedAt: 0,
|
||
runStartedAt: 0,
|
||
now: RECTIFICATION_AGENT_ATTEMPT_TIMEOUT_MS - RECTIFICATION_TIMEOUT_WARN_BEFORE_MS,
|
||
}),
|
||
"即将超时,会自动重试或提示",
|
||
);
|
||
});
|
||
|
||
test("read_only compare reuses a passed vedastro validation and still names the helper", () => {
|
||
assert.equal(hasPersistedVedastroValidation({
|
||
vedastro_event_validation: { status: "failed" },
|
||
}), true);
|
||
assert.equal(hasPersistedVedastroValidation({
|
||
gates: { exact_confirmation: { vedastro_event_validation: { status: "passed" } } },
|
||
}), true);
|
||
assert.equal(hasPersistedVedastroValidation({}), false);
|
||
});
|
||
|
||
test("GET turn receipts expose elapsed_ms and set-focus failure detail", async () => {
|
||
const accounting = fakeAccounting({
|
||
get_agentic_rectification_turn_receipt: () => ({
|
||
turn_id: TURN_ID,
|
||
attempt_id: "dddddddd-dddd-4ddd-8ddd-dddddddddddd",
|
||
status: "completed",
|
||
skill_name: "jyotish-birth-time-rectification",
|
||
skill_version: "9.0.0",
|
||
engine_version: null,
|
||
phases: [],
|
||
tool_activities: [
|
||
{
|
||
tool: "rectification-compare-candidates",
|
||
status: "completed",
|
||
methods: ["d1-rashi"],
|
||
started_at: "2026-09-02T13:27:10.000Z",
|
||
elapsed_ms: 120000,
|
||
result_fingerprint: JSON.stringify({
|
||
hash: "abc",
|
||
engine_compare_ms: 80000,
|
||
vedastro_validate_ms: 30000,
|
||
persist_ms: 10000,
|
||
}),
|
||
},
|
||
{
|
||
tool: "rectification-set-focus",
|
||
status: "failed",
|
||
methods: [],
|
||
started_at: "2026-09-02T13:29:50.000Z",
|
||
elapsed_ms: 400,
|
||
error: "invalid_spoken_prompt",
|
||
result_fingerprint: JSON.stringify({ reason: "probe_mismatch" }),
|
||
},
|
||
],
|
||
tools: ["rectification-compare-candidates"],
|
||
methods: ["d1-rashi"],
|
||
started_at: "2026-09-02T13:27:05.000Z",
|
||
completed_at: "2026-09-02T13:30:03.000Z",
|
||
}),
|
||
});
|
||
const receipt = await loadV9TurnReceipt(accounting.client, USER_ID, CASE_ID, TURN_ID);
|
||
assert.equal(receipt?.toolActivities[0]?.elapsedMs, 120000);
|
||
assert.equal(receipt?.toolActivities[0]?.detail?.engine_compare_ms, 80000);
|
||
assert.equal(receipt?.toolActivities[1]?.detail?.reason, "probe_mismatch");
|
||
});
|