d0f99052cf
Финальный линк Фазы 1б. Инвентарь тулов жил в 4 рукописных прозаических копиях (SERVER_INSTRUCTIONS под regex-тестом; <tool_catalog>; имена в ai-chat.prompt.ts без гарда; README) — роадмап #416 планировал 4 последовательных ручных правки этого текста (#411/#412/#413/#415). - SERVER_INSTRUCTIONS разбит (новый модуль server-instructions.ts): ROUTING_ PROSE (рукописные intent-подсказки «когда что» — осмысленно ручные, перенесены ДОСЛОВНО со всеми предостережениями: <=250 у create_comment, soft-delete у delete_page, baseHash у drawio_update, PUBLIC у share_page) + buildToolInventory() — генерирует <tool_inventory> из реестра (mcpName + purpose из catalogLine, группировка по TOOL_FAMILY, бакет OTHER ловит незамаппленное → тул нельзя тихо потерять) + 5 inline MCP-only (INLINE_MCP_INVENTORY). Детерминирован (семейства FAMILY_ORDER, имена localeCompare). regex-тест server-instructions удалён; структурные гарантии — в новом tool-inventory.test.mjs (точное членство множества сильнее старого \b-скрейпа). - Имена тулов в ai-chat.prompt.ts → через экспорт PROMPT_TOOL_NAMES; новый гард ai-chat.prompt.tool-names.spec.ts: каждое имя — реальный тул реестра, скан guidance-нот на camelCase-токены падает на несуществующем (escape- нейтрализация против ложных nThe-токенов). - INLINE_TOOL_TIERS уже содержал ровно 8 genuinely-inline тулов (после #445) — сжатие не потребовалось. Критерий: добавление/переименование спека меняет инвентарь БЕЗ правки прозы. Внутреннее ревью: APPROVE — фактическим прогоном подтверждено, что НИ ОДИН тул из старого SERVER_INSTRUCTIONS не выпал (диф старый-vs-новый пуст; добавился get_workspace, раньше прятавшийся в EXCEPTIONS); проза дословна; инвентарь полон/детерминирован/без фантомов; гард краснеет на обеих ветках провала. 613 node + 289 jest зелёные. Стоит на #445 — мержить последним в стопке 1б. README-каталоги вне обязательного скоупа (docs-скрипт) — в чек-лист #412. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
105 lines
4.1 KiB
JavaScript
105 lines
4.1 KiB
JavaScript
// Guard: the GENERATED <tool_inventory> in SERVER_INSTRUCTIONS (issue #448)
|
|
// names every tool the server registers. The inventory is BUILT from the
|
|
// registry (SHARED_TOOL_SPECS' mcpName/catalogLine + INLINE_MCP_INVENTORY), so
|
|
// the shared-registry tools can never drift by construction; this test's job is
|
|
// to catch the ONE remaining manual list — INLINE_MCP_INVENTORY — falling out
|
|
// of sync with the inline `server.registerTool(...)` calls in index.ts.
|
|
//
|
|
// It also asserts the composed guide keeps its routing prose (the hand-written
|
|
// intent hints) and is a valid non-empty string — the structural guarantees the
|
|
// old name-scraper test (server-instructions.test.mjs, now deleted) carried,
|
|
// minus its now-redundant per-name prose scrape.
|
|
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import { fileURLToPath } from "node:url";
|
|
import { dirname, join } from "node:path";
|
|
|
|
import {
|
|
SERVER_INSTRUCTIONS,
|
|
ROUTING_PROSE,
|
|
buildToolInventoryLines,
|
|
} from "../../build/server-instructions.js";
|
|
|
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
const SRC = join(HERE, "..", "..", "src");
|
|
|
|
/**
|
|
* Every tool name the MCP server registers, scraped from the SOURCE:
|
|
* - inline `server.registerTool("name", ...)` calls in index.ts;
|
|
* - shared specs in tool-specs.ts (`mcpName: 'name'`).
|
|
* Same two registration mechanisms the old guard covered.
|
|
*/
|
|
function registeredToolNames() {
|
|
const indexSrc = readFileSync(join(SRC, "index.ts"), "utf8");
|
|
const specsSrc = readFileSync(join(SRC, "tool-specs.ts"), "utf8");
|
|
const names = new Set();
|
|
for (const m of indexSrc.matchAll(/registerTool\(\s*"([a-z0-9_]+)"/g)) {
|
|
names.add(m[1]);
|
|
}
|
|
for (const m of specsSrc.matchAll(/mcpName:\s*['"]([a-z0-9_]+)['"]/g)) {
|
|
names.add(m[1]);
|
|
}
|
|
return names;
|
|
}
|
|
|
|
test("the generated inventory names every registered tool", () => {
|
|
const registered = registeredToolNames();
|
|
// Sanity: if the scrape regressed (regex drift), fail loudly rather than
|
|
// vacuously passing on an empty set.
|
|
assert.ok(
|
|
registered.size >= 40,
|
|
`sanity: expected 40+ registered tools, got ${registered.size} — ` +
|
|
"the extraction regexes in this test likely drifted from the source",
|
|
);
|
|
const inventory = new Set(buildToolInventoryLines().map((l) => l.name));
|
|
const missing = [...registered].filter((n) => !inventory.has(n)).sort();
|
|
assert.deepEqual(
|
|
missing,
|
|
[],
|
|
`tools missing from the generated <tool_inventory>: ${missing.join(", ")} — ` +
|
|
"a SHARED spec is covered automatically; an INLINE MCP-only tool needs a " +
|
|
"line added to INLINE_MCP_INVENTORY in src/server-instructions.ts",
|
|
);
|
|
});
|
|
|
|
test("the inventory has no phantom tool (every line is a real registered tool)", () => {
|
|
const registered = registeredToolNames();
|
|
const phantom = buildToolInventoryLines()
|
|
.map((l) => l.name)
|
|
.filter((n) => !registered.has(n))
|
|
.sort();
|
|
assert.deepEqual(
|
|
phantom,
|
|
[],
|
|
`<tool_inventory> lists tools that are NOT registered: ${phantom.join(", ")}`,
|
|
);
|
|
});
|
|
|
|
test("every inventory line has a non-empty purpose", () => {
|
|
for (const line of buildToolInventoryLines()) {
|
|
assert.equal(typeof line.purpose, "string");
|
|
assert.ok(line.purpose.trim().length > 0, `${line.name}: empty purpose`);
|
|
}
|
|
});
|
|
|
|
test("SERVER_INSTRUCTIONS keeps the routing prose and the generated inventory", () => {
|
|
assert.equal(typeof SERVER_INSTRUCTIONS, "string");
|
|
assert.ok(SERVER_INSTRUCTIONS.length > 0, "SERVER_INSTRUCTIONS is empty");
|
|
// Routing prose is spliced in verbatim (the hand-written intent hints).
|
|
assert.ok(
|
|
SERVER_INSTRUCTIONS.startsWith(ROUTING_PROSE),
|
|
"the routing prose is not preserved at the head of the guide",
|
|
);
|
|
// The generated inventory block is present.
|
|
assert.match(SERVER_INSTRUCTIONS, /<tool_inventory>/);
|
|
assert.match(SERVER_INSTRUCTIONS, /<\/tool_inventory>/);
|
|
// The routing families are still present in the prose.
|
|
for (const family of ["READ:", "EDIT:", "PAGES:", "COMMENTS:", "HISTORY:"]) {
|
|
assert.ok(
|
|
SERVER_INSTRUCTIONS.includes(family),
|
|
`routing prose lost its ${family} section`,
|
|
);
|
|
}
|
|
});
|