Ctrl K
ComunidadGuías…

test/mate-knowledge-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 fs = require("fs");
var os = require("os");
var path = require("path");

var attachService = require("../lib/mate-knowledge-service").attachMateKnowledgeService;
var attachProjectMateKnowledge = require("../lib/project-mate-knowledge").attachProjectMateKnowledge;
var attachKnowledge = require("../lib/project-knowledge").attachKnowledge;
var knowledgeMcp = require("../lib/mate-knowledge-mcp-server");
var knowledgeSearch = require("../lib/knowledge-search");
var knowledgeImport = require("../lib/knowledge-import");
var mateSync = require("../lib/mate-knowledge-sync");
var createKnowledgeDbService = require("../lib/knowledge-db-service").createKnowledgeDbService;

var MATE_TOOLS = ["list_knowledge", "search_knowledge", "read_knowledge", "read_knowledge_document", "create_knowledge_document", "update_knowledge_document", "list_knowledge_document_history", "restore_knowledge_document", "list_databases", "create_database", "read_database", "rename_database", "archive_database", "recover_database", "update_database_schema", "save_table_view", "query_database", "create_database_record", "read_database_record", "update_database_record", "archive_database_record", "recover_database_record", "retry_database_sync", "import_database_records", "read_database_import_progress"];
var CLAY_TOOLS = ["list_mate_knowledge", "search_mate_knowledge", "read_mate_knowledge"];
var databaseOperationSequence = 0;
function databaseOperation(prefix) { databaseOperationSequence++; return prefix + "_" + Date.now() + "_" + databaseOperationSequence; }

function tmp(label) {
  return fs.mkdtempSync(path.join(os.tmpdir(), "clay-mk-" + label + "-"));
}

function write(target, content) {
  fs.mkdirSync(path.dirname(target), { recursive: true });
  fs.writeFileSync(target, content);
}

function handle(status, sessions, databaseService) {
  var manager = { sessions: new Map() };
  for (var i = 0; i < (sessions || []).length; i++) manager.sessions.set(sessions[i].localId, sessions[i]);
  return { getStatus: function () { return status; }, getSessionManager: function () { return manager; }, getKnowledgeDbService: function () { return databaseService || null; } };
}

// Two users, each with two Mates, plus a plain Project Driver project.
function workspace(opts) {
  var options = opts || {};
  var home = tmp("ws");
  var baseDir = path.join(home, "knowledge");
  var roots = {
    alice: path.join(home, "mates", "alice"),
    bob: path.join(home, "mates", "bob"),
  };

  function seed(owner, mateId, files) {
    var mateDir = path.join(roots[owner], mateId);
    var names = Object.keys(files);
    for (var i = 0; i < names.length; i++) write(path.join(mateDir, "knowledge", names[i]), files[names[i]]);
    // Identity must never reach Knowledge.
    write(path.join(mateDir, "CLAUDE.md"), "# Identity\nYou are " + mateId + ".\n");
    write(path.join(mateDir, "knowledge", "identity-backup.md"), "# secret identity backup\n");
    mateSync.reconcileMate({ mateDir: mateDir, baseDir: baseDir, actor: { type: "migration" } });
    return mateDir;
  }

  var arch = seed("alice", "mate_arch", {
    "architecture.md": "# Architecture\nWe chose an append-only journal for durability and replay.\n",
    "session-digests.jsonl": JSON.stringify({ date: "2026-04-01", topic: "caching", summary: "decided to memoize the index" }) + "\n"
      + JSON.stringify({ date: "2026-04-02", topic: "재고 관리", summary: "재고 관리 시스템 설계를 논의했다" }) + "\n",
  });
  var scribe = seed("alice", "mate_scribe", {
    "style-guide.md": "# Style\nShort sentences. No exclamation marks.\n",
  });
  var clayDir = seed("alice", "mate_clay", {
    "coordination.md": "# Coordination\nClay routes work between Mates.\n",
  });
  var bobMate = seed("bob", "mate_bobmate", {
    "private.md": "# Bob private\nBob's confidential roadmap for the acquisition.\n",
  });

  var sessions = {
    arch: { localId: 1, cliSessionId: "cli-arch", ownerId: "alice" },
    scribe: { localId: 2, cliSessionId: "cli-scribe", ownerId: "alice" },
    clay: { localId: 3, cliSessionId: "cli-clay", ownerId: "alice" },
    bob: { localId: 4, cliSessionId: "cli-bob", ownerId: "bob" },
    driver: { localId: 5, cliSessionId: "cli-driver", ownerId: "alice" },
  };

  var projects = new Map();
  projects.set("mate-arch", handle({ slug: "mate-arch", path: arch, projectOwnerId: "alice", isMate: true, mateId: "mate_arch" }, [sessions.arch], createKnowledgeDbService({ projectDir: arch, authorizeTextReference: function () { return false; } })));
  projects.set("mate-scribe", handle({ slug: "mate-scribe", path: scribe, projectOwnerId: "alice", isMate: true, mateId: "mate_scribe" }, [sessions.scribe], createKnowledgeDbService({ projectDir: scribe, authorizeTextReference: function () { return false; } })));
  projects.set("mate-clay", handle({ slug: "mate-clay", path: clayDir, projectOwnerId: "alice", isMate: true, mateId: "mate_clay" }, [sessions.clay], createKnowledgeDbService({ projectDir: clayDir, authorizeTextReference: function () { return false; } })));
  projects.set("mate-bob", handle({ slug: "mate-bob", path: bobMate, projectOwnerId: "bob", isMate: true, mateId: "mate_bobmate" }, [sessions.bob], createKnowledgeDbService({ projectDir: bobMate, authorizeTextReference: function () { return false; } })));
  projects.set("app", handle({ slug: "app", path: "/srv/app", projectOwnerId: "alice" }, [sessions.driver]));

  var registry = {
    mate_arch: { id: "mate_arch", name: "Arch", createdBy: "alice", builtinKey: null, dir: arch },
    mate_scribe: { id: "mate_scribe", name: "Scribe", createdBy: "alice", builtinKey: null, dir: scribe },
    mate_clay: { id: "mate_clay", name: "Clay", createdBy: "alice", builtinKey: options.clayKey === undefined ? "clay" : options.clayKey, dir: clayDir },
    mate_bobmate: { id: "mate_bobmate", name: "BobMate", createdBy: "bob", builtinKey: null, dir: bobMate },
  };

  var service = attachService({
    baseDir: baseDir,
    getProjects: function () { return projects; },
    isMultiUser: function () { return options.multiUser !== false; },
    resolveMate: function (userId, mateId) {
      var mate = registry[mateId];
      if (!mate) return null;
      if (options.multiUser !== false && mate.createdBy !== userId) return null;
      return mate;
    },
    listMates: function (userId) {
      var out = [];
      var keys = Object.keys(registry);
      for (var i = 0; i < keys.length; i++) {
        if (registry[keys[i]].createdBy !== userId) continue;
        out.push(registry[keys[i]]);
      }
      return out;
    },
  });

  function attach(slug, mateId, owner) {
    return attachProjectMateKnowledge({
      service: service,
      sm: projects.get(slug).getSessionManager(),
      projectSlug: slug,
      getProjectOwnerId: function () { return owner; },
      isMate: mateId !== null,
      mateId: mateId,
    });
  }

  return {
    home: home, baseDir: baseDir, service: service, projects: projects, sessions: sessions, registry: registry,
    dirs: { arch: arch, scribe: scribe, clay: clayDir, bob: bobMate },
    arch: attach("mate-arch", "mate_arch", "alice"),
    scribe: attach("mate-scribe", "mate_scribe", "alice"),
    clay: attach("mate-clay", "mate_clay", "alice"),
    bob: attach("mate-bob", "mate_bobmate", "bob"),
    driver: attach("app", null, "alice"),
  };
}

