Ctrl K
ComunidadGuías…

test/claude-scheduled-tools.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 Client = require("@modelcontextprotocol/sdk/client/index.js").Client;
var InMemoryTransport = require("@modelcontextprotocol/sdk/inMemory.js").InMemoryTransport;
var execFileSync = require("node:child_process").execFileSync;
var createSDKBridge = require("../lib/sdk-bridge").createSDKBridge;
var createClaudeAdapter = require("../lib/yoke/adapters/claude").createClaudeAdapter;
var scheduledTasksModule = require("../lib/project-scheduled-tasks");
var attachScheduledTasks = scheduledTasksModule.attachScheduledTasks;

function sdkFixture(captures) {
  return {
    query: function (args) {
      captures.push(args.options);
      var finish;
      var closed = false;
      return {
        close: function () { closed = true; if (finish) finish({ done: true }); },
        setPermissionMode: function () {},
        // Keep the transport live while exercising its query-bound tools.
        [Symbol.asyncIterator]: function () { return { next: function () {
          return closed ? Promise.resolve({ done: true }) : new Promise(function (resolve) { finish = resolve; });
        } }; },
      };
    },
  };
}

test("actual Claude adapter receives query-bound schedule and structured-input MCP servers on fresh and resumed queries", async function (t) {
  var captures = [];
  var sdk = sdkFixture(captures);
  var adapter = createClaudeAdapter({ cwd: process.cwd(), loadSDK: function () { return Promise.resolve(sdk); } });
  var directServer = adapter.createToolServer({
    name: "review",
    version: "1.0.0",
    tools: [{
      name: "probe",
      description: "probe",
      inputSchema: { type: "object", properties: { name: { type: "string" } }, required: ["name"] },
      handler: function () { return { content: [{ type: "text", text: "ok" }] }; },
    }],
  });
  assert.equal(directServer.type, "sdk");
  assert.equal(typeof directServer.instance._registeredTools.probe.inputSchema.shape.name.parse, "function");
  var fresh = { localId: 81, vendor: "claude", mode: "gui", history: [], pendingAskUser: {}, pendingPermissions: {}, pendingElicitations: {} };
  var resumed = { localId: 82, vendor: "claude", mode: "gui", cliSessionId: "resume-82", history: [], pendingAskUser: {}, pendingPermissions: {}, pendingElicitations: {} };
  var outcomeOnly = { localId: 83, vendor: "claude", mode: "gui", history: [], pendingAskUser: {}, pendingPermissions: {}, pendingElicitations: {} };
  var sessions = new Map([[81, fresh], [82, resumed], [83, outcomeOnly]]);
  t.after(function () { sessions.forEach(function (session) { if (session.queryInstance) session.queryInstance.close(); }); });
  var sm = {
    sessions: sessions, availableModels: [], modelsByVendor: { claude: [] }, capabilitiesByVendor: { claude: {} },
    saveSessionFile: function () {}, appendToSessionFile: function () {}, broadcastSessionList: function () {}, sendAndRecord: function () {}, sendToSession: function () {},
  };
  var service = attachScheduledTasks({
    cwd: process.cwd(), sm: sm, registry: { getAll: function () { return []; } }, isMate: false,
    sendTo: function () {}, isDriverOperatedSession: function () { return false; }, canUseSession: function () { return true; },
  });
  var outcomeCalls = 0;
  function toolDefs(session) {
    var outcome = { name: "report_scheduled_task_outcome", queryBound: true, description: "Report outcome", inputSchema: { type: "object", properties: { runId: { type: "string" }, outcome: { type: "string" } }, required: ["runId", "outcome"] }, handler: function () { outcomeCalls++; return { content: [{ type: "text", text: "recorded" }] }; } };
    return session === outcomeOnly ? [outcome] : service.getToolDefs(session).concat([outcome]);
  }
  var bridge = createSDKBridge({
    cwd: process.cwd(), sessionManager: sm, adapter: adapter, adapters: { claude: adapter },
    getSessionToolDefs: toolDefs,
    onUserInputRequest: function (session, request, respond) { respond({ task: "Check the build" }); },
    send: function () {},
  });

  var blankInterviewPrompt = scheduledTasksModule.interviewPrompt();
  assert.match(blankInterviewPrompt, /first ask what task they want scheduled/);
  assert.match(blankInterviewPrompt, /mcp__yoke-user-input__ask_user_questions/);
  assert.match(blankInterviewPrompt, /mcp__clay-scheduled-tasks__propose_scheduled_task/);
  await bridge.startQuery(fresh, blankInterviewPrompt, null, null);
  await bridge.startQuery(resumed, "Schedule this existing conversation", null, null);
  assert.equal(captures.length, 2);
  for (var i = 0; i < captures.length; i++) {
    var servers = captures[i].mcpServers;
    assert.deepEqual(Object.keys(servers).sort(), ["clay-scheduled-tasks", "yoke-user-input"]);
    assert.ok(servers["clay-scheduled-tasks"]);
    assert.ok(servers["yoke-user-input"]);
    assert.equal(captures[i].dynamicTools, undefined, "Claude receives tools through native SDK MCP servers, not Codex dynamicTools");
    var scheduleTools = servers["clay-scheduled-tasks"].instance._registeredTools;
    assert.deepEqual(Object.keys(scheduleTools).sort(), ["begin_scheduled_task_interview", "list_scheduled_tasks", "propose_scheduled_task", "read_scheduled_task", "report_scheduled_task_outcome", "update_scheduled_task"]);
    assert.equal(typeof scheduleTools.propose_scheduled_task.inputSchema.shape.name.parse, "function");
    assert.equal(scheduleTools.propose_scheduled_task.inputSchema.shape.cron.parse(null), null);
    assert.deepEqual(scheduleTools.propose_scheduled_task.inputSchema.shape.scheduleRule.parse({ anchorDate: "2099-08-09", time: "16:45" }), { anchorDate: "2099-08-09", time: "16:45" });
    var ordinaryBundled = await captures[i].canUseTool("AskUserQuestion", { questions: [{ question: "First?" }, { question: "Second?" }] }, { toolUseID: "ordinary-bundled-" + i });
    assert.equal(ordinaryBundled.behavior, "deny");
    assert.match(ordinaryBundled.message, /structured input fallback/);
    assert.doesNotMatch(ordinaryBundled.message, /exactly one question/);
    var began = await scheduleTools.begin_scheduled_task_interview.handler({});
    assert.equal(JSON.parse(began.content[0].text).status, "interview_started");
    await scheduleTools.report_scheduled_task_outcome.handler({ runId: "run-" + i, outcome: "completed" });
    var nativeBundled = await captures[i].canUseTool("AskUserQuestion", { questions: [{ question: "What task?" }, { question: "When?" }] }, { toolUseID: "native-bundled-" + i });
    assert.equal(nativeBundled.behavior, "deny");
    assert.match(nativeBundled.message, /exactly one question at a time/);
    var nativeSingle = await captures[i].canUseTool("AskUserQuestion", { questions: [{ question: "What task?" }] }, { toolUseID: "native-single-" + i });
    assert.equal(nativeSingle.behavior, "deny");
    assert.match(nativeSingle.message, /structured input fallback/);
    var proposed = await scheduleTools.propose_scheduled_task.handler({ name: "Claude task", instructions: "Run the focused checks.", cron: "0 9 * * 1-5" });
    assert.equal(JSON.parse(proposed.content[0].text).status, "proposed");
    var once = await scheduleTools.propose_scheduled_task.handler({ name: "Claude once", instructions: "Run once.", cron: null, date: "2099-08-09", time: "16:45" });
    assert.equal(JSON.parse(once.content[0].text).draft.cron, null);
    var fallback = servers["yoke-user-input"].instance._registeredTools.ask_user_questions;
    var bundled = await fallback.handler({ questions: [{ question: "What task?" }, { question: "When?" }] });
    assert.equal(bundled.isError, true);
    assert.match(bundled.content[0].text, /exactly one question at a time/);
    var answer = await fallback.handler({ questions: [{ id: "task", question: "What task should run?", options: [] }] });
    assert.equal(JSON.parse(answer.content[0].text).task[0], "Check the build");
  }
  assert.equal(outcomeCalls, 2);
  var transports = InMemoryTransport.createLinkedPair();
  var client = new Client({ name: "scheduled-task-regression", version: "1.0.0" });
  await Promise.all([
    client.connect(transports[0]),
    captures[0].mcpServers["clay-scheduled-tasks"].instance.connect(transports[1]),
  ]);
  var catalog = await client.listTools();
  var proposalTool = catalog.tools.find(function (tool) { return tool.name === "propose_scheduled_task"; });
  assert.deepEqual(proposalTool.inputSchema.required.sort(), ["cron", "instructions", "name"]);
  assert.equal(proposalTool.inputSchema.properties.name.type, "string");
  assert.ok(proposalTool.inputSchema.properties.cron.anyOf || Array.isArray(proposalTool.inputSchema.properties.cron.type));
  assert.ok(proposalTool.inputSchema.properties.scheduleRule);
  var catalogCall = await client.callTool({
    name: "propose_scheduled_task",
    arguments: { name: "Catalog task", instructions: "Validate the native MCP call.", cron: "15 10 * * 1-5" },
  });
  assert.equal(JSON.parse(catalogCall.content[0].text).status, "proposed");
  var invalidCall = await client.callTool({
    name: "propose_scheduled_task",
    arguments: { instructions: "Missing required name.", cron: "15 10 * * 1-5" },
  });
  assert.equal(invalidCall.isError, true);
  assert.match(invalidCall.content[0].text, /invalid|validation|required/i);
  await client.close();
  assert.equal(captures[1].resume, "resume-82");
  await bridge.startQuery(outcomeOnly, "Finish the scheduled run", null, null);
  var outcomeServer = captures[2].mcpServers["clay-scheduled-tasks"].instance._registeredTools;
  assert.deepEqual(Object.keys(outcomeServer), ["report_scheduled_task_outcome"]);
  await outcomeServer.report_scheduled_task_outcome.handler({ runId: "run-only", outcome: "completed" });
  assert.equal(outcomeCalls, 3);
});

