Ctrl K
ComunidadGuías…

test/home-debate-live.test.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
var test = require("node:test");
var assert = require("node:assert/strict");
var fs = require("node:fs");
var os = require("node:os");
var path = require("node:path");
var attachDebate = require("../lib/project-debate").attachDebate;
var createSessionManager = require("../lib/sessions").createSessionManager;
var mates = require("../lib/mates");
var homeEvents = require("../lib/server-home-chat-events");

function settle() { return new Promise(function (resolve) { setImmediate(resolve); }); }

function fixture(home) {
  var originalGetMate = mates.getMate;
  var originalBuildMateCtx = mates.buildMateCtx;
  mates.getMate = function (ctx, id) {
    return {
      id: id,
      name: id === "builtin:clay" ? "Clay" : "Panel",
      vendor: "claude",
      profile: {
        displayName: id === "builtin:clay" ? "Clay" : "Panel",
        avatarCatalogId: "C12",
        avatarStyle: "imprint",
        avatarSeed: id,
        avatarColor: "#555",
        avatarCustom: "data:image/svg+xml," + encodeURIComponent(id)
      }
    };
  };
  mates.buildMateCtx = function (userId) { return { userId: userId }; };
  var sessions = new Map();
  var created = 0;
  var events = [];
  var mentionOptions = [];
  var session = { localId: 7, ownerId: "user-a", title: "Debate planning", history: [], homeDebatePlanning: home === true, homeDebatePhase: home === true ? "planning" : null, debateSetupMode: home === true, isProcessing: home === true, queryInstance: { close: function () {} }, abortController: { abort: function () {} }, messageQueue: { end: function () {} } };
  sessions.set(session.localId, session);
  var manager = {
    sessions: sessions,
    createSession: function (options, ws) { created++; var next = { localId: 7 + created, ownerId: options && options.ownerId, history: [] }; sessions.set(next.localId, next); if (ws) ws._clayActiveSession = next.localId; return next; },
    saveSessionFile: function () {}, switchSession: function () {},
    sendAndRecord: function (target, event) { target.history.push(event); events.push({ session: target, event: event }); },
  };
  var engine = attachDebate({
    cwd: "/tmp/clay-home-debate-live-test", slug: "mate-builtin:clay", isMate: false,
    projectOwnerId: "user-a", send: function () {}, sendTo: function () {}, sendToSession: function () {}, sm: manager,
    sdk: { createMentionSession: function (options) { mentionOptions.push(options); return Promise.resolve({ isAlive: function () { return true; }, pushMessage: function () {}, close: function () {} }); } },
    getMateProfile: function (ctx, id) { return { name: id === "builtin:clay" ? "Clay" : "Panel", avatarStyle: "imprint", avatarSeed: id, avatarColor: "#555", avatarCustom: "data:image/svg+xml," + encodeURIComponent(id) }; },
    loadMateClaudeMd: function () { return ""; }, loadMateDigests: function () { return ""; }, hydrateImageRefs: function () {}, onProcessingChanged: function () {},
    getLinuxUserForSession: function () { return null; }, getSessionForWs: function () { return session; }, updateMemorySummary: function () {}, initMemorySummary: function () {},
  });
  function restore() { mates.getMate = originalGetMate; mates.buildMateCtx = originalBuildMateCtx; }
  return { engine: engine, manager: manager, session: session, events: events, mentionOptions: mentionOptions, created: function () { return created; }, restore: restore };
}

function brief() { return { topic: "Housing policy", format: "round_table", panelists: [{ mateId: "panel-1", role: "Analyst", brief: "Examine trade-offs" }] }; }

test("Home debate pins approved moderator and panelist models for the exact debate", async function (t) {
  var f = fixture(true);
  t.after(f.restore);
  var selected = brief();
  selected.participantModels = [
    { mateId: "builtin:clay", vendor: "claude", model: "claude-opus" },
    { mateId: "panel-1", vendor: "codex", model: "gpt-5.6" },
  ];
  var ws = { _clayUser: { id: "user-a" }, _homeChatTap: { mateSlug: "mate-builtin:clay", sessionId: 7 } };
  f.engine.handleMcpDebateApproval(f.session, selected, "builtin:clay", ws);
  await settle();
  assert.equal(f.mentionOptions[0].vendor, "claude");
  assert.equal(f.mentionOptions[0].model, "claude-opus");
  assert.deepEqual(f.session.debateState.participantModels, selected.participantModels);
  assert.deepEqual(f.session.history[0].participantModels, selected.participantModels);
  f.mentionOptions[0].onDone("@Panel, respond.");
  await settle();
  assert.equal(f.mentionOptions[1].vendor, "codex");
  assert.equal(f.mentionOptions[1].model, "gpt-5.6");
});