function call(attached, session, name, args) {
  var defs = attached.getToolDefs(session);
  for (var i = 0; i < defs.length; i++) {
    if (defs[i].name === name) return defs[i].handler(args || {});
  }
  throw new Error("tool not advertised: " + name);
}

async function json(promise) {
  var result = await promise;
  if (result.isError) throw new Error(result.content[0].text);
  return JSON.parse(result.content[0].text);
}

// --- Search engine ------------------------------------------------------

test("ranking uses the shared BM25 engine and handles CJK bigrams", function () {
  var items = [
    { id: "a", name: "architecture.md", body: "append only journal durability replay" },
    { id: "b", name: "notes.md", body: "a passing mention of journal" },
    { id: "c", name: "styles.md", body: "unrelated prose about typography" },
    { id: "d", name: "korean.md", body: "재고 관리 시스템 설계를 논의했다" },
  ];
  function project(item) {
    return { id: item.id, fields: [{ text: item.name, weight: 3 }, { text: item.body, weight: 1 }], meta: item };
  }

  var ranked = knowledgeSearch.rank(items, "journal durability", project, 10);
  assert.equal(ranked[0].id, "a", "the denser match outranks the passing mention");
  assert.ok(ranked.length >= 2);
  assert.ok(ranked[0].score > ranked[1].score, "BM25 produces distinct scores, not a flat count");
  assert.equal(ranked.filter(function (r) { return r.id === "c"; }).length, 0, "non-matching documents score nothing");

  // Field weighting is by controlled repetition, so a title hit outranks a body hit.
  var byTitle = knowledgeSearch.rank(items, "architecture", project, 10);
  assert.equal(byTitle[0].id, "a");

  // CJK: no spaces between terms, matched through the shared tokenizer's bigrams.
  var cjk = knowledgeSearch.rank(items, "재고 관리", project, 10);
  assert.equal(cjk[0].id, "d");
  assert.ok(knowledgeSearch.tokenize("재고 관리").length > 2, "CJK input produces bigram tokens");
  assert.deepEqual(knowledgeSearch.rank(items, "   ", project, 10), [], "an empty query ranks nothing");

  // Snippets centre on the matched term.
  var snippet = knowledgeSearch.snippet("aaa bbb ccc durability ddd eee", "durability", 40);
  assert.ok(snippet.indexOf("durability") !== -1);
});

// --- Ordinary Mate isolation -------------------------------------------

test("an ordinary Mate sees only its own Knowledge and cannot express another scope", async function () {
  var w = workspace();
  var defs = w.arch.getToolDefs(w.sessions.arch);
  assert.deepEqual(defs.map(function (d) { return d.name; }), MATE_TOOLS);

  // No tool accepts an identity, owner, or scope argument.
  for (var i = 0; i < defs.length; i++) {
    var keys = Object.keys(defs[i].inputSchema);
    assert.equal(keys.indexOf("mateId"), -1, defs[i].name);
    assert.equal(keys.indexOf("ownerId"), -1, defs[i].name);
    assert.equal(keys.indexOf("userId"), -1, defs[i].name);
    assert.equal(keys.indexOf("scopeId"), -1, defs[i].name);
    assert.equal(keys.indexOf("projectSlug"), -1, defs[i].name);
  }

  var listed = await json(call(w.arch, w.sessions.arch, "list_knowledge", {}));
  assert.equal(listed.total, 3, "one markdown file and two digest lines");
  for (var j = 0; j < listed.entries.length; j++) {
    assert.equal(listed.entries[j].mateId, undefined, "a Mate is never told which Mate it is reading");
    assert.equal(listed.entries[j].mateName, undefined);
    assert.doesNotMatch(listed.entries[j].name || "", /identity/i);
  }

  // Forged arguments cannot widen the scope or reveal another Mate.
  var forged = await json(call(w.arch, w.sessions.arch, "list_knowledge", {
    mateId: "mate_scribe", ownerId: "bob", userId: "bob", scopeId: "mate/u-bob/mate_bobmate", projectSlug: "mate-bob",
  }));
  assert.equal(forged.total, 3, "forged arguments are ignored entirely");
  var serialized = JSON.stringify(forged);
  assert.equal(serialized.indexOf("Style"), -1);
  assert.equal(serialized.indexOf("Bob"), -1);
  assert.equal(serialized.indexOf("mate_scribe"), -1);

  var found = await json(call(w.arch, w.sessions.arch, "search_knowledge", { query: "exclamation marks" }));
  assert.equal(found.total, 0, "another Mate's content is not searchable");

  // Clay's cross-Mate tools are not advertised to an ordinary Mate.
  for (var c = 0; c < CLAY_TOOLS.length; c++) {
    assert.equal(defs.filter(function (d) { return d.name === CLAY_TOOLS[c]; }).length, 0);
  }
  assert.throws(function () { call(w.arch, w.sessions.arch, "list_mate_knowledge", {}); }, /not advertised/);
});

test("an ordinary Mate cannot read another Mate's record by its opaque ref", async function () {
  var w = workspace();
  var scribeList = await json(call(w.scribe, w.sessions.scribe, "list_knowledge", {}));
  var foreignRef = scribeList.entries[0].ref;
  assert.match(foreignRef, /^know:/);

  var denied = await call(w.arch, w.sessions.arch, "read_knowledge", { ref: foreignRef });
  assert.equal(denied.isError, true, "a ref from another scope simply does not resolve");
  assert.match(denied.content[0].text, /not found/i);

  var own = await json(call(w.arch, w.sessions.arch, "list_knowledge", {}));
  var mine = await json(call(w.arch, w.sessions.arch, "read_knowledge", { ref: own.entries[0].ref }));
  assert.ok(mine.content.length > 0);
  assert.equal(mine.preview, undefined, "read returns the record, not a preview");

  var badRef = await call(w.arch, w.sessions.arch, "read_knowledge", { ref: "know:../../etc/passwd" });
  assert.equal(badRef.isError, true);
});

