var test = require("node:test"); var assert = require("node:assert/strict"); var attachHomeChat = require("../lib/server-home-chat").attachHomeChat; var attachDebateProposal = require("../lib/project-debate-proposal").attachDebateProposal; var planningPrompt = require("../lib/server-home-debate-planning").INITIATION_PROMPT; function settle() { return new Promise(function (resolve) { setImmediate(resolve); }); } function markTopicAnswered(session) { session.history.push({ type: "tool_executing", id: "topic-ready", name: "AskUserQuestion", input: { questions: [{ question: "Topic?", options: [] }] } }); session.history.push({ type: "ask_user_answered", toolId: "topic-ready", answers: { 0: "User topic" } }); } function fixture(options) { var opts = options || {}; var clay = { id: "builtin:clay", builtinKey: "clay", name: "Clay", vendor: "claude", model: "sonnet" }; var other = { id: "mate-other", name: "Other", vendor: "claude", model: "sonnet" }; var sessions = new Map(); var nextId = 1; var subscribers = {}; var starts = []; var records = []; var ordering = []; var saved = []; var catalogError = opts.catalogError === true; var startError = opts.startError === true; var manager = { sessions: sessions, createSessionRaw: function (sessionOptions) { var session = { localId: nextId++, cliSessionId: null, ownerId: sessionOptions.ownerId, vendor: sessionOptions.vendor || null, model: sessionOptions.model || null, effort: sessionOptions.effort || null, title: "", history: [], pendingAskUser: {}, isProcessing: false, createdAt: Date.now(), lastActivity: Date.now() }; sessions.set(session.localId, session); ordering.push({ type: "created", session: session }); return session; }, switchSession: function (localId, target) { var session = sessions.get(localId); target._clayActiveSession = localId; ordering.push({ type: "switched", title: session.title, debatePlanning: session.debateSetupMode === true, debatePhase: session.homeDebatePhase, target: target }); target.send(JSON.stringify({ type: "session_switched", id: localId, cliSessionId: session.cliSessionId || null, debatePlanning: session.debateSetupMode === true, debatePhase: session.homeDebatePhase || null })); }, subscribeSession: function (localId, callback) { subscribers[localId] = callback; return function () { delete subscribers[localId]; }; }, sendAndRecord: function (session, event) { session.history.push(event); records.push(event); if (subscribers[session.localId]) subscribers[session.localId](event); }, saveSessionFile: function (session) { saved.push({ title: session.title, debatePlanning: session.debateSetupMode === true, debatePhase: session.homeDebatePhase }); return true; }, }; var proposalStarts = []; var debateControls = []; var proposal = attachDebateProposal({ cwd: "/mates/builtin:clay", isMate: true, isHostAgent: true, sendTo: function () {}, buildMateCtx: function () { return {}; }, getMate: function (ctx, id) { return id === "panel-1" || id === "builtin:clay" ? { id: id } : null; }, getProjectOwnerId: function () { return "u1"; }, recordSessionEvent: function (session, event) { manager.sendAndRecord(session, event); }, startDebate: function (session, brief, moderatorId) { proposalStarts.push({ session: session, brief: brief, moderatorId: moderatorId }); return { ok: true }; }, }); var project = { getSessionManager: function () { return manager; }, getVendorModelCatalog: function () { if (catalogError) return Promise.resolve({ status: "error", models: [], error: "Sign in to the configured model provider and retry." }); return Promise.resolve({ status: "ready", models: ["sonnet"], defaultModel: "sonnet", error: "" }); }, getVendorModelAvailability: function () { return [{ id: "claude", displayName: "Claude", installed: true }]; }, getMemoryState: function () { return { entries: [], summary: "" }; }, listKnowledgeFiles: function () { return []; }, forEachClient: function () {}, sdk: { startQuery: function (session, prompt) { starts.push({ session: session, prompt: prompt }); if (startError) throw new Error("query startup failed"); }, pushMessage: function () {}, }, handleHomeDebateProposalResponse: function (ws, msg, session) { return proposal.handleHomeMessage(ws, msg, session); }, handleHomeAskUserResponse: function (msg, session) { var pending = session.pendingAskUser[msg.toolId]; if (!pending) return false; delete session.pendingAskUser[msg.toolId]; manager.sendAndRecord(session, { type: "ask_user_answered", toolId: msg.toolId, answers: msg.answers || {} }); pending.resolve({ behavior: "deny", message: "answered" }); return true; }, handleHomeDebateControl: function (ws, msg, session) { debateControls.push({ ws: ws, msg: msg, session: session }); return true; }, }; var mateList = [other, clay]; var handler = attachHomeChat({ users: { isMultiUser: function () { return true; } }, mates: { buildMateCtx: function () { return {}; }, getAllMates: function () { return mateList; }, getMate: function (ctx, id) { return mateList.find(function (mate) { return mate.id === id; }) || null; }, updateMate: function (ctx, id, patch) { for (var i = 0; i < mateList.length; i++) if (mateList[i].id === id) mateList[i] = Object.assign({}, mateList[i], patch); clay = mateList[1]; return clay; }, getMateDir: function () { return "/tmp/clay-home-debate-test"; }, }, projects: new Map([["mate-builtin:clay", project]]), addProject: function () {}, resolveDefaultAi: opts.defaultRuntime ? function () { return Promise.resolve(Object.assign({ ready: true }, opts.defaultRuntime)); } : null, }); var messages = []; var ws = { readyState: 1, _clayUser: { id: "u1" }, send: function (value) { messages.push(JSON.parse(value)); } }; return { handler: handler, ws: ws, messages: messages, sessions: sessions, starts: starts, records: records, ordering: ordering, saved: saved, proposal: proposal, proposalStarts: proposalStarts, debateControls: debateControls, setCatalogError: function (value) { catalogError = value; }, setStartError: function (value) { startError = value; }, emit: function (localId, event) { var session = sessions.get(localId); session.history.push(event); if (subscribers[localId]) subscribers[localId](event); }, adapter: { createToolServer: function (definition) { return definition; } } }; } test("Home Start debate creates a fresh exact Clay planning session with a hidden one-shot initiation", async function () { var f = fixture({ defaultRuntime: { vendor: "codex", model: "gpt-6-astra", effort: "high" } }); assert.equal(f.handler.handleMessage(f.ws, { type: "home_mate_debate_plan", mateId: "mate-other", requestId: "plan-1" }), true); await settle(); var session = Array.from(f.sessions.values())[0]; assert.equal(session.ownerId, "u1"); assert.equal(session.title, "Debate planning"); assert.equal(session.debateSetupMode, true); assert.equal(session.homeDebatePlanning, true); assert.equal(session.vendor, "codex"); assert.equal(session.model, "gpt-6-astra"); assert.equal(session.effort, "high"); assert.deepEqual(f.saved[0], { title: "Debate planning", debatePlanning: true, debatePhase: "planning" }); assert.equal(f.ordering[1].type, "switched"); assert.equal(f.ordering[1].debatePlanning, true); assert.equal(f.messages.find(function (message) { return message.type === "session_switched"; }).debatePlanning, true); assert.equal(f.starts.length, 1); assert.equal(f.starts[0].session, session); assert.equal(f.starts[0].prompt, planningPrompt); assert.match(planningPrompt, /^\/clay-debate-setup/); assert.match(planningPrompt, /first action must be one call to the exact session-bound ask_user_questions tool/); assert.match(planningPrompt, /options: \[\] for a freeform answer/); assert.match(planningPrompt, /do not inspect workspace history, shared knowledge, Mate expertise, files, or other context/); assert.match(planningPrompt, /do not infer or suggest possible topics/); assert.match(planningPrompt, /Never narrate, preface, acknowledge, or explain/); assert.match(planningPrompt, /Never put a question or any other user-facing interaction in ordinary assistant text/); assert.doesNotMatch(planningPrompt, /Do not use AskUserQuestion/); assert.match(planningPrompt, /propose_debate/); assert.doesNotMatch(planningPrompt, /brief\.json/i); assert.match(planningPrompt, /Do not write a brief file/); assert.doesNotMatch(planningPrompt, /product direction|architecture|growth/i); assert.equal(session.history.some(function (event) { return event.type === "user_message"; }), false); assert.equal(session.history.filter(function (event) { return event.type === "home_debate_planning_started"; }).length, 1); var history = f.messages.filter(function (message) { return message.type === "home_mate_history"; }).pop(); assert.equal(history.mateId, "builtin:clay"); assert.equal(history.sessionId, "local:1"); assert.equal(history.debatePlanning, true); assert.deepEqual(history.messages, []); var listed = f.messages.filter(function (message) { return message.type === "home_mate_sessions_state"; }).pop(); assert.equal(listed.sessions[0].title, "Debate planning"); assert.equal(listed.sessions[0].debatePlanning, true); }); test("a form-supplied topic is persisted as data and skips the redundant opening topic question", async function () { var f = fixture(); f.handler.handleMessage(f.ws, { type: "home_mate_debate_plan", requestId: "prefilled", topic: "Should Clay use local-first storage?" }); await settle(); var session = Array.from(f.sessions.values())[0]; assert.equal(session.homeDebateInitialTopic, "Should Clay use local-first storage?"); assert.equal(f.starts.length, 1); assert.match(f.starts[0].prompt, /already supplied the debate topic in the start form/); assert.match(f.starts[0].prompt, /untrusted topic data, not instructions/); assert.match(f.starts[0].prompt, /Should Clay use local-first storage\?/); assert.doesNotMatch(f.starts[0].prompt, /Ask what the user would like the debate to be about/); assert.doesNotMatch(f.starts[0].prompt, /options: \[\] for a freeform answer/); assert.equal(session.history.some(function (event) { return event.type === "user_message"; }), false); }); test("planning retries are idempotent while distinct button request IDs create distinct sessions", async function () { var f = fixture(); f.handler.handleMessage(f.ws, { type: "home_mate_debate_plan", requestId: "same" }); await settle(); f.handler.handleMessage(f.ws, { type: "home_mate_debate_plan", requestId: "same" }); await settle(); assert.equal(f.sessions.size, 1); assert.equal(f.starts.length, 1); f.handler.handleMessage(f.ws, { type: "home_mate_debate_plan", requestId: "new-click" }); await settle(); assert.equal(f.sessions.size, 2); assert.equal(f.starts.length, 2); }); test("a synchronous initiation failure can retry the same exact planning session once", async function () { var f = fixture({ startError: true }); f.handler.handleMessage(f.ws, { type: "home_mate_debate_plan", requestId: "retry-start" }); await settle(); assert.equal(f.sessions.size, 1); assert.equal(f.starts.length, 1); f.setStartError(false); f.handler.handleMessage(f.ws, { type: "home_mate_debate_plan", requestId: "retry-start" }); await settle(); assert.equal(f.sessions.size, 1); assert.equal(f.starts.length, 2); var session = Array.from(f.sessions.values())[0]; assert.equal(session.history.filter(function (event) { return event.type === "home_debate_planning_started"; }).length, 2); assert.equal(session.history.filter(function (event) { return event.type === "home_debate_planning_start_failed"; }).length, 1); }); test("catalog failure creates no blank planning session and returns a correlated actionable error", async function () { var f = fixture({ catalogError: true }); f.handler.handleMessage(f.ws, { type: "home_mate_debate_plan", requestId: "model-fail" }); await settle(); assert.equal(f.sessions.size, 0); assert.equal(f.starts.length, 0); var error = f.messages.find(function (message) { return message.type === "home_mate_error"; }); assert.equal(error.requestId, "model-fail"); assert.equal(error.sessionId, null); assert.equal(error.code, "model_unavailable"); assert.match(error.text, /sign in|retry/i); }); test("retrying after catalog recovery creates one initialized planning session", async function () { var f = fixture({ catalogError: true }); f.handler.handleMessage(f.ws, { type: "home_mate_debate_plan", requestId: "recover" }); await settle(); f.setCatalogError(false); f.handler.handleMessage(f.ws, { type: "home_mate_debate_plan", requestId: "recover" }); await settle(); assert.equal(f.sessions.size, 1); assert.equal(f.starts.length, 1); assert.equal(f.starts[0].session.localId, 1); assert.equal(f.starts[0].session.history.filter(function (event) { return event.type === "home_debate_planning_started"; }).length, 1); assert.equal(f.starts[0].session.model, "sonnet"); }); test("Clay's planning AskUserQuestion projects into the exact Home transcript and canonical answer resumes it", async function () { var f = fixture(); f.handler.handleMessage(f.ws, { type: "home_mate_debate_plan", requestId: "stream" }); await settle(); var session = Array.from(f.sessions.values())[0]; var resolved = null; var topicInput = { questions: [{ header: "Topic", question: "What would you like the debate to be about?", options: [] }] }; session.pendingAskUser["ask-1"] = { input: topicInput, resolve: function (value) { resolved = value; } }; f.messages.length = 0; f.emit(session.localId, { type: "delta", text: "Let us shape this carefully." }); f.emit(session.localId, { type: "tool_executing", id: "ask-1", name: "AskUserQuestion", input: topicInput }); var question = f.messages.find(function (message) { return message.type === "home_debate_question"; }); assert.equal(f.messages.some(function (message) { return message.type === "home_mate_delta"; }), false); assert.equal(question.sessionId, "local:1"); assert.equal(question.requestId, "stream"); assert.equal(question.questions[0].options.length, 0); f.handler.handleMessage(f.ws, { type: "home_debate_question_response", mateId: "builtin:clay", sessionId: "local:1", requestId: "stream", toolId: "ask-1", answers: { 0: "도시 주거 정책" } }); assert.deepEqual(resolved, { behavior: "deny", message: "answered" }); var answered = f.messages.find(function (message) { return message.type === "home_debate_question_resolved"; }); assert.equal(answered.status, "answered"); assert.deepEqual(answered.answers, { 0: "도시 주거 정책" }); f.messages.length = 0; f.handler.handleMessage(f.ws, { type: "home_mate_session_open", mateId: "builtin:clay", sessionId: "local:1", requestId: "stream" }); await settle(); var restoredTopic = f.messages.find(function (message) { return message.type === "home_mate_history"; }).messages[0]; assert.equal(restoredTopic.questions[0].options.length, 0); assert.deepEqual(restoredTopic.answers, { 0: "도시 주거 정책" }); f.messages.length = 0; f.emit(session.localId, { type: "tool_executing", id: "ask-2", name: "AskUserQuestion", input: { questions: [{ header: "Format", question: "Which format?", options: [{ label: "Round table" }, { label: "Pro/con" }] }] } }); f.emit(session.localId, { type: "done" }); assert.equal(f.messages.filter(function (message) { return message.type === "home_mate_done"; }).pop().text, ""); }); test("answered question ignores late prior-turn result and done before the next question", async function () { var f = fixture(); f.handler.handleMessage(f.ws, { type: "home_mate_debate_plan", requestId: "late-terminal" }); await settle(); var session = Array.from(f.sessions.values())[0]; session.pendingAskUser["ask-late"] = { input: { questions: [{ question: "Topic?", options: [] }] }, resolve: function () {} }; f.emit(session.localId, { type: "tool_executing", id: "ask-late", name: "AskUserQuestion", input: { questions: [{ question: "Topic?", options: [] }] } }); f.handler.handleMessage(f.ws, { type: "home_debate_question_response", sessionId: "local:1", requestId: "late-terminal", toolId: "ask-late", answers: { 0: "Housing" } }); f.messages.length = 0; f.emit(session.localId, { type: "delta", text: "I am using an internal skill and tool." }); f.emit(session.localId, { type: "result" }); f.emit(session.localId, { type: "done" }); assert.equal(f.messages.some(function (message) { return message.type === "home_mate_delta"; }), false); assert.equal(f.messages.some(function (message) { return message.type === "home_mate_error"; }), false); f.emit(session.localId, { type: "tool_executing", id: "ask-next", name: "AskUserQuestion", input: { questions: [{ question: "Format?", options: [{ label: "Round table" }, { label: "Pro/con" }] }] } }); assert.ok(f.messages.find(function (message) { return message.type === "home_debate_question" && message.toolId === "ask-next"; })); f.handler.handleMessage(f.ws, { type: "home_mate_session_open", mateId: "builtin:clay", sessionId: "local:1", requestId: "late-terminal" }); await settle(); var history = f.messages.find(function (message) { return message.type === "home_mate_history"; }); assert.equal(history.messages.some(function (message) { return message.role === "assistant" && /internal skill/.test(message.text); }), false); }); test("Home debate question relay rejects stale, duplicate, and cross-user answers", async function () { var f = fixture(); f.handler.handleMessage(f.ws, { type: "home_mate_debate_plan", requestId: "question-owner" }); await settle(); var session = Array.from(f.sessions.values())[0]; var count = 0; session.pendingAskUser["ask-owner"] = { input: { questions: [{ question: "Proceed?" }] }, resolve: function () { count++; } }; f.handler.handleMessage(f.ws, { type: "home_debate_question_response", sessionId: "local:1", requestId: "stale", toolId: "ask-owner", answers: { 0: "No" } }); f.handler.handleMessage(f.ws, { type: "home_debate_question_response", sessionId: "local:1", toolId: "ask-owner", answers: { 0: "No request" } }); var attacker = { readyState: 1, _clayUser: { id: "u2" }, send: function () {} }; f.handler.handleMessage(attacker, { type: "home_debate_question_response", sessionId: "local:1", requestId: "question-owner", toolId: "ask-owner", answers: { 0: "No" } }); assert.equal(count, 0); f.handler.handleMessage(f.ws, { type: "home_debate_question_response", sessionId: "local:1", requestId: "question-owner", toolId: "ask-owner", answers: { 0: "Yes" } }); f.handler.handleMessage(f.ws, { type: "home_debate_question_response", sessionId: "local:1", requestId: "question-owner", toolId: "ask-owner", answers: { 0: "Again" } }); assert.equal(count, 1); }); test("Home live controls require the owned exact active debate session", async function () { var f = fixture(); f.handler.handleMessage(f.ws, { type: "home_mate_debate_plan", requestId: "live-control" }); await settle(); var session = Array.from(f.sessions.values())[0]; session.homeDebatePhase = "live"; session._debate = { phase: "live" }; f.handler.handleMessage(f.ws, { type: "home_debate_control", action: "stop", mateId: "builtin:clay", sessionId: "local:1", requestId: "live-control" }); assert.equal(f.debateControls.length, 1); assert.equal(f.debateControls[0].session, session); f.handler.handleMessage(f.ws, { type: "home_debate_control", action: "cancel_stop", mateId: "builtin:clay", sessionId: "local:1", requestId: "live-control" }); assert.equal(f.debateControls.length, 2); assert.equal(f.debateControls[1].msg.action, "cancel_stop"); session.homeDebatePhase = "ended"; session._debate = { phase: "ended" }; f.handler.handleMessage(f.ws, { type: "home_debate_control", action: "resume", mateId: "builtin:clay", sessionId: "local:1", requestId: "live-control" }); assert.equal(f.debateControls.length, 3); assert.equal(f.debateControls[2].msg.action, "resume"); f.handler.handleMessage(f.ws, { type: "home_debate_control", action: "stop", mateId: "builtin:clay", sessionId: "local:1", requestId: "stale" }); assert.equal(f.debateControls.length, 3); assert.equal(f.messages.some(function (message) { return message.code === "debate_not_active"; }), true); var attackerMessages = []; var attacker = { readyState: 1, _clayUser: { id: "u2" }, _homeChatTap: f.ws._homeChatTap, send: function (value) { attackerMessages.push(JSON.parse(value)); } }; f.handler.handleMessage(attacker, { type: "home_debate_control", action: "stop", mateId: "builtin:clay", sessionId: "local:1", requestId: "live-control" }); assert.equal(f.debateControls.length, 3); assert.equal(attackerMessages.some(function (message) { return message.code === "debate_not_active"; }), true); }); test("restored unanswered question becomes actionably expired when its backend callback is gone", async function () { var f = fixture(); f.handler.handleMessage(f.ws, { type: "home_mate_debate_plan", requestId: "restore-question" }); await settle(); var session = Array.from(f.sessions.values())[0]; f.emit(session.localId, { type: "tool_executing", id: "ask-expired", name: "AskUserQuestion", input: { questions: [{ question: "Still there?", options: [{ label: "Yes" }, { label: "No" }] }] } }); f.messages.length = 0; f.handler.handleMessage(f.ws, { type: "home_mate_session_open", mateId: "builtin:clay", sessionId: "local:1", requestId: "restore-question" }); await settle(); var restored = f.messages.filter(function (message) { return message.type === "home_mate_history"; }).pop(); assert.equal(restored.messages[0].role, "question"); assert.equal(restored.messages[0].status, "pending"); f.messages.length = 0; f.handler.handleMessage(f.ws, { type: "home_debate_question_response", sessionId: "local:1", requestId: "restore-question", toolId: "ask-expired", answers: { 0: "Yes" } }); var expired = f.messages.find(function (message) { return message.type === "home_debate_question_resolved"; }); assert.equal(expired.status, "expired"); assert.match(expired.error, /repeat/i); var history = f.messages.find(function (message) { return message.type === "home_mate_error"; }); assert.equal(history.code, "question_expired"); }); test("session-bound propose_debate records an inline Home proposal and exact approval starts with Clay as moderator", async function () { var f = fixture(); f.handler.handleMessage(f.ws, { type: "home_mate_debate_plan", requestId: "proposal" }); await settle(); var session = Array.from(f.sessions.values())[0]; markTopicAnswered(session); var server = f.proposal.createMcpServer(f.adapter, session); var pending = server.tools[0].handler({ topic: "Architecture direction", panelists: JSON.stringify([{ mateId: "panel-1", role: "Skeptic", brief: "Challenge assumptions" }]) }); var proposalMessage = f.messages.filter(function (message) { return message.type === "home_debate_proposal"; }).pop(); assert.equal(proposalMessage.sessionId, "local:1"); assert.equal(proposalMessage.proposal.topic, "Architecture direction"); f.handler.handleMessage(f.ws, { type: "home_debate_proposal_response", mateId: "builtin:clay", sessionId: "local:1", requestId: "proposal", proposalId: proposalMessage.proposal.proposalId, action: "start" }); var result = await pending; assert.equal(f.proposalStarts.length, 1); assert.equal(f.proposalStarts[0].session, session); assert.equal(f.proposalStarts[0].moderatorId, "builtin:clay"); assert.equal(result.isError, undefined); var resolved = f.messages.filter(function (message) { return message.type === "home_debate_proposal_resolved"; }).pop(); assert.equal(resolved.action, "start"); }); test("Home planning cannot propose before the user supplies a topic", async function () { var f = fixture(); f.handler.handleMessage(f.ws, { type: "home_mate_debate_plan", requestId: "proposal-before-topic" }); await settle(); var session = Array.from(f.sessions.values())[0]; var server = f.proposal.createMcpServer(f.adapter, session); var result = await server.tools[0].handler({ topic: "Invented topic", panelists: JSON.stringify([{ mateId: "panel-1" }]) }); assert.equal(result.isError, true); assert.match(result.content[0].text, /debate topic/); assert.equal(f.messages.some(function (message) { return message.type === "home_debate_proposal"; }), false); }); test("Home proposal relay rejects stale exact sessions without consuming the real pending proposal", async function () { var f = fixture(); f.handler.handleMessage(f.ws, { type: "home_mate_debate_plan", requestId: "proposal-stale" }); await settle(); var session = Array.from(f.sessions.values())[0]; markTopicAnswered(session); var server = f.proposal.createMcpServer(f.adapter, session); var pending = server.tools[0].handler({ topic: "Secure routing", panelists: JSON.stringify([{ mateId: "panel-1" }]) }); var proposalMessage = f.messages.filter(function (message) { return message.type === "home_debate_proposal"; }).pop(); f.handler.handleMessage(f.ws, { type: "home_debate_proposal_response", sessionId: "local:99", proposalId: proposalMessage.proposal.proposalId, action: "start", requestId: "stale" }); assert.equal(f.proposalStarts.length, 0); f.handler.handleMessage(f.ws, { type: "home_debate_proposal_response", sessionId: "local:1", proposalId: proposalMessage.proposal.proposalId, action: "cancel", requestId: "proposal-stale" }); var result = await pending; assert.match(result.content[0].text, /cancelled/i); }); test("Home proposal relay rejects a stale request for the same exact session", async function () { var f = fixture(); f.handler.handleMessage(f.ws, { type: "home_mate_debate_plan", requestId: "current-request" }); await settle(); var session = Array.from(f.sessions.values())[0]; markTopicAnswered(session); var server = f.proposal.createMcpServer(f.adapter, session); var pending = server.tools[0].handler({ topic: "Correlation", panelists: JSON.stringify([{ mateId: "panel-1" }]) }); var proposalMessage = f.messages.filter(function (message) { return message.type === "home_debate_proposal"; }).pop(); f.handler.handleMessage(f.ws, { type: "home_debate_proposal_response", sessionId: "local:1", proposalId: proposalMessage.proposal.proposalId, action: "start", requestId: "stale-request" }); assert.equal(f.proposalStarts.length, 0); f.handler.handleMessage(f.ws, { type: "home_debate_proposal_response", sessionId: "local:1", proposalId: proposalMessage.proposal.proposalId, action: "cancel", requestId: "current-request" }); await pending; }); test("Home proposal approval survives local to durable session identity promotion", async function () { var f = fixture(); f.handler.handleMessage(f.ws, { type: "home_mate_debate_plan", requestId: "promote" }); await settle(); var session = Array.from(f.sessions.values())[0]; markTopicAnswered(session); var server = f.proposal.createMcpServer(f.adapter, session); var pending = server.tools[0].handler({ topic: "Durable identity", panelists: JSON.stringify([{ mateId: "panel-1" }]) }); var proposalMessage = f.messages.filter(function (message) { return message.type === "home_debate_proposal"; }).pop(); session.cliSessionId = "cli-planning"; f.emit(session.localId, { type: "session_id", cliSessionId: "cli-planning" }); f.handler.handleMessage(f.ws, { type: "home_debate_proposal_response", sessionId: "cli-planning", proposalId: proposalMessage.proposal.proposalId, action: "cancel", requestId: "promote" }); var result = await pending; assert.match(result.content[0].text, /cancelled/i); }); test("Home proposal relay rejects another user without consuming the owner's proposal", async function () { var f = fixture(); f.handler.handleMessage(f.ws, { type: "home_mate_debate_plan", requestId: "owner" }); await settle(); var session = Array.from(f.sessions.values())[0]; markTopicAnswered(session); var server = f.proposal.createMcpServer(f.adapter, session); var pending = server.tools[0].handler({ topic: "Ownership", panelists: JSON.stringify([{ mateId: "panel-1" }]) }); var proposalMessage = f.messages.filter(function (message) { return message.type === "home_debate_proposal"; }).pop(); var attacker = { readyState: 1, _clayUser: { id: "u2" }, send: function () {} }; f.handler.handleMessage(attacker, { type: "home_debate_proposal_response", sessionId: "local:1", proposalId: proposalMessage.proposal.proposalId, action: "start", requestId: "attack" }); assert.equal(f.proposalStarts.length, 0); f.handler.handleMessage(f.ws, { type: "home_debate_proposal_response", sessionId: "local:1", proposalId: proposalMessage.proposal.proposalId, action: "cancel", requestId: "owner" }); var result = await pending; assert.match(result.content[0].text, /cancelled/i); }); test("proposal boundary suppresses the preface and clears pending without a duplicate final", async function () { var f = fixture(); f.handler.handleMessage(f.ws, { type: "home_mate_debate_plan", requestId: "boundary" }); await settle(); var session = Array.from(f.sessions.values())[0]; markTopicAnswered(session); f.messages.length = 0; f.emit(session.localId, { type: "delta", text: "I have a complete brief." }); var server = f.proposal.createMcpServer(f.adapter, session); var pending = server.tools[0].handler({ topic: "Boundary", panelists: JSON.stringify([{ mateId: "panel-1" }]) }); var proposalMessage = f.messages.find(function (message) { return message.type === "home_debate_proposal"; }); f.handler.handleMessage(f.ws, { type: "home_debate_proposal_response", sessionId: "local:1", proposalId: proposalMessage.proposal.proposalId, action: "cancel", requestId: "boundary" }); await pending; f.emit(session.localId, { type: "done" }); var done = f.messages.filter(function (message) { return message.type === "home_mate_done"; }).pop(); assert.equal(done.text, ""); var history = f.messages.filter(function (message) { return message.type === "home_debate_proposal" || message.type === "home_debate_proposal_resolved"; }); assert.deepEqual(history.map(function (message) { return message.type; }), ["home_debate_proposal", "home_debate_proposal_resolved"]); }); test("an expired restored proposal re-enables with actionable server-confirmed state", async function () { var f = fixture(); f.handler.handleMessage(f.ws, { type: "home_mate_debate_plan", requestId: "expired" }); await settle(); var session = Array.from(f.sessions.values())[0]; f.emit(session.localId, { type: "debate_proposal", proposal: { proposalId: "expired-proposal", topic: "Old proposal", panelists: [] } }); f.messages.length = 0; f.handler.handleMessage(f.ws, { type: "home_debate_proposal_response", sessionId: "local:1", proposalId: "expired-proposal", action: "start", requestId: "expired" }); var resolved = f.messages.find(function (message) { return message.type === "home_debate_proposal_resolved"; }); assert.equal(resolved.action, "error"); assert.match(resolved.error, /no longer active/i); var error = f.messages.find(function (message) { return message.type === "home_mate_error"; }); assert.equal(error.code, "proposal_not_found"); });