test(sync): preserve question bootstrap and reducer invariants (#3439)
* test(sync): preserve question bootstrap and reducer invariants Port the main-independent test assets from the superseded hybrid V2 question PRs (#3266/#3267/#3288) onto current main's V1-only architecture: - listPendingQuestions: unscoped + per-directory fan-out, merge with id-dedupe (first wins), failure throws instead of empty success, malformed-item skip, true-empty success (client.questions.test.ts) - question reducer invariants: idempotent upsert-by-id, replace-not- first-wins, removal by session/request pair, duplicate-terminal no-op, late-asked re-registration (no tombstone) - bootstrap deferred-phase question merge: signature-based replace and disappearance-deletion, in-flight-change preservation (stale guard), retry-then-merge on transient failure Tests only: zero production changes, zero V2 SDK calls, zero fallback logic, zero raw V2 event aliases. * test(questions): clarify V1 merge id-filter test title --------- Co-authored-by: bashrusakh <bashrusakh@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
co-authored by
bashrusakh
parent
5c127a1e3c
commit
c1f19b1158
@@ -355,3 +355,98 @@ describe("applyDirectoryEvent", () => {
|
||||
expect(draft.question.ses_1).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe("question reducer invariants (main contract)", () => {
|
||||
const questionRequest = (id: string, sessionID = "ses_1"): QuestionRequest => ({
|
||||
id,
|
||||
sessionID,
|
||||
questions: [],
|
||||
})
|
||||
|
||||
const askedEvent = (id: string, sessionID = "ses_1"): Event => ({
|
||||
id: `evt_${id}`,
|
||||
type: "question.asked",
|
||||
properties: questionRequest(id, sessionID),
|
||||
})
|
||||
|
||||
const repliedEvent = (requestID: string, sessionID = "ses_1"): Event => ({
|
||||
id: `evt_${requestID}`,
|
||||
type: "question.replied",
|
||||
properties: { sessionID, requestID, answers: [] },
|
||||
})
|
||||
|
||||
const rejectedEvent = (requestID: string, sessionID = "ses_1"): Event => ({
|
||||
id: `evt_${requestID}`,
|
||||
type: "question.rejected",
|
||||
properties: { sessionID, requestID },
|
||||
})
|
||||
|
||||
test("question.asked is an idempotent upsert-by-id — replaying does not duplicate", () => {
|
||||
const draft = state({ question: { ses_1: [questionRequest("ques_1")] } })
|
||||
|
||||
expect(applyDirectoryEvent(draft, askedEvent("ques_1"))).toBe(true)
|
||||
expect(applyDirectoryEvent(draft, askedEvent("ques_1"))).toBe(true)
|
||||
|
||||
expect(draft.question.ses_1).toHaveLength(1)
|
||||
expect(draft.question.ses_1[0]?.id).toBe("ques_1")
|
||||
})
|
||||
|
||||
test("question.asked replaces the stored record in place (not first-wins)", () => {
|
||||
const draft = state({ question: { ses_1: [questionRequest("ques_1")] } })
|
||||
const replacement: QuestionRequest = {
|
||||
id: "ques_1",
|
||||
sessionID: "ses_1",
|
||||
questions: [
|
||||
{ question: "updated?", header: "Build", options: [{ label: "Yes", description: "Go" }] },
|
||||
],
|
||||
}
|
||||
|
||||
expect(applyDirectoryEvent(draft, {
|
||||
id: "evt_ques_1",
|
||||
type: "question.asked",
|
||||
properties: replacement,
|
||||
})).toBe(true)
|
||||
|
||||
expect(draft.question.ses_1).toHaveLength(1)
|
||||
expect(draft.question.ses_1[0]).toEqual(replacement)
|
||||
})
|
||||
|
||||
test("question.replied and question.rejected remove exactly the matching request; unknown removal is a no-op returning false", () => {
|
||||
const draft = state({
|
||||
question: { ses_1: [questionRequest("ques_1"), questionRequest("ques_2")] },
|
||||
})
|
||||
|
||||
expect(applyDirectoryEvent(draft, repliedEvent("ques_1"))).toBe(true)
|
||||
expect(draft.question.ses_1.map((q) => q.id)).toEqual(["ques_2"])
|
||||
|
||||
expect(applyDirectoryEvent(draft, rejectedEvent("ques_2"))).toBe(true)
|
||||
expect(draft.question.ses_1).toEqual([])
|
||||
|
||||
// Removal for an unknown request is a safe no-op.
|
||||
expect(applyDirectoryEvent(draft, repliedEvent("ques_missing"))).toBe(false)
|
||||
expect(applyDirectoryEvent(draft, rejectedEvent("ques_missing"))).toBe(false)
|
||||
expect(draft.question.ses_1).toEqual([])
|
||||
})
|
||||
|
||||
test("a duplicate terminal event after removal is a safe no-op", () => {
|
||||
const draft = state({ question: { ses_1: [questionRequest("ques_1")] } })
|
||||
|
||||
expect(applyDirectoryEvent(draft, repliedEvent("ques_1"))).toBe(true)
|
||||
expect(draft.question.ses_1).toEqual([])
|
||||
|
||||
// Replayed terminal event: no error, no duplicate, no state change.
|
||||
expect(applyDirectoryEvent(draft, repliedEvent("ques_1"))).toBe(false)
|
||||
expect(draft.question.ses_1).toEqual([])
|
||||
})
|
||||
|
||||
test("a late question.asked after a terminal event re-registers the request (no tombstone)", () => {
|
||||
const draft = state({ question: { ses_1: [questionRequest("ques_1")] } })
|
||||
|
||||
expect(applyDirectoryEvent(draft, repliedEvent("ques_1"))).toBe(true)
|
||||
expect(draft.question.ses_1).toEqual([])
|
||||
|
||||
// Ordered-stream replay: a late asked re-inserts; there is no tombstone.
|
||||
expect(applyDirectoryEvent(draft, askedEvent("ques_1"))).toBe(true)
|
||||
expect(draft.question.ses_1.map((q) => q.id)).toEqual(["ques_1"])
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user