// v2.6.10 Phase 3 (server wiring) — notifyCoderClose fire-and-forget helper. // // The guarantee under test: the helper NEVER throws (so it can't break the // user's delete/archive path), targets the correct coder URL shape, and folds // every failure mode (non-2xx, network error) into a `false` result. import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { notifyCoderClose } from '../coder-notify.js'; const ORIGINAL_BOOCODER_URL = process.env.BOOCODER_URL; describe('notifyCoderClose', () => { beforeEach(() => { delete process.env.BOOCODER_URL; }); afterEach(() => { if (ORIGINAL_BOOCODER_URL === undefined) delete process.env.BOOCODER_URL; else process.env.BOOCODER_URL = ORIGINAL_BOOCODER_URL; }); it('POSTs the chat close hook at the default coder origin and resolves true on 2xx', async () => { const fetcher = vi.fn().mockResolvedValue(new Response(null, { status: 200 })); const ok = await notifyCoderClose('chat', 'chat-123', undefined, fetcher as unknown as typeof fetch); expect(ok).toBe(true); expect(fetcher).toHaveBeenCalledTimes(1); const [url, init] = fetcher.mock.calls[0]!; expect(url).toBe('http://boocoder:3000/api/chats/chat-123/close'); expect(init).toEqual({ method: 'POST' }); }); it('POSTs the session close hook with the sessions segment', async () => { const fetcher = vi.fn().mockResolvedValue(new Response(null, { status: 200 })); const ok = await notifyCoderClose('session', 'sess-abc', undefined, fetcher as unknown as typeof fetch); expect(ok).toBe(true); expect(fetcher.mock.calls[0]![0]).toBe('http://boocoder:3000/api/sessions/sess-abc/close'); }); it('honors BOOCODER_URL for the origin', async () => { process.env.BOOCODER_URL = 'http://100.114.205.53:9502'; const fetcher = vi.fn().mockResolvedValue(new Response(null, { status: 200 })); await notifyCoderClose('chat', 'c1', undefined, fetcher as unknown as typeof fetch); expect(fetcher.mock.calls[0]![0]).toBe('http://100.114.205.53:9502/api/chats/c1/close'); }); it('resolves false on a non-2xx response (does not throw)', async () => { const fetcher = vi.fn().mockResolvedValue(new Response(null, { status: 500 })); const log = { debug: vi.fn() }; const ok = await notifyCoderClose('chat', 'c1', log, fetcher as unknown as typeof fetch); expect(ok).toBe(false); expect(log.debug).toHaveBeenCalledTimes(1); }); it('resolves false on a network error (coder unreachable) — never rejects', async () => { const fetcher = vi.fn().mockRejectedValue(new Error('ECONNREFUSED')); const log = { debug: vi.fn() }; const ok = await notifyCoderClose('session', 's1', log, fetcher as unknown as typeof fetch); expect(ok).toBe(false); expect(log.debug).toHaveBeenCalledTimes(1); }); it('does not require a logger', async () => { const fetcher = vi.fn().mockRejectedValue(new Error('boom')); await expect( notifyCoderClose('chat', 'c1', undefined, fetcher as unknown as typeof fetch), ).resolves.toBe(false); }); });