// Unit tests with `@voltro/testing` — the headline of this template is the // durable KV behavior, so we exercise it thoroughly against the REAL in-memory // `ctx.kv` that `makeTestContext` provides (same contract as the production KV: // durable, TTL-honouring, never capacity-evicted). // // - `sync.pull` reads/advances a durable cursor and writes per-event // idempotency markers → asserted across TWO calls (the cursor must carry // over so the second call continues past it, not re-pull from 0). // - `sync.reset` clears the cursor (and optionally the markers). // // `sync.status` is written in Effect mode (`yield* Kv` from `@voltro/kv`), which // needs the `Kv` service layer provided — an integration concern, not a plain // unit run — so it is intentionally not driven here. // // The simulated upstream is deterministic: event N has externalId `ext-N` and // sequence N, so pulling `limit` fresh events advances the cursor by exactly // `limit`. Run with `voltro test` (vitest). import { describe, it, expect } from 'vitest' import { makeTestContext, mockStore } from '@voltro/testing' import { database } from '../database/schema' // registers synced_events / actors / tenants import syncPull from '../actions/sync.pull.action.server' import syncReset from '../actions/sync.reset.action.server' const subject = { type: 'user', id: 'user_1', tenantId: 'acme' } as const describe('sync.pull (durable cursor + idempotency markers)', () => { it('first pull ingests N events, inserts the rows, and advances the cursor to N', async () => { const ctx = makeTestContext({ subject, store: mockStore({ synced_events: [] }) }) const result = await syncPull({ limit: 3 }, ctx) expect(result.pulled).toBe(3) expect(result.skipped).toBe(0) expect(result.cursor).toBe(3) // The event rows landed in the relational store, tenant-stamped. const rows = await ctx.store.query(database.syncedEvents.descriptor) expect(rows).toHaveLength(3) expect(rows.every((r) => r['tenantId'] === 'acme')).toBe(true) expect((rows.map((r) => r['externalId']) as string[]).sort()).toEqual([ 'ext-1', 'ext-2', 'ext-3', ]) // The durable cursor persisted in ctx.kv. expect(await ctx.kv.get('sync:acme:cursor')).toBe(3) }) it('a second pull continues PAST the cursor rather than re-pulling from 0', async () => { const ctx = makeTestContext({ subject, store: mockStore({ synced_events: [] }) }) const first = await syncPull({ limit: 2 }, ctx) expect(first.cursor).toBe(2) const second = await syncPull({ limit: 2 }, ctx) // Continues at 3..4 — all fresh (distinct externalIds), none skipped. expect(second.pulled).toBe(2) expect(second.skipped).toBe(0) expect(second.cursor).toBe(4) const rows = await ctx.store.query(database.syncedEvents.descriptor) expect(rows).toHaveLength(4) // 2 + 2, no duplicates expect((rows.map((r) => r['sequence']) as number[]).sort((a, b) => a - b)).toEqual([ 1, 2, 3, 4, ]) }) }) describe('sync.reset', () => { it('clears the durable cursor so the next pull restarts from 0', async () => { const ctx = makeTestContext({ subject, store: mockStore({ synced_events: [] }) }) await syncPull({ limit: 2 }, ctx) expect(await ctx.kv.get('sync:acme:cursor')).toBe(2) const reset = await syncReset({}, ctx) expect(reset.cursorCleared).toBe(true) // the key existed and was removed expect(await ctx.kv.has('sync:acme:cursor')).toBe(false) // Cursor gone → the next pull re-reads from the getOrElse default (0). const afterReset = await syncPull({ limit: 1 }, ctx) expect(afterReset.cursor).toBe(1) }) it('reports cursorCleared=false when there was no cursor to clear', async () => { const ctx = makeTestContext({ subject, store: mockStore({ synced_events: [] }) }) const reset = await syncReset({}, ctx) expect(reset.cursorCleared).toBe(false) expect(reset.markersCleared).toBe(0) }) it('sweeps the idempotency markers when { markers: true }', async () => { const ctx = makeTestContext({ subject, store: mockStore({ synced_events: [] }) }) await syncPull({ limit: 3 }, ctx) // writes 3 seen-markers const reset = await syncReset({ markers: true }, ctx) expect(reset.markersCleared).toBe(3) expect(await ctx.kv.list('sync:acme:seen:')).toHaveLength(0) }) })