test/home-debate-planning.test.js
Rama main · réplica de lectura del repositorio original.
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");
});