test("session-bound text Knowledge tools guard revisions, history, sync, paths, and Mate ownership", async function (t) {
  var w = workspace();
  var captured = w.arch.getToolDefs(w.sessions.arch).filter(function (tool) { return tool.name === "read_knowledge_document"; })[0];
  var unknownId = await call(w.arch, w.sessions.arch, "read_knowledge_document", { documentId: "missing-document-id", name: "architecture.md" });
  assert.equal(unknownId.isError, true);
  var created = await json(call(w.arch, w.sessions.arch, "create_knowledge_document", {
    name: "areas/agent-notes.md", content: "# Agent notes\nInitial narrative.\n",
  }));
  assert.match(created.documentId, /^[a-f0-9-]{20,50}$/);
  assert.ok(created.revision);

  var read = await json(call(w.arch, w.sessions.arch, "read_knowledge_document", { documentId: created.documentId }));
  assert.equal(read.content, "# Agent notes\nInitial narrative.\n");
  assert.equal(read.name, "areas/agent-notes.md");
  assert.equal(read.revision, created.revision);

  var updated = await json(call(w.arch, w.sessions.arch, "update_knowledge_document", {
    documentId: created.documentId, content: "# Agent notes\nUpdated narrative with searchable marker.\n", expectedRevision: created.revision,
  }));
  assert.notEqual(updated.revision, created.revision);
  var stale = await call(w.arch, w.sessions.arch, "update_knowledge_document", {
    documentId: created.documentId, content: "stale", expectedRevision: created.revision,
  });
  assert.equal(stale.isError, true);
  assert.match(stale.content[0].text, /changed since it was opened|revision/i);

  var history = await json(call(w.arch, w.sessions.arch, "list_knowledge_document_history", { documentId: created.documentId }));
  assert.equal(history.versions.length, 1);
  var restored = await json(call(w.arch, w.sessions.arch, "restore_knowledge_document", {
    documentId: created.documentId, version: history.versions[0].version, expectedRevision: updated.revision,
  }));
  assert.ok(restored.revision);
  var restoredRead = await json(call(w.arch, w.sessions.arch, "read_knowledge_document", { name: "areas/agent-notes.md" }));
  assert.equal(restoredRead.content, "# Agent notes\nInitial narrative.\n");

  var found = await json(call(w.arch, w.sessions.arch, "search_knowledge", { query: "searchable marker" }));
  assert.equal(found.total, 0, "restore removes the revised text from the search projection");
  var updatedAgain = await json(call(w.arch, w.sessions.arch, "update_knowledge_document", {
    name: "areas/agent-notes.md", content: "# Agent notes\nSearchable marker survives restart.\n", expectedRevision: restoredRead.revision,
  }));
  assert.ok(updatedAgain.indexSyncPending === false);
  var searched = await json(call(w.arch, w.sessions.arch, "search_knowledge", { query: "survives restart" }));
  assert.equal(searched.total, 1);

  var restartedService = attachService({
    baseDir: w.baseDir,
    getProjects: function () { return w.projects; },
    isMultiUser: function () { return true; },
    resolveMate: function (userId, mateId) {
      var mate = w.registry[mateId];
      return mate && mate.createdBy === userId ? mate : null;
    },
    listMates: function (userId) {
      return Object.keys(w.registry).map(function (key) { return w.registry[key]; }).filter(function (mate) { return mate.createdBy === userId; });
    },
  });
  var restarted = attachProjectMateKnowledge({
    service: restartedService, sm: w.projects.get("mate-arch").getSessionManager(), projectSlug: "mate-arch",
    getProjectOwnerId: function () { return "alice"; }, isMate: true, mateId: "mate_arch",
  });
  var restartRead = await json(call(restarted, w.sessions.arch, "read_knowledge_document", { documentId: created.documentId }));
  assert.equal(restartRead.content, "# Agent notes\nSearchable marker survives restart.\n");
  var restartSearch = await json(call(restarted, w.sessions.arch, "search_knowledge", { query: "survives restart" }));
  assert.equal(restartSearch.total, 1);

  var foreign = await call(w.scribe, w.sessions.scribe, "read_knowledge_document", { documentId: created.documentId });
  assert.equal(foreign.isError, true);
  var bob = await call(w.bob, w.sessions.bob, "read_knowledge_document", { documentId: created.documentId });
  assert.equal(bob.isError, true);
  assert.deepEqual(w.driver.getToolDefs(w.sessions.driver), []);

  w.sessions.arch.ownerId = "bob";
  var mutableOwner = await captured.handler({ documentId: created.documentId });
  assert.equal(mutableOwner.isError, true, "a captured handler rechecks the live session owner");
  w.sessions.arch.ownerId = "alice";

  var outside = path.join(w.home, "outside");
  fs.mkdirSync(outside);
  fs.symlinkSync(outside, path.join(w.dirs.arch, "knowledge", "linked"));
  var linked = await call(w.arch, w.sessions.arch, "create_knowledge_document", { name: "linked/escape.md", content: "nope" });
  assert.equal(linked.isError, true);
  assert.equal(fs.existsSync(path.join(outside, "escape.md")), false);
});

test("text tools and the Knowledge workbench share ids, history, conflicts, and restart state", async function (t) {
  var w = workspace();
  var sent = [];
  var mates = {
    buildMateCtx: function () { return {}; },
    isPromoted: function () { return false; },
    getCommonKnowledgeForMate: function () { return []; },
    readCommonKnowledgeFile: function () { throw new Error("no shared file"); },
  };
  function attachUi() {
    return attachKnowledge({
      cwd: w.dirs.arch, isMate: true, knowledgeBaseDir: w.baseDir,
      sendTo: function (_ws, message) { sent.push(message); },
      matesModule: mates, getProjectOwnerId: function () { return "alice"; },
    });
  }
  var ui = attachUi();
  function uiMessage(message) {
    sent.length = 0;
    ui.handleKnowledgeMessage({ _clayUser: { id: "alice" } }, message);
    return sent.filter(function (entry) { return entry.type; })[0];
  }
  var uiCreated = uiMessage({ type: "knowledge_save", requestId: "ui-create", name: "interop.md", content: "UI version" });
  assert.ok(uiCreated.revision);
  var agentRead = await json(call(w.arch, w.sessions.arch, "read_knowledge_document", { name: "interop.md" }));
  assert.equal(agentRead.documentId, uiCreated.id);
  assert.equal(agentRead.revision, uiCreated.revision);

  var agentUpdate = await json(call(w.arch, w.sessions.arch, "update_knowledge_document", {
    documentId: agentRead.documentId, content: "Agent version", expectedRevision: agentRead.revision,
  }));
  var uiStale = uiMessage({ type: "knowledge_save", requestId: "ui-stale", name: "interop.md", content: "lost", expectedRevision: uiCreated.revision });
  assert.equal(uiStale.type, "knowledge_error");
  assert.equal(uiStale.code, "STALE");
  assert.equal(fs.readFileSync(path.join(w.dirs.arch, "knowledge", "interop.md"), "utf8"), "Agent version");

  var uiRead = uiMessage({ type: "knowledge_read", requestId: "ui-read", documentId: agentUpdate.documentId, name: "interop.md" });
  assert.equal(uiRead.content, "Agent version");
  var uiHistory = uiMessage({ type: "knowledge_history", requestId: "ui-history", documentId: agentUpdate.documentId });
  assert.ok(uiHistory.versions.length >= 1);
  ui = attachUi();
  var afterRestart = uiMessage({ type: "knowledge_read", requestId: "ui-restart", documentId: agentUpdate.documentId, name: "interop.md" });
  assert.equal(afterRestart.revision, agentUpdate.revision);
});

test("text Knowledge tools surface a committed write when projection synchronization fails", async function (t) {
  var w = workspace();
  var original = mateSync.syncMateSource;
  mateSync.syncMateSource = function () { throw new Error("fixture projection unavailable"); };
  t.after(function () { mateSync.syncMateSource = original; });
  var saved = await json(call(w.arch, w.sessions.arch, "create_knowledge_document", { name: "offline-agent.md", content: "Canonical text" }));
  assert.equal(saved.indexSyncPending, true);
  assert.match(saved.syncError, /projection unavailable/);
  assert.equal(fs.readFileSync(path.join(w.dirs.arch, "knowledge", "offline-agent.md"), "utf8"), "Canonical text");
});

