Files
Jyotisha/frontend/tests/rectification-coverage-collect.test.ts
T

198 lines
6.5 KiB
TypeScript

import assert from "node:assert/strict";
import test from "node:test";
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 UNCOVERED_BLOCKING = new Set(["relatives", "occupation"]);
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 },
};
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);
assert.equal(decision.nextAction, "ask_fact_collection");
assert.equal(decision.canConfirmExactMinute, false);
const plan = collectionFollowup(dossier);
assert.equal(blockingMethodsCovered(plan.methods), false);
assert.ok(plan.next_followup, "collection must still have a question");
assert.ok(
UNCOVERED_BLOCKING.has(plan.next_followup.method_id),
`expected relatives or occupation, got ${plan.next_followup.method_id}`,
);
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 and occupation leaves collection after scores already separated", () => {
const { state, dossier: collecting } = producedDossier(TRAINING_EVIDENCE);
assert.equal(decideFromDossier(collecting).nextAction, "ask_fact_collection");
const covered = [...TRAINING_EVIDENCE, FAMILY_EVIDENCE, OCCUPATION_EVIDENCE];
const { dossier } = producedDossier(covered, { previous: state });
const decision = decideFromDossier(dossier);
assert.notEqual(decision.nextAction, "ask_fact_collection");
assert.ok(
decision.nextAction === "ask_holdout_validation" || decision.nextAction === "ready_to_adopt",
decision.nextAction,
);
assert.equal(decision.canConfirmExactMinute, false);
assert.equal(decision.separation.sufficient, true);
});
test("declining family and occupation also leaves collection 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);
assert.notEqual(decision.nextAction, "ask_fact_collection");
assert.ok(
decision.nextAction === "ask_holdout_validation" || decision.nextAction === "ready_to_adopt",
decision.nextAction,
);
assert.equal(decision.canConfirmExactMinute, false);
});