var test = require("node:test"); var assert = require("node:assert/strict"); var attachHomeChat = require("../lib/server-home-chat").attachHomeChat; function settle() { return new Promise(function (resolve) { setImmediate(resolve); }); } function fixture(options) { var opts = options || {}; var mate = { id: "mate-a", name: "A", vendor: "claude", model: "sonnet" }; if (opts.clay) mate.builtinKey = "clay"; var sessions = new Map([ [1, { localId: 1, cliSessionId: "session-old", ownerId: "u1", title: "Older", lastActivity: 10, vendor: "claude", model: "sonnet", history: [] }], [2, { localId: 2, cliSessionId: "session-new", ownerId: "u1", title: "Newer", lastActivity: 30, vendor: "codex", model: "gpt-5.6", history: [{ type: "user_message", text: "Hello" }] }], [3, { localId: 3, cliSessionId: "session-other", ownerId: "u2", title: "Other user's chat", lastActivity: 40, history: [] }], [4, { localId: 4, cliSessionId: "session-hidden", ownerId: "u1", title: "Hidden", lastActivity: 50, hidden: true, history: [] }], [5, { localId: 5, cliSessionId: "session-single", title: "Single-user chat", lastActivity: 20, history: [] }], ]); var subscribed = null; var subscription = null; var subscriptions = {}; var recorded = []; var manager = { sessions: sessions, subscribeSession: function (id, callback) { subscribed = id; subscription = callback; subscriptions[id] = callback; return function () { if (subscriptions[id] === callback) delete subscriptions[id]; }; }, saveSessionFile: function () {}, createSession: function (createOptions) { if (!opts.allowCreate) throw new Error("unexpected session creation"); var session = { localId: 6, ownerId: createOptions.ownerId, vendor: createOptions.vendor, model: createOptions.model, effort: createOptions.effort || null, history: [], isProcessing: false }; sessions.set(6, session); return session; }, sendAndRecord: function (session, event) { recorded.push(event); session.history.push(event); if (subscribed === session.localId && subscription) subscription(event); }, }; var project = { getSessionManager: function () { return manager; }, getVendorModelCatalog: function () { return Promise.resolve({ status: "ready", models: ["sonnet"], defaultModel: "sonnet", error: "" }); }, getMemoryState: function () { return { entries: [], summary: "" }; }, listKnowledgeFiles: function () { return []; }, forEachClient: function () {}, handleHomePermissionResponse: function (ws, message, session) { if (typeof opts.onPermissionResponse === "function") return opts.onPermissionResponse(ws, message, session, manager); return false; }, stopHomeSession: function (session) { if (typeof opts.onStop === "function") return opts.onStop(session); return false; }, sdk: Object.prototype.hasOwnProperty.call(opts, "sdk") ? opts.sdk : null, }; var handler = attachHomeChat({ users: { isMultiUser: function () { return opts.singleUser !== true; } }, mates: { buildMateCtx: function () { return {}; }, getAllMates: function () { return [mate]; }, getMate: function () { return mate; }, updateMate: function (ctx, mateId, patch) { mate = Object.assign({}, mate, patch); return mate; }, getMateDir: function () { return "/tmp/mate-a"; }, }, projects: new Map([["mate-mate-a", project]]), addProject: function () {}, resolveDefaultAi: opts.resolveDefaultAi, }); var messages = []; var ws = { readyState: 1, _clayUser: opts.unauthenticated ? null : { id: "u1" }, send: function (value) { messages.push(JSON.parse(value)); }, }; return { handler: handler, ws: ws, messages: messages, getSubscribed: function () { return subscribed; }, emit: function (event) { if (subscription) subscription(event); }, emitFor: function (sessionId, event) { if (subscriptions[sessionId]) subscriptions[sessionId](event); }, emitRecorded: function (event) { var session = sessions.get(subscribed); if (event.type === "session_id") session.cliSessionId = event.cliSessionId; session.history.push(event); if (subscription) subscription(event); }, addSession: function (session) { sessions.set(session.localId, session); }, getSession: function (id) { return sessions.get(id); }, getRecorded: function () { return recorded; }, record: function (sessionId, event) { manager.sendAndRecord(sessions.get(sessionId), event); }, }; } test("fresh built-in Clay Home and search sessions use the shared Default AI runtime", async function () { function resolveDefaultAi() { return Promise.resolve({ ready: true, vendor: "codex", model: "gpt-6-astra", effort: "high" }); } var home = fixture({ clay: true, allowCreate: true, resolveDefaultAi: resolveDefaultAi }); home.handler.handleMessage(home.ws, { type: "home_mate_new_session", mateId: "mate-a", requestId: "fresh-home" }); await settle(); assert.deepEqual({ vendor: home.getSession(6).vendor, model: home.getSession(6).model, effort: home.getSession(6).effort }, { vendor: "codex", model: "gpt-6-astra", effort: "high" }); var search = fixture({ clay: true, allowCreate: true, resolveDefaultAi: resolveDefaultAi, sdk: { startQuery: function () {} } }); search.handler.handleMessage(search.ws, { type: "home_clay_ask", requestId: "fresh-search", text: "Find the roadmap" }); await settle(); assert.deepEqual({ vendor: search.getSession(6).vendor, model: search.getSession(6).model, effort: search.getSession(6).effort }, { vendor: "codex", model: "gpt-6-astra", effort: "high" }); }); test("Mate conversation list includes only the requesting user's visible sessions", function () { var f = fixture(); assert.strictEqual(f.handler.handleMessage(f.ws, { type: "home_mate_sessions_list", mateId: "mate-a" }), true); assert.deepStrictEqual(f.messages[0], { type: "home_mate_sessions_state", mateId: "mate-a", sessions: [ { id: "session-new", cliSessionId: "session-new", localId: 2, title: "Newer", vendor: "codex", model: "gpt-5.6", createdAt: 0, lastActivity: 30, isProcessing: false, sessionOriginId: null, sessionRole: "driver", parentSessionId: null, parentAvailable: false, workerGeneration: null }, { id: "session-old", cliSessionId: "session-old", localId: 1, title: "Older", vendor: "claude", model: "sonnet", createdAt: 0, lastActivity: 10, isProcessing: false, sessionOriginId: null, sessionRole: "driver", parentSessionId: null, parentAvailable: false, workerGeneration: null }, ], }); }); test("Mate conversation details expose local-only identity without weakening ownership filtering", function () { var f = fixture(); f.addSession({ localId: 6, cliSessionId: null, ownerId: "u1", title: "Local\ndraft\u0000", createdAt: 35, lastActivity: 60, vendor: "claude", model: "haiku", isProcessing: true, history: [] }); f.handler.handleMessage(f.ws, { type: "home_mate_sessions_list", mateId: "mate-a" }); var sessions = f.messages[0].sessions; assert.deepStrictEqual(sessions[0], { id: "local:6", cliSessionId: null, localId: 6, title: "Local draft", vendor: "claude", model: "haiku", createdAt: 35, lastActivity: 60, isProcessing: true, sessionOriginId: null, sessionRole: "driver", parentSessionId: null, parentAvailable: false, workerGeneration: null, }); assert.strictEqual(sessions.some(function (session) { return session.title === "Other user's chat" || session.title === "Hidden"; }), false); }); test("Home lists repair legacy global-search boilerplate titles", function () { var f = fixture(); f.addSession({ localId: 6, cliSessionId: "legacy-search", ownerId: "u1", title: "This conversation began in Clay Studio's global search. Resp", titleAutoGenerated: true, lastActivity: 60, vendor: "codex", model: "gpt-5.6-sol", history: [{ type: "user_message", text: "Find our accessibility review" }], }); f.handler.handleMessage(f.ws, { type: "home_mate_sessions_list", mateId: "mate-a" }); assert.equal(f.messages[0].sessions[0].title, "Find our accessibility review"); assert.equal(f.getSession(6).titleAutoGenerated, false); }); test("Mate settings memory and knowledge responses preserve request correlation", function () { var f = fixture(); f.handler.handleMessage(f.ws, { type: "home_mate_memory_list", mateId: "mate-a", requestId: "memory-a" }); f.handler.handleMessage(f.ws, { type: "home_mate_knowledge_list", mateId: "mate-a", requestId: "knowledge-a" }); assert.deepStrictEqual(f.messages.map(function (message) { return [message.type, message.mateId, message.requestId]; }), [ ["home_mate_memory_state", "mate-a", "memory-a"], ["home_mate_knowledge_state", "mate-a", "knowledge-a"], ]); }); test("Explicit Mate conversation open restores the exact requested session", async function () { var f = fixture(); f.handler.handleMessage(f.ws, { type: "home_mate_session_open", mateId: "mate-a", sessionId: "session-old", requestId: "open-old" }); await settle(); assert.strictEqual(f.getSubscribed(), 1); assert.strictEqual(f.messages[0].type, "home_mate_history"); assert.strictEqual(f.messages[0].sessionId, "session-old"); assert.strictEqual(f.messages[0].requestId, "open-old"); assert.strictEqual(f.messages[0].vendor, "claude"); assert.strictEqual(f.messages[0].model, "sonnet"); }); test("global Ask Clay keeps its stream subscription when Home opens another conversation", async function () { var starts = []; var f = fixture({ clay: true, allowCreate: true, sdk: { startQuery: function (session, text) { starts.push({ session: session, text: text }); } }, }); f.handler.handleMessage(f.ws, { type: "home_clay_ask", requestId: "search-request", text: "Find fruit" }); await settle(); assert.equal(starts.length, 1); assert.equal(f.ws._searchClayTap.sessionId, 6); f.handler.handleMessage(f.ws, { type: "home_mate_session_open", mateId: "mate-a", sessionId: "session-old", requestId: "home-request" }); await settle(); assert.equal(f.ws._homeChatTap.sessionId, 1); assert.equal(f.ws._searchClayTap.sessionId, 6); f.messages.length = 0; f.getSession(6).responsePreview = "The fruit conversation is here."; f.getSession(6).history.push({ type: "delta", text: "The fruit conversation is here." }); f.emitFor(6, { type: "done" }); var reply = f.messages.find(function (message) { return message.type === "home_mate_done"; }); assert.equal(reply.requestId, "search-request"); assert.equal(reply.text, "The fruit conversation is here."); f.getSession(6).isProcessing = false; f.handler.handleMessage(f.ws, { type: "home_mate_send", mateId: "mate-a", sessionId: "local:6", requestId: "search-request", text: "Which one?" }); await settle(); assert.equal(starts.length, 2); assert.equal(starts[1].session.localId, 6); assert.equal(f.getSession(1).history.length, 0); assert.equal(f.getSession(6).history[f.getSession(6).history.length - 1].text, "Which one?"); }); test("global Ask Clay restores and resolves only the exact session-bound pending permission", async function () { var resolved = []; var f = fixture({ clay: true, allowCreate: true, sdk: { startQuery: function () {} }, onPermissionResponse: function (ws, message, session, manager) { var pending = session.pendingPermissions[message.requestId]; delete session.pendingPermissions[message.requestId]; pending.resolve(message.decision); manager.sendAndRecord(session, { type: "permission_resolved", requestId: message.requestId, decision: message.decision }); return true; }, }); f.handler.handleMessage(f.ws, { type: "home_clay_ask", requestId: "search-control", text: "Change a file" }); await settle(); var session = f.getSession(6); session.pendingPermissions = { "permission-1": { toolName: "Edit", toolInput: { file_path: "/repo/a.js" }, decisionReason: "Update the file", resolve: function (decision) { resolved.push(decision); } }, }; f.messages.length = 0; f.handler.handleMessage(f.ws, { type: "home_clay_ask", requestId: "search-control", text: "Change a file" }); await settle(); var history = f.messages.find(function (message) { return message.type === "home_mate_history"; }); assert.deepEqual(history.pendingPermissions, [{ permissionRequestId: "permission-1", toolName: "Edit", toolInput: { file_path: "/repo/a.js" }, decisionReason: "Update the file" }]); f.handler.handleMessage(f.ws, { type: "home_mate_permission_response", mateId: "mate-a", sessionId: "local:6", requestId: "wrong-search", permissionRequestId: "permission-1", decision: "allow" }); assert.deepEqual(resolved, []); f.handler.handleMessage(f.ws, { type: "home_mate_permission_response", mateId: "mate-a", sessionId: "local:6", requestId: "search-control", permissionRequestId: "permission-1", decision: "allow" }); assert.deepEqual(resolved, ["allow"]); assert.equal(session.pendingPermissions["permission-1"], undefined); assert.equal(f.messages.some(function (message) { return message.type === "home_mate_permission_resolved" && message.permissionRequestId === "permission-1"; }), true); }); test("global Ask Clay Stop targets its exact session and rejects stale session controls", async function () { var stopped = []; var f = fixture({ clay: true, allowCreate: true, sdk: { startQuery: function () {} }, onStop: function (session) { stopped.push(session.localId); return true; } }); f.handler.handleMessage(f.ws, { type: "home_clay_ask", requestId: "search-stop", text: "Keep working" }); await settle(); f.handler.handleMessage(f.ws, { type: "home_mate_stop", mateId: "mate-a", sessionId: "session-old", requestId: "search-stop" }); assert.deepEqual(stopped, []); f.handler.handleMessage(f.ws, { type: "home_mate_stop", mateId: "mate-a", sessionId: "local:6", requestId: "search-stop" }); assert.deepEqual(stopped, [6]); assert.equal(f.messages.some(function (message) { return message.type === "home_mate_stopping" && message.sessionId === "local:6"; }), true); }); test("exact Home sessions retain distinct committed model metadata", async function () { var f = fixture(); f.handler.handleMessage(f.ws, { type: "home_mate_session_open", mateId: "mate-a", sessionId: "session-old", requestId: "open-a" }); f.handler.handleMessage(f.ws, { type: "home_mate_session_open", mateId: "mate-a", sessionId: "session-new", requestId: "open-b" }); await settle(); var histories = f.messages.filter(function (message) { return message.type === "home_mate_history"; }); assert.deepStrictEqual(histories.map(function (message) { return [message.sessionId, message.vendor, message.model, message.requestId]; }), [ ["session-old", "claude", "sonnet", "open-a"], ["session-new", "codex", "gpt-5.6", "open-b"], ]); }); test("Home live session events refresh committed model metadata", async function () { var f = fixture(); f.handler.handleMessage(f.ws, { type: "home_mate_session_open", mateId: "mate-a", sessionId: "session-new", requestId: "open-live" }); await settle(); f.messages.length = 0; f.emit({ type: "delta", text: "Hi" }); f.emit({ type: "done" }); assert.deepStrictEqual(f.messages.slice(0, 2).map(function (message) { return [message.type, message.sessionId, message.vendor, message.model, message.requestId]; }), [ ["home_mate_delta", "session-new", "codex", "gpt-5.6", "open-live"], ["home_mate_done", "session-new", "codex", "gpt-5.6", "open-live"], ]); }); test("fresh Home taps promote session identity before streaming notification-visible output", async function () { var f = fixture(); f.addSession({ localId: 6, cliSessionId: null, ownerId: "u1", title: "Fresh", lastActivity: 60, vendor: "claude", model: "sonnet", history: [] }); f.handler.handleMessage(f.ws, { type: "home_mate_session_open", mateId: "mate-a", sessionId: "local:6", requestId: "open-fresh" }); await settle(); assert.equal(f.messages[0].sessionId, "local:6"); f.messages.length = 0; f.getSession(6).responsePreview = "The visible assistant reply"; f.emitRecorded({ type: "session_id", cliSessionId: "durable-fresh" }); f.emitRecorded({ type: "delta", text: "The visible assistant reply" }); f.emitRecorded({ type: "result" }); var stream = f.messages.filter(function (message) { return message.type === "home_mate_session_identity" || message.type === "home_mate_delta" || message.type === "home_mate_done"; }); assert.deepEqual(stream.map(function (message) { return [message.type, message.previousSessionId || null, message.sessionId, message.requestId, message.text || null]; }), [ ["home_mate_session_identity", "local:6", "durable-fresh", "open-fresh", null], ["home_mate_delta", null, "durable-fresh", "open-fresh", "The visible assistant reply"], ["home_mate_done", null, "durable-fresh", "open-fresh", "The visible assistant reply"], ]); assert.equal(f.getSession(6).responsePreview, "The visible assistant reply"); assert.equal(f.ws._homeChatTap.sessionReference, "durable-fresh"); var refreshed = f.messages.find(function (message) { return message.type === "home_mate_sessions_state"; }); assert.equal(refreshed.sessions[0].id, "durable-fresh"); assert.equal(refreshed.sessions[0].cliSessionId, "durable-fresh"); assert.equal(refreshed.sessions[0].localId, 6); }); test("Home final events recover canonical history text when a live delta was missed", async function () { var f = fixture(); f.handler.handleMessage(f.ws, { type: "home_mate_session_open", mateId: "mate-a", sessionId: "session-old", requestId: "open-final-only" }); await settle(); f.messages.length = 0; f.getSession(1).history.push({ type: "user_message", text: "Question" }); f.getSession(1).history.push({ type: "delta", text: "Recovered from canonical history" }); f.emitRecorded({ type: "done" }); assert.equal(f.messages[0].type, "home_mate_done"); assert.equal(f.messages[0].text, "Recovered from canonical history"); assert.equal(f.messages[0].requestId, "open-final-only"); }); test("Explicit Mate conversation open rejects another user's session", function () { var f = fixture(); f.handler.handleMessage(f.ws, { type: "home_mate_session_open", mateId: "mate-a", sessionId: "session-other", requestId: "open-denied" }); assert.strictEqual(f.getSubscribed(), null); assert.strictEqual(f.messages[0].type, "home_mate_error"); assert.strictEqual(f.messages[0].code, "session_not_found"); assert.strictEqual(f.messages[0].sessionId, "session-other"); assert.strictEqual(f.messages[0].requestId, "open-denied"); }); test("Home send failures inherit the active session correlation", async function () { var f = fixture(); f.handler.handleMessage(f.ws, { type: "home_mate_session_open", mateId: "mate-a", sessionId: "session-old", requestId: "open-send" }); await settle(); f.messages.length = 0; f.handler.handleMessage(f.ws, { type: "home_mate_send", mateId: "mate-a", text: "Hello" }); await settle(); assert.strictEqual(f.messages[0].type, "home_mate_error"); assert.strictEqual(f.messages[0].sessionId, "session-old"); assert.strictEqual(f.messages[0].requestId, "open-send"); assert.match(f.messages[0].text, /SDK bridge unavailable/); }); test("Home send records one canonical user turn and restores user plus assistant history", async function () { var startCalls = 0; var f = fixture({ sdk: { startQuery: function () { startCalls++; }, pushMessage: function () { throw new Error("unexpected push"); }, } }); f.handler.handleMessage(f.ws, { type: "home_mate_session_open", mateId: "mate-a", sessionId: "session-old", requestId: "open-record" }); await settle(); f.messages.length = 0; f.handler.handleMessage(f.ws, { type: "home_mate_send", mateId: "mate-a", sessionId: "session-old", requestId: "open-record", text: "Remember my question" }); await settle(); assert.equal(startCalls, 1); assert.deepEqual(f.getRecorded(), [{ type: "user_message", text: "Remember my question" }]); assert.equal(f.messages.some(function (message) { return message.type === "home_mate_user_message" || message.type === "user_message"; }), false); f.record(1, { type: "delta", text: "Here is the answer" }); f.record(1, { type: "result" }); f.handler.handleMessage(f.ws, { type: "home_mate_close" }); f.messages.length = 0; f.handler.handleMessage(f.ws, { type: "home_mate_session_open", mateId: "mate-a", sessionId: "session-old", requestId: "reopen-record" }); await settle(); assert.deepEqual(f.messages[0].messages, [ { role: "user", text: "Remember my question" }, { role: "assistant", text: "Here is the answer" }, ]); }); test("Capsule creation starts one exact Mate conversation without mutating a Capsule", async function () { var starts = []; var f = fixture({ allowCreate: true, sdk: { startQuery: function (session, text) { starts.push({ session: session, text: text }); }, pushMessage: function () { throw new Error("unexpected push"); }, } }); f.handler.handleMessage(f.ws, { type: "home_mate_new_session", mateId: "mate-a", requestId: "capsule-create-1", intent: { type: "capsule_creation", description: " A weekly review board " }, }); await settle(); assert.equal(f.getSubscribed(), 6); assert.deepEqual(f.getRecorded(), [{ type: "user_message", text: "A weekly review board" }]); assert.equal(starts.length, 1); assert.equal(starts[0].session.localId, 6); assert.match(starts[0].text, /durable Capsule interface/); assert.match(starts[0].text, /A weekly review board/); assert.match(starts[0].text, /explicitly approved/); var history = f.messages.find(function (message) { return message.type === "home_mate_history"; }); assert.equal(history.mateId, "mate-a"); assert.equal(history.sessionId, "local:6"); assert.equal(history.requestId, "capsule-create-1"); assert.deepEqual(history.messages, [{ role: "user", text: "A weekly review board" }]); assert.equal(history.isProcessing, true); }); test("invalid Capsule creation intent is correlated and creates no conversation", async function () { var f = fixture({ allowCreate: true, sdk: { startQuery: function () { throw new Error("unexpected start"); } } }); f.handler.handleMessage(f.ws, { type: "home_mate_new_session", mateId: "mate-a", requestId: "capsule-create-empty", intent: { type: "capsule_creation", description: " " }, }); await settle(); assert.equal(f.getSession(6), undefined); assert.equal(f.messages[0].type, "home_mate_error"); assert.equal(f.messages[0].requestId, "capsule-create-empty"); assert.equal(f.messages[0].code, "capsule_description_required"); }); test("Home push-to-existing-query records one canonical user turn", async function () { var pushCalls = 0; var f = fixture({ sdk: { startQuery: function () { throw new Error("unexpected start"); }, pushMessage: function () { pushCalls++; }, } }); f.getSession(1).isProcessing = true; f.handler.handleMessage(f.ws, { type: "home_mate_session_open", mateId: "mate-a", sessionId: "session-old", requestId: "open-push" }); await settle(); f.messages.length = 0; f.handler.handleMessage(f.ws, { type: "home_mate_send", mateId: "mate-a", sessionId: "session-old", requestId: "open-push", text: "Follow up" }); await settle(); assert.equal(pushCalls, 1); assert.deepEqual(f.getRecorded(), [{ type: "user_message", text: "Follow up" }]); assert.equal(f.messages.length, 0); }); test("a synchronous Home dispatch failure preserves one submitted user turn", async function () { var f = fixture({ sdk: { startQuery: function () { throw new Error("dispatch failed"); }, pushMessage: function () { throw new Error("unexpected push"); }, } }); f.handler.handleMessage(f.ws, { type: "home_mate_session_open", mateId: "mate-a", sessionId: "session-old", requestId: "open-fail" }); await settle(); f.messages.length = 0; f.handler.handleMessage(f.ws, { type: "home_mate_send", mateId: "mate-a", sessionId: "session-old", requestId: "open-fail", text: "Keep this submission" }); await settle(); assert.deepEqual(f.getRecorded(), [{ type: "user_message", text: "Keep this submission" }]); assert.equal(f.messages[0].type, "home_mate_error"); assert.match(f.messages[0].text, /dispatch failed/); }); test("single-user Mate conversation lists include only ownerless sessions", function () { var f = fixture({ singleUser: true }); f.handler.handleMessage(f.ws, { type: "home_mate_sessions_list", mateId: "mate-a" }); assert.deepStrictEqual(f.messages[0].sessions, [ { id: "session-single", cliSessionId: "session-single", localId: 5, title: "Single-user chat", vendor: null, model: null, createdAt: 0, lastActivity: 20, isProcessing: false, sessionOriginId: null, sessionRole: "driver", parentSessionId: null, parentAvailable: false, workerGeneration: null }, ]); }); test("unauthenticated multi-user Mate conversation requests reveal no sessions", function () { var f = fixture({ unauthenticated: true }); f.handler.handleMessage(f.ws, { type: "home_mate_sessions_list", mateId: "mate-a", requestId: "list-auth" }); assert.strictEqual(f.messages[0].type, "home_mate_error"); assert.strictEqual(f.messages[0].text, "Not authenticated."); assert.strictEqual(f.messages[0].requestId, "list-auth"); });