Files
Jyotisha/frontend/tests/rectification-turn-intent-classifier.test.ts
T
Jesse_Chen 02d4c91626
Independent Staging Quality Gate / validate (push) Successful in 11m40s
Independent Staging Quality Gate / publish (push) Successful in 9m36s
fix(rectification): stop occupation coverage from locking questions and the range exit
Occupation answers were stored as career, so coverage never closed, yearless
cards never fired, and the decision layer suppressed an engine-allowed range.
Normalize occupation-collect writes, allow yearless cards once training is
open, offer a range when no distinguish card remains, and align the public
house table to the representative minute.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-30 03:05:28 +08:00

146 lines
5.8 KiB
TypeScript

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import {
optionIdForAnswerClass,
parseRectificationTurnIntent,
shouldContinueAgentForDatedEvent,
shouldDeclineCollectFocus,
} 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("missing has_new_dated_event is treated as false and true is fail-closed optional", () => {
const legacy = parseRectificationTurnIntent({
intent: "answer_current_focus",
answer_class: "no",
});
assert.equal(legacy?.intent, "answer_current_focus");
assert.equal(legacy?.answer_class, "no");
assert.equal(legacy?.has_new_dated_event, undefined);
assert.equal(shouldContinueAgentForDatedEvent(legacy), false);
assert.equal(shouldContinueAgentForDatedEvent(null), false);
assert.equal(shouldContinueAgentForDatedEvent({
intent: "answer_current_focus",
answer_class: "no",
has_new_dated_event: false,
}), false);
const withEvent = parseRectificationTurnIntent({
intent: "answer_current_focus",
answer_class: "no",
has_new_dated_event: true,
});
assert.equal(withEvent?.has_new_dated_event, true);
assert.equal(shouldContinueAgentForDatedEvent(withEvent), true);
assert.equal(shouldDeclineCollectFocus(withEvent), true);
assert.equal(parseRectificationTurnIntent({
intent: "answer_current_focus",
answer_class: "no",
has_new_dated_event: "yes",
}), 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.doesNotMatch(fastPath, /目前没有可继续区分/);
assert.match(route, /可以先按当前区间看盘,也可以再补一件记得时间的经历/);
assert.match(fastPath, /DISCRIMINATOR_EXHAUSTED_NARRATION/);
assert.match(fastPath, /!plan\.next_followup/);
assert.doesNotMatch(route, /classified\.answer_class!/);
});