test("session-bound native Knowledge tools create and operate only the owning Mate database", async function () {
  var w = workspace();
  var createOperation = databaseOperation("create-cases");
  var recordOperation = databaseOperation("create-record");
  var created = await json(call(w.arch, w.sessions.arch, "create_database", { name: "Cases", operationId: createOperation }));
  assert.match(created.database.id, /^db_/); var database = created.database; var title = database.schema[0].id;
  var discovered = await json(call(w.arch, w.sessions.arch, "list_databases", {})); assert.equal(discovered.databases[0].id, database.id);
  var record = await json(call(w.arch, w.sessions.arch, "create_database_record", { databaseId: database.id, expectedDatabaseRevision: database.revision, operationId: recordOperation, values: (function () { var values = {}; values[title] = "Alpha"; return values; })() }));
  var queried = await json(call(w.arch, w.sessions.arch, "query_database", { databaseId: database.id })); assert.equal(queried.records[0].id, record.record.id);
  var foreign = await call(w.scribe, w.sessions.scribe, "read_database", { databaseId: database.id }); assert.equal(foreign.isError, true); assert.match(foreign.content[0].text, /not found/i);
  var duplicate = await json(call(w.arch, w.sessions.arch, "create_database_record", { databaseId: database.id, expectedDatabaseRevision: database.revision, operationId: recordOperation, values: (function () { var values = {}; values[title] = "Alpha"; return values; })() })); assert.equal(duplicate.record.id, record.record.id);
  var retried = await json(call(w.arch, w.sessions.arch, "retry_database_sync", { databaseId: database.id })); assert.equal(retried.database.id, database.id);
});

test("static and stale-session descriptors fail closed", async function () {
  var w = workspace();
  var adapter = { createToolServer: function (definition) { return definition; } };

  var staticServer = w.arch.createMcpServer(adapter, null);
  assert.equal(staticServer.name, "clay-knowledge");
  assert.equal(staticServer.tools.length, MATE_TOOLS.length);
  for (var i = 0; i < staticServer.tools.length; i++) {
    assert.equal((await staticServer.tools[i].handler({})).isError, true, staticServer.tools[i].name);
  }

  // An impostor object with a matching localId is not the live session.
  var impostor = { localId: 1, cliSessionId: "cli-arch", ownerId: "alice" };
  assert.deepEqual(w.arch.getToolDefs(impostor), []);
  assert.equal((await w.arch.createMcpServer(adapter, impostor).tools[0].handler({})).isError, true);

  // A session that was live at bind time but has since been dropped.
  w.projects.get("mate-arch").getSessionManager().sessions.delete(1);
  assert.deepEqual(w.arch.getToolDefs(w.sessions.arch), []);
  assert.equal(w.arch.getSystemPrompt(w.sessions.arch), "");
});

// --- Clay cross-Mate ----------------------------------------------------

test("authoritative Clay reads across the same user's Mates and never across users", async function () {
  var w = workspace();
  var defs = w.clay.getToolDefs(w.sessions.clay);
  assert.deepEqual(defs.map(function (d) { return d.name; }), CLAY_TOOLS);
  for (var i = 0; i < defs.length; i++) {
    assert.equal(MATE_TOOLS.indexOf(defs[i].name), -1, "no tool name is advertised by both sets");
  }
  assert.equal(Object.keys(defs[0].inputSchema).indexOf("mateId") !== -1, true, "list accepts an explicit mateId");
  assert.equal(Object.keys(defs[2].inputSchema).indexOf("mateId"), -1, "read resolves an opaque ref, not a mateId");

  var all = await json(call(w.clay, w.sessions.clay, "list_mate_knowledge", { limit: 50 }));
  var mateIds = {};
  for (var j = 0; j < all.entries.length; j++) {
    assert.ok(all.entries[j].mateId, "Clay sees which Mate owns each record");
    mateIds[all.entries[j].mateId] = true;
  }
  assert.ok(mateIds.mate_arch && mateIds.mate_scribe, "Clay spans the user's Mates");
  assert.equal(mateIds.mate_bobmate, undefined, "another user's Mate is never included");
  assert.equal(JSON.stringify(all).indexOf("acquisition"), -1, "no cross-user content leaks");

  var scoped = await json(call(w.clay, w.sessions.clay, "list_mate_knowledge", { mateId: "mate_scribe" }));
  assert.equal(scoped.entries.length, 1);
  assert.equal(scoped.entries[0].mateName, "Scribe");

  var hits = await json(call(w.clay, w.sessions.clay, "search_mate_knowledge", { query: "exclamation marks" }));
  assert.equal(hits.total, 1);
  assert.equal(hits.results[0].mateId, "mate_scribe");
  assert.ok(hits.results[0].score > 0);
  assert.ok(hits.results[0].snippet.indexOf("exclamation") !== -1);

  var read = await json(call(w.clay, w.sessions.clay, "read_mate_knowledge", { ref: hits.results[0].ref }));
  assert.match(read.content, /No exclamation marks/);
  assert.equal(read.mateId, "mate_scribe");

  // Naming another user's Mate is refused, and its refs do not resolve.
  var crossUser = await call(w.clay, w.sessions.clay, "list_mate_knowledge", { mateId: "mate_bobmate" });
  assert.equal(crossUser.isError, true);
  assert.match(crossUser.content[0].text, /not found/i);

  var bobList = await json(call(w.bob, w.sessions.bob, "list_knowledge", {}));
  var bobRef = bobList.entries[0].ref;
  var stolen = await call(w.clay, w.sessions.clay, "read_mate_knowledge", { ref: bobRef });
  assert.equal(stolen.isError, true, "a ref resolves only inside the authorized user's Mate scopes");
});

test("a Mate that is not authoritative builtin Clay gets only its own scope", async function () {
  // Same Mate id and project, but the registry says it is not builtin Clay.
  var notClay = workspace({ clayKey: null });
  var defs = notClay.clay.getToolDefs(notClay.sessions.clay);
  assert.deepEqual(defs.map(function (d) { return d.name; }), MATE_TOOLS);
  var listed = await json(call(notClay.clay, notClay.sessions.clay, "list_knowledge", {}));
  assert.equal(listed.total, 1, "only its own record");
  assert.equal(listed.entries[0].mateId, undefined);

  // A non-host builtin is equally not Clay.
  var otherBuiltin = workspace({ clayKey: "researcher" });
  assert.deepEqual(otherBuiltin.clay.getToolDefs(otherBuiltin.sessions.clay).map(function (d) { return d.name; }), MATE_TOOLS);
});

// --- Project Driver -----------------------------------------------------

test("a non-Mate Project Driver session gets no Mate Knowledge at all", function () {
  var w = workspace();
  var adapter = { createToolServer: function (definition) { return definition; } };
  assert.deepEqual(w.driver.getToolDefs(w.sessions.driver), []);
  assert.deepEqual(w.driver.getToolDefs(null), []);
  assert.equal(w.driver.createMcpServer(adapter, w.sessions.driver), null);
  assert.equal(w.driver.createMcpServer(adapter, null), null, "no descriptor is advertised either");
  assert.deepEqual(w.driver.getBridgeTools(w.sessions.driver, function () { return {}; }), []);
  assert.equal(w.driver.getSystemPrompt(w.sessions.driver), "");
});

