Files
Jyotisha/frontend/tests/rectification-coverage-collect.test.ts
T
Jesse_ChenandClaude Opus 5.5 ea21743b09 fix(rectification): stop spoken collect once the training gate opens (BUG-1084..1087)
Once the discriminator training gate is open, only choice cards are asked
and the range card goes out when they are exhausted; targeted lines, their
re-ask and guided windows no longer hold the card or invite more events.
Delivery body says how many choice questions were used instead of the event
fit percent; narration names an excluded cluster instead of "range
unchanged"; a delivered turn no longer carries a collect question.

Offline replay (v4, 3 radii x 2 directions): truth in range 20/20 in every
cell; guided-window injections give the same width in truth and opposite
directions, so red line 1 was revised by product to truth-in-range only.
Skill 10.0.31 -> 10.0.32 (10.0.31 kept as deprecated for pinned cases).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
2026-09-29 10:06:57 +08:00

236 lines
9.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from "node:assert/strict";
import test from "node:test";
import { sessionOutcomeAllowsDelivery } from "../src/lib/rectification-agentic/core/rectification-decision.ts";
import { decideFromDossier, rectificationFollowupCatalog } from "../src/lib/rectification-agentic/v9/decision-from-dossier.ts";
import { buildCaseInferenceState } from "../src/lib/rectification-agentic/v9/inference-adapter.ts";
import {
blockingMethodsCovered,
buildMethodFollowupPlan,
} from "../src/lib/rectification-agentic/v9/method-followup.ts";
import { trainingScoreableGate } from "../src/lib/rectification-agentic/v9/evidence-model.ts";
import { evidenceLedgerFingerprint } from "../src/lib/rectification-agentic/v9/tool-service.ts";
import type { InferenceState } from "../src/lib/rectification-agentic/core/types.ts";
const TRAINING_EVIDENCE = [
{
id: "e-edu",
status: "confirmed",
domain: "education",
datePrecision: "year",
occurredFrom: "2016-01-01",
occurredTo: null,
eventKind: "education_milestone",
},
{
id: "e-career-a",
status: "confirmed",
domain: "career",
datePrecision: "year",
occurredFrom: "2018-01-01",
occurredTo: null,
eventKind: "career_entry",
},
{
id: "e-career-b",
status: "confirmed",
domain: "career",
datePrecision: "year",
occurredFrom: "2020-01-01",
occurredTo: null,
eventKind: "career_change",
},
{
id: "e-rel",
status: "confirmed",
domain: "relationship",
datePrecision: "year",
occurredFrom: "2024-01-01",
occurredTo: null,
eventKind: "relationship_start",
},
] as const;
const FAMILY_EVIDENCE = {
id: "e-fam",
status: "confirmed",
domain: "family",
datePrecision: "year",
occurredFrom: "2023-01-01",
occurredTo: null,
eventKind: "family_event",
} as const;
const OCCUPATION_EVIDENCE = {
id: "e-occ",
status: "confirmed",
domain: "occupation",
datePrecision: "unknown",
occurredFrom: null,
occurredTo: null,
eventKind: "occupation_note",
} as const;
function producedDossier(
evidence: readonly Readonly<{
id: string;
status: string;
domain: string;
datePrecision: string;
occurredFrom: string | null;
occurredTo: string | null;
eventKind?: string | null;
}>[],
options: {
previous?: InferenceState | null;
declinedTopics?: readonly Readonly<Record<string, unknown>>[];
} = {},
) {
const state = buildCaseInferenceState({
range: { start_time: "04:55", end_time: "05:02" },
candidates: [
{ candidateId: "05:02", time: "05:02", relativeSupport: 58 },
{ candidateId: "04:55", time: "04:55", relativeSupport: 42 },
],
evidence,
probes: [],
previous: options.previous,
});
const latest = {
resultId: "55555555-5555-4555-8555-555555555555",
candidates: state.candidates.map((item) => ({
candidateId: item.id,
time: item.time,
rank: item.rank,
relativeSupport: Math.round(item.posterior_score),
})),
representativeTime: state.representative_time,
evidenceLedgerFingerprint: evidenceLedgerFingerprint(evidence as never),
decisionReceipt: {
inference_state: state,
accept_allowed: true,
acceptance_allowed: true,
propose_allowed: true,
selection_allowed: true,
confirmation_allowed: false,
},
};
return {
state,
dossier: {
evidence,
conversationSummary: {
activeFocus: null,
declinedSkippedTopics: options.declinedTopics ?? [],
},
latestResult: latest,
case: { acceptedTime: null },
},
};
}
function collectionFollowup(
dossier: ReturnType<typeof producedDossier>["dossier"],
sessionOutcome: "collect_evidence" | "validate_holdout" | "adopt_representative" = "collect_evidence",
) {
const catalog = rectificationFollowupCatalog(dossier.latestResult, dossier.evidence);
return buildMethodFollowupPlan({
evidence: dossier.evidence,
declinedTopics: dossier.conversationSummary.declinedSkippedTopics,
sessionOutcome,
...catalog,
candidatesSeparated: true,
});
}
test("separated scores with uncovered relatives stay in collection and still ask a blocking method", () => {
const { dossier } = producedDossier(TRAINING_EVIDENCE);
const gate = trainingScoreableGate(TRAINING_EVIDENCE);
assert.equal(gate.open, true);
assert.equal(gate.trainingCount, 3);
assert.ok(gate.trainingDomainCount >= 2);
const decision = decideFromDossier(dossier);
assert.equal(decision.separation.sufficient, true);
// 原值: offer_provisional_range
// 新值: ask_fact_collection
// 原因: D5 让七条线全部轮到 + D6 要求所有线问完才交付;未覆盖的
// relatives / 搬家等仍是待问的线,出卡门槛只是必要条件(BUG-751)
// 原值(2): ask_fact_collection
// 新值(2): offer_provisional_range(回到 BUG-751 之前)
// 原因(2): 门开后定向线与引导题不再挡卡(BUG-1084,2026-09-29 D1),题名原意重新成立
assert.equal(decision.nextAction, "offer_provisional_range");
assert.equal(decision.canConfirmExactMinute, false);
const plan = collectionFollowup(dossier);
assert.equal(blockingMethodsCovered(plan.methods), false);
assert.notEqual(plan.next_followup?.method_id, "relatives");
assert.notEqual(plan.next_followup?.domain, "family");
assert.equal(plan.methods.find((item) => item.method_id === "relatives")?.status, "uncovered");
assert.equal(plan.methods.find((item) => item.method_id === "occupation")?.status, "uncovered");
});
test("covering family without occupation still opens adopt after scores already separated", () => {
const familyOnly = producedDossier([...TRAINING_EVIDENCE, FAMILY_EVIDENCE]);
const familyDecision = decideFromDossier(familyOnly.dossier);
// 原值: ready_to_adopt / adopt_representative / canAdopt=true
// 新值: ask_fact_collection / collect_evidence(canAdopt 仍是引擎能力位,不变)
// 原因: D5 + D6——财务、搬家、健康、职业四条线还没问,交付要等问完(BUG-751)
// 原值(2): ask_fact_collection / collect_evidence
// 新值(2): ready_to_adopt / adopt_representative(回到 BUG-751 之前)
// 原因(2): 门开后定向线与引导题不再挡卡(BUG-1084,2026-09-29 D1),题名原意重新成立
assert.equal(familyDecision.nextAction, "ready_to_adopt");
assert.equal(familyDecision.sessionOutcome, "adopt_representative");
assert.equal(familyDecision.canAdopt, true);
assert.equal(familyDecision.canConfirmExactMinute, false);
});
test("covering family and occupation still opens adopt after scores already separated", () => {
const { state, dossier: collecting } = producedDossier(TRAINING_EVIDENCE);
// 原值: offer_provisional_range
// 新值: ask_fact_collection
// 原因: D5 + D6——七条线还没问完(BUG-751)
// 原值(2): ask_fact_collection / 新值(2): offer_provisional_range
// 原因(2): 门开后定向线与引导题不再挡卡(BUG-1084,2026-09-29 D1),题名原意重新成立
assert.equal(decideFromDossier(collecting).nextAction, "offer_provisional_range");
const covered = [...TRAINING_EVIDENCE, FAMILY_EVIDENCE, OCCUPATION_EVIDENCE];
const { dossier } = producedDossier(covered, { previous: state });
const decision = decideFromDossier(dossier);
// 原值: S3(ready_to_adopt 或 offer_provisional_range)、canAdopt=true
// 新值: ask_fact_collection(canAdopt 仍是引擎能力位,不变)
// 原因: 同上——覆盖家人与职业后,剩下五条线仍要问(BUG-751)
// 原值(2): ask_fact_collection / collect_evidence
// 新值(2): S3 交付(ready_to_adopt 或 offer_provisional_range,sessionOutcome 允许交付)
// 原因(2): 门开后定向线与引导题不再挡卡(BUG-1084,2026-09-29 D1),题名原意重新成立
assert.ok(decision.nextAction === "ready_to_adopt" || decision.nextAction === "offer_provisional_range", decision.nextAction);
assert.equal(sessionOutcomeAllowsDelivery(decision.sessionOutcome), true);
assert.equal(decision.canAdopt, true);
assert.equal(decision.canConfirmExactMinute, false);
assert.equal(decision.separation.sufficient, true);
});
test("declining family and occupation also opens adopt after scores already separated", () => {
const declinedTopics = [
{ targetDomain: "family" },
{ target_domain: "occupation" },
];
const { dossier } = producedDossier(TRAINING_EVIDENCE, { declinedTopics });
const plan = collectionFollowup(dossier);
assert.equal(blockingMethodsCovered(plan.methods), true);
assert.equal(plan.methods.find((item) => item.method_id === "relatives")?.status, "covered");
assert.equal(plan.methods.find((item) => item.method_id === "occupation")?.status, "covered");
const decision = decideFromDossier(dossier);
// 原值: ready_to_adopt / adopt_representative / canAdopt=true
// 新值: ask_fact_collection / collect_evidence(canAdopt 仍是引擎能力位,不变)
// 原因: D5 + D6——只拒答了家人与职业,其余五条线仍在引导池里(BUG-751)
// 原值(2): ask_fact_collection / collect_evidence
// 新值(2): ready_to_adopt / adopt_representative(回到 BUG-751 之前)
// 原因(2): 门开后定向线与引导题不再挡卡(BUG-1084,2026-09-29 D1),题名原意重新成立
assert.equal(decision.nextAction, "ready_to_adopt");
assert.equal(decision.sessionOutcome, "adopt_representative");
assert.equal(decision.canAdopt, true);
assert.equal(decision.canConfirmExactMinute, false);
});