Ctrl K
ComunidadGuías…

test/agent-behavior-modules.test.js

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

Archivo original ↗Preguntar a KiBot
// Mode-aware prompts, catalogs and services of the modules the Agent behavior
// policy governs, using the real modules with injected dependencies only.
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 attachGate = require("../lib/agent-behavior-gate").attachAgentBehavior;
var notesModule = require("../lib/project-session-notes");
var issuesMcp = require("../lib/issues-mcp-server");
var logsMcp = require("../lib/project-logs-mcp-server");
var guidance = require("../lib/issues-filing-guidance");
var userKnowledge = require("../lib/project-user-knowledge");
var pairPrompt = require("../lib/project-session-pair-prompt");

var SESSION = { localId: 1, ownerId: "u1" };

function gateFor(settings, extra) {
  return attachGate(Object.assign({ isMate: false, isMultiUser: function () { return true; }, getPolicy: function () { return settings; } }, extra || {}));
}

function notesFor(gate) {
  var sessions = new Map([[1, SESSION]]);
  return notesModule.attachSessionNotes({
    agentBehavior: gate,
    nm: { list: function () { return [{ id: "n1", text: "Check the cache", state: "open", createdAt: 1 }]; } },
    canUseSession: function (s) { return sessions.get(s.localId) === s; },
    send: function () {},
  });
}

var NOTE_MARK = "short linked Sticky Note";

test("default notes prompt and catalog are byte-identical with and without a gate", function () {
  var plain = notesFor(null);
  var gated = notesFor(gateFor({}));
  assert.equal(gated.getSystemPrompt(SESSION, gateFor({}).view(SESSION)), plain.getSystemPrompt(SESSION));
  var a = plain.getToolDefs(SESSION).map(function (t) { return t.description; });
  var b = gated.getToolDefs(SESSION).map(function (t) { return t.description; });
  assert.deepEqual(b, a);
  assert.ok(plain.getSystemPrompt(SESSION).indexOf(NOTE_MARK) !== -1, "the shipped default keeps the Issue-linked note exception");
});