// --- Degraded content ---------------------------------------------------

test("an unreadable record is refused on read and excluded from ranking", async function () {
  var w = workspace();
  var scope = mateSync.scopeForMateDir(w.dirs.arch);
  var importer = knowledgeImport.createKnowledgeImporter({ scopeId: scope.scopeId, baseDir: w.baseDir });

  // A chunked record whose chunk is then tombstoned behind the reader's back.
  var big = new Array(30000).join("z");
  write(path.join(w.dirs.arch, "knowledge", "big.md"), big);
  mateSync.syncMateSource({ mateDir: w.dirs.arch, fileName: "big.md", baseDir: w.baseDir, actor: { type: "user" } });

  var before = await json(call(w.arch, w.sessions.arch, "list_knowledge", {}));
  assert.equal(before.total, 4);
  assert.equal(before.degraded, 0);

  importer.removeRecord(knowledgeImport.chunkKey("file:big.md", 1), { actor: { type: "system" } });

  var after = await json(call(w.arch, w.sessions.arch, "list_knowledge", {}));
  assert.equal(after.total, 3, "the damaged record is excluded, not half-listed");
  assert.equal(after.degraded, 1, "and the exclusion is reported, not silent");

  var searched = await json(call(w.arch, w.sessions.arch, "search_knowledge", { query: "zzz" }));
  assert.equal(searched.degraded, 1);
  assert.equal(searched.results.filter(function (r) { return r.name === "big.md"; }).length, 0);
});

// --- Bounds and MCP wiring ---------------------------------------------

test("list, search, and read limits are enforced server-side", async function () {
  var w = workspace();
  var lines = [];
  for (var i = 0; i < 80; i++) lines.push(JSON.stringify({ date: "2026-05-01", topic: "topic " + i, summary: "bounded corpus entry " + i }));
  write(path.join(w.dirs.arch, "knowledge", "session-digests.jsonl"), lines.join("\n") + "\n");
  mateSync.syncMateSource({ mateDir: w.dirs.arch, fileName: "session-digests.jsonl", baseDir: w.baseDir, actor: { type: "agent" } });

  var huge = await json(call(w.arch, w.sessions.arch, "list_knowledge", { limit: 9999 }));
  assert.equal(huge.entries.length, 50, "the page size is clamped, not honoured verbatim");
  assert.ok(huge.total > 50);
  assert.ok(huge.nextCursor);

  var next = await json(call(w.arch, w.sessions.arch, "list_knowledge", { limit: 50, cursor: huge.nextCursor }));
  assert.notEqual(next.entries[0].ref, huge.entries[0].ref);

  var defaulted = await json(call(w.arch, w.sessions.arch, "list_knowledge", {}));
  assert.equal(defaulted.entries.length, 20, "the default page size is bounded too");

  var searched = await json(call(w.arch, w.sessions.arch, "search_knowledge", { query: "bounded corpus", limit: 9999 }));
  assert.equal(searched.results.length, 50);
  assert.equal((await call(w.arch, w.sessions.arch, "search_knowledge", { query: "   " })).isError, true);

  // A read is clamped, not refused.
  var readAll = await json(call(w.arch, w.sessions.arch, "read_knowledge", { ref: huge.entries[0].ref, maxChars: 9999999 }));
  assert.ok(readAll.content.length <= 40000);
});

test("a large record reads back completely through continuable slices", async function () {
  var w = workspace();
  var body = "";
  for (var i = 0; i < 3000; i++) body += "paragraph " + i + " with ünïcode 漢字 🎉 content\n";
  assert.ok(body.length > 60000);
  write(path.join(w.dirs.scribe, "knowledge", "huge.md"), body);

  var listed = await json(call(w.scribe, w.sessions.scribe, "list_knowledge", {}));
  var ref = listed.entries.filter(function (e) { return e.name === "huge.md"; })[0].ref;

  var first = await json(call(w.scribe, w.sessions.scribe, "read_knowledge", { ref: ref }));
  assert.equal(first.offset, 0);
  assert.equal(first.totalChars, body.length);
  assert.equal(first.complete, false, "one response did not carry the whole record");
  assert.ok(first.nextOffset > 0);
  assert.equal(first.content.length, first.nextOffset, "the default slice is bounded");
  assert.equal(first.preview, undefined);

  // Page to the end and reassemble byte-exactly.
  var assembled = first.content;
  var offset = first.nextOffset;
  var pages = 1;
  while (offset !== null && pages < 200) {
    var next = await json(call(w.scribe, w.sessions.scribe, "read_knowledge", { ref: ref, offset: offset, maxChars: 20000 }));
    assert.equal(next.offset, offset);
    assert.equal(next.totalChars, body.length);
    assert.equal(next.complete, false, "a continuation is never reported as complete");
    assembled += next.content;
    offset = next.nextOffset;
    pages++;
  }
  assert.equal(offset, null, "paging terminates");
  assert.equal(assembled, body, "the slices reassemble exactly");
  assert.ok(pages > 2);

  // A small record is complete in one call.
  var small = listed.entries.filter(function (e) { return e.name === "style-guide.md"; })[0];
  var whole = await json(call(w.scribe, w.sessions.scribe, "read_knowledge", { ref: small.ref }));
  assert.equal(whole.complete, true);
  assert.equal(whole.nextOffset, null);
  assert.equal(whole.offset, 0);
  assert.match(whole.content, /No exclamation marks/);

  // Bounds are clamped server-side and a bad offset is refused.
  var clamped = await json(call(w.scribe, w.sessions.scribe, "read_knowledge", { ref: ref, maxChars: 9999999 }));
  assert.equal(clamped.content.length, 40000);
  var negative = await json(call(w.scribe, w.sessions.scribe, "read_knowledge", { ref: ref, offset: -5, maxChars: -1 }));
  assert.equal(negative.offset, 0);
  assert.equal(negative.content.length, 1, "maxChars is clamped to at least one character");
  var past = await call(w.scribe, w.sessions.scribe, "read_knowledge", { ref: ref, offset: body.length + 10 });
  assert.equal(past.isError, true);
  assert.match(past.content[0].text, /beyond the end/);

  // No slice ever ends inside a surrogate pair.
  for (var probe = 0; probe < 6; probe++) {
    var at = 8000 + probe;
    var sliced = await json(call(w.scribe, w.sessions.scribe, "read_knowledge", { ref: ref, offset: 0, maxChars: at }));
    var last = sliced.content.charCodeAt(sliced.content.length - 1);
    assert.equal(last >= 0xd800 && last <= 0xdbff, false, "a slice never ends on a lone high surrogate");
  }

  // Clay's cross-Mate read is continuable in the same way.
  var clayHits = await json(call(w.clay, w.sessions.clay, "search_mate_knowledge", { query: "paragraph" }));
  var clayRead = await json(call(w.clay, w.sessions.clay, "read_mate_knowledge", { ref: clayHits.results[0].ref, maxChars: 100 }));
  assert.equal(clayRead.complete, false);
  assert.equal(clayRead.nextOffset, 100);
  assert.equal(clayRead.totalChars, body.length);
  assert.equal(clayRead.mateId, "mate_scribe");
});

