import { describe, it, expect, vi } from 'vitest'; import { runSync, type SyncDeps } from '../src/sync.js'; import type { DirectoryReader, CognitoGroupTarget, SyncStateWriter } from '../src/clients.js'; function fakes( directoryMembers: Record, cognitoMembers: Record, ) { const added: string[] = []; const removed: string[] = []; const writes: number[] = []; const directory: DirectoryReader = { listGroupMembers: vi.fn(async (email: string) => directoryMembers[email] ?? []), }; const cognito: CognitoGroupTarget = { ensureGroup: vi.fn(async () => {}), listMembers: vi.fn(async (g: string) => cognitoMembers[g] ?? []), addMember: vi.fn(async (g: string, e: string) => { added.push(`${g}:${e}`); }), removeMember: vi.fn(async (g: string, e: string) => { removed.push(`${g}:${e}`); }), }; const syncState: SyncStateWriter = { writeLastSuccessfulSync: vi.fn(async (ms: number) => { writes.push(ms); }), }; const deps: SyncDeps = { directory, cognito, syncState, domain: 'seahavenind.com', now: () => 1000, }; return { deps, added, removed, writes, directory, cognito }; } describe('runSync', () => { it('reconciles each group and writes the freshness marker once, on full success', async () => { const { deps, added, removed, writes } = fakes( { 'sh-mcp-finance@seahavenind.com': ['adam@seahavenind.com', 'new@seahavenind.com'] }, { 'sh-mcp-finance': ['adam@seahavenind.com', 'gone@seahavenind.com'] }, ); await runSync(deps); expect(added).toContain('sh-mcp-finance:new@seahavenind.com'); expect(removed).toContain('sh-mcp-finance:gone@seahavenind.com'); expect(writes).toEqual([1000]); // marker written exactly once }); it('ensures every managed group exists', async () => { const { deps, cognito } = fakes({}, {}); await runSync(deps); expect(cognito.ensureGroup).toHaveBeenCalledTimes(4); }); it('FAIL CLOSED: a Directory error aborts and the marker is NOT written', async () => { const { deps, writes, directory } = fakes({}, {}); (directory.listGroupMembers as ReturnType).mockRejectedValueOnce( new Error('directory 503'), ); await expect(runSync(deps)).rejects.toThrow('directory 503'); expect(writes).toEqual([]); // stale marker preserved → pre-token fails closed }); it('FAIL CLOSED: a Cognito reconcile error aborts before the marker write', async () => { const { deps, writes, cognito } = fakes( { 'sh-mcp-ops@seahavenind.com': ['x@seahavenind.com'] }, {}, ); (cognito.addMember as ReturnType).mockRejectedValueOnce( new Error('cognito throttled'), ); await expect(runSync(deps)).rejects.toThrow('cognito throttled'); expect(writes).toEqual([]); }); });