fix: recover committed dynamic transport errors
This commit is contained in:
@@ -89,12 +89,17 @@ result/action coherence.
|
||||
the reloaded receipt. SQL and RPC fakes now reproduce exact duplicate success, concurrent
|
||||
cross-action success, changed answer/commit payloads, lease reclaim, and corrupted completed
|
||||
result/action replay.
|
||||
- Final review RED: a generic transport error after a committed dynamic turn reloaded state but
|
||||
skipped receipt classification unless the error text itself said stale. The adapter now treats
|
||||
an exact committed receipt as lost-response replay, a processed conflicting receipt as stale,
|
||||
and only an uncommitted generic error as a store failure. Scoring-job creation already used
|
||||
these semantics; a generic-error regression test locks that parity.
|
||||
|
||||
Final verification:
|
||||
|
||||
- Focused Task 6 TypeScript: **43/43 passed**.
|
||||
- Focused Task 6 TypeScript: **47/47 passed**.
|
||||
- Route/telemetry regression subset after v2 assessment routing: **28/28 passed**.
|
||||
- Full frontend TypeScript tests: **358/358 passed**.
|
||||
- Full frontend TypeScript tests: **362/362 passed**.
|
||||
- Dynamic action-receipt and scoring-job SQL contracts: **7/7 passed**.
|
||||
- ESLint: **0 errors**, with the two pre-existing `page.tsx` hook warnings.
|
||||
- `git diff --check`: passed.
|
||||
@@ -121,3 +126,5 @@ Commit message: `feat: orchestrate dynamic rectification turns`
|
||||
Focused review fix commit message: `fix: harden dynamic rectification orchestration`
|
||||
|
||||
Exact receipt fix commit message: `fix: close dynamic receipt replay gaps`
|
||||
|
||||
Lost-response fix commit message: `fix: recover committed dynamic transport errors`
|
||||
|
||||
@@ -144,7 +144,7 @@ export function createDynamicTurnPersistence(
|
||||
});
|
||||
if (result.error) {
|
||||
const current = await loadCase(value.userId, value.id);
|
||||
if (isStaleRpc(result.error) && current?.journeyProtocol === "dynamic-choice-v2"
|
||||
if (current?.journeyProtocol === "dynamic-choice-v2"
|
||||
&& current.processedActionIds.includes(receipt)) {
|
||||
if (samePersistedDynamicReceipt(value, current, receipt, expectedVersion)) return current;
|
||||
throw new StaleJourneyTurnError(value.id, expectedVersion, current.turnVersion);
|
||||
|
||||
@@ -3,7 +3,10 @@ import test from "node:test";
|
||||
import { createDynamicScoringJobStore } from "../src/lib/birth-time-dynamic-scoring-job-store.ts";
|
||||
import { createDynamicTurnPersistence } from "../src/lib/birth-time-journey-dynamic-persistence.ts";
|
||||
import { createDynamicScoringJobSpec } from "../src/lib/birth-time-scoring-job.ts";
|
||||
import { StaleJourneyTurnError } from "../src/lib/birth-time-journey-turn-persistence.ts";
|
||||
import {
|
||||
BirthTimeJourneyStoreError,
|
||||
StaleJourneyTurnError,
|
||||
} from "../src/lib/birth-time-journey-turn-persistence.ts";
|
||||
import { answerTransition } from "../src/lib/birth-time-dynamic-transitions.ts";
|
||||
import type { DynamicStoredRectificationCase } from "../src/lib/birth-time-journey-service.ts";
|
||||
import { actionId, dynamicCase, persistedQuestion } from "./birth-time-dynamic-persistence-fixture.ts";
|
||||
@@ -55,6 +58,43 @@ test("a concurrent different action cannot use a successful RPC as replay", asyn
|
||||
);
|
||||
});
|
||||
|
||||
test("a transport error returns an exact committed dynamic turn", async () => {
|
||||
const loaded = savedTurn();
|
||||
const proposed = { ...loaded, turnVersion: 7 };
|
||||
const persistence = createDynamicTurnPersistence({
|
||||
async rpc() { return { data: null, error: { message: "connection reset" } }; },
|
||||
}, async () => loaded, () => "2026-07-18");
|
||||
|
||||
const replay = await persistence.saveDynamicTurn(proposed, 7, actionId);
|
||||
|
||||
assert.equal(replay, loaded);
|
||||
});
|
||||
|
||||
test("a transport error maps a conflicting committed receipt to stale", async () => {
|
||||
const proposed = { ...savedTurn(), turnVersion: 7 };
|
||||
const concurrent = savedTurn("finish");
|
||||
const persistence = createDynamicTurnPersistence({
|
||||
async rpc() { return { data: null, error: { message: "connection reset" } }; },
|
||||
}, async () => concurrent, () => "2026-07-18");
|
||||
|
||||
await assert.rejects(
|
||||
persistence.saveDynamicTurn(proposed, 7, actionId),
|
||||
StaleJourneyTurnError,
|
||||
);
|
||||
});
|
||||
|
||||
test("a transport error remains a store error when nothing committed", async () => {
|
||||
const proposed = dynamicCase();
|
||||
const persistence = createDynamicTurnPersistence({
|
||||
async rpc() { return { data: null, error: { message: "connection reset" } }; },
|
||||
}, async () => proposed, () => "2026-07-18");
|
||||
|
||||
await assert.rejects(
|
||||
persistence.saveDynamicTurn(proposed, 7, actionId),
|
||||
BirthTimeJourneyStoreError,
|
||||
);
|
||||
});
|
||||
|
||||
function pendingTurn(): DynamicStoredRectificationCase {
|
||||
const stored = dynamicCase();
|
||||
const option = persistedQuestion.options.find((item) => item.kind === "primary");
|
||||
@@ -120,3 +160,27 @@ test("dynamic scoring creation accepts only exact duplicate-success receipts", a
|
||||
StaleJourneyTurnError,
|
||||
);
|
||||
});
|
||||
|
||||
test("dynamic scoring transport errors return an exact committed receipt", async () => {
|
||||
const pending = pendingTurn();
|
||||
const spec = createDynamicScoringJobSpec(
|
||||
"85b22d7e-3adc-473d-81e1-6ad29e9b06f4",
|
||||
pending.choiceEvidence,
|
||||
new Date("2026-07-18T08:00:00.000Z"),
|
||||
);
|
||||
const loaded = {
|
||||
...pending,
|
||||
turnVersion: 8,
|
||||
dynamicTurnState: { ...pending.dynamicTurnState, turnVersion: 8 },
|
||||
processedActionIds: [actionId],
|
||||
};
|
||||
const store = createDynamicScoringJobStore({
|
||||
async rpc() { return { data: null, error: { message: "connection reset" } }; },
|
||||
}, async () => loaded);
|
||||
|
||||
const replay = await store.createDynamicScoringJob(
|
||||
pending, 7, actionId, persistedQuestion.questionId, spec,
|
||||
);
|
||||
|
||||
assert.equal(replay, loaded);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user