test("bridge advertising mirrors the adapter path without duplicates", async function () {
  var w = workspace();
  var normalize = function () { return { type: "object", properties: {} }; };

  var mateBridge = w.arch.getBridgeTools(w.sessions.arch, normalize);
  assert.equal(mateBridge.length, MATE_TOOLS.length);
  var seen = {};
  for (var i = 0; i < mateBridge.length; i++) {
    assert.equal(mateBridge[i].server, "clay-knowledge");
    assert.equal(seen[mateBridge[i].name], undefined, "no duplicate advertised tool");
    seen[mateBridge[i].name] = true;
  }
  assert.deepEqual(Object.keys(seen), MATE_TOOLS);
  assert.deepEqual(w.arch.getBridgeTools(null, normalize), []);

  var clayBridge = w.clay.getBridgeTools(w.sessions.clay, normalize);
  assert.deepEqual(clayBridge.map(function (t) { return t.name; }), CLAY_TOOLS);

  var listed = JSON.parse((await w.arch.callBridgeTool(w.sessions.arch, "list_knowledge", {})).content[0].text);
  assert.equal(listed.total, 3);
  await assert.rejects(function () { return w.arch.callBridgeTool(w.sessions.arch, "list_mate_knowledge", {}); }, /not found/);
  await assert.rejects(function () { return w.arch.callBridgeTool(null, "list_knowledge", {}); }, /valid Mate session/);

  var dynamic = w.arch.getDynamicToolDefs(w.sessions.arch);
  for (var d = 0; d < dynamic.length; d++) {
    assert.equal(dynamic[d].permissionName, "mcp__clay-knowledge__" + dynamic[d].name);
  }
});

test("system prompt guidance is scoped and carries no record dump", function () {
  var w = workspace();
  var matePrompt = w.arch.getSystemPrompt(w.sessions.arch);
  assert.match(matePrompt, /durable personal and expertise context you own/);
  assert.match(matePrompt, /project Logs are a separate, unrelated surface/);
  assert.match(matePrompt, /Do not enumerate or restate your Knowledge/);
  assert.equal(matePrompt.indexOf("append-only journal"), -1, "no record content is dumped into the prompt");
  assert.equal(matePrompt.indexOf("Scribe"), -1, "no other Mate is named");

  var clayPrompt = w.clay.getSystemPrompt(w.sessions.clay);
  assert.match(clayPrompt, /Mates belonging to the current user/);
  assert.match(clayPrompt, /cite the owning Mate/);
  assert.equal(clayPrompt.indexOf("exclamation"), -1);
  assert.notEqual(clayPrompt, matePrompt);
});

test("the two MCP tool sets are disjoint and unbound defs fail closed", async function () {
  var mate = knowledgeMcp.getToolDefs(null, false);
  var clay = knowledgeMcp.getToolDefs(null, true);
  assert.deepEqual(mate.map(function (d) { return d.name; }), MATE_TOOLS);
  assert.deepEqual(clay.map(function (d) { return d.name; }), CLAY_TOOLS);
  for (var i = 0; i < mate.length; i++) {
    assert.ok(mate[i].description.indexOf(knowledgeMcp.MATE_CONTRACT) === 0);
    assert.equal((await mate[i].handler({})).isError, true);
  }
  for (var j = 0; j < clay.length; j++) {
    assert.ok(clay[j].description.indexOf(knowledgeMcp.CLAY_CONTRACT) === 0);
    assert.equal((await clay[j].handler({})).isError, true);
  }
  assert.equal(knowledgeMcp.createMcpServer(null, null, false), null);
});

// --- Tail search beyond the old truncation limit ------------------------

test("a match far beyond 20k characters is still found, including CJK", async function () {
  var w = workspace();
  // Filler that must not match, then the only occurrence of the needles well
  // past the old 20000-character field truncation limit.
  var filler = "";
  for (var i = 0; i < 4000; i++) filler += "routine filler paragraph number " + i + " about nothing in particular\n";
  assert.ok(filler.length > 60000);
  var body = filler + "\nThe hexokinase pathway note lives here.\n재고 관리 시스템 설계를 논의했다\n";
  var needleAt = body.indexOf("hexokinase");
  assert.ok(needleAt > 60000, "the needle sits far past the old truncation point");

  write(path.join(w.dirs.arch, "knowledge", "long-notes.md"), body);

  var hits = await json(call(w.arch, w.sessions.arch, "search_knowledge", { query: "hexokinase" }));
  assert.equal(hits.total, 1, "a tail-only match is findable");
  assert.equal(hits.results[0].name, "long-notes.md");
  assert.ok(hits.results[0].score > 0);
  assert.ok(hits.results[0].snippet.indexOf("hexokinase") !== -1, "the snippet centres on the tail match");

  // CJK tail content routes through the shared tokenizer's bigrams.
  var cjk = await json(call(w.arch, w.sessions.arch, "search_knowledge", { query: "재고 관리" }));
  var names = cjk.results.map(function (r) { return r.name; });
  assert.ok(names.indexOf("long-notes.md") !== -1, "a CJK tail match is findable");

  // One item yields one result, however many segments it was indexed as.
  assert.equal(hits.results.filter(function (r) { return r.name === "long-notes.md"; }).length, 1);

  // Clay finds the same tail content across the user's Mates.
  var clayHits = await json(call(w.clay, w.sessions.clay, "search_mate_knowledge", { query: "hexokinase" }));
  assert.equal(clayHits.total, 1);
  assert.equal(clayHits.results[0].mateId, "mate_arch");
});

test("segmentation preserves anchor weighting and collapses to best segment", function () {
  var tail = "";
  for (var i = 0; i < 4000; i++) tail += "filler line " + i + "\n";
  var items = [
    { id: "titled", name: "durability.md", body: "unrelated body" },
    { id: "tail", name: "misc.md", body: tail + "\ndurability appears only at the very end\n" },
    { id: "none", name: "other.md", body: "nothing relevant" },
  ];
  function project(item) {
    return { id: item.id, fields: [{ text: item.name, weight: 3 }, { text: item.body, weight: 1 }], meta: item };
  }

  var segments = knowledgeSearch.segmentTexts(project(items[1]).fields);
  assert.ok(segments.length > 1, "a long body is segmented, not truncated");
  for (var s = 0; s < segments.length; s++) {
    assert.ok(segments[s].indexOf("misc.md") !== -1, "the name anchors every segment");
    assert.ok(segments[s].split("misc.md").length - 1 >= 3, "anchor weighting is identical in each segment");
  }

  var ranked = knowledgeSearch.rank(items, "durability", project, 10);
  var ids = ranked.map(function (r) { return r.id; });
  assert.ok(ids.indexOf("tail") !== -1, "the tail match is ranked at all");
  assert.equal(ids.indexOf("none"), -1);
  assert.equal(ids.filter(function (id) { return id === "tail"; }).length, 1, "segments collapse to one result");
  assert.equal(ids[0], "titled", "a name hit still outranks a body-tail hit");

  // Deterministic and stable across repeated runs.
  assert.deepEqual(knowledgeSearch.rank(items, "durability", project, 10).map(function (r) { return r.id; }), ids);
  assert.equal(knowledgeSearch.rank(items, "durability", project, 1).length, 1, "maxResults applies after collapsing");
});

