Files
Jyotisha/frontend/tests/rectification-occupation-coverage-exit.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

450 lines
17 KiB
TypeScript

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import { decideRectification } from "../src/lib/rectification-agentic/core/rectification-decision.ts";
import { trainingScoreableGate } from "../src/lib/rectification-agentic/v9/evidence-model.ts";
import { RECTIFICATION_SKILL_VERSION } from "../src/lib/rectification-agentic/v9/case-status.ts";
import { buildMethodFollowupPlan } from "../src/lib/rectification-agentic/v9/method-followup.ts";
import {
parseRectificationCandidateResult,
workingRectificationHouseTable,
} from "../src/lib/rectification-candidate-result.ts";
import { decideFromDossier } from "../src/lib/rectification-agentic/v9/decision-from-dossier.ts";
import { latestResultToolProjection } from "../src/mastra/rectification-v9-tools.ts";
import { isRenderableChoiceOpenQuestion } from "../src/lib/rectification-agentic/v9/server-focus.ts";
const STYLE_OPTIONS = [
{ label: "明确发生且时间吻合", answer_class: "yes" as const },
{ label: "发生过但程度较弱", answer_class: "weak_yes" as const },
{ label: "明确没有发生", answer_class: "no" as const },
{ label: "这段记不清楚", answer_class: "unsure" as const },
];
const TIED = [
{ time: "04:47", score: 12 },
{ time: "04:51", score: 12 },
{ time: "04:53", score: 11 },
{ time: "04:59", score: 11 },
{ time: "05:00", score: 11 },
{ time: "05:07", score: 11 },
{ time: "05:12", score: 11 },
{ time: "05:14", score: 8 },
{ time: "05:15", score: 7 },
];
function dated(
id: string,
domain: string,
eventKind: string,
occurredFrom: string,
extra: {
occurredTo?: string | null;
datePrecision?: "year" | "month" | "day" | "range" | "unknown";
status?: "confirmed" | "draft";
} = {},
) {
return {
id,
status: extra.status ?? ("confirmed" as const),
domain,
datePrecision: extra.datePrecision ?? ("month" as const),
occurredFrom,
occurredTo: extra.occurredTo ?? null,
eventKind,
};
}
/** 19-row ledger from the stalled case: 15 scoreable, 4 domains; occupation never recorded. */
const CASE_EVIDENCE = [
dated("e-edu-start", "education", "education_start", "2016-09-01"),
dated("e-edu-complete", "education", "education_completion", "2020-06-01"),
dated("e-edu-interrupt", "education", "education_interruption", "2021-01-01", {
occurredTo: "2023-07-01",
datePrecision: "range",
}),
dated("e-edu-change", "education", "education_change", "2023-01-01", { datePrecision: "year" }),
dated("e-career-entry-2020", "career", "career_entry", "2020-04-01"),
dated("e-career-exit-2020", "career", "career_exit", "2020-10-01"),
dated("e-career-entry-2024", "career", "career_entry", "2024-04-07", { datePrecision: "day" }),
dated("e-career-exit-2026", "career", "career_exit", "2026-08-11", { datePrecision: "day" }),
dated("e-career-change-2022", "career", "career_change", "2022-12-01"),
dated("e-career-change-2024", "career", "career_change", "2024-01-01", { datePrecision: "year" }),
dated("e-career-range", "career", "career_change", "2020-04-01", {
occurredTo: "2020-10-01",
datePrecision: "range",
}),
dated("e-career-draft-a", "career", "career_change", "", {
datePrecision: "unknown",
status: "draft",
occurredTo: null,
}),
dated("e-career-draft-b", "career", "career_entry", "", {
datePrecision: "unknown",
status: "draft",
occurredTo: null,
}),
dated("e-rel-2024-05", "relationship", "relationship_change", "2024-05-01"),
dated("e-rel-start", "relationship", "relationship_start", "2024-06-01"),
dated("e-rel-end", "relationship", "relationship_end", "2024-08-08", { datePrecision: "day" }),
dated("e-reloc", "relocation", "home_change", "2022-10-01"),
dated("e-family", "family", "family_event", "2016-05-01"),
].map((item) => ({
...item,
occurredFrom: item.occurredFrom || null,
}));
const OCCUPATION_FOCUS = {
intent: "collect_method_evidence" as const,
targetDomain: "occupation",
targetKind: "occupation_note",
questionId: "collect:occupation:collect_method_evidence",
};
function yearlessProbe(layer: string, gain: number, domain: string) {
return {
probeId: `contrast:varga.${layer}.04:47/05:00`,
candidateSetVersion: "04:47-05:15",
question: `当前几个候选在 ${layer} 上还分得开。`,
expectedOutcomes: [
{ outcomeId: "yes" as const, supportsCandidateIds: ["04:47"], conflictsCandidateIds: ["05:00"] },
{ outcomeId: "no" as const, supportsCandidateIds: ["05:00"], conflictsCandidateIds: ["04:47"] },
],
candidateSplitHash: `varga.${layer}.04:47/05:00`,
informationGain: gain,
sourceFeatures: [{ technique: layer.toUpperCase(), calculationResultId: null }],
domain,
year: null as number | null,
semanticKey: `varga.${layer}.04:47/05:00`,
choiceKind: "existence" as const,
styleOptions: STYLE_OPTIONS.map((item) => ({
label: item.label,
answerClass: item.answer_class,
})),
};
}
const YEARLESS_PACKET = {
candidateSetVersion: "04:47-05:15",
vargaDifferences: [] as const,
probes: [
yearlessProbe("d24", 2.5, "education"),
yearlessProbe("d7", 1.22, "family"),
yearlessProbe("d4", 0.99, "relocation"),
yearlessProbe("d5", 0.5, "education"),
],
};
function occupationPlan(
extra: Partial<Parameters<typeof buildMethodFollowupPlan>[0]> = {},
) {
return buildMethodFollowupPlan({
evidence: CASE_EVIDENCE,
contrastPacket: YEARLESS_PACKET,
candidatesSeparated: false,
...extra,
});
}
function twelveHouses(sign: string, occupant?: string) {
return Array.from({ length: 12 }, (_, index) => ({
house: index + 1,
sign,
occupants: index === 0 && occupant ? [occupant] : [],
}));
}
const CANDIDATE_IDS = [
"88888888-8888-4888-8888-888888888881",
"88888888-8888-4888-8888-888888888882",
] as const;
test("skill version stays 10.0.13 for the occupation-coverage exit fix", () => {
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.13");
});
test("nineteen-row ledger opens the training gate with four scoreable domains", () => {
const gate = trainingScoreableGate(CASE_EVIDENCE);
assert.equal(gate.open, true);
assert.ok(gate.trainingCount >= 3);
assert.ok(gate.trainingDomainCount >= 2);
assert.ok(CASE_EVIDENCE.length >= 15);
});
test("career answers under an occupation collect focus do not cover occupation before ledger norm", () => {
const plan = occupationPlan();
assert.equal(plan.methods.find((item) => item.method_id === "occupation")?.status, "uncovered");
assert.equal(
CASE_EVIDENCE.some((item) => item.domain === "occupation" && item.eventKind === "occupation_note"),
false,
);
});
test("answering occupation collect remaps a career-domain job description onto occupation_note", async () => {
const { applyOccupationCollectLedgerNorm } = await import(
"../src/lib/rectification-agentic/v9/evidence-model.ts"
);
const remapped = applyOccupationCollectLedgerNorm(OCCUPATION_FOCUS, [{
domain: "career",
eventKind: "career_entry" as const,
datePrecision: "unknown" as const,
occurredFrom: null,
occurredTo: null,
summary: "长期做有机合成",
quote: "有机合成",
subject: "self" as const,
}]);
assert.equal(remapped[0]?.domain, "occupation");
assert.equal(remapped[0]?.eventKind, "occupation_note");
const plan = occupationPlan({
evidence: [
...CASE_EVIDENCE,
{
status: "confirmed" as const,
domain: remapped[0]!.domain,
datePrecision: remapped[0]!.datePrecision,
occurredFrom: remapped[0]!.occurredFrom,
occurredTo: remapped[0]!.occurredTo,
eventKind: remapped[0]!.eventKind,
summary: remapped[0]!.summary,
},
],
contrastPacket: { candidateSetVersion: "04:47-05:15", vargaDifferences: [], probes: [] },
});
assert.equal(plan.methods.find((item) => item.method_id === "occupation")?.status, "covered");
assert.notEqual(plan.next_followup?.method_id, "occupation");
assert.notEqual(plan.next_followup?.ask_theme, "occupation");
});
test("occupation coverage fallback uses a closed occupation collect focus plus career evidence", () => {
const uncovered = occupationPlan({
contrastPacket: { candidateSetVersion: "04:47-05:15", vargaDifferences: [], probes: [] },
});
assert.equal(uncovered.methods.find((item) => item.method_id === "occupation")?.status, "uncovered");
const covered = occupationPlan({
contrastPacket: { candidateSetVersion: "04:47-05:15", vargaDifferences: [], probes: [] },
closedCollectFocuses: [{
questionId: OCCUPATION_FOCUS.questionId,
targetDomain: "occupation",
intent: "collect_method_evidence",
status: "resolved",
}],
});
assert.equal(covered.methods.find((item) => item.method_id === "occupation")?.status, "covered");
assert.notEqual(covered.next_followup?.method_id, "occupation");
});
test("career evidence alone still does not cover occupation", () => {
const plan = occupationPlan({
contrastPacket: { candidateSetVersion: "04:47-05:15", vargaDifferences: [], probes: [] },
});
assert.equal(plan.methods.find((item) => item.method_id === "occupation")?.status, "uncovered");
assert.equal(plan.next_followup?.method_id, "occupation");
});
test("training gate open and coverage incomplete can select a yearless varga card", () => {
const plan = occupationPlan();
assert.equal(plan.methods.find((item) => item.method_id === "occupation")?.status, "uncovered");
assert.equal(plan.next_followup?.intent, "distinguish_candidates");
assert.ok(plan.next_followup?.choice_frame);
assert.match(plan.next_followup?.semantic_key ?? "", /^varga\.d(24|7|4|5)\./);
assert.notEqual(plan.next_followup?.method_id, "occupation");
assert.notEqual(plan.next_followup?.source, "method_coverage");
});
test("training gate closed still withholds yearless varga cards", () => {
const short = [
dated("e-edu", "education", "education_start", "2016-09-01"),
dated("e-career", "career", "career_entry", "2020-04-01"),
];
assert.equal(trainingScoreableGate(short).open, false);
const plan = buildMethodFollowupPlan({
evidence: short,
contrastPacket: YEARLESS_PACKET,
candidatesSeparated: false,
});
assert.doesNotMatch(plan.next_followup?.semantic_key ?? "", /^varga\.d(24|7|4|5)\./);
assert.notEqual(plan.next_followup?.intent, "distinguish_candidates");
});
test("training complete with no renderable distinguish card allows offering a range without adopt", () => {
const blocked = decideRectification({
methodCoverageAll: false,
trainingGateOpen: true,
candidateScores: TIED,
discriminatorProbe: null,
engineAcceptAllowed: true,
engineProposeAllowed: true,
});
assert.equal(blocked.canOfferRange, true);
assert.equal(blocked.canAdopt, false);
assert.equal(blocked.canConfirmExactMinute, false);
assert.equal(blocked.nextAction, "offer_provisional_range");
assert.equal(blocked.sessionOutcome, "provisional_range");
const stillClosed = decideRectification({
methodCoverageAll: false,
trainingGateOpen: false,
candidateScores: TIED,
discriminatorProbe: null,
engineAcceptAllowed: true,
engineProposeAllowed: true,
});
assert.equal(stillClosed.canOfferRange, false);
assert.equal(stillClosed.canAdopt, false);
assert.equal(stillClosed.nextAction, "ask_fact_collection");
});
test("decideFromDossier offers a range when training is complete and the ask layer has no card", () => {
const decision = decideFromDossier({
evidence: CASE_EVIDENCE,
conversationSummary: { activeFocus: null, declinedSkippedTopics: [] },
latestResult: {
resultId: "55555555-5555-4555-8555-555555555555",
selectionAllowed: true,
confirmationAllowed: false,
candidates: TIED.map((item, index) => ({
candidateId: `88888888-8888-4888-8888-88888888888${index}`,
time: item.time,
rank: index + 1,
relativeSupport: item.score,
})),
representativeTime: "04:47",
decisionReceipt: {
accept_allowed: true,
propose_allowed: true,
selection_allowed: true,
},
},
case: { acceptedTime: null },
});
assert.equal(decision.canOfferRange, true);
assert.equal(decision.canAdopt, false);
assert.equal(decision.canConfirmExactMinute, false);
});
test("agent route narrates the binary range exit when no renderable distinguish card remains", () => {
const route = readFileSync(new URL("../src/app/api/rectification/agent/route.ts", import.meta.url), "utf8");
assert.match(route, /可以先按当前区间看盘,也可以再补一件记得时间的经历/);
const fastPath = route.slice(
route.indexOf('if (action === "message")'),
route.indexOf("const requestTime"),
);
assert.match(fastPath, /DISCRIMINATOR_EXHAUSTED_NARRATION|isNonConvergingRangeOffer|canOfferRange/);
assert.doesNotMatch(fastPath, /USER_STOP_PATTERN|parseChoiceKeyFromUserMessage/);
});
test("public house_table and natal_recast follow the representative minute", () => {
const matched = parseRectificationCandidateResult({
resultId: "11111111-1111-4111-8111-111111111111",
candidates: [
{ candidateId: CANDIDATE_IDS[0], rank: 1, time: "04:47", relativeSupport: 12, tiedMinuteCount: 1 },
{ candidateId: CANDIDATE_IDS[1], rank: 2, time: "05:00", relativeSupport: 11, tiedMinuteCount: 1 },
],
overallConfidence: "low",
selectionAllowed: true,
confirmationAllowed: false,
representativeTime: "04:47",
selectedTime: null,
decisionReceipt: {
house_table: { time: "05:00", lagna: "双子座", houses: twelveHouses("双子座", "水星") },
house_tables_by_time: {
"04:47": { time: "04:47", lagna: "金牛座", houses: twelveHouses("金牛座", "太阳") },
"05:00": { time: "05:00", lagna: "双子座", houses: twelveHouses("双子座", "水星") },
},
natal_recast: {
time: "05:00",
lagna: "双子座",
user_meaning: "本命宫位已按 05:00 重算(上升 双子座)。下面是本轮实际执行的技法,不能当作唯一分钟确认。",
unique_minute_claim: false,
confirmation_allowed: false,
},
},
});
assert.equal(matched?.representativeTime, "04:47");
assert.equal(matched?.houseTable?.time, "04:47");
assert.equal(matched?.houseTable?.lagna, "金牛座");
assert.equal(matched?.natalRecast?.time, "04:47");
assert.equal(workingRectificationHouseTable(matched!)?.time, "04:47");
});
test("a mismatched receipt house_table is not emitted with another representative minute", () => {
const mismatched = parseRectificationCandidateResult({
resultId: "11111111-1111-4111-8111-111111111111",
candidates: [
{ candidateId: CANDIDATE_IDS[0], rank: 1, time: "04:47", relativeSupport: 12, tiedMinuteCount: 1 },
{ candidateId: CANDIDATE_IDS[1], rank: 2, time: "05:00", relativeSupport: 11, tiedMinuteCount: 1 },
],
overallConfidence: "low",
selectionAllowed: true,
confirmationAllowed: false,
representativeTime: "04:47",
selectedTime: null,
decisionReceipt: {
house_table: { time: "05:00", lagna: "双子座", houses: twelveHouses("双子座", "水星") },
natal_recast: {
time: "05:00",
lagna: "双子座",
user_meaning: "本命宫位已按 05:00 重算(上升 双子座)。下面是本轮实际执行的技法,不能当作唯一分钟确认。",
unique_minute_claim: false,
confirmation_allowed: false,
},
},
});
assert.equal(mismatched?.representativeTime, "04:47");
assert.notEqual(mismatched?.houseTable?.time, "05:00");
assert.equal(mismatched?.houseTable, null);
assert.notEqual(mismatched?.natalRecast?.time, "05:00");
});
test("tool projection does not pair a 05:00 house table with a 04:47 representative time", () => {
const decision = decideRectification({
methodCoverageAll: true,
trainingGateOpen: true,
candidateScores: TIED,
discriminatorProbe: null,
});
const projection = latestResultToolProjection({
resultId: "55555555-5555-4555-8555-555555555555",
candidates: TIED.map((item, index) => ({
candidateId: `88888888-8888-4888-8888-88888888888${index}`,
time: item.time,
rank: index + 1,
relativeSupport: item.score,
tiedMinuteCount: 1,
})),
selectionAllowed: true,
confirmationAllowed: false,
representativeTime: "04:47",
selectedTime: null,
selectionKind: null,
algorithmVersion: "test",
decisionReceipt: {
house_table: { time: "05:00", lagna: "双子座", houses: twelveHouses("双子座", "水星") },
natal_recast: {
time: "05:00",
lagna: "双子座",
user_meaning: "本命宫位已按 05:00 重算(上升 双子座)。",
unique_minute_claim: false,
confirmation_allowed: false,
},
},
}, decision);
assert.notEqual(projection.house_table && (projection.house_table as { time?: string }).time, "05:00");
if (projection.representative_time === "04:47" && projection.house_table) {
assert.equal((projection.house_table as { time?: string }).time, "04:47");
}
});
test("collect focus is still not a renderable choice card", () => {
assert.equal(isRenderableChoiceOpenQuestion({
question_id: OCCUPATION_FOCUS.questionId,
prompt: "你长期做什么工作?",
status: "already_open",
kind: "collect_spoken",
intent: "collect_method_evidence",
domain: "occupation",
focus_id: "11111111-1111-4111-8111-111111111111",
probe_id: null,
}), false);
});