144 lines
5.7 KiB
TypeScript
144 lines
5.7 KiB
TypeScript
import assert from "node:assert/strict";
|
||
import test from "node:test";
|
||
import { extractLifeEventEvidence } from "../src/lib/conversational-rectification/evidence-extractor.ts";
|
||
import { lifeEventEvidenceSchema } from "../src/lib/conversational-rectification/persistence-contracts.ts";
|
||
|
||
const sourceTurnId = "00000000-0000-4000-8000-000000000610";
|
||
|
||
test("preserves raw text and splits two clear facts sharing an explicit month", () => {
|
||
const rawText = "2021年7月毕业并去外地工作";
|
||
const evidence = extractLifeEventEvidence({
|
||
rawText,
|
||
sourceTurnId,
|
||
asOfDate: "2026-07-20",
|
||
});
|
||
|
||
assert.equal(evidence.length, 2);
|
||
assert.deepEqual(evidence.map((item) => item.rawText), [rawText, rawText]);
|
||
assert.deepEqual(evidence.map((item) => item.eventSummary), ["毕业", "去外地工作"]);
|
||
assert.deepEqual(evidence.map((item) => item.domain), ["education", "relocation"]);
|
||
assert.deepEqual(evidence.map((item) => item.dateValue), ["2021-07", "2021-07"]);
|
||
assert.deepEqual(evidence.map((item) => item.datePrecision), ["month", "month"]);
|
||
assert.ok(evidence.every((item) => item.extractionStatus === "clear" && item.scoreable));
|
||
assert.ok(evidence.every((item) => lifeEventEvidenceSchema.safeParse(item).success));
|
||
assert.equal(new Set(evidence.map((item) => item.id)).size, 2);
|
||
});
|
||
|
||
test("keeps vague evidence non-scoreable and asks for clarification", () => {
|
||
const rawText = "那几年工作不太顺";
|
||
const [evidence] = extractLifeEventEvidence({ rawText, sourceTurnId, asOfDate: "2026-07-20" });
|
||
|
||
assert.equal(evidence?.rawText, rawText);
|
||
assert.equal(evidence?.eventSummary, rawText);
|
||
assert.equal(evidence?.dateValue, null);
|
||
assert.equal(evidence?.datePrecision, "unknown");
|
||
assert.equal(evidence?.extractionStatus, "needs_clarification");
|
||
assert.equal(evidence?.scoreable, false);
|
||
});
|
||
|
||
test("preserves a nonblank punctuation-only answer as one clarification row", () => {
|
||
const rawText = "?";
|
||
const evidence = extractLifeEventEvidence({ rawText, sourceTurnId, asOfDate: "2026-07-20" });
|
||
|
||
assert.equal(evidence.length, 1);
|
||
assert.equal(evidence[0]?.rawText, rawText);
|
||
assert.equal(evidence[0]?.extractionStatus, "needs_clarification");
|
||
assert.equal(evidence[0]?.scoreable, false);
|
||
});
|
||
|
||
test("never invents a missing month or day", () => {
|
||
const [evidence] = extractLifeEventEvidence({
|
||
rawText: "2021年毕业",
|
||
sourceTurnId,
|
||
asOfDate: "2026-07-20",
|
||
});
|
||
|
||
assert.equal(evidence?.dateValue, "2021");
|
||
assert.equal(evidence?.datePrecision, "year");
|
||
assert.equal(evidence?.eventSummary, "毕业");
|
||
});
|
||
|
||
test("keeps a bare year as non-scoreable clarification instead of an event summary", () => {
|
||
const rawText = "2021年";
|
||
const [evidence] = extractLifeEventEvidence({
|
||
rawText,
|
||
sourceTurnId,
|
||
asOfDate: "2026-07-20",
|
||
});
|
||
|
||
assert.equal(evidence?.rawText, rawText);
|
||
assert.equal(evidence?.dateValue, "2021");
|
||
assert.equal(evidence?.datePrecision, "year");
|
||
assert.notEqual(evidence?.eventSummary, rawText);
|
||
assert.equal(evidence?.extractionStatus, "needs_clarification");
|
||
assert.equal(evidence?.scoreable, false);
|
||
assert.equal(lifeEventEvidenceSchema.safeParse(evidence).success, true);
|
||
});
|
||
|
||
for (const rawText of [
|
||
"2021年7月毕业并次年工作",
|
||
"2021年7月毕业并后来工作",
|
||
"2021年7月毕业并第二年工作",
|
||
"2021年7月毕业并此前工作",
|
||
"2021年7月毕业然后工作",
|
||
"2021年7月毕业后来又工作",
|
||
]) {
|
||
test(`does not propagate a shared date through an unresolved relative clause: ${rawText}`, () => {
|
||
const evidence = extractLifeEventEvidence({ rawText, sourceTurnId, asOfDate: "2026-07-20" });
|
||
|
||
assert.equal(evidence.length, 2);
|
||
assert.deepEqual(evidence.map((item) => item.eventSummary), ["毕业", "工作"]);
|
||
assert.equal(evidence[0]?.dateValue, "2021-07");
|
||
assert.equal(evidence[0]?.extractionStatus, "clear");
|
||
assert.equal(evidence[0]?.scoreable, true);
|
||
assert.equal(evidence[1]?.dateValue, null);
|
||
assert.equal(evidence[1]?.datePrecision, "unknown");
|
||
assert.equal(evidence[1]?.extractionStatus, "needs_clarification");
|
||
assert.equal(evidence[1]?.scoreable, false);
|
||
});
|
||
}
|
||
|
||
test("marks a future event as context-only and non-scoreable", () => {
|
||
const [evidence] = extractLifeEventEvidence({
|
||
rawText: "2030年3月计划结婚",
|
||
sourceTurnId,
|
||
asOfDate: "2026-07-20",
|
||
});
|
||
|
||
assert.equal(evidence?.dateValue, "2030-03");
|
||
assert.equal(evidence?.datePrecision, "month");
|
||
assert.equal(evidence?.extractionStatus, "clear");
|
||
assert.equal(evidence?.scoreable, false);
|
||
});
|
||
|
||
test("clear replacement evidence is explicitly marked corrected", () => {
|
||
const targetId = "00000000-0000-4000-8000-000000000611";
|
||
const [evidence] = extractLifeEventEvidence({
|
||
rawText: "更正:2020年11月离职",
|
||
sourceTurnId,
|
||
asOfDate: "2026-07-20",
|
||
correctsEvidenceId: targetId,
|
||
});
|
||
|
||
assert.equal(evidence?.eventSummary, "离职");
|
||
assert.equal(evidence?.extractionStatus, "corrected");
|
||
assert.equal(evidence?.scoreable, true);
|
||
assert.deepEqual(evidence?.correctsEvidenceIds, [targetId]);
|
||
assert.equal(lifeEventEvidenceSchema.safeParse(evidence).success, true);
|
||
});
|
||
|
||
test("an unclear correction keeps durable lineage while withholding both old and new scoring", () => {
|
||
const targetId = "00000000-0000-4000-8000-000000000611";
|
||
const [evidence] = extractLifeEventEvidence({
|
||
rawText: "更正:具体年月记不清",
|
||
sourceTurnId,
|
||
asOfDate: "2026-07-20",
|
||
correctsEvidenceId: targetId,
|
||
});
|
||
|
||
assert.equal(evidence?.extractionStatus, "needs_clarification");
|
||
assert.equal(evidence?.scoreable, false);
|
||
assert.deepEqual(evidence?.correctsEvidenceIds, [targetId]);
|
||
assert.equal(lifeEventEvidenceSchema.safeParse(evidence).success, true);
|
||
});
|