108 lines
4.2 KiB
TypeScript
108 lines
4.2 KiB
TypeScript
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<string, unknown>): 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.doesNotMatch(route, /classified\.answer_class!/);
|
|
});
|