test("Home approval reuses the exact planning session and projects canonical live turns", async function (t) {
  var f = fixture(true);
  t.after(f.restore);
  var ws = { _clayUser: { id: "user-a" }, _homeChatTap: { mateSlug: "mate-builtin:clay", sessionId: 7 }, _clayActiveSession: 99 };
  assert.deepEqual(f.engine.handleMcpDebateApproval(f.session, brief(), "builtin:clay", ws), { ok: true });
  await settle();
  assert.equal(f.created(), 0);
  assert.equal(f.manager.sessions.size, 1);
  assert.equal(ws._clayActiveSession, 7);
  assert.equal(f.session.homeDebatePhase, "live");
  assert.equal(f.session.title, "Debate: Housing policy");
  assert.equal(f.session.debateState.phase, "live");
  assert.deepEqual(f.events.slice(0, 2).map(function (entry) { return entry.event.type; }), ["debate_started", "debate_turn"]);
  assert.equal(f.events[0].session, f.session);
  assert.equal(f.events[1].event.mateName, "Clay");
  assert.equal(f.events[1].event.avatarCatalogId, "C12");
  assert.equal(f.events[1].event.avatarSeed, "builtin:clay");
  assert.match(f.events[1].event.avatarCustom, /builtin%3Aclay/);
  f.mentionOptions[0].onActivity("Thinking");
  f.mentionOptions[0].onDelta("Opening ");
  f.mentionOptions[0].onDone("Opening statement without a panel call.");
  var types = f.events.map(function (entry) { return entry.event.type; });
  assert.deepEqual(types.slice(2, 6), ["debate_activity", "debate_stream", "debate_turn_done", "debate_conclude_confirm"]);
  assert.equal(f.engine.handleHomeControl(ws, { action: "conclude", response: "end" }, f.session), true);
  assert.equal(f.session.homeDebatePhase, "ended");
  assert.equal(f.events[f.events.length - 1].event.type, "debate_ended");
  var projected = f.events.map(function (entry) { return homeEvents.transformEvent(entry.event, "builtin:clay", f.session, "request-a", "durable-a"); }).filter(Boolean);
  assert.equal(projected.every(function (event) { return event.type === "home_debate_event"; }), true);
  assert.equal(projected[1].sessionId, "durable-a");
  assert.equal(projected[1].requestId, "request-a");
  assert.equal(projected[1].avatarCatalogId, "C12");
  assert.equal(projected[1].avatarSeed, "builtin:clay");
  assert.match(projected[1].avatarCustom, /builtin%3Aclay/);
});

test("Home conclude control rebuilds a missing live runtime and resumes the exact debate", async function (t) {
  var f = fixture(true);
  t.after(f.restore);
  var ws = { _clayUser: { id: "user-a" }, _homeChatTap: { mateSlug: "mate-builtin:clay", sessionId: 7 }, _clayActiveSession: 7 };
  f.session.homeDebatePhase = "live";
  f.session.history = [
    { type: "debate_started", topic: "Housing policy", format: "round_table", moderatorId: "builtin:clay", panelists: [{ mateId: "panel-1", role: "Analyst", brief: "Examine trade-offs" }] },
    { type: "debate_turn_done", mateId: "builtin:clay", mateName: "Clay", role: "moderator", round: 2, text: "A closing thought." },
    { type: "debate_conclude_confirm", topic: "Housing policy", round: 2 },
  ];

  assert.equal(f.session._debate, undefined);
  assert.equal(f.engine.handleHomeControl(ws, { action: "resume", text: "Compare implementation paths" }, f.session), true);
  await settle();

  assert.equal(f.created(), 0);
  assert.equal(f.manager.sessions.size, 1);
  assert.equal(f.session._debate.phase, "live");
  assert.equal(f.session._debate.awaitingConcludeConfirm, false);
  assert.equal(f.session.homeDebatePhase, "live");
  assert.equal(f.session.debateState.awaitingConcludeConfirm, false);
  assert.equal(f.mentionOptions.length, 1);
  assert.match(f.mentionOptions[0].initialMessage, /Compare implementation paths/);
  assert.deepEqual(f.events.map(function (entry) { return entry.event.type; }), ["debate_user_resume", "debate_resumed", "debate_turn"]);
});

