Ctrl K
ComunidadGuías…

test/home-debate-ask-user-canonical.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 attachSessions = require("../lib/project-sessions").attachSessions;
var attachAskUser = require("../lib/project-ask-user").attachAskUser;
var attachDebateProposal = require("../lib/project-debate-proposal").attachDebateProposal;
var historyToHomeChat = require("../lib/server-home-chat-events").historyToHomeChat;
var transformEvent = require("../lib/server-home-chat-events").transformEvent;
var createSDKBridge = require("../lib/sdk-bridge").createSDKBridge;
var yokeUserInput = require("../lib/yoke/user-input");

function fixture(users) {
  var recorded = [];
  var manager = {
    currentPermissionMode: "default",
    permissionRequestIndex: {},
    sendAndRecord: function (session, event) { session.history.push(event); recorded.push(event); },
    appendToSessionFile: function () {},
  };
  var sessions = attachSessions({
    cwd: "/tmp/project", slug: "mate-builtin:clay", isMate: true, osUsers: null,
    currentVersion: "test", sm: manager, sdk: { startQuery: function () {}, pushMessage: function () {} }, tm: {}, clients: new Set(),
    send: function () {}, sendTo: function () {}, sendToAdmins: function () {}, sendToSession: function () {}, sendToSessionOthers: function () {},
    opts: {}, usersModule: users || {}, userPresence: {}, matesModule: {}, getSessionForWs: function () { return null; },
    getLinuxUserForSession: function () {}, ensureProjectAccessForSession: function () {}, getOsUserInfoForWs: function () {},
    hydrateImageRefs: function () {}, onProcessingChanged: function () {}, broadcastPresence: function () {}, adapter: {},
  });
  return { sessions: sessions, recorded: recorded };
}

test("canonical AskUserQuestion response accepts the exact pending tool once", function () {
  var f = fixture();
  var result = null;
  var session = { localId: 1, history: [], pendingAskUser: { "ask-1": { input: { questions: [{ question: "Outcome?" }] }, resolve: function (value) { result = value; } } } };
  assert.equal(f.sessions.respondToAskUser(session, { toolId: "ask-1", answers: { 0: "Decision" } }), true);
  assert.equal(f.sessions.respondToAskUser(session, { toolId: "ask-1", answers: { 0: "Again" } }), false);
  assert.deepEqual(f.recorded, [{ type: "ask_user_answered", toolId: "ask-1", answers: { 0: "Decision" } }]);
  assert.equal(result.behavior, "deny");
  assert.match(result.message, /Decision/);
});

test("project persistence resumes one canonical Yoke request and rejects stale answers", async function () {
  var f = fixture();
  var askUser = attachAskUser({ record: function (session, event) { session.history.push(event); } });
  var session = { localId: 11, history: [], pendingAskUser: {} };
  var pending = yokeUserInput.dispatchUserInput(askUser.createHandler(session), {
    questions: [{ id: "topic", header: "Topic", question: "What topic?", options: [] }],
  }, { requestId: "yoke-ask" });
  assert.equal(f.sessions.respondToAskUser(session, { toolId: "yoke-ask", answers: { topic: "Housing" } }), true);
  assert.equal(f.sessions.respondToAskUser(session, { toolId: "yoke-ask", answers: { topic: "Again" } }), false);
  assert.deepEqual(await pending, { status: "submitted", answers: { topic: ["Housing"] } });
  assert.equal(session.pendingAskUser["yoke-ask"], undefined);
  assert.equal(session.history[0].name, "AskUserQuestion");
  assert.equal(f.recorded[0].type, "ask_user_answered");
});

test("MCP AskUserQuestion answer continues canonically without a duplicate Home user bubble", function () {
  var f = fixture();
  var input = { questions: [{ question: "Outcome?", options: [{ label: "Decision" }, { label: "Trade-offs" }] }] };
  var session = { localId: 2, history: [{ type: "tool_executing", id: "ask-mcp", name: "AskUserQuestion", input: input }], pendingAskUser: { "ask-mcp": { mode: "mcp", input: input } }, isProcessing: true };
  assert.equal(f.sessions.respondToAskUser(session, { toolId: "ask-mcp", answers: { 0: "Decision" } }), true);
  var injected = session.history.filter(function (event) { return event.type === "user_message"; });
  assert.equal(injected.length, 1);
  assert.equal(injected[0].askUserAnswer, true);
  var home = historyToHomeChat(session.history, true);
  assert.equal(home.length, 1);
  assert.equal(home[0].role, "question");
  assert.equal(home[0].status, "answered");
});

