import { afterEach, describe, expect, test } from "bun:test"; import type { ConfigAccess, HostAPI, Logger } from "@dispatch/kernel"; import { createApp } from "./app.js"; import { createTransportHttpExtension } from "./index.js"; import type { ConversationStore, CredentialStore, LspService, SessionOrchestrator, } from "./seam.js"; function fakeLogger(): Logger { return { debug() {}, info() {}, warn() {}, error() {}, child() { return fakeLogger(); }, span() { return { id: "fake-span", log: fakeLogger(), setAttributes() {}, addLink() {}, child() { return this; }, end() {}, }; }, }; } function fakeConversationStore(): ConversationStore { return { async append() {}, async load() { return []; }, async loadSince() { return []; }, async appendMetrics() {}, async loadMetrics() { return []; }, async getCwd() { return null; }, async setCwd() {}, async getReasoningEffort() { return null; }, async setReasoningEffort() {}, async listConversations() { return []; }, async getConversationMeta() { return null; }, async setConversationTitle() {}, async getConversationStatus() { return null; }, async setConversationStatus() {}, async replaceHistory() {}, async getCompactPercent() { return null; }, async setCompactPercent() {}, async forkHistory() {}, async setCompactedFrom() {}, async getWorkspace() { return null; }, async ensureWorkspace() { return { id: "default", title: "default", defaultCwd: null, createdAt: 0, lastActivityAt: 0 }; }, async setWorkspaceTitle() { return { id: "default", title: "default", defaultCwd: null, createdAt: 0, lastActivityAt: 0 }; }, async setWorkspaceDefaultCwd() { return { id: "default", title: "default", defaultCwd: null, createdAt: 0, lastActivityAt: 0 }; }, async deleteWorkspace() { return { closedCount: 0 }; }, async listWorkspaces() { return []; }, async getWorkspaceId() { return "default"; }, async setWorkspaceId() {}, async getEffectiveCwd() { return null; }, }; } function fakeOrchestrator(): SessionOrchestrator { return { startTurn() { return { started: true, turnId: "fake-turn" }; }, subscribe() { return () => {}; }, isActive() { return false; }, enqueue() { return { startedTurn: false, queue: [] }; }, closeConversation() { return { abortedTurn: false }; }, stopTurn() { return { abortedTurn: false }; }, async handleMessage() {}, }; } function fakeCredentialStore(): CredentialStore { return { resolve() { return undefined; }, async getModelInfo() { return undefined; }, async listCatalog() { return []; }, }; } function fakeLspService(): LspService { return { async status() { return []; }, }; } function fakeConfig(overrides: Record = {}): ConfigAccess { return { get(key: string): T | undefined { return overrides[key] as T | undefined; }, getAll() { return overrides; }, }; } const SERVICES = new Map([ ["conversation-store/store", fakeConversationStore()], ["session-orchestrator/orchestrator", fakeOrchestrator()], ["credential-store/registry", fakeCredentialStore()], ["lsp", fakeLspService()], ]); function createFakeHostAPI(configOverrides: Record = {}): HostAPI { return { defineTool() {}, defineProvider() {}, defineAuth() {}, on() { return () => {}; }, emit() {}, addFilter() { return () => {}; }, async applyFilters(_hook, value) { return value; }, provideService() {}, getService(handle) { return SERVICES.get(handle.id) as never; }, storage() { return { get: async () => null, set: async () => {}, delete: async () => {}, has: async () => false, keys: async () => [], }; }, config: fakeConfig(configOverrides), secrets: { get: async () => null, set: async () => {}, delete: async () => {} }, permissions: { check: async () => ({ allowed: true }) }, events: { emit() {} }, logger: fakeLogger(), getProviders() { return new Map(); }, getTools() { return new Map(); }, getAuthProviders() { return new Map(); }, getAuthProvider() { return undefined; }, getExtensions() { return []; }, scheduler: { register() {} }, }; } // ── Server helper (mirrors extension.ts logic for direct testing) ──────── function startServer(port = 0) { const app = createApp({ conversationStore: fakeConversationStore(), orchestrator: fakeOrchestrator(), credentialStore: fakeCredentialStore(), }); return Bun.serve({ port, fetch: app.fetch }); } // ── Tests ──────────────────────────────────────────────────────────────── describe("serves HTTP on the configured port", () => { let server: ReturnType; let port: number; afterEach(() => { server.stop(); }); test("GET /health returns 200", async () => { server = startServer(); port = server.port as number; const res = await fetch(`http://localhost:${port}/health`); expect(res.status).toBe(200); const body = (await res.json()) as { ok: boolean }; expect(body.ok).toBe(true); }); test("POST /chat returns NDJSON", async () => { server = startServer(); port = server.port as number; const res = await fetch(`http://localhost:${port}/chat`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ message: "hi", conversationId: "conv1" }), }); expect(res.status).toBe(200); expect(res.headers.get("Content-Type")).toBe("application/x-ndjson"); }); test("GET /models returns 200", async () => { server = startServer(); port = server.port as number; const res = await fetch(`http://localhost:${port}/models`); expect(res.status).toBe(200); const body = (await res.json()) as { models: readonly string[] }; expect(body.models).toEqual([]); }); test("GET /conversations/:id returns 200", async () => { server = startServer(); port = server.port as number; const res = await fetch(`http://localhost:${port}/conversations/conv1`); expect(res.status).toBe(200); const body = (await res.json()) as { chunks: readonly unknown[]; latestSeq: number }; expect(body.chunks).toEqual([]); expect(body.latestSeq).toBe(0); }); }); describe("extension lifecycle", () => { test("activate starts server on config port and deactivate stops it", async () => { const ext = createTransportHttpExtension(); const host = createFakeHostAPI({ httpPort: 0 }); await ext.activate(host); const server = ext._testServer; expect(server).toBeDefined(); const port = server?.port as number; expect(port).toBeGreaterThan(0); const res = await fetch(`http://localhost:${port}/health`); expect(res.status).toBe(200); ext.deactivate?.(); try { await fetch(`http://localhost:${port}/health`); expect(true).toBe(false); } catch { // expected — server stopped } }); });