135 lines
6.5 KiB
TypeScript
135 lines
6.5 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import { randomUUID } from "node:crypto";
|
|
import { buildCandidateClusters } from "../src/lib/rectification-v4/candidate-clusters.ts";
|
|
import { dateRangeFromDeclared, sampledDates } from "../src/lib/rectification-v4/date-range.ts";
|
|
import { evaluateDecisionGate } from "../src/lib/rectification-v4/decision-gate.ts";
|
|
import { appendEventRevision, latestEventRevisions } from "../src/lib/rectification-v4/evidence-ledger.ts";
|
|
import { extractV4EventRevisions } from "../src/lib/rectification-v4/extraction.ts";
|
|
import { planNextQuestion } from "../src/lib/rectification-v4/question-planner.ts";
|
|
|
|
const now = new Date("2026-07-26T00:00:00.000Z");
|
|
|
|
test("declared month, quarter and year retain real boundaries instead of invented midpoints", () => {
|
|
assert.deepEqual(dateRangeFromDeclared("2024-02", "month"), {
|
|
start: "2024-02-01", end: "2024-02-29", precision: "month", label: "2024-02",
|
|
});
|
|
assert.deepEqual(dateRangeFromDeclared("2024-Q2", "quarter"), {
|
|
start: "2024-04-01", end: "2024-06-30", precision: "quarter", label: "2024-Q2",
|
|
});
|
|
assert.deepEqual(dateRangeFromDeclared("2024", "year"), {
|
|
start: "2024-01-01", end: "2024-12-31", precision: "year", label: "2024",
|
|
});
|
|
assert.equal(sampledDates(dateRangeFromDeclared("2024-02", "month")).includes("2024-02-15"), false);
|
|
});
|
|
|
|
test("relationship start and end remain separate immutable events", () => {
|
|
const startId = randomUUID();
|
|
const endId = randomUUID();
|
|
const start = appendEventRevision([], {
|
|
eventId: startId, domain: "relationship", eventKind: "relationship_start", summary: "关系开始",
|
|
rawText: "2024年5月开始", dateRange: dateRangeFromDeclared("2024-05", "month"),
|
|
}, { id: randomUUID(), now });
|
|
const end = appendEventRevision([start], {
|
|
eventId: endId, domain: "relationship", eventKind: "relationship_end", summary: "关系结束",
|
|
rawText: "2024年8月结束", dateRange: dateRangeFromDeclared("2024-08", "month"),
|
|
}, { id: randomUUID(), now });
|
|
assert.notEqual(start.eventId, end.eventId);
|
|
assert.equal(start.eventKind, "relationship_start");
|
|
assert.equal(end.eventKind, "relationship_end");
|
|
});
|
|
|
|
test("family evidence is retained explicitly as context only", () => {
|
|
const revision = appendEventRevision([], {
|
|
eventId: randomUUID(), domain: "family", eventKind: "family_event", summary: "家庭变化",
|
|
rawText: "家庭发生变化", dateRange: dateRangeFromDeclared("2020", "year"),
|
|
}, { id: randomUUID(), now });
|
|
assert.equal(revision.scoreability, "context_only");
|
|
});
|
|
|
|
test("candidate minutes merge into ranked contiguous clusters", () => {
|
|
const id = randomUUID();
|
|
const clusters = buildCandidateClusters([
|
|
{ time: "05:13", score: 100, supportingEventIds: [id], conflictingEventIds: [] },
|
|
{ time: "05:14", score: 99, supportingEventIds: [id], conflictingEventIds: [] },
|
|
{ time: "05:15", score: 98, supportingEventIds: [id], conflictingEventIds: [] },
|
|
{ time: "05:16", score: 70, supportingEventIds: [], conflictingEventIds: [id] },
|
|
{ time: "05:17", score: 97, supportingEventIds: [id], conflictingEventIds: [] },
|
|
{ time: "05:18", score: 97, supportingEventIds: [id], conflictingEventIds: [] },
|
|
]);
|
|
assert.deepEqual(clusters.map((cluster) => [cluster.rank, cluster.startTime, cluster.endTime]), [
|
|
[1, "05:13", "05:15"], [2, "05:17", "05:18"],
|
|
]);
|
|
});
|
|
|
|
test("decision gate can accept a stable range but never an exact minute", () => {
|
|
const result = evaluateDecisionGate({
|
|
clusters: [{ rank: 1, startTime: "05:13", endTime: "05:15", representativeTime: "05:13", widthMinutes: 3, peakScore: 10, scoreMass: 29 }],
|
|
robustness: { neighborSupportMinutes: 3, leaveOneOutRetentionRate: 1, dateSensitivityRetentionRate: 0.9, calculationSpecHashMatched: true },
|
|
scoreableEventCount: 10,
|
|
scoreableDomainCount: 5,
|
|
});
|
|
assert.equal(result.canAcceptRange, true);
|
|
assert.equal(result.canConfirmExactMinute, false);
|
|
});
|
|
|
|
test("question planner asks one deterministic low-recall question at a time", () => {
|
|
const question = planNextQuestion({
|
|
askedDomains: ["education"], coveredDomains: ["education"],
|
|
candidateSplitByDomain: { relocation: 0.8, relationship: 0.2 }, id: randomUUID(),
|
|
});
|
|
assert.equal(question.domain, "relocation");
|
|
assert.equal(question.targetEventId, null);
|
|
assert.equal(question.prompt.includes("搬家"), true);
|
|
});
|
|
|
|
test("question planner refines imprecise scoreable events after domain coverage", () => {
|
|
const eventId = randomUUID();
|
|
const event = appendEventRevision([], {
|
|
eventId, domain: "education", eventKind: "education_milestone", summary: "高中毕业",
|
|
rawText: "2016年高中毕业", dateRange: dateRangeFromDeclared("2016", "year"),
|
|
}, { id: randomUUID(), now });
|
|
const question = planNextQuestion({
|
|
askedDomains: ["education", "relocation", "relationship", "career", "finance", "health_pressure", "family"],
|
|
coveredDomains: ["education", "relocation", "relationship", "career", "finance", "health_pressure", "family"],
|
|
events: [event],
|
|
attemptedRefinementEventIds: [],
|
|
id: randomUUID(),
|
|
});
|
|
assert.equal(question.targetEventId, eventId);
|
|
assert.equal(question.prompt.includes("高中毕业"), true);
|
|
|
|
const fallback = planNextQuestion({
|
|
askedDomains: ["education", "relocation", "relationship", "career", "finance", "health_pressure", "family"],
|
|
coveredDomains: ["education", "relocation", "relationship", "career", "finance", "health_pressure", "family"],
|
|
events: [event],
|
|
attemptedRefinementEventIds: [eventId],
|
|
id: randomUUID(),
|
|
});
|
|
assert.equal(fallback.targetEventId, null);
|
|
assert.equal(fallback.domain, "other");
|
|
assert.equal(fallback.prompt.includes("暂停"), true);
|
|
});
|
|
|
|
test("targeted date answer appends a revision without duplicating the scoreable event", () => {
|
|
const eventId = randomUUID();
|
|
const original = appendEventRevision([], {
|
|
eventId, domain: "education", eventKind: "education_milestone", summary: "高中毕业",
|
|
rawText: "2016年高中毕业", dateRange: dateRangeFromDeclared("2016", "year"),
|
|
}, { id: randomUUID(), now });
|
|
const revisions = extractV4EventRevisions({
|
|
answer: "2016年6月8日",
|
|
sourceTurnId: randomUUID(),
|
|
asOfDate: "2026-07-26",
|
|
existing: [original],
|
|
targetEventId: eventId,
|
|
now,
|
|
});
|
|
assert.equal(revisions.length, 1);
|
|
assert.equal(revisions[0]?.eventId, eventId);
|
|
assert.equal(revisions[0]?.revision, 2);
|
|
assert.equal(revisions[0]?.dateRange.precision, "day");
|
|
assert.equal(revisions[0]?.dateRange.start, "2016-06-08");
|
|
assert.equal(latestEventRevisions([original, ...revisions]).length, 1);
|
|
});
|