import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import test from "node:test"; import { optionIdForAnswerClass, parseRectificationTurnIntent, } from "../src/lib/rectification-agentic/v9/turn-intent-classifier.ts"; import type { ConversationFocus } from "../src/lib/rectification-agentic/v9/tool-service.ts"; import { CASE_ID, FOCUS_ID } from "./rectification-v9-test-support.ts"; function focusWithOptions(expectedAnswerSchema: Record): ConversationFocus { return { id: FOCUS_ID, caseId: CASE_ID, questionId: "question-1", intent: "distinguish_candidates", targetEvidenceId: null, targetDomain: "career", targetKind: "career_change", expectedAnswerSchema, status: "active", askedAt: "2026-08-27T00:00:00.000Z", resolvedAt: null, }; } const SHUFFLED_CHOICE = { choice: { prompt: "2023 年前后,工作状态是否出现明显变化?", option_a: "这段时间没有明显变化", option_b: "记不清当时的情况", option_c: "变化明显而且时间吻合", option_d: "有变化但程度比较弱", options: [ { key: "A", label: "这段时间没有明显变化", answer_class: "no" }, { key: "B", label: "记不清当时的情况", answer_class: "unsure" }, { key: "C", label: "变化明显而且时间吻合", answer_class: "yes" }, { key: "D", label: "有变化但程度比较弱", answer_class: "weak_yes" }, ], }, }; test("turn intent parser enforces answer_class only for current-focus answers", () => { assert.deepEqual(parseRectificationTurnIntent({ intent: "answer_current_focus", answer_class: "no", }), { intent: "answer_current_focus", answer_class: "no", }); assert.deepEqual(parseRectificationTurnIntent({ intent: "provide_new_evidence", answer_class: null, }), { intent: "provide_new_evidence", answer_class: null, }); assert.equal(parseRectificationTurnIntent({ intent: "answer_current_focus", answer_class: null, }), null); assert.equal(parseRectificationTurnIntent({ intent: "stop_rectification", answer_class: "no", }), null); assert.equal(parseRectificationTurnIntent({ intent: "unclear", answer_class: null, extra: true, }), null); }); test("answer classes resolve through each dynamic option instead of A/B/C/D position", () => { const focus = focusWithOptions(SHUFFLED_CHOICE); assert.equal(optionIdForAnswerClass(focus, "no"), "A"); assert.equal(optionIdForAnswerClass(focus, "unsure"), "B"); assert.equal(optionIdForAnswerClass(focus, "yes"), "C"); assert.equal(optionIdForAnswerClass(focus, "weak_yes"), "D"); }); test("missing or invalid dynamic choice schemas fail closed", () => { assert.equal(optionIdForAnswerClass(focusWithOptions({}), "no"), null); assert.equal(optionIdForAnswerClass(focusWithOptions({ choice: { ...SHUFFLED_CHOICE.choice, options: SHUFFLED_CHOICE.choice.options.map(({ key, label }) => ({ key, label })), }, }), "no"), null); }); test("production intent handling contains no semantic regex or positional text parser", () => { const route = readFileSync(new URL("../src/app/api/rectification/agent/route.ts", import.meta.url), "utf8"); const classifier = readFileSync(new URL("../src/lib/rectification-agentic/v9/turn-intent-classifier.ts", import.meta.url), "utf8"); const choiceCard = readFileSync(new URL("../src/lib/rectification-agentic/v9/choice-card.ts", import.meta.url), "utf8"); const inference = readFileSync(new URL("../src/lib/rectification-agentic/v9/inference-adapter.ts", import.meta.url), "utf8"); const source = [route, classifier, choiceCard, inference].join("\n"); const fastPath = route.slice( route.indexOf('if (action === "message")'), route.indexOf("const requestTime"), ); assert.doesNotMatch(source, /USER_STOP_PATTERN|parseChoiceKeyFromUserMessage/); assert.doesNotMatch(classifier + fastPath, /\.test\([^\n]*(?:userMessage|user_message|message)/); assert.doesNotMatch(classifier + fastPath, /(?:userMessage|user_message|message)\.(?:match|search|includes|startsWith|endsWith)\(/); assert.ok(route.indexOf("classifyRectificationTurnIntent") < route.indexOf("runV9AgentTurn({")); assert.ok(route.indexOf("persistServerOwnedFocus") < route.indexOf("runV9AgentTurn({")); assert.ok(fastPath.includes("ask_candidate_discriminator")); assert.match(fastPath, /目前没有可继续区分/); assert.doesNotMatch(route, /classified\.answer_class!/); });