// Row 164: the chat switch and fixed recovery message stay local to chat HTTP. import test from "node:test"; import assert from "node:assert/strict"; import { request } from "node:http"; import { createChat } from "../src/chat.mjs"; import { startServer } from "../src/serve.mjs"; import { BusReadError } from "../src/bus.mjs"; import { close } from "./fixture.mjs"; import { createSsoSession } from "./helpers/sso-session.mjs"; const P = "10000000-0000-4000-8000-000000000001"; const T = "20000000-0000-4000-8000-000000000002"; function http(port, headers, path, body) { const raw = body === undefined ? undefined : JSON.stringify(body); return new Promise((resolve, reject) => { const req = request({ host: '127.0.0.1', port, path, method: raw === undefined ? 'GET' : 'POST', headers: { ...headers, ...(raw === undefined ? {} : { 'content-length': Buffer.byteLength(raw) }) }, }, res => { let text = ''; res.on('data', chunk => text += chunk); res.on('end', () => resolve({ status: res.statusCode, data: JSON.parse(text) })); }); req.on('error', reject); req.end(raw); }); } async function setup(t, selected) { const calls = [], logs = []; const auth = await createSsoSession(t, { host: 'chat.example.com' }); const chat = createChat({ call: async (verb, args) => { calls.push([verb, args]); if (verb === "plugins") return { business: "demo", revision: "a".repeat(64), plugins: selected === undefined ? {} : { channel: selected } }; return []; } }); const server = await startServer({ port: 0, chat, sso: auth.sso, send: async () => ({}), log: line => logs.push(line) }); t.after(() => close(server)); const session = await auth.signIn(server.address().port); const headers = auth.writeHeaders(session); return { calls, logs, async request(path, body) { return http(server.address().port, headers, path, body); } }; } test("WebUI channel off returns 503 and named recovery for every chat route without a chat operation", async t => { for (const selected of [[], undefined, ["cli"]]) { const s = await setup(t, selected); for (const [path, body] of [ ["/api/chat/projects"], ["/api/chat/threads"], [`/api/chat/threads/${T}/messages`], ["/api/chat/projects", { name: "work" }], ["/api/chat/threads", { project: P, name: "chat" }], [`/api/chat/threads/${T}/messages`, { body: "private body" }], [`/api/chat/threads/${T}/open`, {}], ]) { s.calls.length = 0; const res = await s.request(path, body); assert.equal(res.status, 503); assert.equal(res.data.error, "channel-disabled"); assert.match(res.data.message, /mosaic plugins.*mosaic plugins set/s); assert.match(res.data.message, /plugins\.channel/); assert.match(res.data.message, /cli.*webui/); if (body !== undefined) assert.equal(res.data.sent, false); assert.deepEqual(s.calls, [["plugins", {}]]); assert.doesNotMatch(JSON.stringify(res.data), /private body/); } } }); test("WebUI only selection keeps web chat enabled while retaining the old read envelope", async t => { const s = await setup(t, ["webui"]); const res = await s.request("/api/chat/projects"); assert.equal(res.status, 200); assert.deepEqual(res.data.rows, []); assert.deepEqual(s.calls, [["plugins", {}], ["project.list", {}]]); }); test("WebUI channel-disabled recovery text is fixed and never echoes transport text", async t => { const chat = createChat({ call: async () => { throw new BusReadError("channel-disabled", "SENTINEL-private-input"); } }); const auth = await createSsoSession(t, { host: 'chat.example.com' }); const server = await startServer({ port: 0, chat, sso: auth.sso, send: async () => ({}), log: () => {} }); t.after(() => close(server)); const session = await auth.signIn(server.address().port); const res = await http(server.address().port, auth.writeHeaders(session), '/api/chat/projects'); const result = res.data; assert.equal(res.status, 503); assert.equal(result.error, "channel-disabled"); assert.match(result.message, /mosaic plugins.*mosaic plugins set/s); assert.doesNotMatch(JSON.stringify(result), /SENTINEL/); });