test("Claude MCP catalog accepts all scheduled schemas with repository SDK and Zod v3", async function () {
  var adapter = createClaudeAdapter({ cwd: process.cwd() });
  var sessions = new Map();
  var service = attachScheduledTasks({
    cwd: process.cwd(), sm: { sessions: sessions }, registry: { getAll: function () { return []; } },
    isMate: false, sendTo: function () {}, isDriverOperatedSession: function () { return false; },
    canUseSession: function () { return true; },
  });
  var session = { localId: 91, vendor: "claude", mode: "gui", history: [], _sdkQueryGeneration: 1 };
  sessions.set(session.localId, session);
  var server = adapter.createToolServer({ name: "clay-scheduled-tasks", version: "1.0.0", tools: service.getToolDefs(session) });
  var transports = InMemoryTransport.createLinkedPair();
  var client = new Client({ name: "repository-sdk-regression", version: "1.0.0" });
  try {
    await Promise.all([client.connect(transports[0]), server.instance.connect(transports[1])]);
    var catalog = await client.listTools();
    assert.deepEqual(catalog.tools.map(function (tool) { return tool.name; }).sort(), [
      "begin_scheduled_task_interview", "list_scheduled_tasks", "propose_scheduled_task",
      "read_scheduled_task", "update_scheduled_task",
    ]);
  } finally {
    await client.close();
    await server.instance.close();
  }
});

