apps/server fire-and-forgets BooCoder's Phase-3 close hooks (new coder-notify.ts, reuses BOOCODER_URL, never-rejects) on session-delete + chat archive/archive-all/delete, so warm backends + worktrees tear down immediately (idle-evict/reaper was the backstop). 3.7: BooCoder DiffPanel shows a muted one-liner when the selected provider can't see another agent's unapplied worktree edits (pure derivation from per-change agent + current provider, no new state). 6 new server tests (coder-notify); 537 server tests pass; web+server tsc/build clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
68 lines
3.0 KiB
TypeScript
68 lines
3.0 KiB
TypeScript
// 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);
|
|
});
|
|
});
|