cfa824994e
A completed tool left only checkmarks under 正在分析, so the run looked stuck. The composer and scoring-card 先这样 buttons were unused early exits. Co-authored-by: Cursor <cursoragent@cursor.com>
538 lines
20 KiB
TypeScript
538 lines
20 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";
|
|
|
|
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();
|
|
assert.equal(decision.sessionOutcome, "collect_evidence");
|
|
assert.equal(decision.canAdopt, true);
|
|
const fields = publicDecisionFields(decision);
|
|
assert.equal(fields.can_adopt, false);
|
|
assert.equal(fields.selection_allowed, decision.selectionAllowed);
|
|
assert.equal(publicCanAdopt(decision), false);
|
|
});
|
|
|
|
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");
|
|
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", () => {
|
|
const chat = readFileSync(
|
|
new URL("../src/components/rectification-agentic-chat.tsx", import.meta.url),
|
|
"utf8",
|
|
);
|
|
assert.match(chat, /canShowRectificationSelectionCards/);
|
|
assert.match(chat, /canShowRectificationReadonlyRange/);
|
|
assert.match(chat, /candidateOffer/);
|
|
assert.match(chat, /!candidateResult\?\.selectedTime/);
|
|
assert.match(chat, /rectification-adopt-status/);
|
|
// 旧 状态条含「用这个时间看盘」→ 新 产品删按钮 → 保留 状态条仍在且含「改选」,不得再渲染 consult-handoff
|
|
assert.doesNotMatch(chat, /用这个时间看盘/);
|
|
assert.match(chat, /改选/);
|
|
assert.match(chat, /CHOICE_STOP_LABEL/);
|
|
assert.doesNotMatch(chat, /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(chat, /已跳过(已采用/);
|
|
assert.match(chat, /rectificationLiveProgressLabel/);
|
|
assert.doesNotMatch(chat, /rectification-consult-handoff/);
|
|
assert.match(chat, /kind === "reverse_verify"/);
|
|
assert.match(chat, /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 非空时,正文第一句要说明已按该时间采用/);
|
|
assert.match(prompt, /必须先用一句话承接用户本轮给出的事实(年份\+事件)/);
|
|
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",
|
|
);
|
|
assert.match(tools, /resultFingerprint: JSON\.stringify\(\{ reason: spoken\.reason \}\)/);
|
|
assert.match(tools, /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,
|
|
}),
|
|
"引擎在比较候选,可能需要一两分钟",
|
|
);
|
|
assert.equal(
|
|
rectificationLiveProgressLabel({
|
|
tool: "rectification-read-case",
|
|
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");
|
|
});
|