fix(rectification): resolve typed focus answers deterministically
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
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!/);
|
||||
});
|
||||
Reference in New Issue
Block a user