import { Hash, Hex } from 'ox' import * as Idempotency from './Idempotency.js' import * as Store from './Store.js' import * as Ttl from './Ttl.js' const identity = { apiKeyId: 'key_test', idempotencyKey: 'req_01j3j1k2l3m4n5p6q7r8s9t0u', inputHash: `0x${'11'.repeat(32)}`, } function coordinator(namespace = 'test:v1') { return Idempotency.create<{ id: string }>({ namespace, pendingTtl: Ttl.minutes(1), receiptTtl: Ttl.days(1), }) } describe('inputHash', () => { test('binds semantic content instead of JSON property order', () => { expect(Idempotency.inputHash({ a: 1, b: [{ d: 2, c: 3 }] })).toBe( Idempotency.inputHash({ b: [{ c: 3, d: 2 }], a: 1 }), ) expect(Idempotency.inputHash({ a: 1 })).not.toBe(Idempotency.inputHash({ a: 2 })) }) test('rejects values JSON cannot represent', () => { expect(() => Idempotency.inputHash({ callback: () => {} })).toThrowErrorMatchingInlineSnapshot( `[Error: Idempotency inputs must be JSON values.]`, ) expect(() => Idempotency.inputHash({ values: [Number.NaN, Number.POSITIVE_INFINITY] }), ).toThrowErrorMatchingInlineSnapshot(`[Error: Idempotency inputs must be JSON values.]`) }) }) describe('create', () => { test('claims, blocks parallel requests, and replays the completed response', async () => { const state = Store.memory() const requests = coordinator() const claimed = await requests.claim({ identity, state }) expect(claimed.type).toBe('claimed') if (claimed.type !== 'claimed') return await expect(requests.claim({ identity, state })).resolves.toEqual({ type: 'pending' }) await expect( requests.claim({ identity: { ...identity, inputHash: `0x${'22'.repeat(32)}` }, state }), ).resolves.toEqual({ type: 'mismatch' }) await requests.complete({ identity, response: { id: 'ftr_test' }, state, token: claimed.token }) await expect(requests.claim({ identity, state })).resolves.toEqual({ response: { id: 'ftr_test' }, type: 'replay', }) }) test('fences completion and release by claim token', async () => { const state = Store.memory() const requests = coordinator() const claimed = await requests.claim({ identity, state }) if (claimed.type !== 'claimed') throw new Error('Expected a claimed request.') // A stale token can neither complete nor release the live claim. await requests.complete({ identity, response: { id: 'stale' }, state, token: 'stale-token' }) await requests.release({ identity, state, token: 'stale-token' }) await expect(requests.claim({ identity, state })).resolves.toEqual({ type: 'pending' }) await requests.release({ identity, state, token: claimed.token }) await expect(requests.claim({ identity, state })).resolves.toMatchObject({ type: 'claimed' }) }) test('rejects responses JSON cannot represent before completing', async () => { const state = Store.memory() const requests = Idempotency.create({ namespace: 'test:v1', pendingTtl: Ttl.minutes(1), receiptTtl: Ttl.days(1), }) const claimed = await requests.claim({ identity, state }) if (claimed.type !== 'claimed') throw new Error('Expected a claimed request.') await expect( requests.complete({ identity, response: undefined, state, token: claimed.token }), ).rejects.toThrowErrorMatchingInlineSnapshot( `[Error: Idempotency responses must be JSON values.]`, ) await expect(requests.claim({ identity, state })).resolves.toEqual({ type: 'pending' }) }) test('isolates coordinators by namespace over one store', async () => { const state = Store.memory() const first = coordinator('first:v1') const second = coordinator('second:v1') await expect(first.claim({ identity, state })).resolves.toMatchObject({ type: 'claimed' }) await expect(second.claim({ identity, state })).resolves.toMatchObject({ type: 'claimed' }) }) test('preserves the durable key format consumers already stored', async () => { const state = Store.memory() await coordinator('mpp:relay:v1').claim({ identity, state }) const digest = Hash.sha256(Hex.fromString(identity.idempotencyKey)).slice(2) expect((await state.list()).keys.map((key) => key.name)).toEqual([ `mpp:relay:v1:${identity.apiKeyId}:${digest}`, ]) }) })