import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import test from "node:test"; import { composeCollectSpokenAssistantText, detachCollectSpokenAssistantText, stripQuestionSentences, trimEvidenceTurnBody } from "../src/lib/rectification-agentic/v9/collect-prompt.ts"; import { attachQuestionsToTurns } from "../src/lib/rectification-agentic/v9/turn-question.ts"; import { GENERIC_COLLECT_QUESTION, RECTIFICATION_USER_COPY, USER_COLLECT_QUESTION } from "../src/lib/rectification-agentic/user-copy.ts"; import { CASE_ID, FOCUS_ID, TURN_ID } from "./rectification-v9-test-support.ts"; test("composeCollectSpokenAssistantText joins by exact prompt identity", () => { const greeting = "你好,我是生时校正助手。"; const combined = `${greeting}\n\n${GENERIC_COLLECT_QUESTION}`; assert.equal(composeCollectSpokenAssistantText(greeting, GENERIC_COLLECT_QUESTION), combined); assert.equal(composeCollectSpokenAssistantText(combined, GENERIC_COLLECT_QUESTION), combined); assert.equal(composeCollectSpokenAssistantText("", GENERIC_COLLECT_QUESTION), GENERIC_COLLECT_QUESTION); assert.equal(composeCollectSpokenAssistantText(GENERIC_COLLECT_QUESTION, GENERIC_COLLECT_QUESTION), GENERIC_COLLECT_QUESTION); assert.equal(composeCollectSpokenAssistantText(greeting, " "), greeting); assert.equal( composeCollectSpokenAssistantText(` ${greeting} `, ` ${GENERIC_COLLECT_QUESTION} `), combined, ); }); test("detachCollectSpokenAssistantText removes only an exact trailing stem", () => { const greeting = "你好,我是生时校正助手。"; const combined = `${greeting}\n\n${GENERIC_COLLECT_QUESTION}`; assert.equal(detachCollectSpokenAssistantText(combined, GENERIC_COLLECT_QUESTION), greeting); assert.equal(detachCollectSpokenAssistantText(greeting, GENERIC_COLLECT_QUESTION), greeting); assert.equal(detachCollectSpokenAssistantText(GENERIC_COLLECT_QUESTION, GENERIC_COLLECT_QUESTION), GENERIC_COLLECT_QUESTION); assert.equal(detachCollectSpokenAssistantText("", GENERIC_COLLECT_QUESTION), ""); }); test("GET rebuild detaches a legacy composed suffix only when asked_turn_id matches", () => { const greeting = "你好,我是生时校正助手。"; const combined = composeCollectSpokenAssistantText(greeting, GENERIC_COLLECT_QUESTION); const turns = [{ id: TURN_ID, role: "assistant" as const, text: combined, status: "completed", }]; const linked = attachQuestionsToTurns(turns, [{ id: FOCUS_ID, caseId: CASE_ID, questionId: "collect:unknown:collect_method_evidence", intent: "collect_method_evidence", targetEvidenceId: null, targetDomain: null, targetKind: null, expectedAnswerSchema: { prompt: GENERIC_COLLECT_QUESTION, collect: true }, status: "active", askedAt: "2026-09-02T00:00:00.000Z", resolvedAt: null, askedTurnId: TURN_ID, }]); assert.equal(linked[0]?.text, greeting); assert.equal(linked[0]?.question?.prompt, GENERIC_COLLECT_QUESTION); const unlinked = attachQuestionsToTurns(turns, [{ id: FOCUS_ID, caseId: CASE_ID, questionId: "collect:unknown:collect_method_evidence", intent: "collect_method_evidence", targetEvidenceId: null, targetDomain: null, targetKind: null, expectedAnswerSchema: { prompt: GENERIC_COLLECT_QUESTION, collect: true }, status: "active", askedAt: "2026-09-02T00:00:00.000Z", resolvedAt: null, askedTurnId: null, }]); assert.equal(unlinked[0]?.text, combined); assert.equal(unlinked[0]?.question, null); }); test("composeCollectSpokenAssistantText drops a near-duplicate restatement of the stem", () => { const stem = USER_COLLECT_QUESTION.education; const restated = `${stem.slice(0, 12)}还记得大概哪一年吗?`; const body = `这条记下了。${restated}`; const composed = composeCollectSpokenAssistantText(body, stem); assert.equal(composed.includes(restated), false); assert.equal(composed.split(stem).length - 1, 1); assert.ok(composed.endsWith(stem)); }); test("runtime no longer composes the stem into assistant_message", () => { const agentRun = readFileSync(new URL("../src/lib/rectification-agentic/v9/agent-run.ts", import.meta.url), "utf8"); const attach = readFileSync(new URL("../src/lib/rectification-agentic/v9/turn-question.ts", import.meta.url), "utf8"); assert.doesNotMatch(agentRun, /composeCollectSpokenAssistantText/); assert.match(attach, /stripQuestionSentences/); assert.match(attach, /if \(!focus\.askedTurnId\) continue/); }); test("stripQuestionSentences drops a rewritten trailing question and its tag", () => { const body = "范围已经收到,收在 05:00–05:10。你大概哪一年搬过家?有年份就行。"; const stem = "你大概是哪一年搬的家?"; assert.equal(stripQuestionSentences(body, stem), "范围已经收到,收在 05:00–05:10。"); }); test("stripQuestionSentences drops a mid-body stem and keeps the surrounding sentences", () => { const stem = "你大概是哪一年搬的家?"; const body = "记下了。你大概是哪一年搬的家?范围还在 05:00–05:10。"; assert.equal(stripQuestionSentences(body, stem), "记下了。范围还在 05:00–05:10。"); }); test("stripQuestionSentences returns empty when the body is only a question", () => { const stem = "你大概是哪一年搬的家?"; assert.equal(stripQuestionSentences("你大概哪一年搬过家?", stem), ""); assert.equal(RECTIFICATION_USER_COPY.collectHandoff.includes("请回答下面的问题"), false); }); test("attachQuestionsToTurns leaves body unchanged when the turn has no focus", () => { const body = "范围收到 05:00–05:10。你大概哪一年搬过家?"; const turns = attachQuestionsToTurns([{ id: TURN_ID, role: "assistant" as const, text: body, }], []); assert.equal(turns[0]?.text, body); assert.equal(turns[0]?.question, null); }); test("trimEvidenceTurnBody keeps the first sentence and strips value judgments", () => { const three = trimEvidenceTurnBody("记下了:2016 年 9 月入学。这对校正很有帮助。接下来我们继续。"); assert.equal(three, "记下了:2016 年 9 月入学。"); assert.doesNotMatch(three, /很有帮助|很有价值|很有分量|特别有用/); const judged = trimEvidenceTurnBody("记下了:2020 年 6 月毕业,特别有用。"); assert.doesNotMatch(judged, /特别有用/); const two = trimEvidenceTurnBody("记下了:2016 年 9 月入学。\n\n范围收到 04:50–05:10。"); assert.equal(two, "记下了:2016 年 9 月入学。\n\n范围收到 04:50–05:10。"); });