// --- Direct filesystem divergence --------------------------------------

test("a direct filesystem edit is visible on the next tool call", async function () {
  var w = workspace();
  var before = await json(call(w.arch, w.sessions.arch, "list_knowledge", {}));
  assert.equal(before.total, 3);
  assert.equal((await json(call(w.arch, w.sessions.arch, "search_knowledge", { query: "phosphorylase" }))).total, 0);

  // Written straight to disk, exactly as a Bash tool call would.
  write(path.join(w.dirs.arch, "knowledge", "bash-written.md"), "# Added out of band\nphosphorylase kinetics summary\n");

  var after = await json(call(w.arch, w.sessions.arch, "list_knowledge", {}));
  assert.equal(after.total, 4, "the new file is visible without a daemon restart");
  var found = await json(call(w.arch, w.sessions.arch, "search_knowledge", { query: "phosphorylase" }));
  assert.equal(found.total, 1);
  var read = await json(call(w.arch, w.sessions.arch, "read_knowledge", { ref: found.results[0].ref }));
  assert.match(read.content, /phosphorylase kinetics/);

  // An edit to an existing file is picked up too.
  write(path.join(w.dirs.arch, "knowledge", "bash-written.md"), "# Added out of band\nrevised kinetics summary\n");
  assert.equal((await json(call(w.arch, w.sessions.arch, "search_knowledge", { query: "phosphorylase" }))).total, 0);
  assert.equal((await json(call(w.arch, w.sessions.arch, "search_knowledge", { query: "revised kinetics" }))).total, 1);

  // A deletion is reflected immediately, not left as a stale live record.
  fs.unlinkSync(path.join(w.dirs.arch, "knowledge", "bash-written.md"));
  var afterDelete = await json(call(w.arch, w.sessions.arch, "list_knowledge", {}));
  assert.equal(afterDelete.total, 3);
  assert.equal(afterDelete.entries.filter(function (e) { return e.name === "bash-written.md"; }).length, 0);
  var gone = await call(w.arch, w.sessions.arch, "read_knowledge", { ref: found.results[0].ref });
  assert.equal(gone.isError, true, "a deleted record no longer resolves");

  // Repeated no-op calls append nothing to the record store.
  var scope = mateSync.scopeForMateDir(w.dirs.arch);
  var importer = knowledgeImport.createKnowledgeImporter({ scopeId: scope.scopeId, baseDir: w.baseDir });
  var records = importer.stats().records;
  await json(call(w.arch, w.sessions.arch, "list_knowledge", {}));
  await json(call(w.arch, w.sessions.arch, "search_knowledge", { query: "architecture" }));
  await json(call(w.arch, w.sessions.arch, "list_knowledge", {}));
  assert.equal(importer.stats().records, records, "an idempotent refresh appends nothing");
});

test("reconciliation never reaches outside the authorized scopes", async function () {
  var w = workspace();
  var bobScope = mateSync.scopeForMateDir(w.dirs.bob);
  var bobImporter = knowledgeImport.createKnowledgeImporter({ scopeId: bobScope.scopeId, baseDir: w.baseDir });
  var bobBefore = bobImporter.stats().records;

  // Bob writes directly to disk. Alice's Mates and Clay must not touch it.
  write(path.join(w.dirs.bob, "knowledge", "bob-secret.md"), "# Secret\nacquisition target shortlist\n");

  await json(call(w.arch, w.sessions.arch, "list_knowledge", {}));
  await json(call(w.clay, w.sessions.clay, "list_mate_knowledge", { limit: 50 }));
  await json(call(w.clay, w.sessions.clay, "search_mate_knowledge", { query: "acquisition" }));

  assert.equal(bobImporter.stats().records, bobBefore, "another user's scope is never reconciled");
  var clayView = await json(call(w.clay, w.sessions.clay, "search_mate_knowledge", { query: "acquisition" }));
  assert.equal(clayView.total, 0, "and its content is never visible");

  // Bob's own session does see it, through his own authorized scope.
  var bobView = await json(call(w.bob, w.sessions.bob, "search_knowledge", { query: "shortlist" }));
  assert.equal(bobView.total, 1);
  assert.equal(bobView.results[0].name, "bob-secret.md");
  assert.ok(bobImporter.stats().records > bobBefore, "reconciled only by its own owner");
});

// --- Captured handler identity -----------------------------------------

test("a captured tool handler stops working when its exact session goes away", async function () {
  var w = workspace();
  var defs = w.arch.getToolDefs(w.sessions.arch);
  var listTool = defs[0];
  assert.equal((await json(listTool.handler({}))).total, 3, "valid while the session is live");

  // The exact session object is dropped from the manager after the handler
  // was created. The captured handler must not keep working.
  w.projects.get("mate-arch").getSessionManager().sessions.delete(1);
  var afterDrop = await listTool.handler({});
  assert.equal(afterDrop.isError, true, "identity is re-derived on every call");
  assert.match(afterDrop.content[0].text, /no longer valid/);

  // Restoring the exact same object restores the capability.
  w.projects.get("mate-arch").getSessionManager().sessions.set(1, w.sessions.arch);
  assert.equal((await json(listTool.handler({}))).total, 3);

  // A registry change that would alter the tool set also invalidates.
  var clayDefs = w.clay.getToolDefs(w.sessions.clay);
  assert.equal((await json(clayDefs[0].handler({ limit: 50 }))).entries.length > 0, true);
  w.registry.mate_clay.builtinKey = null;
  var demoted = await clayDefs[0].handler({ limit: 50 });
  assert.equal(demoted.isError, true, "a Mate that is no longer Clay loses the cross-Mate view");
});

// --- Fail-closed refresh -------------------------------------------------

// Reconciliation reports a failure for exactly one source, leaving its previous
// record in place. That record must not be served.
function breakSource(mateDir, fileName, options) {
  var settings = options || {};
  var real = mateSync.reconcileMate;
  mateSync.reconcileMate = function (opts) {
    var summary = real(opts);
    // Only the named Mate fails, so the blast radius of a failure is exactly
    // what the test is asserting about.
    if (opts && opts.mateDir === mateDir) {
      summary.failed++;
      summary.errors.push(settings.unattributable
        ? { scopeId: "x", message: "something went wrong" }
        : { scopeId: "x", file: fileName, message: "Unreadable source" });
    }
    return summary;
  };
  return function () { mateSync.reconcileMate = real; };
}