test("an ended Home debate can be resumed repeatedly in the same exact session", async function (t) {
  var f = fixture(true);
  t.after(f.restore);
  var ws = { _clayUser: { id: "user-a" }, _homeChatTap: { mateSlug: "mate-builtin:clay", sessionId: 7 }, _clayActiveSession: 7 };
  f.session.homeDebatePhase = "ended";
  f.session.debateState = { phase: "ended", topic: "Housing policy", format: "round_table", moderatorId: "builtin:clay", panelists: [{ mateId: "panel-1", role: "Analyst", brief: "Examine trade-offs" }], round: 3, debateId: "durable-debate", ownerId: "user-a" };
  f.session.history = [
    { type: "debate_started", topic: "Housing policy", format: "round_table", moderatorId: "builtin:clay", panelists: [{ mateId: "panel-1", role: "Analyst" }] },
    { type: "debate_turn_done", mateId: "panel-1", mateName: "Panel", role: "Analyst", round: 3, text: "First ending." },
    { type: "debate_ended", reason: "natural", round: 3 },
  ];
  assert.equal(f.engine.handleHomeControl(ws, { action: "resume" }, f.session), true);
  await settle();
  assert.equal(f.session.homeDebatePhase, "live");
  assert.equal(f.session._debate.debateId, "durable-debate");
  assert.equal(f.session._debate.panelists[0].brief, "Examine trade-offs");
  assert.equal(f.mentionOptions.length, 1);

  f.session.history.push({ type: "debate_ended", reason: "user_stopped", round: 3 });
  f.session.homeDebatePhase = "ended";
  f.session.debateState.phase = "ended";
  delete f.session._debate;
  assert.equal(f.engine.handleHomeControl(ws, { action: "resume" }, f.session), true);
  await settle();
  assert.equal(f.session.homeDebatePhase, "live");
  assert.equal(f.manager.sessions.size, 1);
  assert.equal(f.mentionOptions.length, 2);
  assert.equal(f.session.history.filter(function (event) { return event.type === "debate_resumed"; }).length, 2);
});

test("a pending Home debate stop is server-confirmed and can be cancelled before the turn ends", async function (t) {
  var f = fixture(true);
  t.after(f.restore);
  var ws = { _clayUser: { id: "user-a" }, _homeChatTap: { mateSlug: "mate-builtin:clay", sessionId: 7 }, _clayActiveSession: 7 };
  f.engine.handleMcpDebateApproval(f.session, brief(), "builtin:clay", ws);
  await settle();
  assert.equal(f.session._debate.turnInProgress, true);
  assert.equal(f.engine.handleHomeControl(ws, { action: "stop" }, f.session), true);
  assert.equal(f.session._debate.phase, "ending");
  assert.equal(f.session.debateState.phase, "ending");
  assert.equal(f.events[f.events.length - 1].event.type, "debate_stop_requested");
  assert.equal(f.engine.handleHomeControl(ws, { action: "cancel_stop" }, f.session), true);
  assert.equal(f.session._debate.phase, "live");
  assert.equal(f.session.debateState.phase, "live");
  assert.equal(f.events[f.events.length - 1].event.type, "debate_stop_cancelled");
  f.mentionOptions[0].onDone("Continue with @Panel on implementation trade-offs.");
  assert.equal(f.session.homeDebatePhase, "live");
  assert.equal(f.events.some(function (entry) { return entry.event.type === "debate_ended"; }), false);
});

