import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import test from "node:test"; import { batchResultFromToolChunk, composeHostFallbackNarration, DEFAULT_RETRY_CONSTRAINT, isToolInputRejection, publicWriteToolCompleted, retryConstraintForAttempt, turnExpectsEvidenceWrite, UNWRITTEN_EVIDENCE_RETRY_CONSTRAINT, } from "../src/lib/rectification-agentic/v9/host-fallback.ts"; test("host fallback recap uses only batch return lines", () => { assert.equal(composeHostFallbackNarration(null), null); assert.equal(composeHostFallbackNarration({}), "记下了。"); assert.equal( composeHostFallbackNarration({ accepted_recaps: [ { display_date_label: "2016年9月", event_phrase: "入学" }, { display_date_label: "2020年6月", event_phrase: "毕业" }, ], }), "记下了:2016年9月 入学、2020年6月 毕业。", ); assert.equal( composeHostFallbackNarration({ items: [ { outcome: "accepted", display_date_label: "2018年7月", event_phrase: "入职" }, { outcome: "rejected", display_date_label: "1999年", event_phrase: "不该出现" }, ], }), "记下了:2018年7月 入职。", ); }); test("write-tool completion is limited to batch, set-focus and compare", () => { const none = new Map([ ["rectification-read-case", "completed"], ]); assert.equal(publicWriteToolCompleted(none), false); const batch = new Map([ ["rectification-record-evidence-batch", "completed"], ]); assert.equal(publicWriteToolCompleted(batch), true); const failedBatch = new Map([ ["rectification-record-evidence-batch", "failed"], ]); assert.equal(publicWriteToolCompleted(failedBatch), false); }); test("schema rejection envelopes are not treated as batch recaps or write completion", () => { const envelope = { error: true, message: "Tool input validation failed", validationErrors: { errors: [], fields: { kind: "debt" } }, }; assert.equal(isToolInputRejection(envelope), true); assert.equal(composeHostFallbackNarration(envelope), null); assert.equal(batchResultFromToolChunk({ type: "tool-result", payload: { toolName: "rectification-record-evidence-batch", result: envelope }, }), null); assert.equal(isToolInputRejection({ error: true, message: "no fields" }), false); }); test("year fallback is gone; only explicit evidence expectedWrite triggers the guard", () => { assert.equal(turnExpectsEvidenceWrite("opening", "evidence"), false); assert.equal(turnExpectsEvidenceWrite("read_only", "none"), false); assert.equal(turnExpectsEvidenceWrite("evidence", "evidence"), true); assert.equal(turnExpectsEvidenceWrite("evidence", "none"), false); assert.equal(turnExpectsEvidenceWrite("evidence", "unknown"), false); assert.equal(turnExpectsEvidenceWrite("evidence", undefined), false); assert.equal(retryConstraintForAttempt("evidence_not_written"), UNWRITTEN_EVIDENCE_RETRY_CONSTRAINT); assert.equal(retryConstraintForAttempt("empty_stream"), DEFAULT_RETRY_CONSTRAINT); }); test("case snapshot receipts expose host_fallback origin from the durable phase", () => { const route = readFileSync( new URL("../src/app/api/rectification/cases/[caseId]/route.ts", import.meta.url), "utf8", ); assert.match(route, /answer_origin: "host_fallback"/); assert.match(route, /answer\.host_fallback/); });