test("Sticky Notes Off removes the board and its instructions; On request writes only on request", function () {
  var off = gateFor({ notes: "off" });
  assert.equal(notesFor(off).getSystemPrompt(SESSION, off.view(SESSION)), "");
  var request = gateFor({ notes: "request" });
  var prompt = notesFor(request).getSystemPrompt(SESSION, request.view(SESSION));
  assert.match(prompt, /Check the cache/, "the board is still readable On request");
  assert.match(prompt, /only when the user's message asks/);
  assert.doesNotMatch(prompt, new RegExp(NOTE_MARK), "On request never carries the automatic Issue note rule");
  assert.doesNotMatch(prompt, /Create a note proactively/);
  var descriptions = notesFor(request).getToolDefs(SESSION).map(function (t) { return t.description; }).join("\n");
  assert.doesNotMatch(descriptions, new RegExp(NOTE_MARK));
  assert.doesNotMatch(descriptions, /confirmed Issue link notes/);
});

test("the linked-note checkbox removes only the automatic Issue note rule", function () {
  var gate = gateFor({ issueLinkedNotes: false });
  var notes = notesFor(gate);
  var prompt = notes.getSystemPrompt(SESSION, gate.view(SESSION));
  assert.doesNotMatch(prompt, new RegExp(NOTE_MARK));
  assert.doesNotMatch(prompt, /Issue link notes/);
  assert.match(prompt, /Create a note proactively only when the user explicitly asks/);
  var write = notes.getToolDefs(SESSION).find(function (t) { return t.name === "write_note"; });
  assert.doesNotMatch(write.description, /confirmed Issue link notes and/);
});

test("Issues Auto with Notes On request does not tell the Driver to write notes", function () {
  var gate = gateFor({ issues: "auto", notes: "request" });
  var view = gate.view(SESSION);
  assert.equal(view.issueLinkedNotes, false);
  var options = { linked: view.issueLinkedNotes, mode: "auto" };
  var contract = issuesMcp.contract(options);
  assert.doesNotMatch(contract, /Sticky Note/);
  assert.match(contract, /Automatic Issue filing/, "Issues keeps its own automatic filing");
  var defs = issuesMcp.getToolDefs(null, options);
  defs.forEach(function (d) { assert.doesNotMatch(d.description, /Sticky Note/); });
  var attention = logsMcp.attentionContract(view);
  assert.doesNotMatch(attention, /Sticky Note/);
  assert.doesNotMatch(attention, new RegExp(guidance.NOTE_GUIDANCE.slice(0, 40)));
  var notePrompt = notesFor(gate).getSystemPrompt(SESSION, view);
  assert.doesNotMatch(notePrompt, new RegExp(NOTE_MARK));
});

test("Issues On request replaces automatic filing; default Issues text is unchanged", function () {
  assert.equal(issuesMcp.contract(undefined), issuesMcp.CONTRACT);
  assert.equal(issuesMcp.contract({ mode: "auto", linked: true }), issuesMcp.CONTRACT);
  var request = issuesMcp.contract({ mode: "request", linked: true });
  assert.match(request, /On request/);
  assert.doesNotMatch(request, /Automatic Issue filing/);
  assert.doesNotMatch(request, /proactively search or reuse/);
  assert.doesNotMatch(request, /Sticky Note/);
});

test("the Logbook contract follows Notes, Issues and learning settings", function () {
  var auto = gateFor({}).view(SESSION);
  assert.equal(logsMcp.attentionContract(auto), logsMcp.ATTENTION_CONTRACT);
  var noNotes = logsMcp.attentionContract(gateFor({ notes: "off" }).view(SESSION));
  assert.doesNotMatch(noNotes, /Sticky Note/);
  assert.match(noNotes, /Project Logs are the durable continuity record/);
  var issuesOff = logsMcp.attentionContract(gateFor({ issues: "off" }).view(SESSION));
  assert.doesNotMatch(issuesOff, /Issue/);
  assert.equal(logsMcp.attentionContract(null), logsMcp.ATTENTION_CONTRACT);
});

function userKnowledgeFor(gate) {
  var bound = {
    list: function () { return { items: [{ ref: "me:1", category: "style", summary: "Prefers short answers" }] }; },
    isClay: function () { return false; },
    read: function () { return {}; }, report: function () { return { queued: true }; },
  };
  var reads = { count: 0 };
  var module = userKnowledge.attachProjectUserKnowledge({
    agentBehavior: gate, slug: "p",
    service: { bind: function () { reads.count++; return bound; } },
    isMultiUser: function () { return true; }, sendTo: function () {}, schedule: function () {},
  });
  return { module: module, reads: reads };
}

test("Me use and Me collection are separate in the prompt and the tool descriptions", function () {
  var plain = userKnowledgeFor(null).module;
  var defaultGate = gateFor({});
  var gated = userKnowledgeFor(defaultGate).module;
  assert.equal(gated.getSystemPrompt(SESSION, defaultGate.view(SESSION)), plain.getSystemPrompt(SESSION));
  assert.deepEqual(gated.getDynamicToolDefs(SESSION).map(function (t) { return t.description; }), plain.getDynamicToolDefs(SESSION).map(function (t) { return t.description; }));

  var noUse = gateFor({ meUse: "off" });
  var fixture = userKnowledgeFor(noUse);
  var promptNoUse = fixture.module.getSystemPrompt(SESSION, noUse.view(SESSION));
  assert.doesNotMatch(promptNoUse, /Prefers short answers/, "no knowledge is injected");
  assert.doesNotMatch(promptNoUse, /consult relevant User Knowledge/);
  assert.match(promptNoUse, /report_user_observation/, "collection guidance remains");
  assert.equal(fixture.reads.count, 1);

  var noCollect = gateFor({ meCollection: "off" });
  var promptNoCollect = userKnowledgeFor(noCollect).module.getSystemPrompt(SESSION, noCollect.view(SESSION));
  assert.match(promptNoCollect, /Prefers short answers/);
  assert.doesNotMatch(promptNoCollect, /Report durable explicit preferences/);

  var both = gateFor({ meUse: "request", meCollection: "off" });
  var untouched = userKnowledgeFor(both);
  assert.equal(untouched.module.getSystemPrompt(SESSION, both.view(SESSION)), "");
  assert.equal(untouched.reads.count, 0, "an Off or On request Me never reads knowledge, so the conversation is not marked owner-only");
});

function pairFor(proposalCalls) {
  var driver = { localId: 1, ownerId: "u1", vendor: "claude", model: "claude-opus-4" };
  return pairPrompt.attachSessionPairPrompt({
    sm: { sessions: new Map([[1, driver]]), defaultModelByVendor: {}, installedVendors: ["claude"] },
    store: { groupForMember: function () { return null; } },
    isMate: false,
    rolesFor: function () { return null; },
    workerProposal: function () { return { getSystemPrompt: function (session, mode) { proposalCalls.push(mode); return "PROPOSAL_TEXT"; } }; },
  });
}

test("Worker proposals Off or On request drop the proactive delegation nudge only", function () {
  var calls = [];
  var pair = pairFor(calls);
  var driver = { localId: 1, ownerId: "u1", vendor: "claude", model: "claude-opus-4" };
  var autoView = gateFor({}).view(driver);
  var autoText = pair.getSystemPrompt(driver, autoView);
  var offText = pair.getSystemPrompt(driver, gateFor({ workerProposals: "off" }).view(driver));
  var requestText = pair.getSystemPrompt(driver, gateFor({ workerProposals: "request" }).view(driver));
  assert.match(autoText, /propose_worker before substantial execution/);
  assert.equal(offText, "");
  assert.equal(requestText, "");
  assert.equal(pair.getSystemPrompt(driver), autoText, "no behavior argument keeps today's prompt");
});

test("the Worker proposal prompt keeps Worker management text when proposals are restricted", function () {
  var source = fs.readFileSync(path.join(__dirname, "../lib/project-worker-proposal.js"), "utf8");
  assert.match(source, /function getSystemPrompt\(session, mode\)/);
  assert.match(source, /if \(!proactive\) return MANAGEMENT_ONLY/);
});

test("Markdown viewer and Sketch prompts follow their own settings", function () {
  var root = fs.mkdtempSync(path.join(os.tmpdir(), "clay-ab-docs-"));
  try {
    var documents = require("../lib/project-session-document").attachSessionDocument({ cwd: root, sendToSession: function () {}, getOsUserInfoForSession: function () { return null; } });
    var both = documents.getSystemPrompt(SESSION);
    assert.match(both, /present_markdown_edit/);
    assert.match(both, /clay-sketch/);
    var noSketch = documents.getSystemPrompt(SESSION, gateFor({ sketch: "off" }).view(SESSION));
    assert.match(noSketch, /present_markdown_edit/);
    assert.doesNotMatch(noSketch, /clay-sketch/);
    var noMarkdown = documents.getSystemPrompt(SESSION, gateFor({ markdownViewer: "request" }).view(SESSION));
    assert.doesNotMatch(noMarkdown, /present_markdown_edit/);
    assert.match(noMarkdown, /clay-sketch/);
    assert.equal(documents.getSystemPrompt(SESSION, gateFor({ sketch: "off", markdownViewer: "off" }).view(SESSION)), "");
    assert.equal(documents.getSystemPrompt(SESSION, gateFor({}).view(SESSION)), both);
  } finally { fs.rmSync(root, { recursive: true, force: true }); }
});

test("skill discovery excludes only the names a caller lists", function () {
  var skillDiscovery = require("../lib/yoke/skill-discovery");
  var home = fs.mkdtempSync(path.join(os.tmpdir(), "clay-ab-skills-"));
  try {
    var base = { homeDir: home, env: {}, scopedHome: true };
    var names = skillDiscovery.discoverSkills(home, base).map(function (s) { return s.name; });
    assert.ok(names.indexOf("clay-sketch") !== -1, "the bundled skill is present by default");
    var filtered = skillDiscovery.discoverSkills(home, Object.assign({ excludeSkills: ["clay-sketch"] }, base)).map(function (s) { return s.name; });
    assert.equal(filtered.indexOf("clay-sketch"), -1);
    assert.deepEqual(filtered, names.filter(function (n) { return n !== "clay-sketch"; }));
  } finally { fs.rmSync(home, { recursive: true, force: true }); }
  var scope = require("../lib/session-skill-scope");
  var bound = scope.bind({ localId: 1, ownerId: null }, new Map(), { env: {} }, function () { return true; }, false, ["clay-sketch"]);
  assert.deepEqual(bound.excludeSkills, ["clay-sketch"]);
  assert.equal(scope.bind({ localId: 1 }, new Map(), { env: {} }, function () { return true; }, false).excludeSkills, undefined);
});

// --- Project Logs learning records at the service call --------------------

function logsFixture(settings) {
  var logsStore = require("../lib/project-logs-store");
  var attachService = require("../lib/project-logs-service").attachProjectLogsService;
  var base = fs.mkdtempSync(path.join(os.tmpdir(), "clay-ab-logs-"));
  var session = { localId: 11, cliSessionId: "cli-11", ownerId: "owner", vendor: "claude" };
  var manager = { sessions: new Map([[11, session]]) };
  var projects = new Map([["app", { getStatus: function () { return { slug: "app", path: "/srv/app", projectOwnerId: "owner", visibility: "private" }; }, getSessionManager: function () { return manager; } }]]);
  var cache = new Map();
  var service = attachService({
    getProjects: function () { return projects; }, isMultiUser: function () { return true; },
    canAccessProject: function (userId, status) { return status.projectOwnerId === userId; },
    findUserById: function (id) { return { id: id, displayName: id }; },
    openStore: function (cwd) { if (!cache.has(cwd)) cache.set(cwd, logsStore.createProjectLogsStore({ root: cwd, baseDir: base })); return cache.get(cwd); },
  });
  var current = { value: settings };
  var gate = attachGate({ isMate: false, isMultiUser: function () { return true; }, getPolicy: function () { return current.value; } });
  var attached = require("../lib/project-logs").attachProjectLogs({
    agentBehavior: gate, service: service, sm: manager, projectSlug: "app", getProjectOwnerId: function () { return "owner"; },
    sendTo: function () {}, getClients: function () { return []; },
  });
  return { attached: attached, session: session, current: current, gate: gate, cleanup: function () { fs.rmSync(base, { recursive: true, force: true }); } };
}

function logTool(fixture, name) {
  return fixture.attached.getDynamicToolDefs(fixture.session).find(function (t) { return t.name === name; });
}

test("Learning records Off refuses learning writes at the service but allows other categories", async function () {
  var fixture = logsFixture({ logsLearning: "off" });
  try {
    var create = fixture.attached.getToolDefs(fixture.session).find(function (t) { return t.name === "create_log"; });
    var refused = await create.handler({ kind: "learning", title: "Idempotency", summary: "Clarified retries." });
    assert.equal(refused.isError, true);
    assert.match(refused.content[0].text, /Learning records are turned off/);
    var spelled = await create.handler({ kind: "  Learning ", title: "Spelled differently", summary: "x" });
    assert.equal(spelled.isError, true, "the category is normalized before comparison");
    var ok = await create.handler({ kind: "decision", title: "Cache decision", summary: "Adopted an append-only cache log." });
    assert.equal(ok.isError, undefined, JSON.stringify(ok));

    fixture.current.value = {};
    var learning = await create.handler({ kind: "learning", title: "Idempotency", summary: "Clarified retries." });
    assert.equal(learning.isError, undefined, "Auto allows learning again");
    var ref = JSON.parse(learning.content[0].text).ref;
    fixture.current.value = { logsLearning: "off" };
    var update = fixture.attached.getToolDefs(fixture.session).find(function (t) { return t.name === "update_log"; });
    var blocked = await update.handler({ ref: ref, summary: "Refined summary of the retry concept." });
    assert.equal(blocked.isError, true, "an existing learning entry cannot be revised while learning is Off");
    var otherRef = JSON.parse(ok.content[0].text).ref;
    var allowed = await update.handler({ ref: otherRef, summary: "Revised decision summary for the cache log." });
    assert.equal(allowed.isError, undefined, JSON.stringify(allowed));
  } finally { fixture.cleanup(); }
});

test("Logs prompt: Auto keeps today's text, learning is conditional, Off and On request add nothing", function () {
  var fixture = logsFixture({});
  try {
    var plain = fixture.attached.getSystemPrompt(fixture.session);
    var auto = fixture.attached.getSystemPrompt(fixture.session, fixture.gate.view(fixture.session));
    assert.equal(auto, plain);
    assert.match(plain, /learning/);
    fixture.current.value = { logsLearning: "request" };
    var noLearning = fixture.attached.getSystemPrompt(fixture.session, fixture.gate.view(fixture.session));
    assert.doesNotMatch(noLearning, /Capture durable user learning moments/);
    assert.match(noLearning, /Project Logs/);
    fixture.current.value = { logs: "off" };
    assert.equal(fixture.attached.getSystemPrompt(fixture.session, fixture.gate.view(fixture.session)), "");
    fixture.current.value = { logs: "request" };
    assert.equal(fixture.attached.getSystemPrompt(fixture.session, fixture.gate.view(fixture.session)), "");
  } finally { fixture.cleanup(); }
});

test("Log feedback is not delivered to a Driver whose Logbook is Off", function () {
  var delivery = require("../lib/project-log-feedback-delivery");
  var session = { localId: 5, ownerId: "u1", cliSessionId: "cli-5", isProcessing: false, history: [] };
  var started = [];
  var gate = gateFor({ logs: "off" });
  var service = delivery.attachProjectLogFeedbackDelivery({
    agentBehavior: gate,
    sm: { sessions: new Map([[5, session]]), sendAndRecord: function () {}, sendToSession: function () {}, broadcastSessionList: function () {} },
    getSdk: function () { return { pushMessage: function () { started.push("push"); return true; }, startQuery: function () { started.push("start"); return Promise.resolve(); } }; },
  });
  var entry = { ref: "log:1", pendingFeedbackCount: 1, updatedBy: { type: "session", sessionKey: "cli-5", userId: "u1" } };
  assert.equal(service.deliver(entry), false);
  assert.deepEqual(started, []);
});

// --- Fresh and resumed provider queries through the real SDK bridge ---------

test('fresh and resumed Codex queries rebuild the catalog and prompt, and a held tool is refused when Off', async function () {
  var liveQuery = require('./helpers/live-query-fixture.cjs');
  var createBridge = require('../lib/sdk-bridge').createSDKBridge;
  var policy = { value: {} };
  var gate = attachGate({ isMate: false, isMultiUser: function () { return true; }, getPolicy: function () { return policy.value; } });
  var executed = [];
  function defs() {
    return [
      { name: 'write_note', description: 'Write a note.', inputSchema: { type: 'object' }, handler: function (args) { executed.push(args.text); return Promise.resolve({ content: [{ type: 'text', text: 'written' }] }); } },
      { name: 'search_user_knowledge', description: 'Search.', inputSchema: { type: 'object' }, handler: function () { return Promise.resolve({ content: [{ type: 'text', text: '[]' }] }); } },
    ];
  }
  var session = { localId: 1, ownerId: 'u1', vendor: 'codex', mode: 'gui', history: [], cliSessionId: 'thread-1', pendingAskUser: {}, pendingPermissions: {}, pendingElicitations: {} };
  var sessions = new Map([[1, session]]);
  var captures = [];
  var sm = { sessions: sessions, modelsByVendor: { codex: [] }, capabilitiesByVendor: { codex: {} }, availableModels: [], saveSessionFile: function () {}, broadcastSessionList: function () {}, sendAndRecord: function () {}, sendToSession: function () {} };
  var adapter = { vendor: 'codex', createQuery: function (options) { captures.push(options); return Promise.resolve(liveQuery.create()); } };
  var bridge = createBridge({
    cwd: process.cwd(), slug: 'ab', isMate: false, sessionManager: sm, adapter: adapter, adapters: { codex: adapter }, send: function () {},
    getSessionToolDefs: function (s) { return gate.filterTools(s, defs()); },
    getSessionSystemPrompt: function (s) { return gate.notice(s); },
    canUseSessionTools: function () { return sessions.get(1) === session; },
  });
  function catalogNames(capture) { return capture.dynamicTools.map(function (t) { return t.name; }).filter(function (name) { return name !== 'ask_user_questions'; }); }
  await bridge.startQuery(session, 'first', null, null);
  assert.deepEqual(catalogNames(captures[0]), ['write_note', 'search_user_knowledge']);
  assert.doesNotMatch(captures[0].appendSystemPrompt || '', /Personal agent behavior/, 'nothing is restricted yet');
  assert.equal((await captures[0].callDynamicTool('write_note', { text: 'before' })).content[0].text, 'written');

  policy.value = { notes: 'off', meUse: 'request' };
  var refused = await captures[0].callDynamicTool('write_note', { text: 'after' });
  assert.equal(refused.isError, true, 'the running query still holds the old catalog but the call is refused');
  assert.deepEqual(executed, ['before']);

  await liveQuery.retire(session);
  await bridge.startQuery(session, 'resumed', null, null);
  assert.deepEqual(catalogNames(captures[1]), ['search_user_knowledge']);
  assert.match(captures[1].dynamicTools.find(function (t) { return t.name === 'search_user_knowledge'; }).description, /Me knowledge|Use what Clay knows about me is On request/);
  assert.match(captures[1].appendSystemPrompt, /Off, do not do these: using Sticky Notes/);
  assert.match(captures[1].appendSystemPrompt, /On request, only when the user's message asks for it: searching or reading Me knowledge/);
  await assert.rejects(captures[1].callDynamicTool('write_note', { text: 'removed' }), /not found/);

  policy.value = {};
  await liveQuery.retire(session);
  await bridge.startQuery(session, 'resumed again', null, null);
  assert.deepEqual(catalogNames(captures[2]), ['write_note', 'search_user_knowledge']);
  assert.equal((await captures[2].callDynamicTool('write_note', { text: 'restored' })).content[0].text, 'written');
  await liveQuery.retire(session);
});

test("Issues Off also removes the linked-note rule from the Notes contract", function () {
  var gate = gateFor({ issues: "off", notes: "auto", issueLinkedNotes: true });
  var view = gate.view(SESSION);
  assert.equal(view.issueLinkedNotes, false);
  var notes = notesFor(gate);
  assert.doesNotMatch(notes.getSystemPrompt(SESSION, view), new RegExp(NOTE_MARK));
  assert.doesNotMatch(logsMcp.attentionContract(view), /Issue/);
});

test("Learning records Off also refuses relabelling a comment review as learning", async function () {
  var fixture = logsFixture({ logsLearning: "off" });
  try {
    var review = fixture.attached.getToolDefs(fixture.session).find(function (t) { return t.name === "review_log_comment"; });
    var refused = await review.handler({ ref: "log:none", commentId: "c1", action: "incorporate", kind: "learning" });
    assert.equal(refused.isError, true);
    assert.match(refused.content[0].text, /Learning records are turned off/);
  } finally { fixture.cleanup(); }
});

test("Claude queries get a filtered bundled-skill plugin only when skills are excluded", function () {
  var home = fs.mkdtempSync(path.join(os.tmpdir(), "clay-ab-claude-"));
  try {
    var build = require("../lib/yoke/adapters/claude").contractTestKit.buildWorkerQueryOptions;
    var claudeOpts = { originalHome: home };
    function bundled(plugins) {
      return plugins.map(function (p) { return p.path; }).filter(function (p) { return /shared-clay-builtin/.test(p); });
    }
    function skillNames(pluginPath) { return fs.readdirSync(path.join(pluginPath, "skills")).sort(); }
    var plain = bundled(build({ env: {} }, claudeOpts, home).plugins);
    assert.equal(plain.length, 1);
    assert.ok(skillNames(plain[0]).indexOf("clay-sketch") !== -1, "unfiltered bridge keeps every bundled skill");
    var filtered = bundled(build({ env: {}, skillOptions: { excludeSkills: ["clay-sketch", "clay-agent-browser"] } }, claudeOpts, home).plugins);
    assert.equal(filtered.length, 1);
    assert.notEqual(filtered[0], plain[0], "the unfiltered bridge is not reused or mutated");
    assert.deepEqual(skillNames(filtered[0]).filter(function (n) { return /^clay-(sketch|agent-browser)$/.test(n); }), []);
    assert.ok(fs.existsSync(path.join(filtered[0], ".claude-plugin", "plugin.json")));
    assert.ok(skillNames(plain[0]).indexOf("clay-sketch") !== -1, "still present after a filtered query");
    var resumed = bundled(build({ env: {}, skillOptions: { excludeSkills: ["clay-sketch"] } }, claudeOpts, home).plugins);
    assert.deepEqual(skillNames(resumed[0]).indexOf("clay-sketch"), -1);
    assert.ok(skillNames(resumed[0]).indexOf("clay-agent-browser") !== -1, "only the listed skill is removed");
    var back = bundled(build({ env: {}, skillOptions: {} }, claudeOpts, home).plugins);
    assert.equal(back[0], plain[0], "no exclusion returns to the original bridge (Mate and default queries)");
  } finally { fs.rmSync(home, { recursive: true, force: true }); }
});