test("debate moderator records auditable Bash commands without exposing unrelated tool input", async function (t) {
  var f = fixture(true);
  t.after(f.restore);
  var ws = { _clayUser: { id: "user-a" }, _homeChatTap: { mateSlug: "mate-builtin:clay", sessionId: 7 }, _clayActiveSession: 7 };
  f.engine.handleMcpDebateApproval(f.session, brief(), "builtin:clay", ws);
  await settle();
  var handler = f.mentionOptions[0].canUseTool;
  var allowed = await handler("Bash", { command: "rg -n housing lib", secret: "never-project-this" }, { toolUseID: "tool-safe" });
  var blocked = await handler("Edit", { file_path: "/tmp/private", new_string: "never-project-this" }, { toolUseID: "tool-write" });
  assert.equal(allowed.behavior, "allow");
  assert.equal(blocked.behavior, "deny");
  assert.match(blocked.message, /moderator blocked/);
  var decisions = f.session.history.filter(function (event) { return event.type === "debate_tool_decision"; });
  assert.deepEqual(decisions.map(function (event) { return [event.decisionId, event.mateName, event.toolName, event.decision]; }), [
    ["tool-safe", "Clay", "Bash", "allowed"],
    ["tool-write", "Clay", "Edit", "blocked"],
  ]);
  assert.equal(decisions[0].action, "Run read-only shell commands (rg)");
  assert.equal(decisions[0].reason, "Read-only investigation");
  assert.equal(decisions[1].action, "Edit a project file");
  assert.equal(decisions[1].reason, "Would modify project files");
  assert.equal(decisions[0].command, "rg -n housing lib");
  assert.equal(decisions[1].command, "");
  assert.doesNotMatch(JSON.stringify(decisions), /never-project-this|\/tmp\/private/);
  var projected = decisions.map(function (event) { return homeEvents.transformEvent(event, "builtin:clay", f.session, "request-tools", "durable-tools"); });
  assert.deepEqual(projected.map(function (event) { return [event.eventType, event.decisionId, event.decision, event.toolName]; }), [
    ["debate_tool_decision", "tool-safe", "allowed", "Bash"],
    ["debate_tool_decision", "tool-write", "blocked", "Edit"],
  ]);
  assert.equal(projected[0].requestId, "request-tools");
  assert.equal(projected[0].sessionId, "durable-tools");
  assert.equal(projected[0].command, "rg -n housing lib");
});

test("legacy project approval still creates a dedicated live debate session", function (t) {
  var f = fixture(false);
  t.after(f.restore);
  var ws = { _clayUser: { id: "user-a" }, _clayActiveSession: 7 };
  f.engine.handleMcpDebateApproval(f.session, brief(), "builtin:clay", ws);
  assert.equal(f.created(), 1);
  assert.equal(f.manager.sessions.size, 2);
  assert.equal(f.session._debate, undefined);
});

test("Home live history restores one finalized turn without duplicating streamed text", function () {
  var history = [
    { type: "debate_started", topic: "Housing", moderatorId: "builtin:clay", moderatorName: "Clay", panelists: [{ mateId: "panel-1", name: "Panel", role: "Analyst" }] },
    { type: "debate_turn", turnId: "d:1", mateId: "builtin:clay", mateName: "Clay", role: "moderator", round: 1, avatarStyle: "imprint", avatarSeed: "clay-seed", avatarCustom: "data:image/svg+xml,clay" },
    { type: "debate_stream", turnId: "d:1", mateId: "builtin:clay", delta: "Hello " },
    { type: "debate_stream", turnId: "d:1", mateId: "builtin:clay", delta: "panel" },
    { type: "debate_turn_done", turnId: "d:1", mateId: "builtin:clay", mateName: "Clay", role: "moderator", round: 1, text: "Hello panel" },
    { type: "debate_stop_requested", topic: "Housing", round: 1 },
    { type: "debate_stop_cancelled", topic: "Housing", round: 1 },
    { type: "debate_tool_decision", decisionId: "tool-1", mateId: "panel-1", mateName: "Panel", toolName: "Bash", action: "Run read-only shell commands (rg)", decision: "allowed", reason: "Read-only investigation" },
    { type: "debate_tool_decision", decisionId: "tool-2", mateId: "panel-1", mateName: "Panel", toolName: "Bash", action: "Run read-only shell commands (rg)", decision: "allowed", reason: "Read-only investigation" },
    { type: "debate_tool_decision", decisionId: "tool-3", mateId: "panel-1", mateName: "Panel", toolName: "Edit", action: "Edit a project file", decision: "blocked", reason: "Would modify project files" },
    { type: "debate_ended", topic: "Housing", round: 1, reason: "interrupted" },
  ];
  var messages = homeEvents.historyToHomeChat(history, true);
  var turns = messages.filter(function (message) { return message.role === "debate_turn"; });
  assert.equal(turns.length, 1);
  assert.equal(turns[0].text, "Hello panel");
  assert.equal(turns[0].status, "done");
  assert.equal(turns[0].mateName, "Clay");
  assert.equal(turns[0].avatarSeed, "clay-seed");
  assert.equal(turns[0].avatarCustom, "data:image/svg+xml,clay");
  var decision = messages.find(function (message) { return message.role === "debate_tool_decision"; });
  assert.equal(decision.decisionId, "tool-1");
  assert.equal(decision.toolName, "Bash");
  assert.equal(decision.decision, "allowed");
  assert.equal(decision.entries.length, 3);
  assert.equal(messages.find(function (message) { return message.role === "debate_header"; }).stopping, false);
  assert.equal(messages.find(function (message) { return message.role === "debate_header"; }).phase, "interrupted");
});

