Files
Jyotisha/frontend/tests/birth-time-journey-actions.test.ts
T
2026-07-18 18:40:23 +08:00

213 lines
7.6 KiB
TypeScript

import assert from "node:assert/strict";
import test from "node:test";
import { StaleJourneyTurnError } from "../src/lib/birth-time-journey-turn-persistence.ts";
import {
confirmActionId,
draftActionId,
existingCareerEvent,
existingEducationEvent,
guidedCase,
journeyCaseId,
lowCandidate,
progressionService,
secondActionId,
thirdActionId,
} from "./birth-time-journey-test-support.ts";
test("confirmed draft advances baseline to the next planned question", async () => {
const flow = progressionService(guidedCase());
const proposed = await flow.service.proposeEvidenceDraft(
"user-1", journeyCaseId, draftActionId, 0,
{ domain: "career", precision: "month", date: "2019-07" },
);
const confirmed = await flow.service.confirmEvidenceDraft(
"user-1", journeyCaseId, confirmActionId, proposed.turnVersion,
proposed.evidenceDraft?.draftId ?? "",
);
assert.equal(confirmed.progress.confirmedEvidenceCount, 1);
assert.equal(confirmed.nextAction.kind, "ask_baseline_evidence");
assert.equal(confirmed.turnVersion, 2);
assert.equal(flow.scoreEventsCalls(), 0);
});
test("third confirmed baseline event persists score pending without engine work", async () => {
const flow = progressionService(guidedCase({
version: 4,
domain: "relationship",
askedDomains: ["education", "career"],
lifeEvents: [existingEducationEvent, existingCareerEvent],
}));
const proposed = await flow.service.proposeEvidenceDraft(
"user-1", journeyCaseId, draftActionId, 4,
{ domain: "relationship", precision: "year", date: "2021" },
);
const draftId = proposed.evidenceDraft?.draftId ?? "";
const confirmed = await flow.service.confirmEvidenceDraft(
"user-1", journeyCaseId, confirmActionId, proposed.turnVersion, draftId,
);
assert.equal(confirmed.nextAction.kind, "score_pending");
assert.equal(confirmed.lifeEvents[2]?.id, draftId);
assert.equal(flow.scoreEventsCalls(), 0);
});
test("draft proposal cannot change the server-selected domain", async () => {
const flow = progressionService(guidedCase());
await assert.rejects(flow.service.proposeEvidenceDraft(
"user-1", journeyCaseId, draftActionId, 0,
{ domain: "relationship", precision: "year", date: "2021" },
));
assert.equal(flow.memory.committedTurnWrites(), 0);
});
test("incomplete draft can be replaced for the same question and confirmed", async () => {
const flow = progressionService(guidedCase());
const incomplete = await flow.service.proposeEvidenceDraft(
"user-1", journeyCaseId, draftActionId, 0,
{ domain: "career", precision: null, date: null },
);
const replacement = await flow.service.proposeEvidenceDraft(
"user-1", journeyCaseId, secondActionId, incomplete.turnVersion,
{ domain: "career", precision: "year", date: "2019" },
);
const confirmed = await flow.service.confirmEvidenceDraft(
"user-1", journeyCaseId, confirmActionId, replacement.turnVersion,
replacement.evidenceDraft?.draftId ?? "",
);
assert.equal(replacement.evidenceDraft?.questionId, incomplete.evidenceDraft?.questionId);
assert.equal(confirmed.lifeEvents[0]?.id, replacement.evidenceDraft?.draftId);
assert.deepEqual(flow.memory.savedCase()?.persistedProgress?.askedDomains, ["career"]);
});
test("incomplete draft can be skipped and discarded", async () => {
const flow = progressionService(guidedCase());
const incomplete = await flow.service.proposeEvidenceDraft(
"user-1", journeyCaseId, draftActionId, 0,
{ domain: "career", precision: null, date: null },
);
const skipped = await flow.service.skipEvidenceQuestion(
"user-1", journeyCaseId, secondActionId, incomplete.turnVersion,
);
assert.equal(skipped.evidenceDraft, null);
assert.equal(skipped.nextAction.kind, "ask_baseline_evidence");
assert.deepEqual(flow.memory.savedCase()?.persistedProgress?.askedDomains, ["career"]);
});
test("baseline skip marks its domain without consuming an adaptive round", async () => {
const flow = progressionService(guidedCase());
const result = await flow.service.skipEvidenceQuestion(
"user-1", journeyCaseId, draftActionId, 0,
);
assert.equal(result.nextAction.kind, "ask_baseline_evidence");
assert.equal(result.progress.adaptiveRound, 0);
assert.deepEqual(flow.memory.savedCase()?.persistedProgress?.askedDomains, ["career"]);
});
test("adaptive skips consume displayed rounds once and terminate at three", async () => {
const flow = progressionService(guidedCase({
phase: "adaptive",
domain: "relationship",
adaptiveRound: 1,
askedDomains: ["education", "relocation"],
lifeEvents: [existingEducationEvent, existingCareerEvent],
candidateResult: lowCandidate,
}));
const second = await flow.service.skipEvidenceQuestion(
"user-1", journeyCaseId, draftActionId, 0,
);
const third = await flow.service.skipEvidenceQuestion(
"user-1", journeyCaseId, secondActionId, second.turnVersion,
);
const terminal = await flow.service.skipEvidenceQuestion(
"user-1", journeyCaseId, thirdActionId, third.turnVersion,
);
assert.equal(second.progress.adaptiveRound, 2);
assert.equal(third.progress.adaptiveRound, 3);
assert.equal(terminal.nextAction.kind, "present_low_result");
});
test("finish preserves evidence and exposes terminal low", async () => {
const flow = progressionService(guidedCase({ lifeEvents: [existingEducationEvent] }));
const finished = await flow.service.finishWithCurrentRange(
"user-1", journeyCaseId, draftActionId, 0,
);
assert.equal(finished.nextAction.kind, "present_low_result");
assert.deepEqual(finished.lifeEvents, [existingEducationEvent]);
});
test("duplicate draft action returns persisted turn without a second write", async () => {
const flow = progressionService(guidedCase());
const first = await flow.service.proposeEvidenceDraft(
"user-1", journeyCaseId, draftActionId, 0,
{ domain: "career", precision: "year", date: "2019" },
);
const duplicate = await flow.service.proposeEvidenceDraft(
"user-1", journeyCaseId, draftActionId, 0,
{ domain: "career", precision: "year", date: "2019" },
);
assert.deepEqual(duplicate.nextAction, first.nextAction);
assert.equal(flow.memory.committedTurnWrites(), 1);
});
test("stale structured action cannot overwrite the current turn", async () => {
const flow = progressionService(guidedCase({ version: 5 }));
await assert.rejects(
flow.service.skipEvidenceQuestion("user-1", journeyCaseId, draftActionId, 4),
StaleJourneyTurnError,
);
assert.equal(flow.memory.savedCase()?.turnVersion, 5);
assert.equal(flow.memory.committedTurnWrites(), 0);
});
test("duplicate structured revision replays even after the draft was confirmed", async () => {
const flow = progressionService(guidedCase());
const proposed = await flow.service.proposeEvidenceDraft(
"user-1", journeyCaseId, draftActionId, 0,
{ domain: "career", precision: null, date: null },
);
const revised = await flow.service.reviseEvidenceDraft({
userId: "user-1",
caseId: journeyCaseId,
actionId: secondActionId,
expectedVersion: proposed.turnVersion,
precision: "year",
date: "2019",
});
const confirmed = await flow.service.confirmEvidenceDraft(
"user-1", journeyCaseId, thirdActionId, revised.turnVersion,
revised.evidenceDraft?.draftId ?? "",
);
const replay = await flow.service.reviseEvidenceDraft({
userId: "user-1",
caseId: journeyCaseId,
actionId: secondActionId,
expectedVersion: proposed.turnVersion,
precision: "year",
date: "2019",
});
assert.equal(replay.turnVersion, confirmed.turnVersion);
assert.deepEqual(replay.nextAction, confirmed.nextAction);
assert.equal(flow.memory.committedTurnWrites(), 3);
});