import { describe, test, expect, beforeAll, afterAll } from 'bun:test'; import { setupFileRig, type FileTestRig } from '../integration/helpers/rig.js'; import { ConflictError, NotFoundError, type FileEntry } from '../../src/index.js'; /** * Replay-window: the dispatcher rejects requests where `signedAt` is more * than ±5 min from the server clock. Plus: idempotent retries on the same * mutation key produce a single side-effect even with stale signedAt. */ describe('Replay window + idempotent retry', () => { let rig: FileTestRig; let writeCount = 0; const blobs = new Map(); beforeAll(async () => { rig = await setupFileRig({ mkdir: async (ctx) => { if (blobs.has(ctx.path)) throw new ConflictError('exists'); blobs.set(ctx.path, new Uint8Array(0)); const e: FileEntry = { name: ctx.path.split('/').filter(Boolean).pop() ?? '', kind: 'dir', size: 0, mtime: Date.now(), metadata: {}, }; return { entry: e }; }, write: async (ctx) => { writeCount++; if (ctx.args.content.kind !== 'inline') throw new Error('inline expected'); blobs.set(ctx.args.path, ctx.args.content.bytes); const e: FileEntry = { name: ctx.args.path.split('/').filter(Boolean).pop() ?? '', kind: 'file', size: ctx.args.content.bytes.byteLength, mtime: Date.now(), metadata: { sha256: ctx.args.content.sha256 }, }; return { entry: e }; }, stat: async (ctx) => { if (!blobs.has(ctx.path)) throw new NotFoundError(ctx.path); return { name: ctx.path.split('/').filter(Boolean).pop() ?? '', kind: 'file', size: 0, mtime: 0, metadata: {}, }; }, }); }); afterAll(async () => { await rig.teardown(); }); test('idempotent retry: same key + same args → single side-effect', async () => { writeCount = 0; // Idempotency keys are exactly 22 chars, base64url alphabet. const key = 'replay_key_1234567890A'; const data = new Uint8Array([1, 2, 3]); const r1 = await rig.fs.write('/replay.bin', data, { idempotencyKey: key }); const r2 = await rig.fs.write('/replay.bin', data, { idempotencyKey: key }); const r3 = await rig.fs.write('/replay.bin', data, { idempotencyKey: key }); expect(writeCount).toBe(1); expect(r1.entry.size).toBe(3); expect(r2.entry.size).toBe(3); expect(r3.entry.size).toBe(3); }); test('out-of-window signedAt → InvalidSignatureError (skew rejection)', async () => { // Build a custom client that LIES about signedAt. We hand-craft an // RpcRequest envelope and ship it via the underlying channel. const { ShadeFileRpcChannel, PendingRpcRegistry, attachClientRouting, KIND_STAT_V1, generateRequestId, } = await import('../../src/index.js'); const aliceChannel = new ShadeFileRpcChannel(rig.alice); const alicePending = new PendingRpcRegistry(); attachClientRouting(aliceChannel, alicePending); const requestId = generateRequestId(); const stalePromise = alicePending.register(requestId, { timeoutMs: 3000 }); await aliceChannel.send('bob', { kind: KIND_STAT_V1, id: requestId, args: { path: '/replay.bin' }, sig: 'unsigned', signedAt: Date.now() - 10 * 60 * 1000, // 10 min in the past — outside ±5 min window }); await expect(stalePromise).rejects.toThrow(/replay window|signature/i); aliceChannel.destroy(); }); test('signedAt far in the future → also rejected', async () => { const { ShadeFileRpcChannel, PendingRpcRegistry, attachClientRouting, KIND_STAT_V1, generateRequestId, } = await import('../../src/index.js'); const aliceChannel = new ShadeFileRpcChannel(rig.alice); const alicePending = new PendingRpcRegistry(); attachClientRouting(aliceChannel, alicePending); const requestId = generateRequestId(); const promise = alicePending.register(requestId, { timeoutMs: 3000 }); await aliceChannel.send('bob', { kind: KIND_STAT_V1, id: requestId, args: { path: '/replay.bin' }, sig: 'unsigned', signedAt: Date.now() + 10 * 60 * 1000, }); await expect(promise).rejects.toThrow(/replay window|signature/i); aliceChannel.destroy(); }); });