* feat(#1077): phase 5f-2 — drive the hookEvents dialect (PostToolUse/AfterTool) from the descriptor postToolEvent (bin/install.js) and preToolEvent (applySettingsJsonHooks in runtime-hooks-surface.cts) now select the event-name dialect from registry.runtimes[id].runtime.hookEvents instead of the hardcoded (runtime === 'gemini' || runtime === 'antigravity') check: hookEvents === 'gemini' → AfterTool/BeforeTool; else → PostToolUse/PreToolUse. hookEvents threaded into the applySettingsJsonHooks opts bag. Equivalence-preserving (Codex-verified): hookEvents 'gemini' is exactly {gemini, antigravity}, 'claude' the rest; undefined → claude dialect (matches the old else). The per-event SET guards (isQwen||claude → SubagentStop/Stop/PreCompact; runtime==='claude' → FileChanged; isGemini → Gemini agent-events) stay HARDCODED — hookEvents (2-value) is too coarse to drive them (the event set differs within hookEvents='claude'); per-event-set drive tracked in #1076. Registry-parity test (enh-1077): asserts BOTH post-tool (AfterTool/PostToolUse) AND pre-tool (BeforeTool/PreToolUse) dialects are a pure function of hookEvents, for gemini/antigravity/claude/augment — non-vacuous (catches a broken hookEvents thread). Closes #1077 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(#1077): build hooks/dist in before() so dialect-drive test passes in scoped CI hooks/dist is gitignored and absent in scoped/windows CI jobs that do not pre-run build:hooks. Without it, install() finds no hook files and all AfterTool/BeforeTool/PostToolUse/PreToolUse event arrays come back empty, failing every hook-presence assertion. Added an idempotent ensureHooksDist() called in a top-level before() — mirrors the pattern from bug-376-claude-js-hook-gsd-rewriter.test.cjs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(#1055): add installSurface/writesSharedSettings/permissionWriter/extendedHookEvents to runtime descriptors Purely additive: four new fields on all 16 runtime capability.json descriptors, validator extended with three new closed-vocab sets, registry regenerated. Test fixtures (VALID_RUNTIME_CAP and makeRuntimeCap) updated to include the new required fields so all 255 capability-registry tests continue to pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(#1076): drive per-event hook guards from extendedHookEvents descriptor Replace hardcoded runtime-name checks (isQwen||runtime==='claude', runtime==='claude', isGemini) in applySettingsJsonHooks with a single descriptor-driven extendedEvents array derived from the new opts field. Remove isQwen and isGemini derivations (no remaining uses after the three guard blocks are migrated). Wire extendedHookEvents from the capability registry in bin/install.js call site. Add behavioral regression test (enh-1076-extended-hook-events-drive.test.cjs) confirming the drive is purely descriptor-based and runtime-name-agnostic. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(#1055): drive resolveRuntimeConfigIntent from the runtime descriptor; retire hand-kept REGISTRY - Rewrites src/runtime-config-adapter-registry.cts to require capability-registry.cjs and read installSurface / writesSharedSettings / permissionWriter from runtimes[id].runtime; deletes the hand-kept REGISTRY const (ADR-857 phase 5g drive 2). - ALLOWED_CONFIG_RUNTIMES is now derived from descriptor entries that have installSurface. - Fixes the configFormat parity gate in scripts/gen-capability-registry.cjs to read installSurface directly from capMap descriptor bodies, breaking the require cycle (adapter now requires the generated registry; gen-script must not require the adapter). - Adds golden-master test tests/enh-1055-config-intent-descriptor-drive.test.cjs (41 tests) pinning all 16 runtimes' return shapes and the TypeError-on-unknown contract. - Updates scripts/lint-test-file-count.allowlist.json (config module, +1 file). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(#1076): make hooksSurface descriptor load-bearing for the settings-json hook-skip - Adds hooksSurface?: string to ApplySettingsJsonHooksOpts and destructuring in applySettingsJsonHooks (src/runtime-hooks-surface.cts). - Replaces the hardcoded !isOpencode && !isKilo hook-skip guard with hooksSurface !== 'none'; removes the now-unused isOpencode/isKilo derivations (ADR-857 phase 5g drive 3). - Passes hooksSurface from the runtime descriptor at the applySettingsJsonHooks call site in bin/install.js using the established _capabilityRegistry?.runtimes?.[runtime]?.runtime?.hooksSurface idiom. - Extends tests/enh-1076-extended-hook-events-drive.test.cjs with two new suites proving: (a) hooksSurface:'none' writes no hooks regardless of runtime name; (b) hooksSurface:'settings-json' writes hooks even for 'opencode' (previously hardcoded to skip). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: record installSurface/writesSharedSettings/permissionWriter/extendedHookEvents descriptor axes in ADR-1016 Add Decision 7a documenting the four axes added in the 5f-completion pass, update axis counts from "six" to "twelve", note 5f-completion drives as done in Decision 8's ladder, update Out of scope to reflect #1055/#1076 are done and 5g (InstallPlan capstone) remains the only open phase. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#1055): parity gate must fire on configFormat↔installSurface mismatch (read installSurface at the descriptor level) The test fixture makeRuntimeCapMap did not include installSurface in the runtime object, so the gate's typeof r.installSurface !== 'string' guard always skipped the entry and never threw. Added installSurface as an optional third parameter to makeRuntimeCapMap and passed the correct installSurface values ('settings-json' for claude, 'codex-toml' for codex) to the two THROWS tests. The gate implementation already reads r.installSurface correctly from the descriptor level. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(#1076): add installSurface↔hooksSurface + extendedHookEvents↔hookEvents consistency gates with rejection tests GATE A: INSTALL_SURFACE_TO_ALLOWED_HOOKS_SURFACES map in validateRuntimeBody enforces that a runtime's hooksSurface is valid for its installSurface (e.g. profile-marker-only only allows none, codex-toml only allows codex-hooks-json). Derived from the 16 real runtime descriptors. GATE B: validateRuntimeBody checks that if extendedHookEvents contains Gemini agent-events (BeforeAgent/AfterAgent/BeforeModel), hookEvents must be 'gemini'; if it contains Claude-family events (SubagentStop/Stop/PreCompact/FileChanged), hookEvents must be 'claude'. Added 10 rejection tests in suite 27 covering each gate + each new field validator. All 16 real runtimes satisfy both gates (verified before coding). Exports: INSTALL_SURFACE_TO_ALLOWED_HOOKS_SURFACES, VALID_INSTALL_SURFACES, VALID_EXTENDED_HOOK_EVENTS, VALID_PERMISSION_WRITERS, validateRuntimeBody. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(#1076): strengthen hooksSurface-drive assertions; defensive hooksSurface fallback; drop vacuous dup 1. bin/install.js: add explicit literal fallback for hooksSurface when the committed capability registry fails to load (opencode/kilo → 'none', all others → 'settings-json'). The descriptor is always the source of truth in normal operation. 2. enh-1076 Suite 7: change SessionStart assertion from key-presence (hasOwnProperty) to at least-one-command (hasHooksFor), so the test fails if hooks are initialized-but-empty. ensureHooksDist() in before() guarantees hook files exist. 3. enh-1055 Test 2: remove vacuous duplicate suite that re-asserted intent.runtime === row.runtime already fully covered by Test 1's deepStrictEqual over all four fields. 4. capability-registry.test.cjs: fix stale comments in the grok-skip test that claimed the parity gate uses the adapter registry; gate reads purely from the descriptor (installSurface absent → typeof r.installSurface !== 'string' → soft-skip). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(#1082): materialize the InstallPlan — collect install-level descriptor axes into resolveInstallPlan; route install()/finishInstall() through it (ADR-58/5g) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: record 5g InstallPlan materialization (ADR-58 Accepted, ADR-1016 phase-5 complete) ADR-1016 Decision 8 step 7 updated to DONE: resolveInstallPlan(runtime) in runtime-config-adapter-registry collects install-level descriptor axes into the typed InstallPlan consumed by install()/finishInstall(). Out-of-scope section updated: 5g capstone is complete, phase 5 fully materialized. ADR-1016 line ~20 updated: InstallPlan IS now materialized (both halves). ADR-58 Implementation note added (2026-06-11): realized in runtime-config-adapter-registry (co-located with adapter-selection). CONTEXT.md Runtime Config Adapter Registry entry extended to document resolveInstallPlan and both-halves realization. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(#1082): update install drift guard to the resolveInstallPlan seam (5g) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
430 lines
18 KiB
JavaScript
430 lines
18 KiB
JavaScript
'use strict';
|
|
|
|
process.env.GSD_TEST_MODE = '1';
|
|
|
|
/**
|
|
* ADR-857 phase 5f-2: hook-events dialect is driven from the registry descriptor.
|
|
*
|
|
* Before this change, postToolEvent and preToolEvent were hardcoded strings
|
|
* derived from runtime-name checks:
|
|
*
|
|
* (runtime === 'gemini' || runtime === 'antigravity') ? 'AfterTool' : 'PostToolUse'
|
|
* (runtime === 'gemini' || runtime === 'antigravity') ? 'BeforeTool' : 'PreToolUse'
|
|
*
|
|
* After phase 5f-2, both are driven by the registry descriptor's
|
|
* `hookEvents` field: hookEvents === 'gemini' → AfterTool/BeforeTool;
|
|
* any other value (or missing) → PostToolUse/PreToolUse.
|
|
*
|
|
* Equivalence (i.e. identical observable behaviour for all runtimes):
|
|
* hookEvents === 'gemini' iff runtime ∈ {gemini, antigravity}
|
|
*
|
|
* This suite asserts the equivalence and the registry-parity invariant:
|
|
* any runtime whose descriptor carries hookEvents='gemini' gets the
|
|
* AfterTool/BeforeTool dialect; all others get PostToolUse/PreToolUse.
|
|
*/
|
|
|
|
const { test, describe, before, beforeEach, afterEach } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const { execFileSync } = require('node:child_process');
|
|
|
|
const { install } = require('../bin/install.js');
|
|
const { createTempDir, cleanup } = require('./helpers.cjs');
|
|
|
|
// ─── hooks/dist build guard ───────────────────────────────────────────────────
|
|
//
|
|
// hooks/dist/ is gitignored and only produced by `npm run build:hooks`.
|
|
// In CI the scoped/windows test jobs do NOT run build:hooks before running
|
|
// tests, so install() finds no hook files → event arrays come back empty →
|
|
// every "expected AfterTool/PostToolUse/BeforeTool/PreToolUse hooks" assertion
|
|
// fails. This mirrors the pattern in bug-376-claude-js-hook-gsd-rewriter.test.cjs.
|
|
|
|
const REPO_ROOT = path.resolve(__dirname, '..');
|
|
const HOOKS_DIST_DIR = path.join(REPO_ROOT, 'hooks', 'dist');
|
|
const BUILD_HOOKS_SCRIPT = path.join(REPO_ROOT, 'scripts', 'build-hooks.js');
|
|
|
|
/**
|
|
* Idempotently ensure hooks/dist contains built .js files.
|
|
* Runs build-hooks.js only when the directory is absent or empty of .js files.
|
|
*/
|
|
function ensureHooksDist() {
|
|
if (!fs.existsSync(HOOKS_DIST_DIR) || fs.readdirSync(HOOKS_DIST_DIR).filter(f => f.endsWith('.js')).length === 0) {
|
|
execFileSync(process.execPath, [BUILD_HOOKS_SCRIPT], { stdio: 'pipe' });
|
|
}
|
|
}
|
|
|
|
before(() => {
|
|
ensureHooksDist();
|
|
});
|
|
|
|
// ─── Registry lookup ──────────────────────────────────────────────────────────
|
|
|
|
const REGISTRY_PATH = path.join(__dirname, '..', 'gsd-core', 'bin', 'lib', 'capability-registry.cjs');
|
|
const registry = (() => {
|
|
try { return require(REGISTRY_PATH); } catch { return undefined; }
|
|
})();
|
|
|
|
/**
|
|
* Return the hookEvents dialect for a runtime ID from the live registry.
|
|
* Returns undefined when the registry is absent or the runtime has no descriptor.
|
|
*/
|
|
function registryHookEvents(runtimeId) {
|
|
return registry?.runtimes?.[runtimeId]?.runtime?.hookEvents;
|
|
}
|
|
|
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
|
|
/** Collect all hook commands registered under a settings event key. */
|
|
function hooksForEvent(settings, eventName) {
|
|
if (!settings || !settings.hooks || !Array.isArray(settings.hooks[eventName])) return [];
|
|
return settings.hooks[eventName].flatMap(entry =>
|
|
(entry && Array.isArray(entry.hooks) ? entry.hooks : [])
|
|
.map(h => h && h.command)
|
|
.filter(Boolean)
|
|
);
|
|
}
|
|
|
|
/** True if at least one hook is registered under eventName. */
|
|
function hasHooksFor(settings, eventName) {
|
|
return hooksForEvent(settings, eventName).length > 0;
|
|
}
|
|
|
|
// ─── Suite 1: Gemini-dialect runtimes use AfterTool/BeforeTool ───────────────
|
|
//
|
|
// Registry runtimes with hookEvents='gemini': gemini, antigravity
|
|
|
|
describe('enh-1077 phase 5f-2: gemini hookEvents dialect → AfterTool/BeforeTool', () => {
|
|
// ── gemini ──
|
|
|
|
describe('gemini install uses AfterTool for post-tool hooks', () => {
|
|
let tmpDir;
|
|
let previousCwd;
|
|
let settings;
|
|
|
|
beforeEach(() => {
|
|
tmpDir = createTempDir('gsd-1077-gemini-');
|
|
previousCwd = process.cwd();
|
|
process.chdir(tmpDir);
|
|
|
|
const geminiDir = path.join(tmpDir, '.gemini');
|
|
fs.mkdirSync(geminiDir, { recursive: true });
|
|
const result = install(false, 'gemini');
|
|
settings = result && result.settings;
|
|
});
|
|
|
|
afterEach(() => {
|
|
process.chdir(previousCwd);
|
|
cleanup(tmpDir);
|
|
});
|
|
|
|
test('registry confirms gemini hookEvents is "gemini"', () => {
|
|
// Parity assertion: if the registry changes, this test fails first.
|
|
const he = registryHookEvents('gemini');
|
|
if (he !== undefined) {
|
|
assert.strictEqual(he, 'gemini',
|
|
'Registry descriptor for gemini must declare hookEvents="gemini"');
|
|
}
|
|
});
|
|
|
|
test('gemini install returns a settings object', () => {
|
|
assert.ok(settings !== null && typeof settings === 'object',
|
|
'gemini install must return a non-null settings object');
|
|
});
|
|
|
|
test('gemini install registers at least one hook under AfterTool (post-tool)', () => {
|
|
assert.ok(hasHooksFor(settings, 'AfterTool'),
|
|
`Expected AfterTool hooks on gemini; got hooks keys: ${JSON.stringify(Object.keys((settings && settings.hooks) || {}))}`);
|
|
});
|
|
|
|
test('gemini install does NOT register context-monitor under PostToolUse (wrong dialect)', () => {
|
|
const cmds = hooksForEvent(settings, 'PostToolUse');
|
|
const hasMonitor = cmds.some(c => c && c.includes('gsd-context-monitor'));
|
|
assert.strictEqual(hasMonitor, false,
|
|
`gemini must NOT use PostToolUse for context-monitor; got PostToolUse commands: ${JSON.stringify(cmds)}`);
|
|
});
|
|
|
|
test('gemini install registers at least one pre-tool hook (prompt-guard) under BeforeTool', () => {
|
|
const cmds = hooksForEvent(settings, 'BeforeTool');
|
|
const hasPromptGuard = cmds.some(c => c && c.includes('gsd-prompt-guard'));
|
|
assert.ok(hasPromptGuard,
|
|
`Expected prompt-guard hook under BeforeTool on gemini; BeforeTool commands: ${JSON.stringify(cmds)}; hooks keys: ${JSON.stringify(Object.keys((settings && settings.hooks) || {}))}`);
|
|
});
|
|
|
|
test('gemini install does NOT register prompt-guard under PreToolUse (wrong pre-tool dialect)', () => {
|
|
const cmds = hooksForEvent(settings, 'PreToolUse');
|
|
const hasPromptGuard = cmds.some(c => c && c.includes('gsd-prompt-guard'));
|
|
assert.strictEqual(hasPromptGuard, false,
|
|
`gemini must NOT use PreToolUse for prompt-guard; got PreToolUse commands: ${JSON.stringify(cmds)}`);
|
|
});
|
|
});
|
|
|
|
// ── antigravity ──
|
|
|
|
describe('antigravity install uses AfterTool/BeforeTool (gemini dialect)', () => {
|
|
let tmpDir;
|
|
let previousCwd;
|
|
let settings;
|
|
|
|
beforeEach(() => {
|
|
tmpDir = createTempDir('gsd-1077-antigrav-');
|
|
previousCwd = process.cwd();
|
|
process.chdir(tmpDir);
|
|
|
|
const agDir = path.join(tmpDir, '.gemini', 'antigravity');
|
|
fs.mkdirSync(agDir, { recursive: true });
|
|
const result = install(false, 'antigravity');
|
|
settings = result && result.settings;
|
|
});
|
|
|
|
afterEach(() => {
|
|
process.chdir(previousCwd);
|
|
cleanup(tmpDir);
|
|
});
|
|
|
|
test('registry confirms antigravity hookEvents is "gemini"', () => {
|
|
const he = registryHookEvents('antigravity');
|
|
if (he !== undefined) {
|
|
assert.strictEqual(he, 'gemini',
|
|
'Registry descriptor for antigravity must declare hookEvents="gemini"');
|
|
}
|
|
});
|
|
|
|
test('antigravity install returns a settings object', () => {
|
|
assert.ok(settings !== null && typeof settings === 'object',
|
|
'antigravity install must return a non-null settings object');
|
|
});
|
|
|
|
test('antigravity install registers at least one hook under AfterTool', () => {
|
|
assert.ok(hasHooksFor(settings, 'AfterTool'),
|
|
`Expected AfterTool hooks on antigravity; got hooks keys: ${JSON.stringify(Object.keys((settings && settings.hooks) || {}))}`);
|
|
});
|
|
|
|
test('antigravity install does NOT register context-monitor under PostToolUse', () => {
|
|
const cmds = hooksForEvent(settings, 'PostToolUse');
|
|
const hasMonitor = cmds.some(c => c && c.includes('gsd-context-monitor'));
|
|
assert.strictEqual(hasMonitor, false,
|
|
`antigravity must NOT use PostToolUse for context-monitor; got: ${JSON.stringify(cmds)}`);
|
|
});
|
|
|
|
test('antigravity install registers at least one pre-tool hook (prompt-guard) under BeforeTool', () => {
|
|
const cmds = hooksForEvent(settings, 'BeforeTool');
|
|
const hasPromptGuard = cmds.some(c => c && c.includes('gsd-prompt-guard'));
|
|
assert.ok(hasPromptGuard,
|
|
`Expected prompt-guard hook under BeforeTool on antigravity; BeforeTool commands: ${JSON.stringify(cmds)}; hooks keys: ${JSON.stringify(Object.keys((settings && settings.hooks) || {}))}`);
|
|
});
|
|
|
|
test('antigravity install does NOT register prompt-guard under PreToolUse (wrong pre-tool dialect)', () => {
|
|
const cmds = hooksForEvent(settings, 'PreToolUse');
|
|
const hasPromptGuard = cmds.some(c => c && c.includes('gsd-prompt-guard'));
|
|
assert.strictEqual(hasPromptGuard, false,
|
|
`antigravity must NOT use PreToolUse for prompt-guard; got PreToolUse commands: ${JSON.stringify(cmds)}`);
|
|
});
|
|
});
|
|
});
|
|
|
|
// ─── Suite 2: Claude-dialect runtimes use PostToolUse/PreToolUse ──────────────
|
|
//
|
|
// Registry runtimes with hookEvents='claude': claude, augment
|
|
|
|
describe('enh-1077 phase 5f-2: claude hookEvents dialect → PostToolUse/PreToolUse', () => {
|
|
// ── claude ──
|
|
|
|
describe('claude install uses PostToolUse for post-tool hooks', () => {
|
|
let tmpDir;
|
|
let previousCwd;
|
|
let settings;
|
|
|
|
beforeEach(() => {
|
|
tmpDir = createTempDir('gsd-1077-claude-');
|
|
previousCwd = process.cwd();
|
|
process.chdir(tmpDir);
|
|
|
|
const claudeDir = path.join(tmpDir, '.claude');
|
|
fs.mkdirSync(claudeDir, { recursive: true });
|
|
const result = install(false, 'claude');
|
|
settings = result && result.settings;
|
|
});
|
|
|
|
afterEach(() => {
|
|
process.chdir(previousCwd);
|
|
cleanup(tmpDir);
|
|
});
|
|
|
|
test('registry confirms claude hookEvents is "claude"', () => {
|
|
const he = registryHookEvents('claude');
|
|
if (he !== undefined) {
|
|
assert.strictEqual(he, 'claude',
|
|
'Registry descriptor for claude must declare hookEvents="claude"');
|
|
}
|
|
});
|
|
|
|
test('claude install returns a settings object', () => {
|
|
assert.ok(settings !== null && typeof settings === 'object',
|
|
'claude install must return a non-null settings object');
|
|
});
|
|
|
|
test('claude install registers at least one hook under PostToolUse', () => {
|
|
assert.ok(hasHooksFor(settings, 'PostToolUse'),
|
|
`Expected PostToolUse hooks on claude; got hooks keys: ${JSON.stringify(Object.keys((settings && settings.hooks) || {}))}`);
|
|
});
|
|
|
|
test('claude install does NOT register context-monitor under AfterTool (wrong dialect)', () => {
|
|
const cmds = hooksForEvent(settings, 'AfterTool');
|
|
const hasMonitor = cmds.some(c => c && c.includes('gsd-context-monitor'));
|
|
assert.strictEqual(hasMonitor, false,
|
|
`claude must NOT use AfterTool for context-monitor; got AfterTool commands: ${JSON.stringify(cmds)}`);
|
|
});
|
|
|
|
test('claude install registers at least one pre-tool hook (prompt-guard) under PreToolUse', () => {
|
|
const cmds = hooksForEvent(settings, 'PreToolUse');
|
|
const hasPromptGuard = cmds.some(c => c && c.includes('gsd-prompt-guard'));
|
|
assert.ok(hasPromptGuard,
|
|
`Expected prompt-guard hook under PreToolUse on claude; PreToolUse commands: ${JSON.stringify(cmds)}; hooks keys: ${JSON.stringify(Object.keys((settings && settings.hooks) || {}))}`);
|
|
});
|
|
|
|
test('claude install does NOT register prompt-guard under BeforeTool (wrong pre-tool dialect)', () => {
|
|
const cmds = hooksForEvent(settings, 'BeforeTool');
|
|
const hasPromptGuard = cmds.some(c => c && c.includes('gsd-prompt-guard'));
|
|
assert.strictEqual(hasPromptGuard, false,
|
|
`claude must NOT use BeforeTool for prompt-guard; got BeforeTool commands: ${JSON.stringify(cmds)}`);
|
|
});
|
|
});
|
|
|
|
// ── augment ──
|
|
|
|
describe('augment install uses PostToolUse/PreToolUse (claude dialect)', () => {
|
|
let tmpDir;
|
|
let previousCwd;
|
|
let settings;
|
|
|
|
beforeEach(() => {
|
|
tmpDir = createTempDir('gsd-1077-augment-');
|
|
previousCwd = process.cwd();
|
|
process.chdir(tmpDir);
|
|
|
|
const augDir = path.join(tmpDir, '.augment');
|
|
fs.mkdirSync(augDir, { recursive: true });
|
|
const result = install(false, 'augment');
|
|
settings = result && result.settings;
|
|
});
|
|
|
|
afterEach(() => {
|
|
process.chdir(previousCwd);
|
|
cleanup(tmpDir);
|
|
});
|
|
|
|
test('registry confirms augment hookEvents is "claude"', () => {
|
|
const he = registryHookEvents('augment');
|
|
if (he !== undefined) {
|
|
assert.strictEqual(he, 'claude',
|
|
'Registry descriptor for augment must declare hookEvents="claude"');
|
|
}
|
|
});
|
|
|
|
test('augment install returns a settings object', () => {
|
|
assert.ok(settings !== null && typeof settings === 'object',
|
|
'augment install must return a non-null settings object');
|
|
});
|
|
|
|
test('augment install registers at least one hook under PostToolUse', () => {
|
|
assert.ok(hasHooksFor(settings, 'PostToolUse'),
|
|
`Expected PostToolUse hooks on augment; got hooks keys: ${JSON.stringify(Object.keys((settings && settings.hooks) || {}))}`);
|
|
});
|
|
|
|
test('augment install does NOT register context-monitor under AfterTool', () => {
|
|
const cmds = hooksForEvent(settings, 'AfterTool');
|
|
const hasMonitor = cmds.some(c => c && c.includes('gsd-context-monitor'));
|
|
assert.strictEqual(hasMonitor, false,
|
|
`augment must NOT use AfterTool for context-monitor; got: ${JSON.stringify(cmds)}`);
|
|
});
|
|
|
|
test('augment install registers at least one pre-tool hook (prompt-guard) under PreToolUse', () => {
|
|
const cmds = hooksForEvent(settings, 'PreToolUse');
|
|
const hasPromptGuard = cmds.some(c => c && c.includes('gsd-prompt-guard'));
|
|
assert.ok(hasPromptGuard,
|
|
`Expected prompt-guard hook under PreToolUse on augment; PreToolUse commands: ${JSON.stringify(cmds)}; hooks keys: ${JSON.stringify(Object.keys((settings && settings.hooks) || {}))}`);
|
|
});
|
|
|
|
test('augment install does NOT register prompt-guard under BeforeTool (wrong pre-tool dialect)', () => {
|
|
const cmds = hooksForEvent(settings, 'BeforeTool');
|
|
const hasPromptGuard = cmds.some(c => c && c.includes('gsd-prompt-guard'));
|
|
assert.strictEqual(hasPromptGuard, false,
|
|
`augment must NOT use BeforeTool for prompt-guard; got BeforeTool commands: ${JSON.stringify(cmds)}`);
|
|
});
|
|
});
|
|
});
|
|
|
|
// ─── Suite 3: Registry-parity invariant ──────────────────────────────────────
|
|
//
|
|
// For every runtime in the registry that exposes a settings.json surface
|
|
// (i.e. hookEvents is defined), assert that the installed hook dialect matches
|
|
// the registry value. This is the generative-fix parity assertion
|
|
// (DEFECT.GENERATIVE-FIX): adding a new runtime with hookEvents to the
|
|
// registry automatically requires a passing install test for that runtime.
|
|
|
|
describe('enh-1077 phase 5f-2: registry-parity — hookEvents descriptor drives install dialect', () => {
|
|
test('all registry runtimes with hookEvents use the matching install dialect', () => {
|
|
if (!registry || !registry.runtimes) {
|
|
// Registry absent — skip parity check (equivalence still verified above)
|
|
return;
|
|
}
|
|
|
|
// Runtimes that have settings.json surfaces and a hookEvents descriptor
|
|
const SETTINGS_JSON_RUNTIMES = ['claude', 'gemini', 'antigravity', 'augment', 'qwen', 'hermes', 'codebuddy'];
|
|
|
|
const failures = [];
|
|
|
|
for (const runtimeId of SETTINGS_JSON_RUNTIMES) {
|
|
const he = registryHookEvents(runtimeId);
|
|
if (he === undefined) continue; // no hookEvents in descriptor — skip
|
|
|
|
const expectedPostEvent = he === 'gemini' ? 'AfterTool' : 'PostToolUse';
|
|
const unexpectedPostEvent = he === 'gemini' ? 'PostToolUse' : 'AfterTool';
|
|
const expectedPreEvent = he === 'gemini' ? 'BeforeTool' : 'PreToolUse';
|
|
const unexpectedPreEvent = he === 'gemini' ? 'PreToolUse' : 'BeforeTool';
|
|
|
|
const previousCwd = process.cwd();
|
|
const tmpDir = createTempDir(`gsd-1077-parity-${runtimeId}-`);
|
|
try {
|
|
process.chdir(tmpDir);
|
|
const result = install(false, runtimeId);
|
|
const settings = result && result.settings;
|
|
if (!settings) continue; // non-settings-json surface, skip
|
|
|
|
// Post-tool event assertions
|
|
const hasExpected = hasHooksFor(settings, expectedPostEvent);
|
|
const hasUnexpected = hooksForEvent(settings, unexpectedPostEvent)
|
|
.some(c => c && c.includes('gsd-context-monitor'));
|
|
|
|
if (!hasExpected) {
|
|
failures.push(`${runtimeId}: expected context-monitor hook under ${expectedPostEvent} (hookEvents=${he}), but none found`);
|
|
}
|
|
if (hasUnexpected) {
|
|
failures.push(`${runtimeId}: must NOT register context-monitor under ${unexpectedPostEvent}, but it was found`);
|
|
}
|
|
|
|
// Pre-tool event assertions: prompt-guard must land under the dialect-correct key.
|
|
const preToolCmdsExpected = hooksForEvent(settings, expectedPreEvent);
|
|
const hasPromptGuardExpected = preToolCmdsExpected.some(c => c && c.includes('gsd-prompt-guard'));
|
|
const preToolCmdsUnexpected = hooksForEvent(settings, unexpectedPreEvent);
|
|
const hasPromptGuardUnexpected = preToolCmdsUnexpected.some(c => c && c.includes('gsd-prompt-guard'));
|
|
|
|
if (!hasPromptGuardExpected) {
|
|
failures.push(`${runtimeId}: expected prompt-guard hook under ${expectedPreEvent} (hookEvents=${he}), but none found; ${expectedPreEvent} cmds: ${JSON.stringify(preToolCmdsExpected)}`);
|
|
}
|
|
if (hasPromptGuardUnexpected) {
|
|
failures.push(`${runtimeId}: must NOT register prompt-guard under ${unexpectedPreEvent} (hookEvents=${he}), but it was found`);
|
|
}
|
|
} finally {
|
|
process.chdir(previousCwd);
|
|
cleanup(tmpDir);
|
|
}
|
|
}
|
|
|
|
assert.deepEqual(failures, [],
|
|
'Registry-parity failures (hookEvents descriptor must drive install dialect):\n' +
|
|
failures.join('\n'));
|
|
});
|
|
});
|