test("Mate AskUserQuestion MCP records against its query-bound session, never a global active session", async function () {
  var records = [];
  var askUser = attachAskUser({ record: function (session, event) { records.push({ session: session, event: event }); } });
  var adapter = { createToolServer: function (definition) { return definition; } };
  var exact = { localId: 7, history: [], pendingAskUser: {} };
  var unrelated = { localId: 8, history: [], pendingAskUser: {} };
  var server = askUser.createMcpServer(adapter, exact);
  var resultPromise = server.tools[0].handler({ questions: [{ header: "Outcome", question: "What result?", options: [{ label: "Decision", description: "Choose" }, { label: "Trade-offs", description: "Compare" }] }] });
  await Promise.resolve();
  assert.equal(records.length, 1);
  assert.equal(records[0].session, exact);
  assert.equal(records[0].event.name, "AskUserQuestion");
  assert.equal(Object.keys(exact.pendingAskUser).length, 1);
  assert.equal(Object.keys(unrelated.pendingAskUser).length, 0);
  exact.pendingAskUser[Object.keys(exact.pendingAskUser)[0]].respond({ 0: "Decision" });
  var result = await resultPromise;
  assert.equal(result.isError, undefined);
  var unbound = askUser.createMcpServer(adapter, null);
  var rejected = await unbound.tools[0].handler({ questions: [{ question: "No session", options: [{ label: "A" }, { label: "B" }] }] });
  assert.equal(rejected.isError, true);
});