var daemonNodeModules = process.env.CLAUDE_COMPAT_NODE_MODULES_ROOT || "";
test("Claude MCP catalog accepts scheduled schemas with optional exact daemon dependencies", { skip: !daemonNodeModules }, function () {
  var root = daemonNodeModules;
  var script = [
    "var Module = require('module'); var path = require('path'); var root = process.env.CLAUDE_COMPAT_NODE_MODULES_ROOT; var sdkPath = path.join(root, '@anthropic-ai/claude-agent-sdk'); var zodPath = path.join(root, 'zod'); var zodV3Path = path.join(zodPath, 'v3'); var exactSdk = require(sdkPath); var exactZodV3 = require(zodV3Path); var sdkPackage = require(path.join(sdkPath, 'package.json')); var zodPackage = require(path.join(zodPath, 'package.json')); if (!sdkPackage.version || !zodPackage.version || require.resolve(sdkPath).indexOf(root) !== 0 || require.resolve(zodV3Path).indexOf(root) !== 0 || !exactSdk.createSdkMcpServer || !exactZodV3.z) throw new Error('Exact dependency path/version assertion failed: SDK ' + sdkPackage.version + ', Zod ' + zodPackage.version); var load = Module._load; Module._load = function (request, parent, isMain) { if (request === 'zod/v3') return exactZodV3; if (request === 'zod') return require(zodPath); if (request === '@anthropic-ai/claude-agent-sdk') return exactSdk; return load.call(this, request, parent, isMain); };",
    "var Client = require('@modelcontextprotocol/sdk/client/index.js').Client;",
    "var T = require('@modelcontextprotocol/sdk/inMemory.js').InMemoryTransport;",
    "var adapter = require('./lib/yoke/adapters/claude').createClaudeAdapter({ cwd: process.cwd() });",
    "var attach = require('./lib/project-scheduled-tasks').attachScheduledTasks;",
    "var sessions = new Map();",
    "var service = attach({ cwd: process.cwd(), sm: { sessions: sessions }, registry: { getAll: function () { return []; } }, isMate: false, sendTo: function () {}, isDriverOperatedSession: function () { return false; }, canUseSession: function () { return true; } });",
    "var session = { localId: 92, vendor: 'claude', mode: 'gui', history: [], _sdkQueryGeneration: 1 }; sessions.set(92, session);",
    "var server = adapter.createToolServer({ name: 'clay-scheduled-tasks', version: '1.0.0', tools: service.getToolDefs(session) });",
    "var pair = T.createLinkedPair(); var client = new Client({ name: 'exact-daemon-regression', version: '1.0.0' });",
    "Promise.all([client.connect(pair[0]), server.instance.connect(pair[1])]).then(function () { return client.listTools(); }).then(function (catalog) { var names = catalog.tools.map(function (tool) { return tool.name; }).sort().join(','); if (names !== 'begin_scheduled_task_interview,list_scheduled_tasks,propose_scheduled_task,read_scheduled_task,update_scheduled_task') throw new Error('Unexpected scheduled tool catalog: ' + names); }).then(function () { return client.close(); }).then(function () { return server.instance.close(); }).catch(function (error) { console.error(error.stack || error); process.exitCode = 1; });",
  ].join("\n");
  execFileSync(process.execPath, ["-e", script], { cwd: process.cwd(), env: Object.assign({}, process.env, { CLAUDE_COMPAT_NODE_MODULES_ROOT: root }) });
});