Ctrl K
ComunidadGuías…

test/debate-proposal.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");
var { attachDebateProposal } = require("../lib/project-debate-proposal");
var { dispatchMessageSafely } = require("../lib/project-connection");

function createHarness(options) {
  var sent = [];
  var starts = [];
  var records = [];
  var mates = options.mates || {};
  var proposal = attachDebateProposal({
    cwd: options.cwd || "/projects/example",
    isMate: !!options.isMate,
    isHostAgent: !!options.isHostAgent,
    sendTo: function (ws, msg) { sent.push(msg); },
    buildMateCtx: function (userId) { return { userId: userId }; },
    getMate: function (mateCtx, mateId) { return mates[mateId] || null; },
    getVendorModelCatalog: options.getVendorModelCatalog,
    getVendorModelCatalogForSession: options.getVendorModelCatalog ? function (session, vendor) { return options.getVendorModelCatalog(null, vendor); } : undefined,
    getProjectOwnerId: function () { return "project-owner"; },
    recordSessionEvent: function (session, event) { records.push(event); },
    startDebate: function (session, briefData, moderatorId, ws) {
      starts.push({ session: session, briefData: briefData, moderatorId: moderatorId, ws: ws });
      if (options.startError) throw options.startError;
      return { ok: true };
    },
  });
  var adapter = {
    createToolServer: function (definition) {
      return { name: definition.name, definition: definition };
    },
  };
  var session = { localId: 7, ownerId: "session-owner" };
  var server = proposal.createMcpServer(adapter, session);
  return {
    proposal: proposal,
    tool: server.definition.tools[0],
    session: session,
    sent: sent,
    starts: starts,
    records: records,
  };
}

function proposalArgs(overrides) {
  return Object.assign({
    topic: "Test debate",
    panelists: JSON.stringify([{ mateId: "mate_panel", role: "Reviewer", brief: "Review the proposal" }]),
  }, overrides || {});
}

test("normal project proposals fail safely when no moderator is provided", async function () {
  var harness = createHarness({ mates: { mate_panel: { id: "mate_panel" } } });
  var resultPromise = harness.tool.handler(proposalArgs());

  assert.equal(harness.proposal.handleMessage({}, { type: "debate_proposal_response", action: "start" }), true);
  var result = await resultPromise;

  assert.equal(harness.starts.length, 0);
  assert.equal(harness.sent[0].type, "debate_error");
  assert.match(harness.sent[0].error, /moderator/i);
  assert.equal(result.isError, true);
});

test("normal project proposals use an explicit moderator and their bound session", async function () {
  var harness = createHarness({
    mates: {
      mate_mod: { id: "mate_mod" },
      mate_panel: { id: "mate_panel" },
    },
  });
  var resultPromise = harness.tool.handler(proposalArgs({ moderatorId: "mate_mod" }));
  var ws = { _clayUser: { id: "user-1" } };

  harness.proposal.handleMessage(ws, { type: "debate_proposal_response", action: "start" });
  var result = await resultPromise;

  assert.equal(harness.starts.length, 1);
  assert.equal(harness.starts[0].session, harness.session);
  assert.equal(harness.starts[0].moderatorId, "mate_mod");
  assert.match(result.content[0].text, /approved and started/i);
});

test("proposal approval exposes defaults and starts with revalidated per-participant model overrides", async function () {
  var harness = createHarness({
    mates: {
      mate_mod: { id: "mate_mod", vendor: "claude", model: "sonnet" },
      mate_panel: { id: "mate_panel", vendor: "codex", model: "gpt-5.6" },
    },
    getVendorModelCatalog: function (ws, vendor) {
      return Promise.resolve(vendor === "claude"
        ? { models: [{ value: "sonnet", displayName: "Sonnet" }, { value: "opus", displayName: "Opus" }] }
        : { models: [{ value: "gpt-5.6", displayName: "GPT-5.6" }, { value: "gpt-5.6-mini", displayName: "GPT-5.6 mini" }] });
    },
  });
  var resultPromise = harness.tool.handler(proposalArgs({ moderatorId: "mate_mod" }));
  await new Promise(function (resolve) { setImmediate(resolve); });
  assert.equal(harness.records[0].proposal.modelSelections[0].role, "moderator");
  assert.equal(harness.records[0].proposal.modelSelections[1].role, "panelist");
  harness.proposal.handleMessage({ _clayUser: { id: "user-1" } }, {
    type: "debate_proposal_response", proposalId: harness.records[0].proposal.proposalId, action: "start",
    modelOverrides: [{ mateId: "mate_mod", model: "opus" }, { mateId: "mate_panel", model: "gpt-5.6-mini" }],
  });
  await resultPromise;
  assert.deepEqual(harness.starts[0].briefData.participantModels, [
    { mateId: "mate_mod", vendor: "claude", model: "opus" },
    { mateId: "mate_panel", vendor: "codex", model: "gpt-5.6-mini" },
  ]);
});