test("Home live debate survives websocket reconnect but a restarted runtime restores as interrupted", async function (t) {
  var f = fixture(true);
  t.after(f.restore);
  var ws = { _clayUser: { id: "user-a" }, _homeChatTap: { mateSlug: "mate-builtin:clay", sessionId: 7 }, _clayActiveSession: 7 };
  f.engine.handleMcpDebateApproval(f.session, brief(), "builtin:clay", ws);
  await settle();
  var activeRuntime = f.session._debate;
  f.engine.restoreDebateState(ws);
  assert.equal(f.session._debate, activeRuntime);
  assert.equal(f.session.homeDebatePhase, "live");
  delete f.session._debate;
  f.engine.restoreDebateState(ws);
  assert.equal(f.session.homeDebatePhase, "interrupted");
  assert.equal(f.session.debateState.phase, "interrupted");
  assert.equal(f.session.history.filter(function (event) { return event.type === "debate_ended" && event.reason === "interrupted"; }).length, 1);
  f.engine.restoreDebateState(ws);
  assert.equal(f.session.history.filter(function (event) { return event.type === "debate_ended" && event.reason === "interrupted"; }).length, 1);
});

test("Home debate identity and phase persist with the exact session", function (t) {
  var root = fs.mkdtempSync(path.join(os.tmpdir(), "clay-home-debate-phase-"));
  t.after(function () { fs.rmSync(root, { recursive: true, force: true }); });
  var options = { cwd: path.join(root, "project"), sessionsBase: path.join(root, "sessions"), cliSessionsDir: path.join(root, "cli"), send: function () {} };
  var first = createSessionManager(options);
  var session = first.createSessionRaw({ cliSessionId: "11111111-2222-4333-8444-555555555555", vendor: "claude", model: "sonnet" });
  session.debateSetupMode = true;
  session.homeDebatePlanning = true;
  session.homeDebatePhase = "live";
  session.title = "Debate planning";
  first.saveSessionFile(session);
  var second = createSessionManager(options);
  var restored = Array.from(second.sessions.values())[0];
  assert.equal(restored.cliSessionId, session.cliSessionId);
  assert.equal(restored.debateSetupMode, true);
  assert.equal(restored.homeDebatePlanning, true);
  assert.equal(restored.homeDebatePhase, "live");
  assert.equal(restored.title, "Debate planning");
});

test("Home debate gives every Mate and participant the same left identity rail", function () {
  var root = path.join(__dirname, "..");
  var css = fs.readFileSync(path.join(root, "lib/public/css/home-debate-live.css"), "utf8");
  assert.match(css, /\.home-debate-live-turn\.msg-assistant,[\s\S]*grid-template-columns: 34px minmax\(0, 1fr\)/);
  assert.match(css, /\.home-debate-live-turn > \.dm-bubble-avatar,[\s\S]*\.home-debate-live-user > \.dm-bubble-avatar \{[\s\S]*width: 34px;[\s\S]*height: 34px;/);
  assert.doesNotMatch(css, /home-debate-live-user\.msg-user \{ grid-template-columns: minmax|home-debate-live-user \.dm-bubble-content \{ display: contents|home-debate-live-user \.dm-bubble-header \{ grid-column/);
  assert.match(css, /@media \(max-width: 600px\)[\s\S]*\.home-debate-live-turn\.msg-assistant,[\s\S]*\.home-debate-live-user\.msg-user \{ grid-template-columns: 32px minmax\(0, 1fr\); gap: 9px;/);
});