test("a source that fails to reconcile is withheld, not served from stale content", async function () {
  var w = workspace();
  var before = await json(call(w.arch, w.sessions.arch, "list_knowledge", {}));
  assert.equal(before.total, 3);
  var stale = before.entries.filter(function (e) { return e.name === "architecture.md"; })[0];
  assert.ok(stale, "the record exists while reconciliation succeeds");
  assert.equal((await json(call(w.arch, w.sessions.arch, "search_knowledge", { query: "append-only journal" }))).total, 1);

  var restore = breakSource(w.dirs.arch, "architecture.md");
  try {
    var listed = await json(call(w.arch, w.sessions.arch, "list_knowledge", {}));
    assert.equal(listed.entries.filter(function (e) { return e.name === "architecture.md"; }).length, 0,
      "the failed source is absent, not stale");
    assert.equal(listed.total, 2, "unaffected sources remain when attribution is exact");
    assert.equal(listed.degraded, 1, "and the exclusion is reported");
    assert.equal(listed.degradedScopes, 0);

    var searched = await json(call(w.arch, w.sessions.arch, "search_knowledge", { query: "append-only journal" }));
    assert.equal(searched.total, 0, "stale content is not searchable either");
    assert.equal(searched.degraded, 1);

    var read = await call(w.arch, w.sessions.arch, "read_knowledge", { ref: stale.ref });
    assert.equal(read.isError, true, "the previous content is refused, not returned");
    assert.match(read.content[0].text, /could not be brought up to date/);

    // The digest lines from an unaffected source still read normally.
    var other = listed.entries.filter(function (e) { return e.kind === "session-digest"; })[0];
    assert.ok(other);
    assert.equal((await json(call(w.arch, w.sessions.arch, "read_knowledge", { ref: other.ref }))).complete, true);
  } finally {
    restore();
  }

  // Once reconciliation succeeds again, the source comes back.
  var after = await json(call(w.arch, w.sessions.arch, "list_knowledge", {}));
  assert.equal(after.total, 3);
  assert.equal(after.degraded, 0);
  assert.equal((await json(call(w.arch, w.sessions.arch, "read_knowledge", { ref: stale.ref }))).complete, true);
});

test("an unattributable refresh failure withholds the whole scope for that call", async function () {
  var w = workspace();
  var before = await json(call(w.arch, w.sessions.arch, "list_knowledge", {}));
  var anyRef = before.entries[0].ref;

  var restore = breakSource(w.dirs.arch, null, { unattributable: true });
  try {
    var listed = await json(call(w.arch, w.sessions.arch, "list_knowledge", {}));
    assert.equal(listed.total, 0, "no record is served when the stale one cannot be identified");
    assert.equal(listed.degradedScopes, 1);
    assert.ok(listed.degraded > 0);

    var searched = await json(call(w.arch, w.sessions.arch, "search_knowledge", { query: "journal" }));
    assert.equal(searched.total, 0);
    assert.equal(searched.degradedScopes, 1);

    var read = await call(w.arch, w.sessions.arch, "read_knowledge", { ref: anyRef });
    assert.equal(read.isError, true);
    assert.match(read.content[0].text, /withheld/);

    // Clay spanning several Mates loses only the failing scope.
    var clayView = await json(call(w.clay, w.sessions.clay, "list_mate_knowledge", { limit: 50 }));
    assert.equal(clayView.entries.filter(function (e) { return e.mateId === "mate_arch"; }).length, 0);
    assert.ok(clayView.entries.filter(function (e) { return e.mateId === "mate_scribe"; }).length > 0,
      "other Mates are unaffected");
    assert.equal(clayView.degradedScopes, 1);
  } finally {
    restore();
  }

  assert.equal((await json(call(w.arch, w.sessions.arch, "list_knowledge", {}))).total, before.total);
});

// --- Honest search coverage ---------------------------------------------

test("search reports incomplete index coverage instead of implying completeness", async function () {
  var w = workspace();
  var small = await json(call(w.arch, w.sessions.arch, "search_knowledge", { query: "journal" }));
  assert.equal(small.incompleteCoverage, 0, "an ordinary corpus is fully covered");
  assert.equal(small.coveredCharsPerRecord, knowledgeSearch.MAX_INDEXED_CHARS);

  // A record whose searchable text runs past the indexed bound.
  var unit = "filler segment text abcdef\n";
  var body = "";
  while (body.length < knowledgeSearch.MAX_INDEXED_CHARS + 40000) body += unit;
  var needle = "pyrophosphatase";
  body += "\n" + needle + " appears only past the indexed bound\n";
  assert.ok(body.indexOf(needle) > knowledgeSearch.MAX_INDEXED_CHARS);
  write(path.join(w.dirs.arch, "knowledge", "enormous.md"), body);

  var hits = await json(call(w.arch, w.sessions.arch, "search_knowledge", { query: "filler segment" }));
  assert.equal(hits.incompleteCoverage, 1, "the partially indexed record is counted");
  assert.equal(hits.coveredCharsPerRecord, knowledgeSearch.MAX_INDEXED_CHARS);
  assert.ok(hits.results.length > 0);

  // The tail term cannot be ranked, and the response says coverage was partial
  // rather than implying the term is absent.
  var tail = await json(call(w.arch, w.sessions.arch, "search_knowledge", { query: needle }));
  assert.equal(tail.total, 0);
  assert.equal(tail.incompleteCoverage, 1, "an empty result is qualified, not silent");

  // Ordinary Mate results still expose no other Mate identity.
  assert.equal(JSON.stringify(hits).indexOf("mate_"), -1);
  assert.equal(JSON.stringify(tail).indexOf("mate_"), -1);
  for (var i = 0; i < hits.results.length; i++) {
    assert.equal(hits.results[i].mateId, undefined);
    assert.equal(hits.results[i].mateName, undefined);
  }

  // Clay sees the same honest accounting, with owner attribution.
  var clayHits = await json(call(w.clay, w.sessions.clay, "search_mate_knowledge", { query: "filler segment" }));
  assert.equal(clayHits.incompleteCoverage, 1);
  assert.equal(clayHits.results[0].mateId, "mate_arch");

  // The full record is still readable in full, even though it is not fully indexed.
  var listed = await json(call(w.arch, w.sessions.arch, "list_knowledge", {}));
  var ref = listed.entries.filter(function (e) { return e.name === "enormous.md"; })[0].ref;
  var read = await json(call(w.arch, w.sessions.arch, "read_knowledge", { ref: ref, maxChars: 40000 }));
  assert.equal(read.totalChars, body.length);
  assert.equal(read.complete, false);
});

test("coverage accounting is deterministic and matches what is indexed", function () {
  var short = [{ text: "title.md", weight: 3 }, { text: "small body", weight: 1 }];
  var shortCoverage = knowledgeSearch.coverage(short);
  assert.equal(shortCoverage.complete, true);
  assert.equal(shortCoverage.indexedChars, shortCoverage.totalChars);

  var body = new Array(knowledgeSearch.MAX_INDEXED_CHARS + 50000).join("a");
  var long = [{ text: "title.md", weight: 3 }, { text: body, weight: 1 }];
  var longCoverage = knowledgeSearch.coverage(long);
  assert.equal(longCoverage.complete, false);
  assert.equal(longCoverage.totalChars, "title.md".length + body.length);
  assert.equal(longCoverage.indexedChars, "title.md".length + knowledgeSearch.MAX_INDEXED_CHARS);
  assert.equal(knowledgeSearch.segmentTexts(long).length, knowledgeSearch.MAX_SEGMENTS_PER_ITEM,
    "the segment cap is respected");
  assert.deepEqual(knowledgeSearch.coverage(long), longCoverage, "repeated accounting is identical");

  // Exactly at the bound is complete.
  var exact = [{ text: new Array(knowledgeSearch.MAX_INDEXED_CHARS + 1).join("b"), weight: 1 }];
  assert.equal(knowledgeSearch.coverage(exact).complete, true);
});