import { afterEach, beforeAll, afterAll, beforeEach, describe, expect, it, vi } from "vitest"; import { GenericContainer, Wait, type StartedTestContainer } from "testcontainers"; import { Client, type PoolConfig } from "pg"; import { createRequire } from "node:module"; import { execFileSync } from "node:child_process"; import { fileURLToPath } from "node:url"; import { z } from "zod"; import type { BatchResult, JsonObject, ReverseEtlContext, ReverseEtlWriter, WriteBatch, } from "@jitsu/protocols/reverse-etl"; import { contentHash } from "@jitsu/destination-functions/src/reverse-etl/identity"; import { Database } from "./persistence"; import { openMirrorPersistence, runSnapshotMirror, resumeSnapshotMirror, type NewMirrorOptions, type MirrorSourceRecord, type SnapshotMirrorAdapter, } from "./mirror"; import { MemoryObjects, persisted } from "./artifacts/test-support"; const objects = new MemoryObjects(); const storage = { objectStorage: { store: objects, signal: new AbortController().signal } }; const durable = () => persisted(admin, objects); afterEach(() => vi.useRealTimers()); let container: StartedTestContainer; let admin: Client; let db: Database; let runtimeConfig: PoolConfig; type Session = Awaited>; type SourceRow = { id: string; value: string }; const record = (key: string, id = key, value = "v"): MirrorSourceRecord => ({ key: contentHash(key), row: { id, value }, }); const member = (id: string) => contentHash(id.toLowerCase()); const accepted = (batch: WriteBatch): BatchResult => ({ outcomes: batch.records.map(row => ({ operationId: row.operationId, status: "accepted" })), }); beforeAll(async () => { container = await new GenericContainer("postgres:18-alpine") .withEnvironment({ POSTGRES_PASSWORD: "test", POSTGRES_DB: "mirror_test" }) .withExposedPorts(5432) .withWaitStrategy(Wait.forLogMessage(/database system is ready to accept connections/, 2)) .start(); const config = { host: container.getHost(), port: container.getMappedPort(5432), database: "mirror_test", user: "postgres", password: "test", }; admin = new Client(config); await admin.connect(); const url = `postgresql://postgres:test@${config.host}:${config.port}/${config.database}?schema=newjitsu`; execFileSync( process.execPath, [ createRequire(import.meta.url).resolve("prisma/build/index.js"), "db", "push", `--schema=${fileURLToPath(new URL("../../../webapps/console/prisma/schema.prisma", import.meta.url))}`, "--skip-generate", ], { // eslint-disable-next-line no-restricted-properties -- disposable test database only. env: { ...process.env, DATABASE_URL: url }, stdio: "inherit", } ); await admin.query( "CREATE ROLE mirror_runtime LOGIN PASSWORD 'runtime'; GRANT USAGE ON SCHEMA newjitsu TO mirror_runtime" ); for (const table of ["control", "target_owner"]) await admin.query(`GRANT SELECT,INSERT,UPDATE,DELETE ON newjitsu.reverse_sync_${table} TO mirror_runtime`); await admin.query("GRANT SELECT,INSERT,UPDATE,DELETE ON newjitsu.source_state TO mirror_runtime"); runtimeConfig = { ...config, user: "mirror_runtime", password: "runtime" }; db = new Database(runtimeConfig, storage); }, 60_000); afterAll(async () => { await db?.close(); await admin?.end(); await container?.stop(); }); beforeEach(async () => { await admin.query("TRUNCATE newjitsu.reverse_sync_control,newjitsu.reverse_sync_target_owner,newjitsu.source_state"); }); async function session(id = "run", taskId = id, database = db) { return openMirrorPersistence(database, { workspaceId: "workspace", syncId: "sync", logicalRunId: id, taskId, configRevision: "revision", targetIdentity: "test/account/audience", mode: "mirror", extraction: "full", }); } async function takeover(run: Session) { return session(run.scope.logicalRunId, "recovery"); } async function phase() { return (await admin.query("SELECT phase FROM newjitsu.reverse_sync_control ORDER BY run_order DESC LIMIT 1")).rows[0] .phase; } async function membership() { return (await durable()).members.map(row => row.effect.upsert); } function fixture() { const calls: string[] = []; const requests: { action: string; batch: WriteBatch }[] = []; const remote = new Map(); const receipts = new Map(); let projections = 0; let onBatch: ((action: string, batch: WriteBatch) => Promise) | undefined; let afterBatch: (() => void | Promise) | undefined; let finishResult: "accepted" | "pending" | "throw" = "accepted"; const controller = new AbortController(); const writer = (ctx: ReverseEtlContext<{}, {}>): ReverseEtlWriter => { expect(ctx).not.toHaveProperty("snapshots"); expect(ctx).not.toHaveProperty("core"); expect(ctx).not.toHaveProperty("db"); const send = async (action: string, batch: WriteBatch) => { calls.push(action); requests.push({ action, batch: JSON.parse(JSON.stringify(batch)) }); for (const row of batch.records) expect(row.row).not.toHaveProperty("identityHash"); const result = onBatch ? await onBatch(action, batch) : accepted(batch); receipts.set(batch.batchId, result); for (const outcome of result.outcomes) if (outcome.status === "accepted") { const row = batch.records.find(row => row.operationId === outcome.operationId)!.row; if (action === "upsert") remote.set(row.member as string, row); else remote.delete(row.member as string); } ctx.store.set("lastBatch", batch.batchId); await afterBatch?.(); return result; }; return { init: async () => { calls.push("init"); await ctx.delivery.saveProviderState({ session: "remote-session" }); }, upsert: batch => send("upsert", batch), remove: batch => send("remove", batch), finish: async () => { calls.push("finish"); if (finishResult === "throw") throw new Error("private-provider-token"); return finishResult === "pending" ? { delivery: "pending", remoteJobIds: ["job"] } : { delivery: "accepted" }; }, abort: async () => { calls.push("abort"); }, }; }; const adapter: SnapshotMirrorAdapter<{}, SourceRow, {}> = { batchDelivery: "accepted", projection: { rowType: z.object({ id: z.string(), value: z.string() }), project: row => { projections++; const identity = member(row.id); return [{ identity, upsert: { member: identity, value: row.value }, remove: { member: identity } }]; }, }, stream: { name: "test", displayName: "Test", rowType: z.object({ member: z.string(), value: z.string() }).strict(), removeRowType: z.object({ member: z.string() }).strict(), options: z.object({}), batchSize: 2, capabilities: { supportsUpsert: true, supportsExplicitRemove: true, mirror: "snapshot-diff", replay: "idempotent-operation", }, createWriter: async ctx => { calls.push("create"); return writer(ctx); }, }, }; const options = (run: Session, rows: MirrorSourceRecord[]): NewMirrorOptions<{}, SourceRow, {}> => ({ persistence: run, adapter, targetBaseline: "tracked", mapping: { id: "id", value: "value" }, sourcePageSize: 2, context: { credentials: {}, options: {}, signal: controller.signal, log: {} as any, fetch: fetch as any }, source: async function* () { calls.push("source"); yield* rows; }, }); const recovery = { attachWriter: async (ctx: ReverseEtlContext<{}, {}>) => { calls.push("attach"); return writer(ctx); }, reconcileBatch: async ( batch: WriteBatch, _action: string, _saved: BatchResult | undefined, ctx: ReverseEtlContext<{}, {}> ) => { calls.push("reconcile"); ctx.store.set("reconciled", true); return receipts.get(batch.batchId)!; }, reconcileFinish: async (_saved: unknown, ctx: ReverseEtlContext<{}, {}>) => { calls.push("reconcileFinish"); ctx.store.set("final", true); return { delivery: "accepted" as const }; }, }; return { adapter, options, recovery, calls, requests, remote, receipts, controller, projections: () => projections, setBatch: (fn: typeof onBatch) => { onBatch = fn; }, setAfterBatch: (fn: typeof afterBatch) => { afterBatch = fn; }, setFinish: (value: typeof finishResult) => { finishResult = value; }, }; } describe("core snapshot mirror lifecycle", () => { it("uploads a complete replacement, waits for every upload, and prunes only after durable cleanup acceptance", async () => { const baseline = fixture(); await runSnapshotMirror(baseline.options(await session("baseline"), [record("stay"), record("old")])); const f = fixture(); f.adapter.stream.capabilities.mirror = "native-replace"; f.adapter.batchDelivery = "asynchronous"; f.setBatch(async (_action, batch) => ({ outcomes: batch.records.map(row => ({ operationId: row.operationId, status: "staged" })), remoteJobIds: [batch.batchId], })); f.setFinish("pending"); const options = (run: Session, rows: MirrorSourceRecord[] = []) => ({ ...f.options(run, rows), targetBaseline: "replace" as const, }); const run = await session("replacement"); expect( (await runSnapshotMirror(options(run, [record("stay"), record("new"), record("alias", "new")]))).delivery ).toBe("pending"); expect(f.requests.flatMap(r => r.batch.records)).toHaveLength(2); // Includes unchanged "stay". expect(f.calls).not.toContain("finish"); await expect(run.delivery.prepareFinish(2, {})).rejects.toThrow("Unaccepted uploads"); await expect(run.snapshots.page("removals")).rejects.toThrow("does not use individual removals"); const wrong = fixture(); await expect(resumeSnapshotMirror(wrong.options(await takeover(run), []), wrong.recovery)).rejects.toThrow( "Snapshot strategy changed" ); for (const { batch } of f.requests) f.receipts.set(batch.batchId, accepted(batch)); expect((await resumeSnapshotMirror(options(await takeover(run)), f.recovery)).delivery).toBe("pending"); expect(await phase()).toBe("finish_pending"); expect((await durable()).head.snapshot?.replacementStatus).toBe("pending"); expect(await membership()).toHaveLength(3); // Cleanup is not yet accepted: retain "old". expect(f.calls.filter(c => c === "source")).toHaveLength(1); expect(f.calls.filter(c => c === "finish")).toHaveLength(1); expect(f.requests.some(r => r.action === "remove")).toBe(false); expect((await resumeSnapshotMirror(options(await takeover(run)), f.recovery)).delivery).toBe("accepted"); expect(await phase()).toBe("complete"); expect((await durable()).head.snapshot?.replacementStatus).toBe("accepted"); expect((await membership()).map(m => m.member).sort()).toEqual([member("stay"), member("new")].sort()); const fresh = await session("next-replacement"); expect([...fresh.core.local.memberPages()].flat()).toHaveLength(2); expect(f.calls.filter(c => c === "finish")).toHaveLength(1); // Status polling never resubmits cleanup. f.setBatch(undefined); f.setFinish("accepted"); f.requests.length = 0; await runSnapshotMirror(options(fresh, [record("stay"), record("new")])); expect(f.requests.flatMap(r => r.batch.records)).toHaveLength(2); // Unchanged members upload again next run. f.requests.length = 0; await runSnapshotMirror(options(await session("empty-replacement"))); expect(f.requests).toHaveLength(0); expect(await membership()).toHaveLength(0); expect([...(await session("after-empty")).core.local.memberPages()].flat()).toHaveLength(0); }); it("never cleans a replacement after rejected uploads or incomplete extraction", async () => { const f = fixture(); f.adapter.stream.capabilities.mirror = "native-replace"; const run = await session(); const input = { ...f.options(run, [record("a")]), targetBaseline: "replace" as const }; f.setBatch(async (_action, batch) => ({ outcomes: batch.records.map(row => ({ operationId: row.operationId, status: "rejected", code: "invalid", safeReason: "Invalid", })), })); await expect(runSnapshotMirror(input)).rejects.toThrow(); expect(f.calls).not.toContain("finish"); const next = await session("incomplete"); const incomplete = { ...f.options(next, []), targetBaseline: "replace" as const, source: async function* () { yield record("a"); throw new Error("source failed"); }, }; await expect(runSnapshotMirror(incomplete)).rejects.toThrow(); expect(f.calls).not.toContain("finish"); }); it("retains an ambiguous replacement cleanup for reconciliation without resubmitting it", async () => { const f = fixture(); f.adapter.stream.capabilities.mirror = "native-replace"; f.setFinish("throw"); const run = await session(); const options = { ...f.options(run, [record("a")]), targetBaseline: "replace" as const }; await expect(runSnapshotMirror(options)).rejects.toThrow(); expect(await phase()).toBe("finish_prepared"); await expect( resumeSnapshotMirror( { ...options, persistence: await takeover(run) }, { ...f.recovery, reconcileFinish: async saved => { expect(saved).toBeUndefined(); throw new Error("Receipt unavailable; do not replay"); }, } ) ).rejects.toThrow(); expect(f.calls.filter(c => c === "finish")).toHaveLength(1); expect((await durable()).head.snapshot?.replacementStatus).toBe("prepared"); }); it("stages a full page with bounded SQL and seals exact retry/deduplication counts", async () => { const run = await session(); await run.delivery.prepareInit({}); await run.delivery.acknowledgeInit({}); await run.snapshots.start(); const identity = { identity: "shared", upsert: { id: "shared" }, remove: { id: "shared" } }; const page = Array.from({ length: 1000 }, (_, i) => ({ key: contentHash(i), identities: [identity] })); const query = vi.spyOn(Client.prototype, "query"); try { await run.snapshots.append(page, 1); expect(query.mock.calls.length).toBeLessThanOrEqual(10); } finally { query.mockRestore(); } const before = await run.snapshots.status(); expect(before?.sourceKeyCount).toBe(1000); await run.snapshots.append(page, 1); expect(await run.snapshots.status()).toEqual(before); await run.snapshots.append([{ key: contentHash(1000), identities: [identity] }], 2); const after = await run.snapshots.status(); expect(after?.sourceKeyCount).toBe(1001); await expect( run.snapshots.append([{ key: contentHash(1001), identities: [{ ...identity, remove: { id: "different" } }] }], 3) ).rejects.toThrow("Conflicting payloads"); expect(await run.snapshots.status()).toEqual(after); expect(await run.snapshots.seal()).toEqual({ uniqueMembers: 1 }); expect((await durable()).head.snapshot).toMatchObject({ sealed: true, keys: 1001, entries: 1001, page: 2 }); expect(await run.snapshots.page("additions")).toHaveLength(1); }); it("refreshes only due unchanged members, using acceptance time rather than source changes", async () => { const f = fixture(); f.adapter.refreshAfterMs = 30 * 86400_000; const rows = [record("old"), record("fresh")]; const now = Date.now(); vi.useFakeTimers({ toFake: ["Date"] }); vi.setSystemTime(now - 31 * 86400_000); await runSnapshotMirror(f.options(await session("baseline"), [record("old")])); vi.setSystemTime(now); // Add a fresh member without refreshing the old one yet. f.adapter.refreshAfterMs = 60 * 86400_000; await runSnapshotMirror(f.options(await session("add-fresh"), rows)); f.requests.length = 0; await runSnapshotMirror(f.options(await session("unchanged"), rows)); expect(f.requests).toHaveLength(0); f.adapter.refreshAfterMs = 30 * 86400_000; const refresh = await session("refresh"); await runSnapshotMirror(f.options(refresh, rows)); expect(f.requests.map(r => r.action)).toEqual(["upsert"]); expect(f.requests[0].batch.records.map(r => r.row.member)).toEqual([member("old")]); f.requests.length = 0; const next = await session("fresh-again"); await runSnapshotMirror(f.options(next, rows)); expect(f.requests).toHaveLength(0); }); it("waits for refresh acceptance before removals and keeps its cutoff and payload on recovery", async () => { const f = fixture(); f.adapter.refreshAfterMs = 30 * 86400_000; await runSnapshotMirror(f.options(await session("baseline"), [record("stay"), record("gone")])); vi.useFakeTimers({ toFake: ["Date"] }); vi.setSystemTime(Date.now() + 31 * 86400_000); f.requests.length = 0; f.adapter.batchDelivery = "asynchronous"; f.setBatch(async (_action, batch) => ({ outcomes: batch.records.map(r => ({ operationId: r.operationId, status: "staged" })), remoteJobIds: [batch.batchId], })); const run = await session("refresh"); expect((await runSnapshotMirror(f.options(run, [record("stay")]))).delivery).toBe("pending"); const cutoff = (await durable()).head.snapshot!.refreshBefore; expect(f.requests.map(r => r.action)).toEqual(["upsert"]); const addition = f.requests[0].batch; // A later recovery must not advance the saved generation cutoff or reproject rows. f.adapter.refreshAfterMs = 1; const resumed = await takeover(run); f.calls.length = 0; expect((await resumeSnapshotMirror(f.options(resumed, []), f.recovery)).delivery).toBe("pending"); expect(f.calls).toEqual(["reconcile"]); expect((await durable()).head.snapshot!.refreshBefore).toEqual(cutoff); expect(f.requests[0].batch).toEqual(addition); f.receipts.set(addition.batchId, accepted(addition)); expect((await resumeSnapshotMirror(f.options(await takeover(run), []), f.recovery)).delivery).toBe("pending"); expect(f.requests.map(r => r.action)).toEqual(["upsert", "remove"]); const removal = f.requests[1].batch; f.receipts.set(removal.batchId, accepted(removal)); expect((await resumeSnapshotMirror(f.options(await takeover(run), []), f.recovery)).delivery).toBe("accepted"); expect(f.projections()).toBe(3); expect(await membership()).toHaveLength(1); }); it("reconciles asynchronous additions before removals and removals before finalization", async () => { const f = fixture(); await runSnapshotMirror(f.options(await session("baseline"), [record("old")])); f.calls.length = 0; f.requests.length = 0; f.adapter.batchDelivery = "asynchronous"; f.setBatch(async (_action, batch) => ({ outcomes: batch.records.map(row => ({ operationId: row.operationId, status: "staged" })), remoteJobIds: [batch.batchId], })); const run = await session("async"); const rows = [record("a"), record("b"), record("c")]; expect(await runSnapshotMirror(f.options(run, rows))).toEqual({ delivery: "pending", sourceSequence: 3 }); expect(f.calls).toEqual(["create", "init", "source", "upsert", "upsert"]); expect(await membership()).toHaveLength(1); expect(await phase()).toBe("running"); expect((await run.snapshots.status())?.sealed).toBe(true); // Poll all durable requests without opening a source or reattaching a writer. f.calls.length = 0; expect((await resumeSnapshotMirror(f.options(await takeover(run), []), f.recovery)).delivery).toBe("pending"); expect(f.calls).toEqual(["reconcile", "reconcile"]); expect(f.requests).toHaveLength(2); for (const request of f.requests) f.receipts.set(request.batch.batchId, accepted(request.batch)); f.calls.length = 0; expect(await resumeSnapshotMirror(f.options(await takeover(run), []), f.recovery)).toEqual({ delivery: "pending", sourceSequence: 4, }); expect(f.calls).toEqual(["reconcile", "reconcile", "attach", "remove"]); expect(await membership()).toHaveLength(4); const removal = f.requests.at(-1)!; expect(removal.action).toBe("remove"); f.receipts.set(removal.batch.batchId, accepted(removal.batch)); f.calls.length = 0; expect((await resumeSnapshotMirror(f.options(await takeover(run), []), f.recovery)).delivery).toBe("accepted"); expect(f.calls).toEqual(["reconcile", "attach", "finish"]); expect(await membership()).toHaveLength(3); expect(await phase()).toBe("complete"); expect(f.projections()).toBe(4); // Once for baseline and once per new source row, never on recovery. }); it.each([ ["snapshot-diff", "staged"], ["snapshot-diff", "accepted"], ["native-replace", "staged"], ["native-replace", "accepted"], ] as const)( "resumes unsent %s uploads while earlier batches are pending and new batches are %s", async (strategy, outcome) => { const baseline = fixture(); await runSnapshotMirror(baseline.options(await session("baseline"), [record("old")])); const f = fixture(); f.adapter.stream.capabilities.mirror = strategy; f.adapter.batchDelivery = "asynchronous"; f.setBatch(async (_action, batch) => ({ outcomes: batch.records.map(row => ({ operationId: row.operationId, status: "staged" })), remoteJobIds: [batch.batchId], })); const options = (run: Session, rows: MirrorSourceRecord[] = []) => ({ ...f.options(run, rows), targetBaseline: strategy === "native-replace" ? ("replace" as const) : ("tracked" as const), }); const run = await session("interrupted"); const acknowledge = run.delivery.acknowledge; let submitted = 0; run.delivery = { ...run.delivery, acknowledge: async (...args) => { await acknowledge(...args); if (++submitted === 2) throw new Error("worker stopped after second upload"); }, }; const rows = Array.from({ length: 7 }, (_, i) => record(`new-${i}`)); await expect(runSnapshotMirror(options(run, rows))).rejects.toThrow(); expect(f.requests.map(r => r.batch.records.length)).toEqual([2, 2]); if (outcome === "accepted") f.setBatch(async (_action, batch) => accepted(batch)); f.calls.length = 0; expect(await resumeSnapshotMirror(options(await takeover(run)), f.recovery)).toEqual({ delivery: "pending", sourceSequence: 7, }); expect(f.calls).toEqual(["reconcile", "reconcile", "attach", "upsert", "upsert"]); expect(f.requests.map(r => r.batch.records.length)).toEqual([2, 2, 2, 1]); const uploaded = f.requests.flatMap(r => r.batch.records); expect(new Set(uploaded.map(r => r.operationId)).size).toBe(7); expect(new Set(uploaded.map(r => r.row.member)).size).toBe(7); expect(uploaded.map(r => r.sourceSequence)).toEqual([1, 2, 3, 4, 5, 6, 7]); expect(await membership()).toContainEqual({ member: member("old"), value: "v" }); expect(await phase()).toBe("running"); // Repeated refreshes don't replay submitted rows or authorize deletions. f.calls.length = 0; expect((await resumeSnapshotMirror(options(await takeover(run)), f.recovery)).delivery).toBe("pending"); expect(f.calls.every(call => call === "reconcile")).toBe(true); expect(f.requests).toHaveLength(4); for (const { batch } of f.requests) f.receipts.set(batch.batchId, accepted(batch)); f.setBatch(async (_action, batch) => accepted(batch)); expect((await resumeSnapshotMirror(options(await takeover(run)), f.recovery)).delivery).toBe("accepted"); expect(await membership()).toHaveLength(7); expect(await membership()).not.toContainEqual({ member: member("old"), value: "v" }); expect(f.projections()).toBe(7); expect(f.requests.filter(r => r.action === "upsert")).toHaveLength(4); expect(await phase()).toBe("complete"); } ); it.each(["unknown", "missing-job-id", "rejected"] as const)( "does not continue unsent uploads when reconciliation is %s", async status => { const f = fixture(); f.adapter.batchDelivery = "asynchronous"; f.setBatch(async (_action, batch) => ({ outcomes: batch.records.map(row => ({ operationId: row.operationId, status: "staged" })), remoteJobIds: [batch.batchId], })); const run = await session(); const acknowledge = run.delivery.acknowledge; run.delivery = { ...run.delivery, acknowledge: async (...args) => { await acknowledge(...args); throw new Error("worker stopped"); }, }; await expect(runSnapshotMirror(f.options(run, [record("a"), record("b"), record("c")]))).rejects.toThrow(); f.calls.length = 0; await expect( resumeSnapshotMirror(f.options(await takeover(run), []), { ...f.recovery, reconcileBatch: async batch => { if (status === "unknown") throw new Error("submission cannot be reconciled"); return { outcomes: batch.records.map(row => status === "rejected" ? { operationId: row.operationId, status: "rejected" as const, code: "invalid", safeReason: "Invalid", } : { operationId: row.operationId, status: "staged" as const } ), }; }, }) ).rejects.toThrow(); expect(f.calls).toEqual([]); expect(f.requests).toHaveLength(1); expect(await phase()).toBe("running"); } ); it("stops async reconciliation on a permanent rejection and preserves known accepted effects", async () => { const f = fixture(); f.adapter.batchDelivery = "asynchronous"; f.setBatch(async (_action, batch) => ({ outcomes: batch.records.map(row => ({ operationId: row.operationId, status: "staged" })), remoteJobIds: [batch.batchId], })); const run = await session(); await runSnapshotMirror(f.options(run, [record("a"), record("b")])); const batch = f.requests[0].batch; f.receipts.set(batch.batchId, { outcomes: batch.records.map((row, i) => i === 0 ? { operationId: row.operationId, status: "accepted" } : { operationId: row.operationId, status: "rejected", code: "invalid", safeReason: "Invalid" } ), }); f.calls.length = 0; await expect(resumeSnapshotMirror(f.options(await takeover(run), []), f.recovery)).rejects.toThrow(); expect(f.calls).toEqual(["reconcile"]); expect(await membership()).toHaveLength(1); expect(await phase()).toBe("running"); }); it("does not resubmit an asynchronous request after an ambiguous acknowledgement", async () => { const f = fixture(); f.adapter.batchDelivery = "asynchronous"; f.setBatch(async (_action, batch) => ({ outcomes: batch.records.map(row => ({ operationId: row.operationId, status: "staged" })), remoteJobIds: [batch.batchId], })); const run = await session(); const acknowledge = run.delivery.acknowledge; run.delivery = { ...run.delivery, acknowledge: async (...args) => { await acknowledge(...args); throw new Error("lost database response"); }, }; await expect(runSnapshotMirror(f.options(run, [record("a")]))).rejects.toThrow(); f.calls.length = 0; expect((await resumeSnapshotMirror(f.options(await takeover(run), []), f.recovery)).delivery).toBe("pending"); expect(f.calls).toEqual(["reconcile"]); expect(f.requests).toHaveLength(1); }); it("keeps an async request without a recoverable ID blocked", async () => { const f = fixture(); f.adapter.batchDelivery = "asynchronous"; f.setBatch(async (_action, batch) => ({ outcomes: batch.records.map(row => ({ operationId: row.operationId, status: "staged" })), })); const run = await session(); await expect(runSnapshotMirror(f.options(run, [record("a")]))).rejects.toThrow(); expect(f.calls).not.toContain("remove"); expect(f.calls).not.toContain("finish"); expect(f.calls).not.toContain("abort"); expect(await phase()).toBe("running"); }); it("collects all input before bounded writes and final promotion", async () => { const f = fixture(), run = await session(); expect(await runSnapshotMirror(f.options(run, [record("a"), record("b"), record("c")]))).toEqual({ delivery: "accepted", sourceSequence: 3, }); expect(f.calls).toEqual(["create", "init", "source", "upsert", "upsert", "finish"]); expect(f.requests.map(r => r.batch.records.length)).toEqual([2, 1]); expect(f.projections()).toBe(3); expect(await membership()).toHaveLength(3); expect(await phase()).toBe("complete"); expect( ( await admin.query( "SELECT committed_generation FROM newjitsu.reverse_sync_control ORDER BY run_order DESC LIMIT 1" ) ).rows[0].committed_generation ).toBe("run"); }); it("handles unchanged, shared and changed identities across generations without rehashing", async () => { const f = fixture(), first = await session(); await runSnapshotMirror(f.options(first, [record("key1", "Shared"), record("key2", "SHARED"), record("old")])); expect(f.requests.flatMap(r => r.batch.records.map(row => row.row.member)).sort()).toEqual( [member("shared"), member("old")].sort() ); const second = await session("second"); f.calls.length = 0; f.requests.length = 0; await runSnapshotMirror(f.options(second, [record("key2", "shared"), record("old", "new")])); expect(f.requests.map(r => [r.action, r.batch.records.map(row => row.row.member)])).toEqual([ ["upsert", [member("new")]], ["remove", [member("old")]], ]); expect(await membership()).toHaveLength(2); const third = await session("third"); f.requests.length = 0; await runSnapshotMirror(f.options(third, [])); expect(f.requests.map(r => r.action)).toEqual(["remove"]); expect(await membership()).toEqual([]); expect(f.remote.size).toBe(0); }); it("excludes empty projections, retains shared members, and removes them only after their last projection disappears", async () => { const f = fixture(); const project = f.adapter.projection.project; f.adapter.projection.project = row => (row.value === "excluded" ? [] : project(row)); await runSnapshotMirror(f.options(await session(), [record("a", "shared"), record("b", "SHARED"), record("old")])); f.requests.length = 0; const rows = [ record("a", "shared", "excluded"), record("b", "SHARED"), record("old", "old", "excluded"), record("new"), ]; expect(await runSnapshotMirror(f.options(await session("second"), rows))).toEqual({ delivery: "accepted", sourceSequence: 2, }); expect(f.requests.map(r => [r.action, r.batch.records.map(row => row.row.member)])).toEqual([ ["upsert", [member("new")]], ["remove", [member("old")]], ]); expect([...f.remote.keys()].sort()).toEqual([member("shared"), member("new")].sort()); expect((await durable()).head.snapshot!.keys).toBe(4); const third = await session("third"); f.requests.length = 0; const excluded = rows.map(row => ({ ...row, row: { ...row.row, value: "excluded" } })); expect(await runSnapshotMirror(f.options(third, excluded))).toEqual({ delivery: "accepted", sourceSequence: 2 }); expect(f.requests.map(r => r.action)).toEqual(["remove"]); expect(f.remote.size).toBe(0); expect(await membership()).toEqual([]); expect(await phase()).toBe("complete"); }); it("finishes an all-excluded initial snapshot without provider batches", async () => { const f = fixture(); f.adapter.projection.project = () => []; expect(await runSnapshotMirror(f.options(await session(), [record("a"), record("b")]))).toEqual({ delivery: "accepted", sourceSequence: 0, }); expect(f.calls).toEqual(["create", "init", "source", "finish"]); expect(f.requests).toEqual([]); expect(await phase()).toBe("complete"); }); it.each(["invalid-row", "invalid-key", "duplicate-key"])( "does not hide %s behind an empty projection", async failure => { const f = fixture(); await runSnapshotMirror(f.options(await session(), [record("old")])); f.requests.length = 0; f.adapter.projection.project = () => []; const bad = failure === "invalid-row" ? { key: contentHash("bad"), row: { id: 42, value: "excluded" } } : failure === "invalid-key" ? { ...record("bad"), key: "invalid" } : record("a"); await expect( runSnapshotMirror(f.options(await session("second"), [record("a"), record("b"), bad])) ).rejects.toThrow("Snapshot mirror stopped"); expect(f.requests).toEqual([]); expect(f.remote.size).toBe(1); expect(await membership()).toHaveLength(1); } ); it.each(["throw", "invalid-payload"])("does not turn a %s projection failure into exclusion", async failure => { const f = fixture(); await runSnapshotMirror(f.options(await session(), [record("old")])); f.requests.length = 0; f.adapter.projection.project = row => { if (row.id !== "bad") return []; if (failure === "throw") throw new Error("Invalid source data"); return [{ identity: "bad", upsert: { member: 42, value: "v" }, remove: { member: "bad" } }]; }; await expect( runSnapshotMirror(f.options(await session("second"), [record("a"), record("b"), record("bad")])) ).rejects.toThrow("Snapshot mirror stopped"); expect(f.requests).toEqual([]); expect(f.remote.size).toBe(1); expect(await membership()).toHaveLength(1); }); it("finishes empty and unchanged snapshots explicitly without synthetic batches", async () => { const f = fixture(), first = await session(); await runSnapshotMirror(f.options(first, [])); expect(f.calls).toEqual(["create", "init", "source", "finish"]); const second = await session("second"); await runSnapshotMirror(f.options(second, [record("a")])); const third = await session("third"); f.calls.length = 0; f.requests.length = 0; expect(await runSnapshotMirror(f.options(third, [record("a")]))).toEqual({ delivery: "accepted", sourceSequence: 0, }); expect(f.calls).toEqual(["create", "init", "source", "finish"]); expect(f.requests).toEqual([]); }); it.each(["invalid", "duplicate", "conflict", "source-throw"])( "suppresses all delivery for late %s source failure", async failure => { const f = fixture(), run = await session(); const rows = [ record("a"), record("b"), failure === "duplicate" ? record("a") : failure === "conflict" ? record("c", "a", "different") : { key: contentHash("c"), row: { id: 42, value: "v" } }, ]; const input = f.options(run, rows); if (failure === "source-throw") input.source = async function* () { yield record("a"); throw new Error("private-source-data"); }; await expect(runSnapshotMirror(input)).rejects.toThrow("Snapshot mirror stopped"); expect(f.requests).toEqual([]); expect(f.calls.at(-1)).toBe("abort"); expect(await phase()).toBe("aborted"); expect(await membership()).toEqual([]); } ); it.each(["staged", "rejected"] as const)( "persists %s results before suppressing removal and promotion", async status => { const f = fixture(), first = await session(); await runSnapshotMirror(f.options(first, [record("old")])); const run = await session("next"); f.calls.length = 0; f.setBatch((_action, batch) => Promise.resolve({ outcomes: batch.records.map((row, i) => i === 0 ? { operationId: row.operationId, status: "accepted" } : status === "staged" ? { operationId: row.operationId, status } : { operationId: row.operationId, status, code: "invalid", safeReason: "Invalid row" } ), }) ); await expect(runSnapshotMirror(f.options(run, [record("a"), record("b")]))).rejects.toThrow(); expect(f.calls).not.toContain("remove"); expect(f.calls).not.toContain("finish"); expect(f.calls.at(-1)).toBe("abort"); expect(await membership()).toHaveLength(2); expect( ( await admin.query( "SELECT committed_generation FROM newjitsu.reverse_sync_control ORDER BY run_order DESC LIMIT 1" ) ).rows[0].committed_generation ).toBe("run"); } ); it("removes accepted failed-run additions on the next successful full snapshot", async () => { const f = fixture(), first = await session(); f.setBatch((_action, batch) => Promise.resolve({ outcomes: batch.records.map((row, i) => i ? { operationId: row.operationId, status: "rejected", code: "invalid", safeReason: "Invalid" } : { operationId: row.operationId, status: "accepted" } ), }) ); await expect(runSnapshotMirror(f.options(first, [record("a"), record("b")]))).rejects.toThrow(); expect(await membership()).toHaveLength(1); const next = await session("next"); f.setBatch(undefined); await runSnapshotMirror(f.options(next, [])); expect(await membership()).toEqual([]); expect(f.remote.size).toBe(0); }); it("reconciles an ambiguous batch from its exact stored payload without source replay", async () => { const f = fixture(), run = await session(); f.setAfterBatch(() => { throw new Error("private-remote-response"); }); await expect(runSnapshotMirror(f.options(run, [record("a"), record("b"), record("c")]))).rejects.toThrow(); expect(await phase()).toBe("running"); expect(f.calls).not.toContain("abort"); expect(await membership()).toEqual([]); const recovered = await takeover(run); f.setAfterBatch(undefined); const before = f.projections(); f.calls.length = 0; const options = f.options(recovered, [record("changed-query")]); expect(await resumeSnapshotMirror(options, f.recovery)).toEqual({ delivery: "accepted", sourceSequence: 3 }); expect(f.calls).toEqual(["reconcile", "attach", "upsert", "finish"]); expect(f.projections()).toBe(before); expect(await membership()).toHaveLength(3); expect((await recovered.core.state()).store).toMatchObject({ reconciled: true }); expect(f.requests[0].batch.records[0].row.member).toMatch(/^[a-f0-9]{64}$/); }); it("keeps unknown work blocked without reconciliation and rejects non-recovery sessions", async () => { const f = fixture(), run = await session(); f.setAfterBatch(() => { throw new Error("lost response"); }); await expect(runSnapshotMirror(f.options(run, [record("a")]))).rejects.toThrow(); const recovered = await takeover(run); const before = f.calls.length; await expect( resumeSnapshotMirror(f.options(recovered, []), { attachWriter: f.recovery.attachWriter }) ).rejects.toThrow(); await expect(resumeSnapshotMirror(f.options(run, []), f.recovery)).rejects.toThrow(); expect(f.calls).toHaveLength(before); expect(await phase()).toBe("running"); }); it.each(["pending", "throw"] as const)( "reconciles %s finalization without a second finish or writer initialization", async outcome => { const f = fixture(), run = await session(); f.setFinish(outcome); if (outcome === "pending") expect((await runSnapshotMirror(f.options(run, [record("a")]))).delivery).toBe("pending"); else await expect(runSnapshotMirror(f.options(run, [record("a")]))).rejects.toThrow(); expect(f.calls).not.toContain("abort"); const recovered = await takeover(run); f.calls.length = 0; expect(await resumeSnapshotMirror(f.options(recovered, []), f.recovery)).toEqual({ delivery: "accepted", sourceSequence: 1, }); expect(f.calls).toEqual(["reconcileFinish"]); expect((await recovered.core.state()).store.final).toBe(true); expect(await phase()).toBe("complete"); } ); it("retries local final-state promotion without repeating accepted finalization", async () => { const f = fixture(), run = await session(); await admin.query( "ALTER TABLE newjitsu.source_state ADD CONSTRAINT injected_commit_failure CHECK (false) NOT VALID" ); try { await expect(runSnapshotMirror(f.options(run, [record("a")]))).rejects.toThrow(); } finally { await admin.query("ALTER TABLE newjitsu.source_state DROP CONSTRAINT injected_commit_failure"); } expect(await phase()).toBe("finish_accepted"); expect(f.calls).not.toContain("abort"); expect( ( await admin.query( "SELECT committed_generation FROM newjitsu.reverse_sync_control ORDER BY run_order DESC LIMIT 1" ) ).rows[0].committed_generation ).toBeNull(); const recovered = await takeover(run); f.calls.length = 0; await resumeSnapshotMirror(f.options(recovered, []), f.recovery); expect(f.calls).toEqual([]); expect(await phase()).toBe("complete"); }); it("stops cancellation after acceptance before any removals", async () => { const f = fixture(), first = await session(); await runSnapshotMirror(f.options(first, [record("old")])); const next = await session("next"); f.calls.length = 0; f.setAfterBatch(() => { f.controller.abort(); }); await expect(runSnapshotMirror(f.options(next, [record("new")]))).rejects.toThrow(); expect(f.calls).not.toContain("remove"); expect(f.calls).not.toContain("finish"); expect(await membership()).toHaveLength(2); }); it("rejects unsupported adapter capabilities before provider calls", async () => { const f = fixture(), run = await session(); f.adapter.batchDelivery = "finish" as any; await expect(runSnapshotMirror(f.options(run, []))).rejects.toThrow(/per-batch/); expect(f.calls).toEqual([]); }); it("splits provider requests by exact manifest bytes", async () => { const f = fixture(), run = await session(); f.adapter.stream.batchSize = 100; const input = f.options( run, Array.from({ length: 5 }, (_, i) => record(String(i), String(i), "x".repeat(400))) ); input.maxBatchBytes = 1400; await runSnapshotMirror(input); expect(f.requests).toHaveLength(5); for (const request of f.requests) expect(Buffer.byteLength(JSON.stringify(request.batch))).toBeLessThan(1400); }); it("stops on a rejected removal while retaining accepted removal progress", async () => { const f = fixture(), first = await session(); await runSnapshotMirror(f.options(first, [record("a"), record("b"), record("c")])); const next = await session("next"); f.calls.length = 0; f.setBatch((_action, batch) => Promise.resolve({ outcomes: batch.records.map((row, i) => i ? { operationId: row.operationId, status: "rejected", code: "invalid", safeReason: "Rejected removal" } : { operationId: row.operationId, status: "accepted" } ), }) ); await expect(runSnapshotMirror(f.options(next, []))).rejects.toThrow(); expect(f.calls.filter(call => call === "remove")).toHaveLength(1); expect(f.calls).not.toContain("finish"); expect(await membership()).toHaveLength(2); expect( ( await admin.query( "SELECT committed_generation FROM newjitsu.reverse_sync_control ORDER BY run_order DESC LIMIT 1" ) ).rows[0].committed_generation ).toBe("run"); }); it("reconciles an ambiguous removal without recreating the writer or replaying the remove", async () => { const f = fixture(), first = await session(); await runSnapshotMirror(f.options(first, [record("a")])); const next = await session("next"); f.setAfterBatch(() => { throw new Error("lost removal response"); }); await expect(runSnapshotMirror(f.options(next, []))).rejects.toThrow(); expect(await membership()).toHaveLength(1); const recovered = await takeover(next); f.setAfterBatch(undefined); f.calls.length = 0; await resumeSnapshotMirror(f.options(recovered, []), f.recovery); expect(f.calls).toEqual(["reconcile", "attach", "finish"]); expect(await membership()).toEqual([]); }); it("does not reopen incomplete input during recovery", async () => { const f = fixture(), first = await session(); await first.delivery.prepareInit({}); await first.delivery.acknowledgeInit({}); await first.snapshots.start(); const recovered = await takeover(first); await expect(resumeSnapshotMirror(f.options(recovered, [record("changed")]), f.recovery)).rejects.toThrow(); expect(f.calls).toEqual([]); expect(await phase()).toBe("running"); }); it("does not initialize a writer after cancellation during construction", async () => { const f = fixture(), run = await session(); const createWriter = f.adapter.stream.createWriter; f.adapter.stream.createWriter = async ctx => { const writer = await createWriter(ctx); f.controller.abort(); return writer; }; await expect(runSnapshotMirror(f.options(run, [record("a")]))).rejects.toThrow(); expect(f.controller.signal.aborted).toBe(true); expect(f.calls).toEqual(["create"]); expect(await phase()).toBe("init_prepared"); expect(await membership()).toEqual([]); }); it("does not construct a writer or open a source after cancellation", async () => { const f = fixture(), run = await session(); f.controller.abort(); await expect(runSnapshotMirror(f.options(run, []))).rejects.toThrow(); expect(f.calls).toEqual([]); expect(await phase()).toBe("new"); }); it("enforces snapshot storage limits before any delivery", async () => { const limited = new Database(runtimeConfig, { ...storage, limits: { snapshotEntries: 1 } }); try { const f = fixture(), run = await session("run", "run", limited); await expect(runSnapshotMirror(f.options(run, [record("a"), record("b")]))).rejects.toThrow(); expect(f.requests).toEqual([]); expect(await phase()).toBe("aborted"); } finally { await limited.close(); } }); // Optional index/pagination scale check; deliberately seeds SQL rows rather than exercising million-row provider calls. // eslint-disable-next-line no-restricted-properties -- opt-in test workload, not application configuration. it.skipIf(process.env.RETL_MIRROR_SCALE_TEST !== "1")( "bounds diff pages with one million desired identities", async () => { const run = await session(); await run.delivery.prepareInit({}); await run.delivery.acknowledgeInit({}); await run.snapshots.start(); run.core.local.sql.exec( "WITH RECURSIVE n(x) AS (VALUES(1) UNION ALL SELECT x+1 FROM n WHERE x<999999) INSERT INTO desired SELECT printf('%064x',x),'unchanged','{}' FROM n; INSERT INTO members SELECT identity,payload,value,'2026-01-01T00:00:00.000Z',0 FROM desired;" ); const f = fixture(); const desired = f.adapter.projection.project({ id: "last", value: "v" })[0]; const effect = { ...desired, identityHash: contentHash(desired.identity), payloadHash: contentHash(desired.upsert), }; run.core.local.sql .prepare("INSERT INTO desired VALUES (?,?,?)") .run(effect.identityHash, effect.payloadHash, JSON.stringify(effect)); run.core.local.sql.exec("ANALYZE"); await run.snapshots.seal(); expect(await run.snapshots.page("additions", "", 2)).toEqual([effect]); expect(await run.snapshots.page("additions", effect.identityHash, 2)).toEqual([]); expect(Number(run.core.local.sql.prepare("SELECT count(*) AS n FROM desired").get()!.n)).toBe(1_000_000); }, 120_000 ); });