test("planning SDK query binds AskUser and proposal tools to the same exact Home session", async function () {
  var captured = [];
  var projected = [];
  var debateStarts = [];
  var manager = {
    defaultVendor: "codex",
    currentPermissionMode: "default",
    capabilitiesByVendor: { codex: {} },
    modelsByVendor: { codex: [] },
    permissionRequestIndex: {},
    saveSessionFile: function () {},
    broadcastSessionList: function () {},
    sendToSession: function () {},
    sendAndRecord: function (session, event) {
      session.history.push(event);
      var message = transformEvent(event, "builtin:clay", session, "plan-sdk", "local:31");
      if (message) projected.push(message);
    },
  };
  var askUser = attachAskUser({ record: manager.sendAndRecord });
  var proposal = attachDebateProposal({
    cwd: "/mates/builtin:clay",
    isMate: true,
    isHostAgent: true,
    sendTo: function () {},
    buildMateCtx: function (userId) { return { userId: userId }; },
    getMate: function (ctx, mateId) { return mateId === "builtin:clay" || mateId === "panel-1" ? { id: mateId } : null; },
    getProjectOwnerId: function () { return "owner-a"; },
    recordSessionEvent: manager.sendAndRecord,
    startDebate: function (session, brief, moderatorId) {
      debateStarts.push({ session: session, brief: brief, moderatorId: moderatorId });
      session.homeDebatePhase = "live";
      return { ok: true };
    },
  });
  var handle = {
    pushMessage: function () { return true; },
    close: function () {},
    [Symbol.asyncIterator]: function () { return { next: function () { return new Promise(function () {}); } }; },
  };
  var adapter = {
    vendor: "codex",
    userInputCapability: { mode: "native", native: true },
    createQuery: function (options) { captured.push(options); return Promise.resolve(handle); },
  };
  var bridge = createSDKBridge({
    cwd: process.cwd(),
    sessionManager: manager,
    adapter: adapter,
    adapters: { codex: adapter },
    send: function () {},
    getSessionToolDefs: function (session) { return proposal.getToolDefs(session); },
    onUserInputRequest: function (boundSession, request, respond) { return askUser.createHandler(boundSession)(request, respond); },
  });
  var session = { localId: 31, ownerId: "owner-a", vendor: "codex", model: "gpt-test", debateSetupMode: true, homeDebatePlanning: true, history: [], pendingAskUser: {}, isProcessing: false };
  await bridge.startQuery(session, "hidden debate setup");
  assert.equal(captured.length, 1);
  assert.equal(captured[0].userInputMode, "fallback");
  assert.deepEqual(captured[0].dynamicTools.map(function (tool) { return tool.name; }), ["propose_debate", "ask_user_questions"]);
  var input = { questions: [{ header: "Topic", question: "What would you like to debate?", options: [{ label: "Product direction", description: "Choose a roadmap" }, { label: "Architecture", description: "Discuss systems" }, { label: "Growth", description: "Explore acquisition" }] }] };
  var firstCall = captured[0].callDynamicTool("ask_user_questions", input);
  await Promise.resolve();
  assert.equal(projected.length, 1);
  assert.equal(projected[0].type, "home_debate_question");
  assert.equal(projected[0].requestId, "plan-sdk");
  assert.equal(projected[0].sessionId, "local:31");
  assert.equal(projected[0].questions[0].question, "What would you like to debate?");
  assert.deepEqual(projected[0].questions[0].options, []);
  assert.equal(session.history[0].input.questions[0].options.length, 0);
  assert.doesNotMatch(JSON.stringify(session.history[0].input), /Product direction|Architecture|Growth/);
  var firstQuestionId = session.history[0].id;
  session.pendingAskUser[firstQuestionId].respond({ 0: "Housing affordability" });
  await firstCall;
  manager.sendAndRecord(session, { type: "ask_user_answered", toolId: firstQuestionId, answers: { 0: "Housing affordability" } });
  var secondCall = captured[0].callDynamicTool("ask_user_questions", {
    questions: [{ header: "Format", question: "How should the panel approach it?", options: [{ label: "Trade-offs", description: "Compare policies" }, { label: "Recommendation", description: "Choose a path" }] }],
  });
  await Promise.resolve();
  var secondQuestion = session.history.filter(function (event) { return event.type === "tool_executing"; })[1];
  session.pendingAskUser[secondQuestion.id].respond({ 0: "Trade-offs" });
  await secondCall;
  manager.sendAndRecord(session, { type: "ask_user_answered", toolId: secondQuestion.id, answers: { 0: "Trade-offs" } });

  session.cliSessionId = "01a05622-durable";
  var proposalPermission = await captured[0].canUseTool("propose_debate", {}, { toolUseID: "proposal-call" });
  assert.equal(proposalPermission.behavior, "allow");
  var proposalResult = captured[0].callDynamicTool("propose_debate", {
    topic: "Housing affordability",
    panelists: JSON.stringify([{ mateId: "panel-1", role: "Skeptic", brief: "Challenge assumptions" }]),
  });
  var proposalEvent = session.history.filter(function (event) { return event.type === "debate_proposal"; }).pop();
  assert.ok(proposalEvent);
  assert.equal(projected.filter(function (message) { return message.type === "home_debate_proposal"; }).length, 1);
  var ws = { _clayUser: { id: "owner-a" } };
  assert.equal(proposal.handleHomeMessage(ws, {
    type: "debate_proposal_response",
    proposalId: proposalEvent.proposal.proposalId,
    action: "start",
  }, session), true);
  var result = await proposalResult;
  assert.equal(result.isError, undefined);
  assert.equal(debateStarts.length, 1);
  assert.equal(debateStarts[0].session, session);
  assert.equal(debateStarts[0].moderatorId, "builtin:clay");
  assert.equal(session.homeDebatePhase, "live");
});

