import { describe, it, before, after, beforeEach, afterEach } from 'node:test'; import assert from 'node:assert/strict'; import { join, dirname, resolve } from 'node:path'; import { mkdir, writeFile, rm, readFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { estimateTokens, stripInjectedHtmlComments, effectiveMemoryBytes, detectGitRoot, walkClaudeMdCascade, readClaudeJsonProjectSlice, enumeratePlugins, enumerateSkills, readActiveHooks, readActiveMcpServers, readActiveConfig, deriveLoadPattern, enumerateRules, enumerateAgents, enumerateOutputStyles, } from '../../scanners/lib/active-config-reader.mjs'; function uniqueDir(suffix) { return join(tmpdir(), `config-audit-acr-${suffix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`); } /** * Build a rich-repo fixture under `root`. * Layout mirrors feature plan §8 — git-repo, CLAUDE.md cascade, settings layers, * .mcp.json, fake-home with plugins + .claude.json. */ async function buildRichRepo(root) { const fakeHome = join(root, 'fake-home'); // Repo marker await mkdir(join(root, '.git'), { recursive: true }); await writeFile(join(root, '.git', 'HEAD'), 'ref: refs/heads/main\n'); // Project CLAUDE.md with @import await mkdir(join(root, 'docs'), { recursive: true }); await writeFile( join(root, 'CLAUDE.md'), '# Project Instructions\n\n@docs/conv.md\n\nBuild with care.\n', ); await writeFile(join(root, 'docs', 'conv.md'), '# Conventions\n\nUse conventional commits.\n'); // Settings cascade await mkdir(join(root, '.claude', 'rules'), { recursive: true }); await writeFile( join(root, '.claude', 'settings.json'), JSON.stringify({ permissions: { allow: ['Read', 'Write'] }, hooks: { PreToolUse: [ { matcher: 'Bash', hooks: [{ type: 'command', command: 'check.sh' }] }, ], }, }, null, 2), ); await writeFile( join(root, '.claude', 'settings.local.json'), JSON.stringify({ env: { DEBUG: 'true' } }, null, 2), ); await writeFile(join(root, '.claude', 'rules', 'team.md'), '# Team Rule\n'); // Project .mcp.json await writeFile( join(root, '.mcp.json'), JSON.stringify({ mcpServers: { alpha: { command: 'npx', args: ['alpha-server'] }, beta: { command: 'npx', args: ['beta-server'] }, }, }, null, 2), ); // Fake HOME — user CLAUDE.md, settings, plugins, .claude.json await mkdir(join(fakeHome, '.claude'), { recursive: true }); await writeFile( join(fakeHome, '.claude', 'CLAUDE.md'), '# User Instructions\n\nBe terse.\n', ); await writeFile( join(fakeHome, '.claude', 'settings.json'), JSON.stringify({ hooks: { Stop: [{ hooks: [{ type: 'command', command: 'reminder.sh' }] }], }, }, null, 2), ); // Plugin: demo plugin with 1 command, 1 skill, 1 hook const pluginRoot = join( fakeHome, '.claude', 'plugins', 'marketplaces', 'mp', 'plugins', 'demo', ); await mkdir(join(pluginRoot, '.claude-plugin'), { recursive: true }); await writeFile( join(pluginRoot, '.claude-plugin', 'plugin.json'), JSON.stringify({ name: 'demo', description: 'test plugin', version: '0.1.0' }, null, 2), ); await mkdir(join(pluginRoot, 'commands'), { recursive: true }); await writeFile( join(pluginRoot, 'commands', 'foo.md'), '---\nname: demo:foo\ndescription: foo\nmodel: sonnet\n---\n\nFoo command.\n', ); await mkdir(join(pluginRoot, 'skills', 'bar'), { recursive: true }); await writeFile( join(pluginRoot, 'skills', 'bar', 'SKILL.md'), '---\nname: bar\ndescription: bar skill\n---\n\nBar skill body.\n', ); await mkdir(join(pluginRoot, 'hooks'), { recursive: true }); await writeFile( join(pluginRoot, 'hooks', 'hooks.json'), JSON.stringify({ hooks: { PostToolUse: [{ hooks: [{ type: 'command', command: 'demo-hook.sh' }] }], }, }, null, 2), ); // ~/.claude.json with projects slice matching the repo root await writeFile( join(fakeHome, '.claude.json'), JSON.stringify({ projects: { [root]: { mcpServers: { gamma: { command: 'gamma-server' }, }, disabledMcpjsonServers: ['beta'], }, }, }, null, 2), ); return { root, fakeHome, pluginRoot }; } // ───────────────────────────────────────────────────────────────────────── // estimateTokens // ───────────────────────────────────────────────────────────────────────── describe('estimateTokens', () => { it('markdown: 4 chars per token, rounded up', () => { assert.equal(estimateTokens(400, 'markdown'), 100); assert.equal(estimateTokens(401, 'markdown'), 101); assert.equal(estimateTokens(0, 'markdown'), 0); }); it('json: 3.5 chars per token, rounded up', () => { assert.equal(estimateTokens(350, 'json'), 100); assert.equal(estimateTokens(100, 'json'), 29); }); it('frontmatter: caps at 600 bytes / 150 tokens', () => { assert.equal(estimateTokens(100, 'frontmatter'), 25); assert.equal(estimateTokens(600, 'frontmatter'), 150); assert.equal(estimateTokens(10_000, 'frontmatter'), 150); }); it('item: flat 15 regardless of bytes', () => { assert.equal(estimateTokens(0, 'item'), 15); assert.equal(estimateTokens(9999, 'item'), 15); }); it('defaults to markdown when kind omitted', () => { assert.equal(estimateTokens(400), 100); }); it('handles invalid bytes gracefully', () => { assert.equal(estimateTokens(-1, 'markdown'), 0); assert.equal(estimateTokens(NaN, 'markdown'), 0); }); // v5 F2: differentiated MCP estimate it('mcp: 0 bytes → at least 500 (base overhead floor)', () => { assert.ok(estimateTokens(0, 'mcp') >= 500, `expected >= 500, got ${estimateTokens(0, 'mcp')}`); }); it('mcp: with toolCount: 10 → at least 2000', () => { assert.ok(estimateTokens(0, 'mcp', { toolCount: 10 }) >= 2000, `expected >= 2000, got ${estimateTokens(0, 'mcp', { toolCount: 10 })}`); }); it('mcp: ratio mcp/item ≥ 30 for 10-tool server', () => { const mcp = estimateTokens(0, 'mcp', { toolCount: 10 }); const item = estimateTokens(0, 'item'); assert.ok(mcp / item >= 30, `expected ratio >= 30, got mcp=${mcp} item=${item} ratio=${mcp / item}`); }); it('mcp: with bytes uses json-rate floor', () => { // 700 bytes JSON ≈ 200 tokens, but mcp keeps 500 floor assert.equal(estimateTokens(700, 'mcp'), 500); // 3500 bytes JSON = 1000 tokens, exceeds floor assert.equal(estimateTokens(3500, 'mcp'), 1000); }); }); // ───────────────────────────────────────────────────────────────────────── // stripInjectedHtmlComments / effectiveMemoryBytes (M-BUG-6) // Claude Code strips block-level HTML comments from a CLAUDE.md/memory file // before injecting it into context (code.claude.com/docs/en/memory), preserving // them only inside fenced code blocks. A byte-accurate token estimate must // discount them. Inline comments (text on the same line) are conservatively // retained — only block-level stripping is verified behavior. // ───────────────────────────────────────────────────────────────────────── describe('stripInjectedHtmlComments (M-BUG-6)', () => { it('strips a single-line block-level comment outside code fences', () => { const src = '# Title\n\n\n\nBody text.\n'; const out = stripInjectedHtmlComments(src); assert.ok(!out.includes('maintainer note'), 'comment text should be removed'); assert.ok(out.includes('# Title') && out.includes('Body text.'), 'surrounding content preserved'); }); it('strips a multi-line block comment outside fences', () => { const src = 'A\n\nB\n'; const out = stripInjectedHtmlComments(src); assert.ok(!out.includes('line one') && !out.includes('line two'), 'all comment lines removed'); assert.ok(out.includes('A') && out.includes('B'), 'surrounding content preserved'); }); it('preserves an HTML comment inside a ``` fenced code block', () => { const src = '# Title\n\n```html\n\n```\n'; const out = stripInjectedHtmlComments(src); assert.ok(out.includes('kept: this is example code'), 'fenced comment must be preserved (CC keeps it)'); }); it('preserves an HTML comment inside a ~~~ fenced code block', () => { const src = '~~~\n\n~~~\n'; const out = stripInjectedHtmlComments(src); assert.ok(out.includes('kept tilde'), 'tilde-fenced comment must be preserved'); }); it('keeps inline comments (only block-level stripping is verified)', () => { // Text on the same line as the comment → conservatively retained; the // verified CC behavior covers block-level comments only (Verifiseringsplikt). const src = 'Visible tail\n'; assert.equal(stripInjectedHtmlComments(src), src); }); it('returns content unchanged when there are no comments', () => { const src = '# Plain\n\nNo comments here.\n'; assert.equal(stripInjectedHtmlComments(src), src); }); it('handles empty and non-string input', () => { assert.equal(stripInjectedHtmlComments(''), ''); assert.equal(stripInjectedHtmlComments(undefined), ''); assert.equal(stripInjectedHtmlComments(null), ''); }); }); describe('effectiveMemoryBytes (M-BUG-6)', () => { it('discounts out-of-fence block comments from the byte count', () => { const src = '# Title\n\n\n\nBody.\n'; const raw = Buffer.byteLength(src, 'utf8'); const eff = effectiveMemoryBytes(src); assert.ok(eff < raw, `effective (${eff}) should be below raw (${raw})`); }); it('counts comments inside fences (CC keeps them)', () => { const src = '```\n\n```\n'; assert.equal(effectiveMemoryBytes(src), Buffer.byteLength(src, 'utf8')); }); it('equals raw bytes when no comments are present', () => { const src = '# Plain markdown\n\nbody\n'; assert.equal(effectiveMemoryBytes(src), Buffer.byteLength(src, 'utf8')); }); it('returns 0 for non-string input', () => { assert.equal(effectiveMemoryBytes(undefined), 0); assert.equal(effectiveMemoryBytes(null), 0); }); }); // ───────────────────────────────────────────────────────────────────────── // detectGitRoot // ───────────────────────────────────────────────────────────────────────── describe('detectGitRoot', () => { let root; before(async () => { root = uniqueDir('git'); await mkdir(join(root, '.git'), { recursive: true }); await mkdir(join(root, 'src', 'deep'), { recursive: true }); await writeFile(join(root, '.git', 'HEAD'), '\n'); }); after(async () => { await rm(root, { recursive: true, force: true }); }); it('finds .git in start dir', async () => { const result = await detectGitRoot(root); assert.equal(result, resolve(root)); }); it('walks up to find .git', async () => { const result = await detectGitRoot(join(root, 'src', 'deep')); assert.equal(result, resolve(root)); }); it('returns null when no .git in chain', async () => { const noGit = uniqueDir('nogit'); await mkdir(noGit, { recursive: true }); try { const result = await detectGitRoot(noGit); // Could resolve to outer repo (the plugin repo) if tmpdir happens to be nested. // Accept null OR a path that is NOT noGit itself. if (result !== null) { assert.notEqual(result, resolve(noGit)); } } finally { await rm(noGit, { recursive: true, force: true }); } }); }); // ───────────────────────────────────────────────────────────────────────── // walkClaudeMdCascade // ───────────────────────────────────────────────────────────────────────── describe('walkClaudeMdCascade', () => { let fixture; let originalHome; beforeEach(async () => { fixture = await buildRichRepo(uniqueDir('cascade')); originalHome = process.env.HOME; process.env.HOME = fixture.fakeHome; }); afterEach(async () => { process.env.HOME = originalHome; await rm(fixture.root, { recursive: true, force: true }); }); it('returns files in load order (user first, then project, then imports)', async () => { const result = await walkClaudeMdCascade(fixture.root); const scopes = result.files.map(f => f.scope); assert.ok(scopes.includes('user'), 'expected user scope'); assert.ok(scopes.includes('project'), 'expected project scope'); assert.ok(scopes.includes('import'), 'expected import scope'); // user CLAUDE.md should come before project CLAUDE.md const userIdx = result.files.findIndex(f => f.scope === 'user'); const projIdx = result.files.findIndex(f => f.scope === 'project'); assert.ok(userIdx < projIdx, 'user scope must come before project'); }); it('resolves @imports and marks them with parent', async () => { const result = await walkClaudeMdCascade(fixture.root); const imp = result.files.find(f => f.path.endsWith('docs/conv.md')); assert.ok(imp, 'import should be discovered'); assert.equal(imp.scope, 'import'); assert.ok(imp.parent && imp.parent.endsWith('CLAUDE.md')); }); it('counts bytes and lines', async () => { const result = await walkClaudeMdCascade(fixture.root); assert.ok(result.totalBytes > 0); assert.ok(result.totalLines > 0); for (const f of result.files) { assert.ok(f.bytes > 0); assert.ok(f.lines > 0); } }); it('computes estimatedTokens via markdown heuristic', async () => { const result = await walkClaudeMdCascade(fixture.root); assert.equal(result.estimatedTokens, Math.ceil(result.totalBytes / 4)); }); it('discounts block-level HTML comments from estimatedTokens (M-BUG-6)', async () => { // CC strips block-level HTML comments before injection, so a CLAUDE.md // padded with a maintainer-note comment must estimate FEWER tokens than its // raw byte size would imply — totalBytes stays the honest on-disk figure. const comment = ``; await writeFile( join(fixture.root, 'CLAUDE.md'), `# Project Instructions\n\n${comment}\n\nBuild with care.\n`, ); const result = await walkClaudeMdCascade(fixture.root); assert.ok( result.estimatedTokens < Math.ceil(result.totalBytes / 4), `expected discounted tokens (${result.estimatedTokens}) below raw heuristic ` + `(${Math.ceil(result.totalBytes / 4)})`, ); }); it('handles missing user CLAUDE.md gracefully', async () => { // Remove user CLAUDE.md await rm(join(fixture.fakeHome, '.claude', 'CLAUDE.md')); const result = await walkClaudeMdCascade(fixture.root); const userFiles = result.files.filter(f => f.scope === 'user'); assert.equal(userFiles.length, 0); }); }); // ───────────────────────────────────────────────────────────────────────── // readClaudeJsonProjectSlice // ───────────────────────────────────────────────────────────────────────── describe('readClaudeJsonProjectSlice', () => { let fixture; let originalHome; beforeEach(async () => { fixture = await buildRichRepo(uniqueDir('slice')); originalHome = process.env.HOME; process.env.HOME = fixture.fakeHome; }); afterEach(async () => { process.env.HOME = originalHome; await rm(fixture.root, { recursive: true, force: true }); }); it('finds exact-match project key', async () => { const slice = await readClaudeJsonProjectSlice(fixture.root); assert.equal(slice.projectKey, fixture.root); assert.deepEqual(slice.disabledMcpjsonServers, ['beta']); assert.ok('gamma' in slice.mcpServers); }); it('returns empty slice when no .claude.json exists', async () => { await rm(join(fixture.fakeHome, '.claude.json')); const slice = await readClaudeJsonProjectSlice(fixture.root); assert.equal(slice.projectKey, null); assert.deepEqual(slice.mcpServers, {}); }); it('longest-prefix match: deeper key wins over shallower', async () => { // Rewrite .claude.json with two keys — ancestor and the repo const parent = dirname(fixture.root); const content = JSON.stringify({ projects: { [parent]: { mcpServers: { shallow: { command: 'shallow' } } }, [fixture.root]: { mcpServers: { deep: { command: 'deep' } } }, }, }, null, 2); await writeFile(join(fixture.fakeHome, '.claude.json'), content); const slice = await readClaudeJsonProjectSlice(fixture.root); assert.equal(slice.projectKey, fixture.root); assert.ok('deep' in slice.mcpServers); assert.ok(!('shallow' in slice.mcpServers)); }); it('ancestor prefix matches when target is a subdir of a key', async () => { const parent = dirname(fixture.root); await writeFile( join(fixture.fakeHome, '.claude.json'), JSON.stringify({ projects: { [parent]: { mcpServers: { anc: {} } } } }, null, 2), ); const slice = await readClaudeJsonProjectSlice(fixture.root); assert.equal(slice.projectKey, parent); }); it('returns null projectKey when no key matches', async () => { await writeFile( join(fixture.fakeHome, '.claude.json'), JSON.stringify({ projects: { '/some/other/path': {} } }, null, 2), ); const slice = await readClaudeJsonProjectSlice(fixture.root); assert.equal(slice.projectKey, null); }); }); // ───────────────────────────────────────────────────────────────────────── // enumeratePlugins // ───────────────────────────────────────────────────────────────────────── describe('enumeratePlugins', () => { let fixture; let originalHome; beforeEach(async () => { fixture = await buildRichRepo(uniqueDir('plugins')); originalHome = process.env.HOME; process.env.HOME = fixture.fakeHome; }); afterEach(async () => { process.env.HOME = originalHome; await rm(fixture.root, { recursive: true, force: true }); }); it('discovers plugin and reads plugin.json version', async () => { const plugins = await enumeratePlugins(); assert.ok(plugins.length >= 1); const demo = plugins.find(p => p.name === 'demo'); assert.ok(demo, 'demo plugin should be discovered'); assert.equal(demo.version, '0.1.0'); }); it('counts commands, skills, hooks', async () => { const plugins = await enumeratePlugins(); const demo = plugins.find(p => p.name === 'demo'); assert.equal(demo.commands, 1); assert.equal(demo.skills, 1); assert.equal(demo.hooks, 1); }); it('returns empty array when HOME has no plugins', async () => { process.env.HOME = uniqueDir('empty'); await mkdir(process.env.HOME, { recursive: true }); try { const plugins = await enumeratePlugins(); assert.deepEqual(plugins, []); } finally { await rm(process.env.HOME, { recursive: true, force: true }); } }); }); // ───────────────────────────────────────────────────────────────────────── // enumeratePlugins — enabledPlugins + installed_plugins.json (M-BUG-1) // ───────────────────────────────────────────────────────────────────────── /** * Build a fake HOME that mirrors a real machine: an installed_plugins.json * manifest (polyrepo plugins live under plugins/cache, monorepo ones under * plugins/marketplaces) + an enabledPlugins toggle map in settings.json. * * The enumerator must inject ONLY enabled plugins, resolving each to its ACTIVE * installPath — including cache/ paths the marketplaces walk never sees — and * must NOT count disabled plugins, plugins absent from enabledPlugins, or * marketplaces plugins absent from the manifest entirely (the real M-BUG-1). */ async function buildManifestHome(home) { const cacheRoot = join(home, '.claude', 'plugins', 'cache', 'mp'); const mpRoot = join(home, '.claude', 'plugins', 'marketplaces', 'mp', 'plugins'); async function makePlugin(root, name, version) { await mkdir(join(root, '.claude-plugin'), { recursive: true }); await writeFile( join(root, '.claude-plugin', 'plugin.json'), JSON.stringify({ name, description: name, version }, null, 2), ); await mkdir(join(root, 'agents'), { recursive: true }); await writeFile( join(root, 'agents', `${name}-agent.md`), `---\nname: ${name}-agent\ndescription: ${name} agent\n---\nbody\n`, ); } const enabledCachePath = join(cacheRoot, 'enabled-cache', '1.0.0'); const disabledCachePath = join(cacheRoot, 'disabled-cache', '1.0.0'); const enabledMpPath = join(mpRoot, 'enabled-mp'); const notToggledMpPath = join(mpRoot, 'not-toggled-mp'); const ghostMpPath = join(mpRoot, 'ghost-mp'); // on disk, absent from manifest await makePlugin(enabledCachePath, 'enabled-cache', '1.0.0'); await makePlugin(disabledCachePath, 'disabled-cache', '1.0.0'); await makePlugin(enabledMpPath, 'enabled-mp', '0.1.0'); await makePlugin(notToggledMpPath, 'not-toggled-mp', '0.1.0'); await makePlugin(ghostMpPath, 'ghost-mp', '0.1.0'); await mkdir(join(home, '.claude', 'plugins'), { recursive: true }); await writeFile( join(home, '.claude', 'plugins', 'installed_plugins.json'), JSON.stringify({ version: 2, plugins: { 'enabled-cache@mp': [{ scope: 'user', installPath: enabledCachePath, version: '1.0.0' }], 'disabled-cache@mp': [{ scope: 'user', installPath: disabledCachePath, version: '1.0.0' }], 'enabled-mp@mp': [{ scope: 'user', installPath: enabledMpPath, version: '0.1.0' }], 'not-toggled-mp@mp': [{ scope: 'user', installPath: notToggledMpPath, version: '0.1.0' }], // ghost-mp deliberately absent from the manifest }, }, null, 2), ); await writeFile( join(home, '.claude', 'settings.json'), JSON.stringify({ enabledPlugins: { 'enabled-cache@mp': true, 'disabled-cache@mp': false, 'enabled-mp@mp': true, // not-toggled-mp@mp deliberately absent (treated as not enabled) }, }, null, 2), ); } describe('enumeratePlugins — enabledPlugins + installed_plugins.json (M-BUG-1)', () => { let home, originalHome; beforeEach(async () => { home = uniqueDir('manifest-home'); await mkdir(home, { recursive: true }); originalHome = process.env.HOME; process.env.HOME = home; }); afterEach(async () => { process.env.HOME = originalHome; await rm(home, { recursive: true, force: true }); }); it('injects only ENABLED plugins (honors enabledPlugins toggle)', async () => { await buildManifestHome(home); const names = (await enumeratePlugins()).map(p => p.name).sort(); assert.deepEqual(names, ['enabled-cache', 'enabled-mp']); }); it('excludes disabled, not-toggled, and manifest-absent (ghost) plugins', async () => { await buildManifestHome(home); const names = (await enumeratePlugins()).map(p => p.name); assert.ok(!names.includes('disabled-cache'), 'disabled plugin must not be injected'); assert.ok(!names.includes('not-toggled-mp'), 'plugin absent from enabledPlugins must not be injected'); assert.ok(!names.includes('ghost-mp'), 'marketplaces plugin absent from manifest must not be injected'); }); it('resolves an enabled plugin from plugins/cache (polyrepo installPath)', async () => { await buildManifestHome(home); const cached = (await enumeratePlugins()).find(p => p.name === 'enabled-cache'); assert.ok(cached, 'cache-installed enabled plugin must be discovered'); assert.ok(cached.path.includes(join('plugins', 'cache')), `expected a cache/ path, got ${cached.path}`); assert.equal(cached.agents, 1); }); it('falls back to the marketplaces walk when installed_plugins.json is absent', async () => { // No manifest → cannot tell enabled from installed → discover all on disk. const legacyRoot = join(home, '.claude', 'plugins', 'marketplaces', 'mp', 'plugins', 'legacy'); await mkdir(join(legacyRoot, '.claude-plugin'), { recursive: true }); await writeFile( join(legacyRoot, '.claude-plugin', 'plugin.json'), JSON.stringify({ name: 'legacy', version: '9.9.9' }, null, 2), ); const plugins = await enumeratePlugins(); assert.ok(plugins.find(p => p.name === 'legacy'), 'legacy marketplaces plugin discovered via fallback'); }); }); // ───────────────────────────────────────────────────────────────────────── // enumerateSkills // ───────────────────────────────────────────────────────────────────────── describe('enumerateSkills', () => { let fixture; let originalHome; beforeEach(async () => { fixture = await buildRichRepo(uniqueDir('skills')); originalHome = process.env.HOME; process.env.HOME = fixture.fakeHome; }); afterEach(async () => { process.env.HOME = originalHome; await rm(fixture.root, { recursive: true, force: true }); }); it('finds plugin skills', async () => { const plugins = await enumeratePlugins(); const skills = await enumerateSkills(plugins); const bar = skills.find(s => s.name === 'bar'); assert.ok(bar, 'plugin skill should be discovered'); assert.equal(bar.source, 'plugin'); assert.equal(bar.pluginName, 'demo'); }); it('finds user skills', async () => { // Add a user skill await mkdir(join(fixture.fakeHome, '.claude', 'skills', 'userskill'), { recursive: true }); await writeFile( join(fixture.fakeHome, '.claude', 'skills', 'userskill', 'SKILL.md'), '# user skill\n', ); const skills = await enumerateSkills([]); const userSkill = skills.find(s => s.name === 'userskill'); assert.ok(userSkill, 'user skill should be discovered'); assert.equal(userSkill.source, 'user'); }); }); // ───────────────────────────────────────────────────────────────────────── // readActiveHooks // ───────────────────────────────────────────────────────────────────────── describe('readActiveHooks', () => { let fixture; let originalHome; beforeEach(async () => { fixture = await buildRichRepo(uniqueDir('hooks')); originalHome = process.env.HOME; process.env.HOME = fixture.fakeHome; }); afterEach(async () => { process.env.HOME = originalHome; await rm(fixture.root, { recursive: true, force: true }); }); it('merges hooks from user + project + plugin', async () => { const plugins = await enumeratePlugins(); const hooks = await readActiveHooks(fixture.root, plugins); const sources = new Set(hooks.map(h => h.source)); assert.ok(sources.has('user'), 'user hook present'); assert.ok(sources.has('project'), 'project hook present'); assert.ok([...sources].some(s => s.startsWith('plugin:')), 'plugin hook present'); }); it('does not dedupe across scopes', async () => { // Add duplicate hook in user and project settings const dupeHook = { hooks: { PreToolUse: [{ matcher: 'Bash', hooks: [{ type: 'command', command: 'same.sh' }] }] }, }; await writeFile(join(fixture.fakeHome, '.claude', 'settings.json'), JSON.stringify(dupeHook)); await writeFile(join(fixture.root, '.claude', 'settings.json'), JSON.stringify(dupeHook)); const hooks = await readActiveHooks(fixture.root, []); const sameCmd = hooks.filter(h => h.command === 'same.sh'); assert.equal(sameCmd.length, 2, 'should report both occurrences'); }); }); // ───────────────────────────────────────────────────────────────────────── // readActiveMcpServers // ───────────────────────────────────────────────────────────────────────── describe('readActiveMcpServers', () => { let fixture; let originalHome; beforeEach(async () => { fixture = await buildRichRepo(uniqueDir('mcp')); originalHome = process.env.HOME; process.env.HOME = fixture.fakeHome; }); afterEach(async () => { process.env.HOME = originalHome; await rm(fixture.root, { recursive: true, force: true }); }); it('merges project .mcp.json + .claude.json slice', async () => { const servers = await readActiveMcpServers(fixture.root); const names = servers.map(s => s.name); assert.ok(names.includes('alpha'), 'alpha from project'); assert.ok(names.includes('beta'), 'beta from project'); assert.ok(names.includes('gamma'), 'gamma from .claude.json'); }); it('honors disabledMcpjsonServers', async () => { const servers = await readActiveMcpServers(fixture.root); const beta = servers.find(s => s.name === 'beta'); assert.equal(beta.enabled, false); assert.equal(beta.disabledBy, 'disabledMcpjsonServers'); const alpha = servers.find(s => s.name === 'alpha'); assert.equal(alpha.enabled, true); assert.equal(alpha.disabledBy, null); }); it('estimatedTokens >= 500 for every MCP server (v5 F2)', async () => { const servers = await readActiveMcpServers(fixture.root); assert.ok(servers.length > 0, 'fixture should produce MCP servers'); for (const s of servers) { assert.ok(s.estimatedTokens >= 500, `${s.name} from ${s.source} has estimatedTokens=${s.estimatedTokens}, expected >= 500`); } }); }); // ───────────────────────────────────────────────────────────────────────── // readActiveMcpServers — tool-count detection (v5 M1) // ───────────────────────────────────────────────────────────────────────── describe('readActiveMcpServers — tool-count detection (v5 M1)', () => { it('detects toolCount from project node_modules//package.json', async () => { const fixturePath = resolve(import.meta.dirname || dirname(new URL(import.meta.url).pathname), '..', 'fixtures', 'mcp-tool-heavy'); const servers = await readActiveMcpServers(fixturePath); const heavy = servers.find(s => s.name === 'heavy'); assert.ok(heavy, 'expected heavy server from fixture'); assert.equal(heavy.toolCount, 20, `expected toolCount=20, got ${heavy.toolCount}`); assert.equal(heavy.toolCountUnknown, false); }); it('falls back to toolCount: null + toolCountUnknown: true when manifest missing', async () => { const fixturePath = resolve(import.meta.dirname || dirname(new URL(import.meta.url).pathname), '..', 'fixtures', 'mcp-tool-heavy'); const servers = await readActiveMcpServers(fixturePath); const light = servers.find(s => s.name === 'light'); assert.ok(light, 'expected light server from fixture'); assert.equal(light.toolCount, null); assert.equal(light.toolCountUnknown, true); }); it('detects toolCount from cache file in $HOME/.claude/config-audit/mcp-cache/', async () => { const fakeHome = uniqueDir('mcp-cache'); const repoRoot = uniqueDir('mcp-cache-repo'); await mkdir(repoRoot, { recursive: true }); await writeFile( join(repoRoot, '.mcp.json'), JSON.stringify({ mcpServers: { cached: { command: 'npx', args: ['unknown-pkg'] } } }, null, 2), ); await mkdir(join(fakeHome, '.claude', 'config-audit', 'mcp-cache'), { recursive: true }); await writeFile( join(fakeHome, '.claude', 'config-audit', 'mcp-cache', 'cached.json'), JSON.stringify({ tools: Array.from({ length: 12 }, (_, i) => ({ name: `t${i}` })) }, null, 2), ); const originalHome = process.env.HOME; process.env.HOME = fakeHome; try { const servers = await readActiveMcpServers(repoRoot); const cached = servers.find(s => s.name === 'cached'); assert.ok(cached, 'expected cached server'); assert.equal(cached.toolCount, 12, `expected toolCount=12 from cache, got ${cached.toolCount}`); assert.equal(cached.toolCountUnknown, false); } finally { process.env.HOME = originalHome; await rm(fakeHome, { recursive: true, force: true }); await rm(repoRoot, { recursive: true, force: true }); } }); it('toolCount drives estimateTokens (heavy > light)', async () => { const fixturePath = resolve(import.meta.dirname || dirname(new URL(import.meta.url).pathname), '..', 'fixtures', 'mcp-tool-heavy'); const servers = await readActiveMcpServers(fixturePath); const heavy = servers.find(s => s.name === 'heavy'); const light = servers.find(s => s.name === 'light'); assert.ok(heavy.estimatedTokens > light.estimatedTokens, `expected heavy (${heavy.estimatedTokens}) > light (${light.estimatedTokens})`); }); }); // ───────────────────────────────────────────────────────────────────────── // readActiveConfig (integration) // ───────────────────────────────────────────────────────────────────────── describe('readActiveConfig (integration)', () => { let fixture; let originalHome; beforeEach(async () => { fixture = await buildRichRepo(uniqueDir('full')); originalHome = process.env.HOME; process.env.HOME = fixture.fakeHome; }); afterEach(async () => { process.env.HOME = originalHome; await rm(fixture.root, { recursive: true, force: true }); }); it('produces expected top-level shape', async () => { const result = await readActiveConfig(fixture.root); const keys = Object.keys(result).sort(); assert.deepEqual(keys, [ 'agents', 'claudeMd', 'hooks', 'mcpServers', 'meta', 'outputStyles', 'plugins', 'rules', 'settings', 'skills', 'suggestDisables', 'totals', 'warnings', ]); }); it('meta contains required fields', async () => { const result = await readActiveConfig(fixture.root); assert.equal(result.meta.tool, 'config-audit:whats-active'); assert.equal(result.meta.version, '1.0.0'); assert.ok(typeof result.meta.generatedAt === 'string'); assert.equal(result.meta.repoPath, resolve(fixture.root)); assert.equal(result.meta.gitRoot, resolve(fixture.root)); assert.equal(result.meta.projectKey, fixture.root); assert.ok(typeof result.meta.durationMs === 'number'); }); it('settings cascade reflects all three layers', async () => { const result = await readActiveConfig(fixture.root); const scopes = result.settings.cascade.map(c => c.scope); assert.deepEqual(scopes, ['user', 'project', 'local']); const user = result.settings.cascade.find(c => c.scope === 'user'); const project = result.settings.cascade.find(c => c.scope === 'project'); assert.equal(user.exists, true); assert.equal(project.exists, true); }); it('totals.grandTotal equals sum of category subtotals', async () => { const result = await readActiveConfig(fixture.root); const t = result.totals.estimatedTokens; assert.equal(t.grandTotal, t.claudeMd + t.plugins + t.skills + t.mcpServers + t.hooks + t.rules + t.agents + t.outputStyles); }); it('performance budget: durationMs < 2000', async () => { const result = await readActiveConfig(fixture.root); assert.ok(result.meta.durationMs < 2000, `expected < 2000ms, got ${result.meta.durationMs}ms`); }); it('token estimate within ±20% of hand-computed value', async () => { const result = await readActiveConfig(fixture.root); const expectedClaudeMd = Math.ceil(result.claudeMd.totalBytes / 4); const low = Math.floor(expectedClaudeMd * 0.8); const high = Math.ceil(expectedClaudeMd * 1.2); assert.ok( result.totals.estimatedTokens.claudeMd >= low && result.totals.estimatedTokens.claudeMd <= high, `claudeMd tokens ${result.totals.estimatedTokens.claudeMd} outside [${low}, ${high}]`, ); }); it('suggestDisables is null by default, object when flag set', async () => { const noFlag = await readActiveConfig(fixture.root); assert.equal(noFlag.suggestDisables, null); const withFlag = await readActiveConfig(fixture.root, { suggestDisables: true }); assert.ok(withFlag.suggestDisables && Array.isArray(withFlag.suggestDisables.candidates)); }); it('suggestDisables flags disabled MCP servers', async () => { const result = await readActiveConfig(fixture.root, { suggestDisables: true }); const betaCandidate = result.suggestDisables.candidates.find( c => c.kind === 'mcp' && c.name === 'beta', ); assert.ok(betaCandidate, 'beta should be flagged as already disabled'); assert.equal(betaCandidate.confidence, 'high'); }); it('enumerates rules/agents/outputStyles into the result and totals', async () => { const result = await readActiveConfig(fixture.root); // buildRichRepo ships one unscoped project rule (.claude/rules/team.md) assert.ok(Array.isArray(result.rules) && result.rules.length >= 1); const team = result.rules.find(r => r.name === 'team.md'); assert.ok(team, 'team.md rule should be enumerated'); assert.equal(team.loadPattern, 'always'); assert.ok(Array.isArray(result.agents)); assert.ok(Array.isArray(result.outputStyles)); assert.equal(result.totals.rules, result.rules.length); assert.equal(result.totals.agents, result.agents.length); assert.equal(result.totals.outputStyles, result.outputStyles.length); }); }); // ───────────────────────────────────────────────────────────────────────── // v5.6 Foundation — deriveLoadPattern + rule/agent/output-style enumeration // ───────────────────────────────────────────────────────────────────────── describe('deriveLoadPattern (v5.6)', () => { const cases = [ ['claude-md-root', {}, 'always', 'yes', 'confirmed'], ['claude-md-nested', {}, 'on-demand', 'no', 'confirmed'], ['claude-md-user', {}, 'always', 'yes', 'inferred'], ['claude-md-managed', {}, 'always', 'yes', 'inferred'], ['claude-md-import', {}, 'always', 'yes', 'inferred'], ['rule', { scoped: true }, 'on-demand', 'no', 'confirmed'], ['rule', { scoped: false }, 'always', 'yes', 'confirmed'], ['skill-listing', {}, 'always', 'n/a', 'confirmed'], ['skill-body', {}, 'on-demand', 'n/a', 'confirmed'], ['agent', {}, 'always', 'n/a', 'inferred'], ['output-style', {}, 'always', 'yes', 'confirmed'], ['hook', {}, 'external', 'n/a', 'confirmed'], ['mcp', {}, 'always', 'yes', 'inferred'], // v5.6 B2 — kinds surfaced by the token-hotspots discovery surface. ['command', {}, 'on-demand', 'n/a', 'inferred'], // body loads on /invoke ['harness-config', {}, 'external', 'n/a', 'inferred'], // settings/manifests: not model context ]; for (const [kind, opts, loadPattern, survivesCompaction, derivationConfidence] of cases) { const label = `${kind}${opts.scoped !== undefined ? ` (scoped=${opts.scoped})` : ''}`; it(`${label} → ${loadPattern}/${survivesCompaction}/${derivationConfidence}`, () => { assert.deepEqual(deriveLoadPattern(kind, opts), { loadPattern, survivesCompaction, derivationConfidence, }); }); } it('falls back safely for an unknown kind', () => { assert.deepEqual(deriveLoadPattern('nope'), { loadPattern: 'unknown', survivesCompaction: 'n/a', derivationConfidence: 'inferred', }); }); }); describe('enumerateRules (v5.6)', () => { let root, emptyHome, originalHome; beforeEach(async () => { root = uniqueDir('rules'); emptyHome = uniqueDir('rules-home'); await mkdir(emptyHome, { recursive: true }); originalHome = process.env.HOME; process.env.HOME = emptyHome; }); afterEach(async () => { process.env.HOME = originalHome; await rm(root, { recursive: true, force: true }); await rm(emptyHome, { recursive: true, force: true }); }); it('tags scoped vs unscoped project rules (block-sequence paths detected)', async () => { await mkdir(join(root, '.claude', 'rules'), { recursive: true }); await writeFile(join(root, '.claude', 'rules', 'always.md'), '# Always-on rule\n'); await writeFile( join(root, '.claude', 'rules', 'scoped.md'), '---\npaths:\n - src/**/*.ts\n---\n# Scoped rule\n', ); const rules = await enumerateRules(root, []); const always = rules.find(r => r.name === 'always.md'); const scoped = rules.find(r => r.name === 'scoped.md'); assert.equal(always.scoped, false); assert.equal(always.loadPattern, 'always'); assert.equal(always.survivesCompaction, 'yes'); assert.equal(always.source, 'project'); assert.equal(scoped.scoped, true); assert.equal(scoped.loadPattern, 'on-demand'); assert.equal(scoped.survivesCompaction, 'no'); }); it('discovers user-level rules', async () => { await mkdir(join(emptyHome, '.claude', 'rules'), { recursive: true }); await writeFile(join(emptyHome, '.claude', 'rules', 'user.md'), '# User rule\n'); const rules = await enumerateRules(root, []); const user = rules.find(r => r.name === 'user.md'); assert.ok(user); assert.equal(user.source, 'user'); }); it('returns [] when there are no rules', async () => { await mkdir(root, { recursive: true }); assert.deepEqual(await enumerateRules(root, []), []); }); }); describe('enumerateAgents (v5.6)', () => { let root, emptyHome, originalHome; beforeEach(async () => { root = uniqueDir('agents'); emptyHome = uniqueDir('agents-home'); await mkdir(emptyHome, { recursive: true }); originalHome = process.env.HOME; process.env.HOME = emptyHome; }); afterEach(async () => { process.env.HOME = originalHome; await rm(root, { recursive: true, force: true }); await rm(emptyHome, { recursive: true, force: true }); }); it('enumerates project agents as always-loaded (inferred)', async () => { await mkdir(join(root, '.claude', 'agents'), { recursive: true }); await writeFile( join(root, '.claude', 'agents', 'reviewer.md'), '---\nname: reviewer\ndescription: reviews code\n---\nbody\n', ); const agents = await enumerateAgents(root, []); const a = agents.find(x => x.name === 'reviewer'); assert.ok(a); assert.equal(a.source, 'project'); assert.equal(a.loadPattern, 'always'); assert.equal(a.survivesCompaction, 'n/a'); assert.equal(a.derivationConfidence, 'inferred'); }); it('returns [] when there are no agents', async () => { await mkdir(root, { recursive: true }); assert.deepEqual(await enumerateAgents(root, []), []); }); // M-BUG-5: CC registers a subagent only when its frontmatter declares both // `name` and `description` (docs: identity comes only from `name`; both are // required). Frontmatter-less / incomplete files are registration no-ops that // cost zero always-loaded tokens — they must NOT be counted as agents. it('M-BUG-5: skips files with no frontmatter at all', async () => { const dir = join(root, '.claude', 'agents'); await mkdir(dir, { recursive: true }); await writeFile(join(dir, 'reviewer.md'), '---\nname: reviewer\ndescription: reviews code\n---\nbody\n'); await writeFile(join(dir, 'not-an-agent.md'), '# Not An Agent\n\nJust a markdown doc, no frontmatter.\n'); const agents = await enumerateAgents(root, []); assert.deepEqual(agents.map(a => a.name).sort(), ['reviewer']); }); it('M-BUG-5: skips files missing name or description', async () => { const dir = join(root, '.claude', 'agents'); await mkdir(dir, { recursive: true }); await writeFile(join(dir, 'reviewer.md'), '---\nname: reviewer\ndescription: reviews code\n---\nbody\n'); await writeFile(join(dir, 'name-only.md'), '---\nname: name-only\n---\nbody\n'); await writeFile(join(dir, 'desc-only.md'), '---\ndescription: has no name\n---\nbody\n'); await writeFile(join(dir, 'empty-name.md'), '---\nname:\ndescription: blank name\n---\nbody\n'); const agents = await enumerateAgents(root, []); assert.deepEqual(agents.map(a => a.name).sort(), ['reviewer']); }); // M-BUG-3: CC scans agents dirs recursively, so a valid agent in a subfolder // (e.g. agents/review/security.md) is registered and must be counted. it('M-BUG-3: recurses into agent subdirectories', async () => { const sub = join(root, '.claude', 'agents', 'review'); await mkdir(sub, { recursive: true }); await writeFile(join(sub, 'security.md'), '---\nname: security\ndescription: security review\n---\nbody\n'); const agents = await enumerateAgents(root, []); const a = agents.find(x => x.name === 'security'); assert.ok(a, 'agent in subdir should be counted'); assert.equal(a.source, 'project'); }); }); // M-BUG-4: when repoPath === $HOME (the `manifest --global` self-scan), the // project agents dir resolves to the SAME path as the user agents dir. The // shared configDirs helper must count it once (as user scope), not twice. describe('enumerateAgents — HOME self-scan dedup (M-BUG-4)', () => { let home, originalHome; beforeEach(async () => { home = uniqueDir('agents-home-selfscan'); await mkdir(join(home, '.claude', 'agents'), { recursive: true }); originalHome = process.env.HOME; process.env.HOME = home; }); afterEach(async () => { process.env.HOME = originalHome; await rm(home, { recursive: true, force: true }); }); it('does not double-count user agents when repoPath === HOME', async () => { await writeFile( join(home, '.claude', 'agents', 'solo.md'), '---\nname: solo\ndescription: only one of me\n---\nbody\n', ); const agents = await enumerateAgents(home, []); const solos = agents.filter(a => a.name === 'solo'); assert.equal(solos.length, 1, 'agent at HOME must be counted once, not twice'); assert.equal(solos[0].source, 'user', 'HOME self-scan agents are user scope'); }); }); describe('enumerateOutputStyles (v5.6)', () => { let root, emptyHome, originalHome; beforeEach(async () => { root = uniqueDir('ost'); emptyHome = uniqueDir('ost-home'); await mkdir(emptyHome, { recursive: true }); originalHome = process.env.HOME; process.env.HOME = emptyHome; }); afterEach(async () => { process.env.HOME = originalHome; await rm(root, { recursive: true, force: true }); await rm(emptyHome, { recursive: true, force: true }); }); it('enumerates project + user output styles as always/yes', async () => { await mkdir(join(root, '.claude', 'output-styles'), { recursive: true }); await writeFile( join(root, '.claude', 'output-styles', 'custom.md'), '---\nname: Custom\ndescription: x\n---\nStyle instructions.\n', ); await mkdir(join(emptyHome, '.claude', 'output-styles'), { recursive: true }); await writeFile( join(emptyHome, '.claude', 'output-styles', 'user-style.md'), '---\nname: UserStyle\n---\nMore.\n', ); const styles = await enumerateOutputStyles(root, []); const proj = styles.find(s => s.name === 'custom'); const user = styles.find(s => s.name === 'user-style'); assert.ok(proj && user); assert.equal(proj.source, 'project'); assert.equal(proj.loadPattern, 'always'); assert.equal(proj.survivesCompaction, 'yes'); assert.equal(proj.derivationConfidence, 'confirmed'); assert.equal(user.source, 'user'); }); it('returns [] when there are no output styles', async () => { await mkdir(root, { recursive: true }); assert.deepEqual(await enumerateOutputStyles(root, []), []); }); });