test("Mate project proposals use the current Mate as moderator", async function () {
  var harness = createHarness({
    cwd: "/mates/mate_self",
    isMate: true,
    mates: {
      mate_self: { id: "mate_self" },
      mate_panel: { id: "mate_panel" },
    },
  });
  var resultPromise = harness.tool.handler(proposalArgs());

  harness.proposal.handleMessage({}, { type: "debate_proposal_response", action: "start" });
  await resultPromise;

  assert.equal(harness.starts[0].moderatorId, "mate_self");
});

test("Home planning proposal tools fail closed unless bound to an owned builtin Clay session", async function () {
  var harness = createHarness({
    cwd: "/mates/builtin:clay",
    isMate: true,
    isHostAgent: true,
    mates: { "builtin:clay": { id: "builtin:clay" }, mate_panel: { id: "mate_panel" } },
  });
  harness.session.homeDebatePlanning = true;
  harness.session.debateSetupMode = true;
  harness.session.history = [
    { type: "tool_executing", id: "topic", name: "AskUserQuestion" },
    { type: "ask_user_answered", toolId: "topic", answers: { 0: "Housing" } },
  ];
  var unbound = harness.proposal.getToolDefs(null)[0];
  var noSession = await unbound.handler(proposalArgs());
  assert.equal(noSession.isError, true);
  assert.match(noSession.content[0].text, /active Clay session/i);

  var nonClay = createHarness({
    cwd: "/mates/custom-mate",
    isMate: true,
    isHostAgent: false,
    mates: { "custom-mate": { id: "custom-mate" }, mate_panel: { id: "mate_panel" } },
  });
  nonClay.session.homeDebatePlanning = true;
  nonClay.session.debateSetupMode = true;
  nonClay.session.history = harness.session.history.slice();
  var rejected = await nonClay.proposal.getToolDefs(nonClay.session)[0].handler(proposalArgs());
  assert.equal(rejected.isError, true);
  assert.match(rejected.content[0].text, /owned Clay planning session/i);

  harness.session.ownerId = "other-user";
  var wrongOwner = await harness.proposal.getToolDefs(harness.session)[0].handler(proposalArgs());
  assert.equal(wrongOwner.isError, true);
  assert.match(wrongOwner.content[0].text, /owned Clay planning session/i);
});

test("a form-seeded owned Home session may propose without a synthetic topic answer event", async function () {
  var harness = createHarness({
    cwd: "/mates/builtin:clay",
    isMate: true,
    isHostAgent: true,
    mates: { "builtin:clay": { id: "builtin:clay" }, mate_panel: { id: "mate_panel" } },
  });
  harness.session.ownerId = "project-owner";
  harness.session.homeDebatePlanning = true;
  harness.session.debateSetupMode = true;
  harness.session.homeDebateInitialTopic = "Local-first storage";
  harness.session.history = [];
  var resultPromise = harness.proposal.getToolDefs(harness.session)[0].handler(proposalArgs());
  harness.proposal.handleMessage({}, { type: "debate_proposal_response", action: "start" });
  var result = await resultPromise;
  assert.equal(result.isError, undefined);
  assert.equal(harness.starts.length, 1);
});

test("invalid panelist payloads are rejected before creating a proposal", async function () {
  var harness = createHarness({ mates: {} });
  var result = await harness.tool.handler(proposalArgs({ panelists: "{}" }));

  assert.equal(result.isError, true);
  assert.match(result.content[0].text, /JSON array/i);
});

test("debate startup exceptions become proposal errors", async function () {
  var harness = createHarness({
    mates: {
      mate_mod: { id: "mate_mod" },
      mate_panel: { id: "mate_panel" },
    },
    startError: new TypeError("The path argument must be of type string"),
  });
  var originalError = console.error;
  console.error = function () {};
  try {
    var resultPromise = harness.tool.handler(proposalArgs({ moderatorId: "mate_mod" }));
    assert.doesNotThrow(function () {
      harness.proposal.handleMessage({}, { type: "debate_proposal_response", action: "start" });
    });
    var result = await resultPromise;
    assert.equal(result.isError, true);
    assert.equal(harness.sent[0].type, "debate_error");
  } finally {
    console.error = originalError;
  }
});

test("project message exceptions are isolated from the daemon", function () {
  var sent = [];
  var originalError = console.error;
  console.error = function () {};
  try {
    var handled = dispatchMessageSafely(function () {
      throw new Error("message failed");
    }, function (ws, msg) {
      sent.push(msg);
    }, "example", {}, { type: "debate_proposal_response" });

    assert.equal(handled, false);
    assert.deepEqual(sent, [{
      type: "error",
      text: "The request could not be completed. Check the server logs for details.",
    }]);
  } finally {
    console.error = originalError;
  }
});

test("a closed WebSocket cannot escape the project message boundary", function () {
  var originalError = console.error;
  console.error = function () {};
  try {
    assert.doesNotThrow(function () {
      dispatchMessageSafely(function () {
        throw new Error("message failed");
      }, function () {
        throw new Error("socket closed");
      }, "example", {}, { type: "debate_proposal_response" });
    });
  } finally {
    console.error = originalError;
  }
});