Files
msd-core/tests/docs-hooks-table-parity.test.cjs
Jakub Zych 6cfa0c55d2 refactor: drop 12 runtimes, keep Claude, Codex, OpenCode, Cursor, ZCode, Antigravity
Removes kilo, kimi, kimi-code, copilot, windsurf, augment, trae, qwen, hermes,
cline, codebuddy and pi end to end: capability descriptors, installer branches
and converters (bin/install.js 14.9k -> 11.2k lines), TypeScript converters,
hook surfaces and runtime homes, review lanes qwen/kimi-code, the two pi
migrations, Kimi payload normalization in the hook guards, dead hostBehaviors
vocabulary, launcher home probes, fixtures, runtime-specific tests and the
prose that presented them as supported.

Installer output for the six kept runtimes is byte-identical to before the
prune. The Kimi tool-vocabulary tests in workflow-guard, read-guard and
read-injection-scanner are left in place pending a decision.
2026-10-06 20:02:40 +02:00

170 lines
7.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// allow-test-rule: source-text-is-the-product (#3839)
// Reads the docs hook tables and the hook-surface source whose registrations
// ARE the deployed contract — asserting the docs rows match the surface.
/**
* Docs hook-table parity — docs-hooks-table-parity.test.cjs
*
* #3839: the shipped hook tables documented `msd-validate-commit.sh` as
* PostToolUse when it is registered PreToolUse (a PreToolUse hook BLOCKS a
* commit via exit 2; a PostToolUse hook cannot — the documented event
* misdescribes the hook's entire contract). The same scan found
* `msd-session-state.sh` documented PostToolUse while registered
* SessionStart. Nine files carried the wrong rows (ARCHITECTURE.md +
* INVENTORY.md × en, ja-JP, zh-CN, ko-KR, pt-BR).
*
* Truth source: the settings.json wiring probe lines in
* `applySettingsJsonHooks` (src/runtime-hooks-surface.cts) —
* `settings.hooks.<Event>.some|find(… referencesHook(…, 'msd-…'))` for
* literal events, and `settings.hooks[preToolEvent|postToolEvent]` for the
* runtime-resolved pre/post tool events (statically resolved to their
* canonical PreToolUse/PostToolUse names). Hooks registered on other
* surfaces (statusline, plugin-surface) are out of scope. Docs rows are
* exempt when their Event cell is not exactly one
* registered event (multi-event `A` / `B` cells, `statusLine`, `(helper)`,
* host-native names) — note docs/how-to/install-on-your-runtime.md also
* documents these mappings in transposed event-first tables, which this
* hook-first row parser intentionally does not read.
*/
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const ROOT = path.join(__dirname, '..');
const SURFACE_PATH = path.join(ROOT, 'src', 'runtime-hooks-surface.cts');
const DOC_TABLES = [
'docs/ARCHITECTURE.md',
'docs/INVENTORY.md',
'docs/ja-JP/ARCHITECTURE.md',
'docs/ja-JP/INVENTORY.md',
'docs/zh-CN/ARCHITECTURE.md',
'docs/zh-CN/INVENTORY.md',
'docs/ko-KR/ARCHITECTURE.md',
'docs/ko-KR/INVENTORY.md',
'docs/pt-BR/ARCHITECTURE.md',
'docs/pt-BR/INVENTORY.md',
];
// The exact basename set the surface parser must resolve. If a registration
// disappears or the parser drifts, this pin fails instead of the parity
// checks silently narrowing to a subset.
const EXPECTED_SURFACE_HOOKS = [
'msd-agent-isolation-guard.js',
'msd-check-update.js',
'msd-config-reload.js',
'msd-context-monitor.js',
'msd-graphify-update.sh',
'msd-phase-boundary.sh',
'msd-prompt-guard.js',
'msd-read-guard.js',
'msd-read-injection-scanner.js',
'msd-secret-read-guard.js',
'msd-session-state.sh',
'msd-validate-commit.sh',
'msd-workflow-guard.js',
'msd-worktree-path-guard.js',
'msd-write-guard.js',
];
/** hook basename → Set of events it is registered under on this surface. */
function registeredHookEvents() {
const src = fs.readFileSync(SURFACE_PATH, 'utf8');
const map = new Map();
// Probe lines name hooks WITHOUT file extensions (`'msd-session-state'`).
// Resolve a bare name against the actual hooks/ directory (the file's
// extension is ground truth) so both spellings resolve to one entry.
const resolveBase = (name) => {
if (/\.(sh|js|cmd)$/.test(name)) return name;
for (const ext of ['.js', '.sh', '.cmd']) {
if (fs.existsSync(path.join(ROOT, 'hooks', `${name}${ext}`))) return `${name}${ext}`;
}
return name; // no file twin (e.g. a name never documented in docs tables)
};
const add = (base, event) => {
const key = resolveBase(base);
if (!map.has(key)) map.set(key, new Set());
map.get(key).add(event);
};
let m;
// Probe lines paired with a literal event:
// settings.hooks.<Event>.some|find(… referencesHook(…, '<name>'))
const probeRe = /settings\.hooks\.([A-Za-z]+)\.(?:some|find)\(\(entry: HookGroup\) =>\s*\n\s*entry\.hooks && entry\.hooks\.some\(\(h: HookEntry\) => referencesHook\(h as Record<string, unknown>, '([^']+)'\)/g;
// allow-test-rule: source-text-is-the-product (#3839)
while ((m = probeRe.exec(src)) !== null) add(m[2], m[1]);
// Probe lines paired with the runtime-resolved variables — statically
// resolved to their non-Gemini canonical events (docs document the
// canonical Claude/GS wiring; BeforeTool/AfterTool are the Gemini twins):
// const preToolEvent = hookEvents === 'gemini' ? 'BeforeTool' : 'PreToolUse'
const dynRe = /settings\.hooks\[(preToolEvent|postToolEvent)\]\.(?:some|find)\(\(entry: HookGroup\) =>\s*\n\s*entry\.hooks && entry\.hooks\.some\(\(h: HookEntry\) => referencesHook\(h as Record<string, unknown>, '([^']+)'\)/g;
// allow-test-rule: source-text-is-the-product (#3839)
while ((m = dynRe.exec(src)) !== null) add(m[2], m[1] === 'preToolEvent' ? 'PreToolUse' : 'PostToolUse');
return map;
}
/** Hook-table rows: [basename, eventCell] for `msd-*` hooks. */
function docHookRows(docPath) {
const lines = fs.readFileSync(path.join(ROOT, docPath), 'utf8').split(/\r?\n/);
const rows = [];
for (const line of lines) {
const m = line.match(/^\|\s*`?(msd-[a-z0-9-]+\.(?:sh|js|cmd))`?\s*\|\s*`([^`]+)`\s*\|/);
if (m) rows.push([m[1], m[2]]);
}
return rows;
}
// Cells that are not a single surface event: multi-event (`A` / `B`),
// non-event identifiers (statusLine, host-native names), or placeholders.
const SINGLE_EVENT_CELL = /^[A-Z][A-Za-z]+$/;
describe('docs hook tables match runtime-hooks-surface registrations', () => {
const surface = registeredHookEvents();
test('surface parser resolves the exact expected hook set (no silent drift)', () => {
const withExt = [...surface.keys()].filter((k) => /\.(sh|js|cmd)$/.test(k)).sort();
assert.deepEqual(
withExt,
[...EXPECTED_SURFACE_HOOKS].sort(),
'parser must resolve exactly the expected surface registrations'
);
});
for (const doc of DOC_TABLES) {
test(`${doc}: every single-event hook row matches the surface`, () => {
const rows = docHookRows(doc);
assert.ok(rows.length >= 3, `${doc}: hook-table parser found rows (guard against silent table drift)`);
const mismatches = [];
for (const [base, cell] of rows) {
const events = surface.get(base);
if (!events) continue; // not registered on this surface (statusline, plugin-surface hooks…)
if (!SINGLE_EVENT_CELL.test(cell)) continue; // multi-event or non-event cell
if (!events.has(cell)) {
mismatches.push(`${base}: docs say \`${cell}\`, surface registers ${[...events].join(', ')}`);
}
}
assert.deepEqual(mismatches, [], `docs rows must match src/runtime-hooks-surface.cts (#3839)`);
});
}
test('#3839 regression pin: the two misdocumented hooks are asserted directly', () => {
assert.ok(surface.get('msd-validate-commit.sh').has('PreToolUse'),
'msd-validate-commit.sh blocks commits — must stay PreToolUse on the surface');
assert.ok(surface.get('msd-session-state.sh').has('SessionStart'),
'msd-session-state.sh orients the session — must stay SessionStart on the surface');
// ja-JP/ARCHITECTURE.md's hook table is a shorter translation that never
// listed these two hooks — it stays in the parity loop above but not here.
const PINNED = DOC_TABLES.filter((d) => d !== 'docs/ja-JP/ARCHITECTURE.md');
for (const doc of PINNED) {
const rows = docHookRows(doc);
const vc = rows.find(([b]) => b === 'msd-validate-commit.sh');
assert.ok(vc, `${doc}: validate-commit row present`);
assert.equal(vc[1], 'PreToolUse', `${doc}: validate-commit documented as PreToolUse`);
const ss = rows.find(([b]) => b === 'msd-session-state.sh');
assert.ok(ss, `${doc}: session-state row present`);
assert.equal(ss[1], 'SessionStart', `${doc}: session-state documented as SessionStart`);
}
});
});