fix(rectification): reject pre-birth scoring evidence
This commit is contained in:
@@ -24,6 +24,7 @@ const resultId = "00000000-0000-4000-8000-000000000708";
|
||||
const laterActionId = "00000000-0000-4000-8000-000000000710";
|
||||
const secondAnswerActionId = "00000000-0000-4000-8000-000000000711";
|
||||
const thirdAnswerActionId = "00000000-0000-4000-8000-000000000712";
|
||||
const fourthAnswerActionId = "00000000-0000-4000-8000-000000000713";
|
||||
|
||||
const declaredBirthInput = {
|
||||
source: "approximate" as const,
|
||||
@@ -163,6 +164,7 @@ function harness(options: {
|
||||
expectedVersion?: number;
|
||||
commandFingerprint?: string;
|
||||
}>();
|
||||
const packetEvidenceCounts: number[] = [];
|
||||
let packetBuilds = 0;
|
||||
let reserveCount = 0;
|
||||
let releaseCount = 0;
|
||||
@@ -355,6 +357,7 @@ function harness(options: {
|
||||
},
|
||||
async buildTechnicalPacket(input) {
|
||||
packetBuilds += 1;
|
||||
packetEvidenceCounts.push(input.evidence.length);
|
||||
events.push(input.evidence.length > 0 ? "score-packet" : "packet");
|
||||
if (options.packetFailure) throw options.packetFailure;
|
||||
return input.evidence.length >= (options.readyAfterEvidenceCount ?? 1)
|
||||
@@ -368,6 +371,7 @@ function harness(options: {
|
||||
return {
|
||||
events,
|
||||
mutations,
|
||||
packetEvidenceCounts,
|
||||
cases,
|
||||
service: createConversationalRectificationService(ports),
|
||||
counts: () => ({ packetBuilds, reserveCount, releaseCount }),
|
||||
@@ -553,6 +557,37 @@ test("family evidence remains stored and public without changing its domain", as
|
||||
assert.equal(turn.status, "active");
|
||||
});
|
||||
|
||||
test("two valid events plus pre-birth evidence wait until a later valid third event scores", async () => {
|
||||
const value = harness({ readyAfterEvidenceCount: 3 });
|
||||
await start(value, null);
|
||||
const answers = [
|
||||
[answerActionId, "2019年7月毕业"],
|
||||
[secondAnswerActionId, "2020年8月搬家"],
|
||||
[thirdAnswerActionId, "1999年12月开始工作"],
|
||||
[fourthAnswerActionId, "2021年9月换工作"],
|
||||
] as const;
|
||||
|
||||
let latest = value.cases.get(startActionId)?.row.latestTurn;
|
||||
for (const [index, [receivedActionId, answer]] of answers.entries()) {
|
||||
latest = await value.service.answer(userId, {
|
||||
type: "answer",
|
||||
caseId: startActionId,
|
||||
actionId: receivedActionId,
|
||||
turnVersion: index,
|
||||
answer,
|
||||
});
|
||||
assert.equal(latest.status, index < 3 ? "active" : "confirming");
|
||||
}
|
||||
|
||||
const stored = value.cases.get(startActionId)?.row;
|
||||
const preBirth = stored?.eventEvidence.find((item) => item.dateValue === "1999-12");
|
||||
assert.equal(preBirth?.scoreable, false);
|
||||
assert.equal(preBirth?.extractionStatus, "needs_clarification");
|
||||
assert.equal(stored?.eventEvidence.length, 4);
|
||||
assert.equal(latest?.evidenceRecap.length, 4);
|
||||
assert.deepEqual(value.packetEvidenceCounts, [0, 1, 2, 3]);
|
||||
});
|
||||
|
||||
test("vague, future, and unmatched answers stay conversational and never score", async () => {
|
||||
for (const [answer, domain] of [
|
||||
["后来换了工作", undefined],
|
||||
|
||||
@@ -70,14 +70,16 @@ function service(overrides: Partial<BirthTimeConversationRouteService> = {}): Bi
|
||||
function syntheticEvidence(
|
||||
index: number,
|
||||
domain: LifeEventEvidence["domain"],
|
||||
dateValue = `${2010 + index}-07`,
|
||||
datePrecision: LifeEventEvidence["datePrecision"] = "month",
|
||||
): LifeEventEvidence {
|
||||
return {
|
||||
id: `00000000-0000-4000-8000-${String(800 + index).padStart(12, "0")}`,
|
||||
rawText: `synthetic event ${index}`,
|
||||
domain,
|
||||
eventSummary: `synthetic summary ${index}`,
|
||||
dateValue: `${2010 + index}-07`,
|
||||
datePrecision: "month",
|
||||
dateValue,
|
||||
datePrecision,
|
||||
extractionStatus: "clear",
|
||||
scoreable: true,
|
||||
};
|
||||
@@ -124,6 +126,15 @@ function packetEngine(options: {
|
||||
},
|
||||
async score() { throw new Error("unexpected questionnaire score"); },
|
||||
async scoreEvents(input) {
|
||||
assert.ok(input.events.length >= 3 && input.events.length <= 6);
|
||||
for (const event of input.events) {
|
||||
const birthBoundary = event.precision === "year"
|
||||
? input.birthDate.slice(0, 4)
|
||||
: event.precision === "month"
|
||||
? input.birthDate.slice(0, 7)
|
||||
: input.birthDate;
|
||||
assert.ok(event.date >= birthBoundary, "synthetic scorer rejected pre-birth evidence");
|
||||
}
|
||||
options.scoreCalls?.push([...input.events]);
|
||||
return {
|
||||
resultId: "00000000-0000-4000-8000-000000000899",
|
||||
@@ -553,3 +564,78 @@ test("a single period-only scan filters duplicate and out-of-range samples from
|
||||
assert.deepEqual(built.packet.candidate.range, { startTime: "08:00", endTime: "11:59" });
|
||||
assert.deepEqual(built.packet.sensitivityScope.sampleTimes, ["08:00", "10:00"]);
|
||||
});
|
||||
|
||||
test("year-precision evidence before birth waits while the birth year can become the valid third event", async () => {
|
||||
const scoreCalls: LifeEvent[][] = [];
|
||||
const engine = packetEngine({ scoreCalls });
|
||||
const valid = [
|
||||
syntheticEvidence(20, "education", "2018", "year"),
|
||||
syntheticEvidence(21, "relocation", "2019", "year"),
|
||||
];
|
||||
const beforeBirth = syntheticEvidence(22, "career", "1999", "year");
|
||||
const birthYear = syntheticEvidence(23, "relationship", "2000", "year");
|
||||
const input = {
|
||||
userId,
|
||||
caseId,
|
||||
asOfDate: "2026-07-21",
|
||||
declaredBirthInput: {
|
||||
source: "approximate" as const,
|
||||
birthDate: "2000-06-15",
|
||||
reportedTime: "05:20",
|
||||
uncertaintyBeforeMinutes: 30 as const,
|
||||
uncertaintyAfterMinutes: 30 as const,
|
||||
birthTimeClue: null,
|
||||
birthplace: packetBirthplace,
|
||||
},
|
||||
privateCandidate: null,
|
||||
};
|
||||
|
||||
const waiting = await buildProductionConversationalRectificationPacket(engine, {
|
||||
...input,
|
||||
evidence: [...valid, beforeBirth],
|
||||
});
|
||||
assert.equal(waiting.resultId, null);
|
||||
assert.equal(scoreCalls.length, 0);
|
||||
|
||||
await buildProductionConversationalRectificationPacket(engine, {
|
||||
...input,
|
||||
evidence: [...valid, beforeBirth, birthYear],
|
||||
});
|
||||
assert.deepEqual(scoreCalls.map((events) => events.map((event) => event.id)), [[
|
||||
...valid.map((item) => item.id),
|
||||
birthYear.id,
|
||||
]]);
|
||||
});
|
||||
|
||||
test("month-precision evidence excludes the month before birth and accepts the birth month", async () => {
|
||||
const scoreCalls: LifeEvent[][] = [];
|
||||
const engine = packetEngine({ scoreCalls });
|
||||
const valid = [
|
||||
syntheticEvidence(30, "education", "2018-01", "month"),
|
||||
syntheticEvidence(31, "relocation", "2019-02", "month"),
|
||||
];
|
||||
const monthBeforeBirth = syntheticEvidence(32, "career", "2000-05", "month");
|
||||
const birthMonth = syntheticEvidence(33, "relationship", "2000-06", "month");
|
||||
|
||||
await buildProductionConversationalRectificationPacket(engine, {
|
||||
userId,
|
||||
caseId,
|
||||
asOfDate: "2026-07-21",
|
||||
declaredBirthInput: {
|
||||
source: "approximate",
|
||||
birthDate: "2000-06-15",
|
||||
reportedTime: "05:20",
|
||||
uncertaintyBeforeMinutes: 30,
|
||||
uncertaintyAfterMinutes: 30,
|
||||
birthTimeClue: null,
|
||||
birthplace: packetBirthplace,
|
||||
},
|
||||
privateCandidate: null,
|
||||
evidence: [...valid, monthBeforeBirth, birthMonth],
|
||||
});
|
||||
|
||||
assert.deepEqual(scoreCalls.map((events) => events.map((event) => event.id)), [[
|
||||
...valid.map((item) => item.id),
|
||||
birthMonth.id,
|
||||
]]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user