test("native structured input callback remains available for an ordinary Claude query", async function () {
  var captured = [];
  var manager = {
    defaultVendor: "claude", currentPermissionMode: "default", permissionRequestIndex: {},
    capabilitiesByVendor: { claude: {} }, modelsByVendor: { claude: [] },
    saveSessionFile: function () {}, broadcastSessionList: function () {}, sendToSession: function () {}, sendAndRecord: function () {},
  };
  var handle = { pushMessage: function () { return true; }, close: function () {}, [Symbol.asyncIterator]: function () { return { next: function () { return new Promise(function () {}); } }; } };
  var askUser = attachAskUser({ record: function (boundSession, event) { boundSession.history.push(event); } });
  var adapter = { vendor: "claude", userInputCapability: { mode: "native", native: true }, createQuery: function (options) { captured.push(options); return Promise.resolve(handle); } };
  var defaultAdapter = { vendor: "legacy", createQuery: function () { throw new Error("wrong adapter"); } };
  var bridge = createSDKBridge({
    cwd: process.cwd(), sessionManager: manager, adapter: defaultAdapter, adapters: { claude: adapter }, send: function () {},
    onUserInputRequest: function (boundSession, request, respond) { return askUser.createHandler(boundSession)(request, respond); },
  });
  var session = { localId: 32, vendor: "claude", model: "sonnet", history: [], pendingAskUser: {}, isProcessing: false };
  await bridge.startQuery(session, "ordinary question");
  assert.equal(captured[0].userInputMode, "native");
  var answer = null;
  var respond = function (value) { answer = value; };
  respond.cancel = function () {};
  captured[0].onUserInputRequest({ id: "native-ask", questions: [{ id: "topic", header: "Topic", question: "What topic?", options: [] }] }, respond);
  assert.ok(session.pendingAskUser["native-ask"]);
  session.pendingAskUser["native-ask"].respond({ topic: "Housing" });
  assert.deepEqual(answer, { topic: "Housing" });

  var typedContent = null;
  var elicitationRespond = function () {};
  elicitationRespond.cancel = function () {};
  elicitationRespond.onSettle = function () {};
  elicitationRespond.submitContent = function (content) { typedContent = content; };
  var elicitation = captured[0].onUserInputRequest({
    id: "elicit-form", presentation: "elicitation", questions: [{ id: "count", question: "Count?", options: [] }],
    diagnostics: { elicitation: { serverName: "Example", message: "Configure", mode: "form", requestedSchema: { type: "object", properties: { count: { type: "number" } } } } },
  }, elicitationRespond);
  var elicitationId = Object.keys(session.pendingElicitations)[0];
  session.pendingElicitations[elicitationId].resolve({ action: "accept", content: { count: 2 } });
  await elicitation;
  assert.deepEqual(typedContent, { count: 2 });
});


test("primary work confirmation accepts only the session owner", async function () {
  var f = fixture({ isMultiUser: function () { return true; } });
  var askUser = attachAskUser({ record: function (session, event) { session.history.push(event); } });
  var session = { localId: 11, ownerId: 'owner', history: [], pendingAskUser: {} };
  var task = yokeUserInput.dispatchUserInput(askUser.createHandler(session), {
    questions: [{ id: 'primary', question: 'Set primary?', options: [{ label: 'Set primary' }, { label: 'Keep current' }] }],
  }, { source: 'work-primary', requestId: 'primary-ask' });
  var msg = { toolId: 'primary-ask', answers: { primary: 'Set primary' } };
  assert.equal(f.sessions.respondToAskUser(session, msg), false);
  assert.equal(f.sessions.respondToAskUser(session, msg, { _clayUser: { id: 'viewer' } }), false);
  assert.ok(session.pendingAskUser['primary-ask']);
  assert.equal(f.sessions.respondToAskUser(session, msg, { _clayUser: { id: 'owner' } }), true);
  assert.equal((await task).status, 'submitted');
});


test("fallback questions and resolutions retain their exact Home planning surface", function () {
  [
    { mode: {}, prefix: "home_mate_question" },
    { mode: { debateSetupMode: true }, prefix: "home_debate_question" },
    { mode: { mateCreationMode: true }, prefix: "home_mate_creation_question" },
  ].forEach(function (item) {
    var event = { type: "tool_executing", name: "AskUserQuestion", id: "surface-question", source: "mcp_fallback", input: { questions: [{ question: "Choose the next step", options: [] }] } };
    var session = Object.assign({ localId: 41, history: [event] }, item.mode);
    var projected = transformEvent(event, "builtin:clay", session, "fixture-request");
    assert.equal(projected.type, item.prefix);
    assert.equal(projected.toolId, event.id);
    var resolved = transformEvent({ type: "ask_user_answered", toolId: event.id, answers: { 0: "Continue" } }, "builtin:clay", session, "fixture-request");
    assert.equal(resolved.type, item.prefix + "_resolved");
    assert.equal(resolved.toolId, event.id);
    assert.deepEqual(resolved.answers, { 0: "Continue" });
  });
});