fix: preserve dynamic action receipts
This commit is contained in:
@@ -11,13 +11,16 @@ import type { JourneyClientResponse } from "@/lib/birth-time-journey-client";
|
||||
import {
|
||||
createIdentityRequestCache,
|
||||
publishCurrentJourney,
|
||||
runStableJourneyAction,
|
||||
scheduleCancellableStart,
|
||||
} from "@/lib/birth-time-guided-effect-coordinator";
|
||||
import type { StableActionIdentityRegistry } from "@/lib/birth-time-guided-effect-coordinator";
|
||||
import { runBirthTimeScoringPoll, scoringPollDelay } from "@/lib/birth-time-guided-polling";
|
||||
|
||||
type AutomaticEffectsInput = {
|
||||
readonly journey: JourneyClientResponse | null;
|
||||
readonly latest: { current: JourneyClientResponse | null };
|
||||
readonly actionRegistry: StableActionIdentityRegistry;
|
||||
readonly preview: boolean;
|
||||
readonly pollRun: number;
|
||||
readonly generationRun: number;
|
||||
@@ -25,9 +28,6 @@ type AutomaticEffectsInput = {
|
||||
readonly setError: (message: string) => void;
|
||||
};
|
||||
|
||||
const guideRequests = createIdentityRequestCache<Awaited<ReturnType<typeof requestBirthTimeGuidePrompt>>>();
|
||||
const generationRequests = createIdentityRequestCache<Awaited<ReturnType<typeof generateDynamicBirthTimeQuestion>>>();
|
||||
|
||||
function fallbackQuestion(turn: JourneyClientResponse): string {
|
||||
const action = turn.nextAction;
|
||||
if (turn.journeyProtocol === "dynamic-choice-v2") {
|
||||
@@ -39,7 +39,9 @@ function fallbackQuestion(turn: JourneyClientResponse): string {
|
||||
}
|
||||
|
||||
export function useBirthTimeAutomaticJourneyEffects(input: AutomaticEffectsInput): string {
|
||||
const { generationRun, journey, latest, onJourney, pollRun, preview, setError } = input;
|
||||
const { actionRegistry, generationRun, journey, latest, onJourney, pollRun, preview, setError } = input;
|
||||
const [guideRequests] = useState(() => createIdentityRequestCache<Awaited<ReturnType<typeof requestBirthTimeGuidePrompt>>>());
|
||||
const [generationRequests] = useState(() => createIdentityRequestCache<Awaited<ReturnType<typeof generateDynamicBirthTimeQuestion>>>());
|
||||
const [agentQuestion, setAgentQuestion] = useState<{ readonly key: string; readonly text: string } | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -55,7 +57,7 @@ export function useBirthTimeAutomaticJourneyEffects(input: AutomaticEffectsInput
|
||||
}
|
||||
}).catch(() => { if (active) setAgentQuestion(null); });
|
||||
return () => { active = false; };
|
||||
}, [journey, preview]);
|
||||
}, [guideRequests, journey, preview]);
|
||||
|
||||
const generationIdentity = journey?.journeyProtocol === "dynamic-choice-v2"
|
||||
&& (journey.nextAction.kind === "generate_dynamic_question"
|
||||
@@ -70,18 +72,20 @@ export function useBirthTimeAutomaticJourneyEffects(input: AutomaticEffectsInput
|
||||
&& turn.nextAction.kind !== "retry_question_generation") return;
|
||||
const expected = turn;
|
||||
const key = `${turn.caseId}:${turn.turnVersion}`;
|
||||
void generationRequests.run(key, () => generateDynamicBirthTimeQuestion(
|
||||
turn.caseId,
|
||||
globalThis.crypto.randomUUID(),
|
||||
turn.turnVersion,
|
||||
)).then((next) => {
|
||||
void generationRequests.run(key, () => runStableJourneyAction(actionRegistry, {
|
||||
caseId: turn.caseId,
|
||||
turnVersion: turn.turnVersion,
|
||||
operation: "generate_dynamic_question",
|
||||
}, (actionId) => generateDynamicBirthTimeQuestion(
|
||||
turn.caseId, actionId, turn.turnVersion,
|
||||
))).then((next) => {
|
||||
if (publishCurrentJourney({ expected, current: latest.current, next, publish: onJourney })) latest.current = next;
|
||||
}).catch((caught: unknown) => {
|
||||
if (latest.current?.caseId === expected.caseId && latest.current.turnVersion === expected.turnVersion) {
|
||||
setError(caught instanceof Error ? caught.message : "暂时无法生成下一题,请重试。");
|
||||
}
|
||||
});
|
||||
}, [generationIdentity, generationRun, latest, onJourney, preview, setError]);
|
||||
}, [actionRegistry, generationIdentity, generationRequests, generationRun, latest, onJourney, preview, setError]);
|
||||
|
||||
const pollIdentity = journey?.nextAction.kind === "score_pending"
|
||||
? `${journey.caseId}:${journey.turnVersion}:${journey.nextAction.jobId}`
|
||||
|
||||
@@ -21,7 +21,9 @@ import {
|
||||
import { confirmReviewedBirthTimeDraft } from "@/lib/birth-time-guided-draft-confirmation";
|
||||
import {
|
||||
claimMutation,
|
||||
createStableActionIdentityRegistry,
|
||||
publishCurrentJourney,
|
||||
runStableJourneyAction,
|
||||
} from "@/lib/birth-time-guided-effect-coordinator";
|
||||
import type { EvidenceDatePrecision } from "@/lib/birth-time-question-planner";
|
||||
import { useBirthTimeAutomaticJourneyEffects } from "@/hooks/use-birth-time-automatic-journey-effects";
|
||||
@@ -59,6 +61,7 @@ export function useBirthTimeGuidedJourney(input: GuidedJourneyInput): BirthTimeG
|
||||
const { journey, onJourney, onReady, onEditBirthTimeDetails, preview } = input;
|
||||
const latest = useRef(journey);
|
||||
const busy = useRef(false);
|
||||
const [actionRegistry] = useState(() => createStableActionIdentityRegistry());
|
||||
const [pending, setPending] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [pollRun, setPollRun] = useState(0);
|
||||
@@ -105,10 +108,28 @@ export function useBirthTimeGuidedJourney(input: GuidedJourneyInput): BirthTimeG
|
||||
});
|
||||
}, [journey, onJourney]);
|
||||
|
||||
const actionId = () => globalThis.crypto.randomUUID();
|
||||
const stableCommand = <T>(
|
||||
caseId: string,
|
||||
turnVersion: number,
|
||||
operation: string,
|
||||
payload: readonly string[],
|
||||
send: (actionId: string) => Promise<T>,
|
||||
) => runStableJourneyAction(actionRegistry, {
|
||||
caseId,
|
||||
turnVersion,
|
||||
operation,
|
||||
payload,
|
||||
}, send);
|
||||
const stableAction = <T>(
|
||||
turn: JourneyClientResponse,
|
||||
operation: string,
|
||||
payload: readonly string[],
|
||||
send: (actionId: string) => Promise<T>,
|
||||
) => stableCommand(turn.caseId, turn.turnVersion, operation, payload, send);
|
||||
const question = useBirthTimeAutomaticJourneyEffects({
|
||||
journey,
|
||||
latest,
|
||||
actionRegistry,
|
||||
preview,
|
||||
pollRun,
|
||||
generationRun,
|
||||
@@ -118,26 +139,36 @@ export function useBirthTimeGuidedJourney(input: GuidedJourneyInput): BirthTimeG
|
||||
|
||||
const submitMessage = (message: string) => operate((turn) => {
|
||||
if (preview) return Promise.resolve(turn);
|
||||
return draftBirthTimeEvidence(turn.caseId, actionId(), turn.turnVersion, message).then((value) => value.turn);
|
||||
return stableAction(turn, "draft_evidence", [message.trim()], (actionId) => (
|
||||
draftBirthTimeEvidence(turn.caseId, actionId, turn.turnVersion, message).then((value) => value.turn)
|
||||
));
|
||||
});
|
||||
|
||||
const confirmDraft = (precision: EvidenceDatePrecision, date: string) => operate(async (turn, publishIntermediate) => {
|
||||
if (preview) return turn;
|
||||
return confirmReviewedBirthTimeDraft({ turn, precision, date }, {
|
||||
createActionId: actionId,
|
||||
revise: reviseBirthTimeEvidenceDraft,
|
||||
revise: (command) => stableCommand(
|
||||
command.caseId, command.turnVersion, "revise_evidence_draft", [command.precision, command.date],
|
||||
(actionId) => reviseBirthTimeEvidenceDraft({ ...command, actionId }),
|
||||
),
|
||||
publish: publishIntermediate,
|
||||
confirm: (command) => confirmBirthTimeEvidenceDraft(
|
||||
command.caseId,
|
||||
command.actionId,
|
||||
command.turnVersion,
|
||||
command.draftId,
|
||||
confirm: (command) => stableCommand(
|
||||
command.caseId, command.turnVersion, "confirm_evidence_draft", [command.draftId],
|
||||
(actionId) => confirmBirthTimeEvidenceDraft(
|
||||
command.caseId, actionId, command.turnVersion, command.draftId,
|
||||
),
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
const skip = () => operate((turn) => preview ? Promise.resolve(turn) : skipBirthTimeEvidenceQuestion(turn.caseId, actionId(), turn.turnVersion));
|
||||
const pause = () => operate((turn) => preview ? Promise.resolve(turn) : pauseBirthTimeRectification(turn.caseId, actionId(), turn.turnVersion));
|
||||
const skip = () => operate((turn) => preview ? Promise.resolve(turn) : stableAction(
|
||||
turn, "skip_evidence_question", [],
|
||||
(actionId) => skipBirthTimeEvidenceQuestion(turn.caseId, actionId, turn.turnVersion),
|
||||
));
|
||||
const pause = () => operate((turn) => preview ? Promise.resolve(turn) : stableAction(
|
||||
turn, "pause_rectification", [],
|
||||
(actionId) => pauseBirthTimeRectification(turn.caseId, actionId, turn.turnVersion),
|
||||
));
|
||||
const resume = () => operate((turn) => preview ? Promise.resolve(turn) : resumeBirthTimeJourney(turn.caseId));
|
||||
const acknowledgeReady = () => {
|
||||
if (journey?.nextAction.kind === "ready") onReady(journey);
|
||||
@@ -153,33 +184,39 @@ export function useBirthTimeGuidedJourney(input: GuidedJourneyInput): BirthTimeG
|
||||
? pollBirthTimeScoring(current.caseId, current.nextAction.jobId)
|
||||
: Promise.resolve(current));
|
||||
};
|
||||
const saveCandidate = (resultId: string) => operate((turn) => preview ? Promise.resolve(turn) : saveGuidedBirthTimeCandidate({ caseId: turn.caseId, actionId: actionId(), turnVersion: turn.turnVersion, resultId }));
|
||||
const confirmCandidate = (resultId: string, time: string) => operate((turn) => preview ? Promise.resolve(turn) : confirmGuidedBirthTimeCandidate({ caseId: turn.caseId, actionId: actionId(), turnVersion: turn.turnVersion, resultId, time }));
|
||||
const saveCandidate = (resultId: string) => operate((turn) => preview ? Promise.resolve(turn) : stableAction(
|
||||
turn, "save_guided_candidate", [resultId],
|
||||
(actionId) => saveGuidedBirthTimeCandidate({ caseId: turn.caseId, actionId, turnVersion: turn.turnVersion, resultId }),
|
||||
));
|
||||
const confirmCandidate = (resultId: string, time: string) => operate((turn) => preview ? Promise.resolve(turn) : stableAction(
|
||||
turn, "confirm_guided_candidate", [resultId, time],
|
||||
(actionId) => confirmGuidedBirthTimeCandidate({ caseId: turn.caseId, actionId, turnVersion: turn.turnVersion, resultId, time }),
|
||||
));
|
||||
const selectOption = (optionId: string) => operate((turn) => {
|
||||
if (preview || turn.journeyProtocol !== "dynamic-choice-v2"
|
||||
|| turn.nextAction.kind !== "ask_dynamic_choice") return Promise.resolve(turn);
|
||||
return answerDynamicBirthTimeChoice({
|
||||
caseId: turn.caseId,
|
||||
actionId: actionId(),
|
||||
turnVersion: turn.turnVersion,
|
||||
questionId: turn.nextAction.question.questionId,
|
||||
optionId,
|
||||
});
|
||||
const questionId = turn.nextAction.question.questionId;
|
||||
return stableAction(turn, "answer_dynamic_choice", [questionId, optionId], (actionId) => (
|
||||
answerDynamicBirthTimeChoice({
|
||||
caseId: turn.caseId, actionId, turnVersion: turn.turnVersion, questionId, optionId,
|
||||
})
|
||||
));
|
||||
});
|
||||
const submitUnmatchedContext = (note: string) => operate((turn) => {
|
||||
if (preview || turn.journeyProtocol !== "dynamic-choice-v2"
|
||||
|| turn.nextAction.kind !== "clarify_unmatched_answer") return Promise.resolve(turn);
|
||||
return reframeUnmatchedBirthTimeAnswer({
|
||||
caseId: turn.caseId,
|
||||
actionId: actionId(),
|
||||
turnVersion: turn.turnVersion,
|
||||
questionId: turn.nextAction.questionId,
|
||||
note,
|
||||
});
|
||||
const questionId = turn.nextAction.questionId;
|
||||
return stableAction(turn, "reframe_unmatched", [questionId, note.trim()], (actionId) => (
|
||||
reframeUnmatchedBirthTimeAnswer({
|
||||
caseId: turn.caseId, actionId, turnVersion: turn.turnVersion, questionId, note,
|
||||
})
|
||||
));
|
||||
});
|
||||
const finish = () => operate((turn) => preview
|
||||
? Promise.resolve(turn)
|
||||
: finishBirthTimeRectification(turn.caseId, actionId(), turn.turnVersion));
|
||||
: stableAction(turn, "finish_rectification", [], (actionId) => (
|
||||
finishBirthTimeRectification(turn.caseId, actionId, turn.turnVersion)
|
||||
)));
|
||||
const retryQuestionGeneration = () => {
|
||||
if (journey?.journeyProtocol !== "dynamic-choice-v2") return;
|
||||
if (journey.nextAction.kind !== "generate_dynamic_question"
|
||||
|
||||
@@ -10,7 +10,6 @@ type ConfirmDraftInput = {
|
||||
|
||||
type RevisionCommand = {
|
||||
readonly caseId: string;
|
||||
readonly actionId: string;
|
||||
readonly turnVersion: number;
|
||||
readonly precision: EvidenceDatePrecision;
|
||||
readonly date: string;
|
||||
@@ -18,13 +17,11 @@ type RevisionCommand = {
|
||||
|
||||
type ConfirmationCommand = {
|
||||
readonly caseId: string;
|
||||
readonly actionId: string;
|
||||
readonly turnVersion: number;
|
||||
readonly draftId: string;
|
||||
};
|
||||
|
||||
type ConfirmDraftPorts = {
|
||||
readonly createActionId: () => string;
|
||||
readonly revise: (command: RevisionCommand) => Promise<JourneyClientResponse>;
|
||||
readonly publish: (turn: JourneyClientResponse) => void;
|
||||
readonly confirm: (command: ConfirmationCommand) => Promise<JourneyClientResponse>;
|
||||
@@ -50,7 +47,6 @@ export async function confirmReviewedBirthTimeDraft(
|
||||
? input.turn
|
||||
: await ports.revise({
|
||||
caseId: input.turn.caseId,
|
||||
actionId: ports.createActionId(),
|
||||
turnVersion: input.turn.turnVersion,
|
||||
precision: parsed.precision,
|
||||
date: parsed.date,
|
||||
@@ -60,7 +56,6 @@ export async function confirmReviewedBirthTimeDraft(
|
||||
if (!currentDraft) throw new GuidedDraftConfirmationError("经历草稿已经变化,请使用最新内容。");
|
||||
return ports.confirm({
|
||||
caseId: revised.caseId,
|
||||
actionId: ports.createActionId(),
|
||||
turnVersion: revised.turnVersion,
|
||||
draftId: currentDraft.draftId,
|
||||
});
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
import type { JourneyClientResponse } from "./birth-time-journey-response-schema.ts";
|
||||
|
||||
type StableActionIdentityInput = {
|
||||
readonly caseId: string;
|
||||
readonly turnVersion: number;
|
||||
readonly operation: string;
|
||||
readonly payload?: readonly string[];
|
||||
};
|
||||
|
||||
export function stableActionIdentity(input: StableActionIdentityInput): string {
|
||||
return JSON.stringify([
|
||||
input.caseId,
|
||||
input.turnVersion,
|
||||
input.operation,
|
||||
...(input.payload ?? []),
|
||||
]);
|
||||
}
|
||||
|
||||
export function createIdentityRequestCache<T>() {
|
||||
const requests = new Map<string, Promise<T>>();
|
||||
return {
|
||||
@@ -16,6 +32,31 @@ export function createIdentityRequestCache<T>() {
|
||||
};
|
||||
}
|
||||
|
||||
export function createStableActionIdentityRegistry(
|
||||
createId: () => string = () => globalThis.crypto.randomUUID(),
|
||||
) {
|
||||
const actionIds = new Map<string, string>();
|
||||
return {
|
||||
async run<T>(identity: string, operation: (actionId: string) => Promise<T>): Promise<T> {
|
||||
const actionId = actionIds.get(identity) ?? createId();
|
||||
actionIds.set(identity, actionId);
|
||||
const result = await operation(actionId);
|
||||
if (actionIds.get(identity) === actionId) actionIds.delete(identity);
|
||||
return result;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export type StableActionIdentityRegistry = ReturnType<typeof createStableActionIdentityRegistry>;
|
||||
|
||||
export function runStableJourneyAction<T>(
|
||||
registry: StableActionIdentityRegistry,
|
||||
identity: StableActionIdentityInput,
|
||||
operation: (actionId: string) => Promise<T>,
|
||||
): Promise<T> {
|
||||
return registry.run(stableActionIdentity(identity), operation);
|
||||
}
|
||||
|
||||
export function scheduleCancellableStart(start: () => void): () => void {
|
||||
const timer = globalThis.setTimeout(start, 0);
|
||||
return () => globalThis.clearTimeout(timer);
|
||||
|
||||
@@ -102,13 +102,9 @@ test("dynamic response parsing rejects private legacy and candidate payloads", (
|
||||
for (const payload of privatePayloads) assert.throws(() => parseJourneyResponse(payload));
|
||||
});
|
||||
|
||||
test("dynamic routes authenticate before parsing and dispatch scoped methods", () => {
|
||||
test("dynamic route boundaries authenticate before parsing user input", () => {
|
||||
const journeyRoute = readFileSync(new URL("../src/app/api/birth-time-journey/route.ts", import.meta.url), "utf8");
|
||||
const guideRoute = readFileSync(new URL("../src/app/api/birth-time-guide/route.ts", import.meta.url), "utf8");
|
||||
assert.ok(journeyRoute.indexOf("auth.getUser") < journeyRoute.indexOf("requestPayload(request)"));
|
||||
assert.ok(guideRoute.indexOf("auth.getUser") < guideRoute.indexOf("requestPayload(request)"));
|
||||
assert.match(journeyRoute, /answerDynamicChoice/);
|
||||
assert.match(journeyRoute, /pollDynamicScoringJob/);
|
||||
assert.match(guideRoute, /generateQuestion/);
|
||||
assert.match(guideRoute, /submitUnmatchedContext/);
|
||||
});
|
||||
|
||||
@@ -36,10 +36,9 @@ test("draft review is explicit, domain locked, and incomplete confirmation stays
|
||||
assert.match(hookSource, /confirmBirthTimeEvidenceDraft/);
|
||||
});
|
||||
|
||||
test("guided orchestration owns fallback copy, unique actions, polling, and retry", () => {
|
||||
test("guided orchestration owns fallback copy, polling, and retry", () => {
|
||||
assert.match(automaticEffectsSource, /fallbackQuestionCopy/);
|
||||
assert.match(automaticEffectsSource, /requestBirthTimeGuidePrompt/);
|
||||
assert.match(automaticEffectsSource, /crypto\.randomUUID\(\)/);
|
||||
assert.match(automaticEffectsSource, /runBirthTimeScoringPoll/);
|
||||
assert.match(hookSource, /retry_scoring/);
|
||||
assert.match(automaticEffectsSource, /AbortController/);
|
||||
|
||||
@@ -24,7 +24,6 @@ test("draft revision publishes its new version before confirmation can fail", as
|
||||
precision: "month",
|
||||
date: "2008-09",
|
||||
}, {
|
||||
createActionId: () => "45857b75-4718-4590-aaf5-7113a03ea765",
|
||||
revise: async () => revised,
|
||||
publish: (turn) => { published.push(turn); },
|
||||
confirm: async () => { throw new TypeError("response unavailable"); },
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import {
|
||||
createStableActionIdentityRegistry,
|
||||
runStableJourneyAction,
|
||||
stableActionIdentity,
|
||||
} from "../src/lib/birth-time-guided-effect-coordinator.ts";
|
||||
import {
|
||||
answerDynamicBirthTimeChoice,
|
||||
generateDynamicBirthTimeQuestion,
|
||||
} from "../src/lib/birth-time-journey-client.ts";
|
||||
import { storedDynamicJourneyResponse } from "../src/lib/birth-time-journey-response.ts";
|
||||
import { dynamicCase, persistedQuestion } from "./birth-time-dynamic-persistence-fixture.ts";
|
||||
|
||||
const firstId = "45857b75-4718-4590-aaf5-7113a03ea765";
|
||||
const secondId = "0790866c-ad5e-4a45-b2b4-a5c73f6be6ea";
|
||||
|
||||
test("stable action identities survive failures and clear only after success", async () => {
|
||||
const ids = [firstId, secondId];
|
||||
const registry = createStableActionIdentityRegistry(
|
||||
() => ids.shift() ?? assert.fail("unexpected id allocation"),
|
||||
);
|
||||
const seen: string[] = [];
|
||||
const identity = stableActionIdentity({
|
||||
caseId: "case-a", turnVersion: 7, operation: "answer_dynamic_choice",
|
||||
payload: ["question-a", "option-a"],
|
||||
});
|
||||
const fail = () => registry.run(identity, async (actionId) => {
|
||||
seen.push(actionId);
|
||||
throw new TypeError("offline");
|
||||
});
|
||||
|
||||
await assert.rejects(fail());
|
||||
await assert.rejects(fail());
|
||||
await registry.run(identity, async (actionId) => { seen.push(actionId); });
|
||||
await registry.run(identity, async (actionId) => { seen.push(actionId); });
|
||||
|
||||
assert.deepEqual(seen, [firstId, firstId, firstId, secondId]);
|
||||
});
|
||||
|
||||
test("action identity separates options, notes, and new turns", () => {
|
||||
const base = {
|
||||
caseId: "case-a", turnVersion: 7, operation: "answer_dynamic_choice",
|
||||
payload: ["question-a", "option-a"],
|
||||
} as const;
|
||||
|
||||
assert.equal(stableActionIdentity(base), stableActionIdentity(base));
|
||||
assert.notEqual(stableActionIdentity(base), stableActionIdentity({ ...base, payload: ["question-a", "option-b"] }));
|
||||
assert.notEqual(stableActionIdentity(base), stableActionIdentity({ ...base, turnVersion: 8 }));
|
||||
assert.notEqual(
|
||||
stableActionIdentity({ ...base, operation: "reframe_unmatched", payload: ["question-a", "较早"] }),
|
||||
stableActionIdentity({ ...base, operation: "reframe_unmatched", payload: ["question-a", "较晚"] }),
|
||||
);
|
||||
});
|
||||
|
||||
async function lostResponseRetry(
|
||||
context: test.TestContext,
|
||||
operation: "answer_dynamic_choice" | "generate_dynamic_question",
|
||||
send: (actionId: string) => Promise<unknown>,
|
||||
) {
|
||||
const bodies: string[] = [];
|
||||
let attempts = 0;
|
||||
context.mock.method(globalThis, "fetch", async (_input: string | URL | Request, init?: RequestInit) => {
|
||||
attempts += 1;
|
||||
bodies.push(String(init?.body));
|
||||
if (attempts <= 2) throw new TypeError("response lost");
|
||||
return new Response(JSON.stringify(storedDynamicJourneyResponse(dynamicCase())), { status: 200 });
|
||||
});
|
||||
const registry = createStableActionIdentityRegistry(() => firstId);
|
||||
const identity = { caseId: dynamicCase().id, turnVersion: 7, operation } as const;
|
||||
|
||||
await assert.rejects(runStableJourneyAction(registry, identity, send));
|
||||
await runStableJourneyAction(registry, identity, send);
|
||||
|
||||
assert.equal(attempts, 3);
|
||||
assert.equal(new Set(bodies).size, 1);
|
||||
assert.equal(JSON.parse(bodies[0]).actionId, firstId);
|
||||
}
|
||||
|
||||
test("two lost choice responses and a manual retry send one exact receipt", async (context) => {
|
||||
await lostResponseRetry(context, "answer_dynamic_choice", (actionId) => answerDynamicBirthTimeChoice({
|
||||
caseId: dynamicCase().id,
|
||||
actionId,
|
||||
turnVersion: 7,
|
||||
questionId: persistedQuestion.questionId,
|
||||
optionId: persistedQuestion.options[0].optionId,
|
||||
}));
|
||||
});
|
||||
|
||||
test("failed automatic generation reuses its action id on manual retry", async (context) => {
|
||||
await lostResponseRetry(context, "generate_dynamic_question", (actionId) => (
|
||||
generateDynamicBirthTimeQuestion(dynamicCase().id, actionId, 7)
|
||||
));
|
||||
});
|
||||
Reference in New Issue
Block a user