import { describe, expect, test } from "bun:test" import { NanobotClient, GatewayConnectionError, connectionEndpoint, fetchAvailableSkills, fetchGatewayConnection, fetchGatewayHealth, fetchHistory, fetchMentionCandidates, fetchRuntimeControls, fetchSessionContext, fetchSessions, fetchSlashCommands, sanitizeConnectionFailure, type ConnectionStatus, type ConnectionStatusInfo, type InboundEvent, } from "./protocol" class FakeSocket { static readonly OPEN = 1 readonly sent: string[] = [] readyState = FakeSocket.OPEN private readonly listeners = new Map void>>() addEventListener(name: string, listener: (event: { data?: string }) => void): void { const listeners = this.listeners.get(name) || [] listeners.push(listener) this.listeners.set(name, listeners) } close(): void { this.readyState = 3 } send(value: string): void { this.sent.push(value) } emit(name: string, event: { data?: string } = {}): void { for (const listener of this.listeners.get(name) || []) listener(event) } } async function waitUntil(predicate: () => boolean, timeout = 1_000): Promise { const deadline = Date.now() + timeout while (!predicate() && Date.now() < deadline) await Bun.sleep(2) if (!predicate()) throw new Error(`condition was not met within ${timeout}ms`) } describe("gateway protocol", () => { test("bootstraps fresh websocket and API credentials", async () => { const original = globalThis.fetch let headers: Headers | undefined globalThis.fetch = (async (_input: string | URL | Request, init?: RequestInit) => { headers = new Headers(init?.headers) return new Response(JSON.stringify({ ws_url: "ws://nanobot.test/ws?mode=local", token: "socket token", api_token: "api-token", })) }) as typeof fetch try { const connection = await fetchGatewayConnection( "http://nanobot.test/webui/bootstrap", "bootstrap-secret", "http://nanobot.test", "tui-42", ) expect(headers?.get("X-Nanobot-Auth")).toBe("bootstrap-secret") expect(connection).toEqual({ wsUrl: "ws://nanobot.test/ws?mode=local&token=socket+token&client_id=tui-42", apiUrl: "http://nanobot.test", apiToken: "api-token", }) } finally { globalThis.fetch = original } }) test("classifies gateway health without sending credentials", async () => { const original = globalThis.fetch const requests: Array<{ url: string; headers: Headers }> = [] const responses = [ new Response(JSON.stringify({ status: "degraded", process: "alive", ready: false, websocket: "unavailable", }), { status: 503 }), new Response(JSON.stringify({ status: "ok", process: "alive", ready: true, websocket: "running", })), new Response("not json"), ] globalThis.fetch = ((input: string | URL | Request, init?: RequestInit) => { requests.push({ url: String(input), headers: new Headers(init?.headers) }) return Promise.resolve(responses.shift() || new Response("missing", { status: 500 })) }) as typeof fetch try { const healthUrl = "http://127.0.0.1:18790/health" expect(await fetchGatewayHealth(healthUrl)).toBe("degraded") expect(await fetchGatewayHealth(healthUrl)).toBe("ready") expect(await fetchGatewayHealth(healthUrl)).toBe("unreachable") expect(requests.map(({ url }) => url)).toEqual([healthUrl, healthUrl, healthUrl]) expect(requests.every(({ headers }) => [...headers].length === 0)).toBe(true) } finally { globalThis.fetch = original } }) test("rejects malformed bootstrap responses without retrying", async () => { const original = globalThis.fetch globalThis.fetch = (() => Promise.resolve(new Response("not json"))) as unknown as typeof fetch try { await expect(fetchGatewayConnection( "http://nanobot.test/webui/bootstrap", "bootstrap-secret", "http://nanobot.test", "tui-42", )).rejects.toMatchObject({ message: "gateway bootstrap response is invalid", retryable: false, }) } finally { globalThis.fetch = original } }) test("retries an API request only once after reauthentication", async () => { const original = globalThis.fetch const authorizations: Array = [] let reauthenticationRequests = 0 globalThis.fetch = ((_input: string | URL | Request, init?: RequestInit) => { authorizations.push(new Headers(init?.headers).get("Authorization")) return Promise.resolve(new Response("Unauthorized", { status: 401 })) }) as typeof fetch try { await expect(fetchSlashCommands( "http://nanobot.test", "expired-api-token", async (rejectedApiToken) => { expect(rejectedApiToken).toBe("expired-api-token") reauthenticationRequests += 1 return { apiUrl: "http://nanobot.test", apiToken: "fresh-api-token" } }, )).rejects.toMatchObject({ message: "command request failed: HTTP 401" }) expect(reauthenticationRequests).toBe(1) expect(authorizations).toEqual([ "Bearer expired-api-token", "Bearer fresh-api-token", ]) } finally { globalThis.fetch = original } }) test("waits for bootstrap before opening the websocket", async () => { const original = globalThis.WebSocket let resolveConnection: ((value: { wsUrl: string apiUrl: string apiToken: string }) => void) | undefined let requestedUrl = "" Object.defineProperty(globalThis, "WebSocket", { configurable: true, value: class extends FakeSocket { constructor(url: string) { super() requestedUrl = url } }, }) try { const connections: string[] = [] let healthChecks = 0 const client = new NanobotClient({ resolveConnection: () => new Promise((resolve) => { resolveConnection = resolve }), checkHealth: async () => { healthChecks += 1 return "ready" }, onConnection: (connection) => connections.push(connection.apiToken), onEvent: () => undefined, onStatus: () => undefined, }) client.connect() await Bun.sleep(1) expect(requestedUrl).toBe("") resolveConnection?.({ wsUrl: "ws://nanobot.test/ws?token=fresh", apiUrl: "http://nanobot.test", apiToken: "fresh-api-token", }) await Bun.sleep(1) expect(requestedUrl).toBe("ws://nanobot.test/ws?token=fresh") expect(connections).toEqual(["fresh-api-token"]) expect(healthChecks).toBe(0) client.close() } finally { Object.defineProperty(globalThis, "WebSocket", { configurable: true, value: original }) } }) test("retries bootstrap while the local gateway starts", async () => { const original = globalThis.WebSocket let attempts = 0 let requestedUrl = "" Object.defineProperty(globalThis, "WebSocket", { configurable: true, value: class extends FakeSocket { constructor(url: string) { super() requestedUrl = url } }, }) try { const client = new NanobotClient({ resolveConnection: async () => { attempts += 1 if (attempts === 1) throw new Error("gateway is still starting") return { wsUrl: "ws://nanobot.test/ws?token=second", apiUrl: "http://nanobot.test", apiToken: "second-api-token", } }, reconnectDelayMs: 1, onEvent: () => undefined, onStatus: () => undefined, }) client.connect() for (let index = 0; index < 20 && !requestedUrl; index += 1) await Bun.sleep(2) expect(attempts).toBe(2) expect(requestedUrl).toBe("ws://nanobot.test/ws?token=second") client.close() } finally { Object.defineProperty(globalThis, "WebSocket", { configurable: true, value: original }) } }) test("escalates a refused bootstrap with safe endpoint and retry diagnostics", async () => { const original = globalThis.fetch const bootstrapUrl = "http://bootstrap-user:bootstrap-pass@127.0.0.1:8769" + "/webui/bootstrap?token=socket-secret" const bootstrapSecret = "bootstrap-secret" const statuses: Array<{ status: ConnectionStatus detail?: string info?: ConnectionStatusInfo }> = [] const refused = new TypeError( `fetch failed for ${bootstrapUrl}&api_token=api-secret`, { cause: Object.assign(new Error("connect ECONNREFUSED 127.0.0.1:8769"), { code: "ECONNREFUSED", }), }, ) globalThis.fetch = (() => Promise.reject(refused)) as unknown as typeof fetch const client = new NanobotClient({ resolveConnection: () => fetchGatewayConnection( bootstrapUrl, bootstrapSecret, "http://127.0.0.1:8769", "tui-42", ), targetEndpoint: connectionEndpoint(bootstrapUrl), checkHealth: async () => "degraded", startupFailureDelayMs: 8, reconnectDelayMs: 100, onEvent: () => undefined, onStatus: (status, detail, info) => statuses.push({ status, detail, info }), }) try { client.connect() await waitUntil(() => statuses.some(({ status }) => status === "unavailable")) const failure = [...statuses].reverse().find(({ status }) => status === "unavailable") expect(statuses[0]?.status).toBe("starting") expect(failure?.detail).toBe("connection refused") expect(failure?.info).toMatchObject({ endpoint: "127.0.0.1:8769", attempt: 1, elapsedMs: expect.any(Number), health: "degraded", }) const visible = JSON.stringify(statuses) expect(visible).not.toContain("bootstrap-user") expect(visible).not.toContain("bootstrap-pass") expect(visible).not.toContain(bootstrapSecret) expect(visible).not.toContain("socket-secret") expect(visible).not.toContain("api-secret") expect(visible).not.toContain("/webui/bootstrap") } finally { client.close() globalThis.fetch = original } }) test("recovers after sustained bootstrap failures without hiding the outage", async () => { const original = globalThis.WebSocket const sockets: FakeSocket[] = [] let available = false let attempts = 0 const statuses: ConnectionStatus[] = [] Object.defineProperty(globalThis, "WebSocket", { configurable: true, value: class extends FakeSocket { constructor() { super() sockets.push(this) } }, }) const client = new NanobotClient({ resolveConnection: async () => { attempts += 1 if (!available) { throw Object.assign(new Error("connect ECONNREFUSED"), { code: "ECONNREFUSED" }) } return { wsUrl: "ws://127.0.0.1:8769/ws?token=fresh", apiUrl: "http://127.0.0.1:8769", apiToken: "fresh-api-token", } }, targetEndpoint: "127.0.0.1:8769", checkHealth: async () => available ? "ready" : "degraded", startupFailureDelayMs: 8, reconnectDelayMs: 2, startupRetryMaxDelayMs: 2, onEvent: () => undefined, onStatus: (status) => statuses.push(status), }) try { client.connect() await waitUntil(() => statuses.includes("unavailable")) available = true await waitUntil(() => sockets.length === 1) sockets[0]?.emit("open") await waitUntil(() => statuses.at(-1) === "connected") expect(attempts).toBeGreaterThan(1) expect(statuses.indexOf("starting")).toBeLessThan(statuses.indexOf("unavailable")) expect(statuses.indexOf("unavailable")).toBeLessThan(statuses.lastIndexOf("connected")) } finally { client.close() Object.defineProperty(globalThis, "WebSocket", { configurable: true, value: original }) } }) test("sanitizes arbitrary connection errors and authenticated URLs", () => { const authenticated = "wss://user:password@127.0.0.1:8769/ws" + "?token=socket-secret&api_token=api-secret" const unknown = new Error(`could not reach ${authenticated}`) const refused = Object.assign(new Error(`ECONNREFUSED ${authenticated}`), { code: "ECONNREFUSED", }) expect(connectionEndpoint(authenticated)).toBe("127.0.0.1:8769") expect(sanitizeConnectionFailure(unknown)).toBe("connection failed") expect(sanitizeConnectionFailure(refused)).toBe("connection refused") expect(sanitizeConnectionFailure(unknown)).not.toContain("socket-secret") }) test("reports a permanent bootstrap rejection without retrying", async () => { let attempts = 0 const statuses: string[] = [] const client = new NanobotClient({ resolveConnection: async () => { attempts += 1 throw new GatewayConnectionError("gateway bootstrap failed: HTTP 401", false) }, reconnectDelayMs: 1, onEvent: () => undefined, onStatus: (status, detail) => statuses.push(`${status}:${detail || ""}`), }) client.connect() await Bun.sleep(5) expect(attempts).toBe(1) expect(statuses.at(-1)).toBe("error:gateway bootstrap failed: HTTP 401") client.close() }) test("represents lifecycle frames without browser state", () => { const events: InboundEvent[] = [ { event: "user_message", chat_id: "one", text: "question", turn_id: "turn-one", active_turn_id: "turn-one", starts_turn: true, media_urls: [{ kind: "file", url: "/api/media/sig/file", name: "report.pdf" }], }, { event: "delta", chat_id: "one", text: "hello" }, { event: "stream_end", chat_id: "one", resuming: false }, { event: "turn_end", chat_id: "one", latency_ms: 12 }, ] expect(events.map((event) => event.event)).toEqual([ "user_message", "delta", "stream_end", "turn_end", ]) }) test("attaches and sends turns through the gateway envelope", () => { const original = globalThis.WebSocket let socket: FakeSocket | undefined Object.defineProperty(globalThis, "WebSocket", { configurable: true, value: class extends FakeSocket { constructor() { super() socket = this } }, }) try { const events: InboundEvent[] = [] const client = new NanobotClient({ url: "ws://nanobot.test/ws", chatId: "terminal", initialWorkspaceScope: { project_path: "/tmp/project", access_mode: "restricted", }, onEvent: (event) => events.push(event), onStatus: () => undefined, }) client.connect() if (!socket) throw new Error("socket was not created") socket.emit("message", { data: JSON.stringify({ event: "ready", chat_id: "", client_id: "client" }), }) socket.emit("message", { data: JSON.stringify({ event: "attached", chat_id: "terminal", model_preset: "Deep Research", }), }) client.send("hello", { media: [{ data_url: "data:image/png;base64,AAAA", name: "clipboard-image-1.png" }], cliApps: [{ name: "github" }], sessionMentions: [{ name: "plan", session_key: "websocket:plan" }], userShell: true, }) client.attach("other-chat") client.newChat() client.forkChat("terminal", 3, "Alternative") client.setWorkspaceScope({ project_path: "/tmp/project", access_mode: "restricted", }) const outbound = socket.sent.map((value) => JSON.parse(value) as Record) expect(outbound[0]).toEqual({ type: "attach", chat_id: "terminal" }) expect(outbound[1]?.type).toBe("message") expect(outbound[1]?.chat_id).toBe("terminal") expect(outbound[1]?.content).toBe("hello") expect(outbound[1]?.user_shell).toBe(true) expect(outbound[1]?.media).toEqual([ { data_url: "data:image/png;base64,AAAA", name: "clipboard-image-1.png" }, ]) expect(outbound[1]?.cli_apps).toEqual([{ name: "github" }]) expect(outbound[1]?.session_mentions).toEqual([ { name: "plan", session_key: "websocket:plan" }, ]) expect(outbound[2]).toEqual({ type: "attach", chat_id: "other-chat" }) expect(outbound[3]).toEqual({ type: "new_chat" }) expect(outbound[4]).toEqual({ type: "fork_chat", source_chat_id: "terminal", before_user_index: 3, title: "Alternative", }) expect(outbound[5]).toEqual({ type: "set_workspace_scope", chat_id: "terminal", workspace_scope: { project_path: "/tmp/project", access_mode: "restricted", }, }) expect(events.map((event) => event.event)).toEqual(["ready", "attached"]) expect(events[1]).toEqual({ event: "attached", chat_id: "terminal", model_preset: "Deep Research", }) } finally { Object.defineProperty(globalThis, "WebSocket", { configurable: true, value: original }) } }) test("starts a fresh chat in the launch workspace", () => { const original = globalThis.WebSocket let socket: FakeSocket | undefined Object.defineProperty(globalThis, "WebSocket", { configurable: true, value: class extends FakeSocket { constructor() { super() socket = this } }, }) try { const client = new NanobotClient({ url: "ws://nanobot.test/ws", initialWorkspaceScope: { project_path: "/tmp/launch-project", access_mode: "restricted", }, onEvent: () => undefined, onStatus: () => undefined, }) client.connect() if (!socket) throw new Error("socket was not created") socket.emit("message", { data: JSON.stringify({ event: "ready", chat_id: "", client_id: "client" }), }) socket.emit("message", { data: JSON.stringify({ event: "attached", chat_id: "draft-chat" }), }) // A gateway restart re-sends ready while the TUI keeps the draft chat alive. socket.emit("message", { data: JSON.stringify({ event: "ready", chat_id: "", client_id: "client" }), }) client.send("hello") expect(socket.sent.map((value) => JSON.parse(value))).toEqual([ { type: "new_chat", workspace_scope: { project_path: "/tmp/launch-project", access_mode: "restricted", }, }, { type: "attach", chat_id: "draft-chat" }, expect.objectContaining({ type: "message", chat_id: "draft-chat", content: "hello", workspace_scope: { project_path: "/tmp/launch-project", access_mode: "restricted", }, }), ]) } finally { Object.defineProperty(globalThis, "WebSocket", { configurable: true, value: original }) } }) test("loads runtime model and access controls from canonical APIs", async () => { const original = globalThis.fetch globalThis.fetch = (async (input: string | URL | Request) => { const url = String(input) if (url.endsWith("/api/settings")) return new Response(JSON.stringify({ model_presets: [ { name: "Codex", model: "openai-codex/gpt-5.6" }, { name: "broken" }, ], })) return new Response(JSON.stringify({ controls: { can_use_full_access: true } })) }) as typeof fetch try { expect(await fetchRuntimeControls("http://nanobot.test", "secret")).toEqual({ modelPresets: [{ name: "Codex", model: "openai-codex/gpt-5.6" }], canUseFullAccess: true, }) } finally { globalThis.fetch = original } }) test("keeps model selection available when workspace controls are unavailable", async () => { const original = globalThis.fetch globalThis.fetch = ((input: string | URL | Request) => Promise.resolve( String(input).endsWith("/api/settings") ? new Response(JSON.stringify({ model_presets: [{ name: "fast", model: "openai/gpt-5.6" }], })) : new Response("", { status: 404 }), )) as typeof fetch try { expect(await fetchRuntimeControls("http://nanobot.test", "secret")).toEqual({ modelPresets: [{ name: "fast", model: "openai/gpt-5.6" }], canUseFullAccess: false, }) } finally { globalThis.fetch = original } }) test("rejects malformed gateway events", () => { const original = globalThis.WebSocket let socket: FakeSocket | undefined Object.defineProperty(globalThis, "WebSocket", { configurable: true, value: class extends FakeSocket { constructor() { super() socket = this } }, }) try { const statuses: string[] = [] const events: InboundEvent[] = [] const client = new NanobotClient({ url: "ws://nanobot.test/ws", onEvent: (event) => events.push(event), onStatus: (status, detail) => statuses.push(`${status}:${detail || ""}`), }) client.connect() if (!socket) throw new Error("socket was not created") socket.emit("message", { data: "[]" }) socket.emit("message", { data: JSON.stringify({ event: "delta", chat_id: "one" }) }) socket.emit("message", { data: JSON.stringify({ event: "user_message", chat_id: "one", text: "missing lifecycle", }), }) socket.emit("message", { data: JSON.stringify({ event: "message", chat_id: "one", text: "bad tool", tool_events: [{ call_id: 42 }], }), }) socket.emit("message", { data: JSON.stringify({ event: "stream_end", chat_id: "one", resuming: "yes" }), }) socket.emit("message", { data: JSON.stringify({ event: "session_updated", chat_id: "one", scope: 42 }), }) socket.emit("message", { data: JSON.stringify({ event: "user_message", chat_id: "one", text: "bad media", starts_turn: false, media_urls: [{ kind: "archive", url: "/api/media/sig/file" }], }), }) socket.emit("message", { data: JSON.stringify({ event: "attached", chat_id: "one", model_preset: 42 }), }) socket.emit("message", { data: JSON.stringify({ event: "turn_end", chat_id: "one", goal_state: [] }), }) socket.emit("message", { data: JSON.stringify({ event: "session_updated", chat_id: "one", scope: "metadata" }), }) socket.emit("message", { data: JSON.stringify({ event: "turn_end", chat_id: "one", goal_state: { active: false, status: "blocked", ui_summary: "Approval required" }, }), }) socket.emit("message", { data: JSON.stringify({ event: "future_gateway_event" }) }) socket.emit("message", { data: JSON.stringify({ event: "error", detail: "global failure" }) }) expect(statuses).toContain("error:gateway sent an invalid event") expect(statuses.filter((status) => status.includes("invalid event"))).toHaveLength(9) expect(events).toContainEqual({ event: "session_updated", chat_id: "one", scope: "metadata", }) expect(events).toContainEqual({ event: "error", detail: "global failure" }) expect(events).toContainEqual({ event: "turn_end", chat_id: "one", goal_state: { active: false, status: "blocked", ui_summary: "Approval required" }, }) } finally { Object.defineProperty(globalThis, "WebSocket", { configurable: true, value: original }) } }) test("validates nested unified diff payloads at the websocket boundary", () => { const original = globalThis.WebSocket let socket: FakeSocket | undefined Object.defineProperty(globalThis, "WebSocket", { configurable: true, value: class extends FakeSocket { constructor() { super() socket = this } }, }) try { const events: InboundEvent[] = [] const statuses: string[] = [] const client = new NanobotClient({ url: "ws://nanobot.test/ws", onEvent: (event) => events.push(event), onStatus: (status, detail) => statuses.push(`${status}:${detail || ""}`), }) client.connect() if (!socket) throw new Error("socket was not created") socket.emit("message", { data: JSON.stringify({ event: "file_edit", chat_id: "chat", edits: [{ call_id: "edit-1", tool: "edit_file", path: "src/app.ts", status: "done", added: 1, deleted: 1, diff: { format: "unified", truncated: false, text: "--- a\n+++ b" }, }], }) }) socket.emit("message", { data: JSON.stringify({ event: "file_edit", chat_id: "chat", edits: [{ diff: { format: "unified", text: ["not", "a", "string"] } }], }) }) expect(events).toHaveLength(1) expect(events[0]?.event).toBe("file_edit") expect(statuses).toContain("error:gateway sent an invalid event") client.close() } finally { Object.defineProperty(globalThis, "WebSocket", { configurable: true, value: original }) } }) test("reattaches the same generated chat after a transient disconnect", async () => { const original = globalThis.WebSocket const sockets: FakeSocket[] = [] Object.defineProperty(globalThis, "WebSocket", { configurable: true, value: class extends FakeSocket { constructor() { super() sockets.push(this) } }, }) try { const statuses: Array<{ status: ConnectionStatus detail?: string info?: ConnectionStatusInfo }> = [] const client = new NanobotClient({ url: "ws://nanobot.test/ws", reconnectDelayMs: 1, onEvent: () => undefined, onStatus: (status, detail, info) => statuses.push({ status, detail, info }), }) client.connect() sockets[0]?.emit("open") sockets[0]?.emit("message", { data: JSON.stringify({ event: "ready", chat_id: "", client_id: "client" }), }) sockets[0]?.emit("message", { data: JSON.stringify({ event: "attached", chat_id: "generated-chat" }), }) sockets[0]?.emit("close") await Bun.sleep(5) expect(sockets).toHaveLength(2) const reconnecting = [...statuses].reverse().find( ({ status }) => status === "reconnecting", ) expect(reconnecting).toMatchObject({ status: "reconnecting", detail: "connection closed", info: { endpoint: "nanobot.test", attempt: 1 }, }) sockets[1]?.emit("open") sockets[1]?.emit("message", { data: JSON.stringify({ event: "ready", chat_id: "", client_id: "client-2" }), }) const outbound = sockets[1]?.sent.map((value) => JSON.parse(value)) || [] expect(outbound).toEqual([{ type: "attach", chat_id: "generated-chat" }]) expect(statuses.at(-1)?.status).toBe("connected") client.close() } finally { Object.defineProperty(globalThis, "WebSocket", { configurable: true, value: original }) } }) test("sends stale-safe recovery mutations and validates their response", async () => { const original = globalThis.WebSocket let socket: FakeSocket | undefined Object.defineProperty(globalThis, "WebSocket", { configurable: true, value: class extends FakeSocket { constructor() { super() socket = this } }, }) try { const statuses: string[] = [] const client = new NanobotClient({ url: "ws://nanobot.test/ws", onEvent: () => undefined, onStatus: (status, detail) => statuses.push(`${status}:${detail || ""}`), }) client.connect() if (!socket) throw new Error("socket was not created") const result = client.updateRecovery("continue", "chat", "recovery-1") const request = JSON.parse(socket.sent.at(-1) || "{}") as { request_id: string action: string payload: Record } expect(request).toMatchObject({ type: "webui_request", action: "recovery.continue", payload: { chat_id: "chat", recovery_id: "recovery-1" }, }) socket.emit("message", { data: JSON.stringify({ event: "webui_response", request_id: request.request_id, ok: true, result: { status: "resuming", recovery_id: "recovery-1", attempts: 1 }, }) }) expect(await result).toEqual({ status: "resuming", recovery_id: "recovery-1", attempts: 1, }) expect(statuses).not.toContain("error:gateway sent an invalid event") const interrupted = client.updateRecovery("dismiss", "chat", "recovery-2") socket.emit("close") await expect(interrupted).rejects.toThrow("gateway connection closed") client.close() } finally { Object.defineProperty(globalThis, "WebSocket", { configurable: true, value: original }) } }) test("reports when the bounded history snapshot omits earlier turns", async () => { const original = globalThis.fetch let requested = "" globalThis.fetch = ((input: string | URL | Request) => { requested = String(input) return Promise.resolve(new Response(JSON.stringify({ messages: [ { role: "user", content: "hello", turnId: "turn-1" }, { role: "user", content: "", turnId: "turn-image", media: [{ kind: "image", url: "/api/media/sig/image", name: "shot.png" }], }, { role: "tool", kind: "trace", content: "read_file", traces: ["read_file"], toolEvents: [{ phase: "end", call_id: "read-1", name: "read_file" }], }, { role: "assistant", kind: "reasoning", content: "private thought" }, { role: "assistant", content: "hi", forkIndex: 2 }, ], page: { has_more_before: true, before_cursor: "older-1" }, }))) }) as typeof fetch try { const history = await fetchHistory("http://nanobot.test", "token", "chat", "newer-page") expect(history).toEqual({ messages: [ { role: "user", content: "hello", turnId: "turn-1" }, { role: "user", content: "", turnId: "turn-image", media: [{ kind: "image", url: "/api/media/sig/image", name: "shot.png" }], }, { role: "activity", content: "read_file", toolEvents: [{ phase: "end", call_id: "read-1", name: "read_file" }], }, { role: "assistant", content: "hi", forkIndex: 2 }, ], hasMoreBefore: true, beforeCursor: "older-1", userMessageOffset: 0, }) expect(requested).toContain("before=newer-page") } finally { globalThis.fetch = original } }) test("loads the explainable session context projection", async () => { const original = globalThis.fetch globalThis.fetch = (() => Promise.resolve(new Response(JSON.stringify({ total_messages: 24, archived_messages: 16, replay_messages: 10, estimated_replay_tokens: 2048, estimated_summary_tokens: 128, estimated_session_tokens: 2176, archived_summary: "Older work was compacted.", archived_summary_at: "2026-08-13T10:00:00Z", last_usage: { prompt_tokens: 1200, completion_tokens: 80, total_tokens: 1280, context_tokens: 1200, cached_tokens: 900, request_count: 1, estimated_tokens: 0, generation_ms: 4000, measured_completion_tokens: 80, ttft_ms: 250, timed_requests: 1, }, })))) as unknown as typeof fetch try { expect(await fetchSessionContext("http://nanobot.test", "secret", "chat")).toEqual({ totalMessages: 24, archivedMessages: 16, replayMessages: 10, estimatedReplayTokens: 2048, estimatedSummaryTokens: 128, estimatedSessionTokens: 2176, archivedSummary: "Older work was compacted.", archivedSummaryAt: "2026-08-13T10:00:00Z", lastUsage: { prompt_tokens: 1200, completion_tokens: 80, total_tokens: 1280, context_tokens: 1200, cached_tokens: 900, request_count: 1, estimated_tokens: 0, generation_ms: 4000, measured_completion_tokens: 80, ttft_ms: 250, timed_requests: 1, }, }) } finally { globalThis.fetch = original } }) test("loads the gateway-owned slash command catalog", async () => { const original = globalThis.fetch let authorization = "" globalThis.fetch = ((_: string | URL | Request, init?: RequestInit) => { authorization = new Headers(init?.headers).get("Authorization") || "" return Promise.resolve(new Response(JSON.stringify({ commands: [ { command: "/history", title: "History", description: "Show recent messages", arg_hint: "[n]", accepts_args: true, lifecycle: "side_channel", }, { title: "invalid" }, ], }))) }) as typeof fetch try { expect(await fetchSlashCommands("http://nanobot.test", "secret")).toEqual([{ command: "/history", title: "History", description: "Show recent messages", argHint: "[n]", lifecycle: "side_channel", acceptsArgs: true, }]) expect(authorization).toBe("Bearer secret") } finally { globalThis.fetch = original } }) test("loads only enabled and available skills", async () => { const original = globalThis.fetch let authorization = "" globalThis.fetch = ((_: string | URL | Request, init?: RequestInit) => { authorization = new Headers(init?.headers).get("Authorization") || "" return Promise.resolve(new Response(JSON.stringify({ skills: [ { name: "verify", description: "Verify public behavior", source: "builtin", enabled: true, available: true, }, { name: "disabled", description: "Disabled skill", source: "workspace", enabled: false, available: true, }, { name: "unavailable", description: "Missing dependency", source: "builtin", enabled: true, available: false, }, { name: "my skill", enabled: true, available: true }, { name: "技能", enabled: true, available: true }, { enabled: true, available: true }, ], }))) }) as typeof fetch try { expect(await fetchAvailableSkills("http://nanobot.test", "secret")).toEqual([{ name: "verify", description: "Verify public behavior", source: "builtin", }]) expect(authorization).toBe("Bearer secret") } finally { globalThis.fetch = original } }) test("drops slash commands with unknown lifecycle metadata", async () => { const original = globalThis.fetch globalThis.fetch = (() => Promise.resolve(new Response(JSON.stringify({ commands: [{ command: "/future", title: "Future", description: "Unknown lifecycle", lifecycle: "future_mode", }], })))) as unknown as typeof fetch try { expect(await fetchSlashCommands("http://nanobot.test", "secret")).toEqual([]) } finally { globalThis.fetch = original } }) test("loads and normalizes WebUI sessions", async () => { const original = globalThis.fetch globalThis.fetch = ((input: string | URL | Request) => { const url = String(input) if (url.endsWith("/api/webui/sidebar-state")) { return Promise.resolve(new Response(JSON.stringify({ pinned_keys: ["websocket:chat-1"], archived_keys: [], title_overrides: { "websocket:chat-1": "Pinned release" }, }))) } return Promise.resolve(new Response(JSON.stringify({ sessions: [ { key: "websocket:chat-1", title: "Release plan", preview: "Prepare the release", created_at: "2026-08-12T10:00:00Z", updated_at: "2026-08-13T10:00:00Z", run_started_at: 123, model_preset: "Deep Research", }, { key: "cli:direct", title: "Not a WebUI session" }, { key: 42 }, ], }))) }) as typeof fetch try { expect(await fetchSessions("http://nanobot.test", "secret")).toEqual([{ chatId: "chat-1", title: "Pinned release", preview: "Prepare the release", createdAt: "2026-08-12T10:00:00Z", updatedAt: "2026-08-13T10:00:00Z", runStartedAt: 123, modelPreset: "Deep Research", pinned: true, archived: false, }]) } finally { globalThis.fetch = original } }) test("combines installed tools and saved sessions in one mention namespace", async () => { const original = globalThis.fetch globalThis.fetch = ((input: string | URL | Request) => { const url = String(input) if (url.includes("cli-apps")) { return Promise.resolve(new Response(JSON.stringify({ apps: [{ name: "github", display_name: "GitHub", description: "Repository tools", installed: true }], }))) } if (url.includes("mcp-presets")) { return Promise.resolve(new Response(JSON.stringify({ presets: [{ name: "linear", display_name: "Linear", description: "Issue tracker", installed: true, configured: true, }], }))) } if (url.includes("sidebar-state")) return Promise.resolve(new Response("{}")) return Promise.resolve(new Response(JSON.stringify({ sessions: [{ key: "websocket:plan", title: "Release plan", preview: "Ship it" }], }))) }) as typeof fetch try { expect(await fetchMentionCandidates("http://nanobot.test", "secret")).toEqual([ { kind: "cli", name: "github", displayName: "GitHub", description: "Repository tools", }, { kind: "mcp", name: "linear", displayName: "Linear", description: "Issue tracker", }, { kind: "session", name: "Release-plan", displayName: "Release plan", description: "Ship it", session: { name: "Release-plan", session_key: "websocket:plan", title: "Release plan", }, }, ]) } finally { globalThis.fetch = original } }) })