fix(acr): feature-gap scopes presence checks to authored config + reads settings cascade (M-BUG-13)
The GAP scanner's 25 presence checks ran over the full includeGlobal discovery, so this plugin's own examples/optimal-setup (vendored across plugin-cache versions) satisfied every tier-3 check — masking real feature gaps to GAP=0 on ANY target. And the real ~/.claude/settings.json is invisible to the settings-key checks (includeGlobal gotcha + maxFiles cap), which would flip statusLine/autoMode to false positives once the maskers were removed. - isAuthoredConfig: exclude plugin-bundled (~/.claude/plugins/) + nested examples/ and tests/fixtures/ (relPath-relative, so a fixture scanned AS the target keeps its own files) from ctx.files + parsedSettings. - readSettingsCascade: read the user->project->local settings cascade directly and merge into parsedSettings — immune to the discovery cap/gotcha. Empty target: ~0 (masked) -> 18 humanized opportunities; no statusLine/autoMode false positives. Frozen v5.0.0 snapshots + SC-5/6/7 byte-stable (marketplace-medium has no nested demo trees; hermetic-HOME cascade adds nothing). Suite 1350->1355/0. Found by dogfooding feature-gap against the machine. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01683eAqVecv9VZfQzL8CQ9h
This commit is contained in:
parent
b58393099a
commit
f4bf3ae2cb
3 changed files with 207 additions and 5 deletions
|
|
@ -1,6 +1,6 @@
|
|||
import { describe, it, beforeEach } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { resolve, join } from 'node:path';
|
||||
import { resolve, join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
|
|
@ -411,3 +411,98 @@ describe('GAP scanner — filter-before lever wiring (chatty hook fixture)', ()
|
|||
assert.equal(lever, undefined, `expected no filter-before lever; got id=${lever?.id}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GAP scanner — test/demo data must not mask real gaps (M-BUG-13)', () => {
|
||||
// Build a throwaway project (+ optional hermetic HOME settings) and run GAP
|
||||
// exactly as posture --global does: includeGlobal discovery + scan().
|
||||
async function runGap({ projectFiles = {}, homeSettings = null } = {}) {
|
||||
const project = await mkdtemp(join(tmpdir(), 'config-audit-gap-mask-proj-'));
|
||||
const home = await mkdtemp(join(tmpdir(), 'config-audit-gap-mask-home-'));
|
||||
for (const [rel, content] of Object.entries(projectFiles)) {
|
||||
const abs = join(project, rel);
|
||||
await mkdir(dirname(abs), { recursive: true });
|
||||
await writeFile(abs, content);
|
||||
}
|
||||
if (homeSettings) {
|
||||
await mkdir(join(home, '.claude'), { recursive: true });
|
||||
await writeFile(join(home, '.claude', 'settings.json'), JSON.stringify(homeSettings));
|
||||
}
|
||||
const original = process.env.HOME;
|
||||
process.env.HOME = home;
|
||||
try {
|
||||
resetCounter();
|
||||
const discovery = await discoverConfigFiles(project, { includeGlobal: true });
|
||||
const result = await scan(project, discovery);
|
||||
return result;
|
||||
} finally {
|
||||
process.env.HOME = original;
|
||||
await rm(project, { recursive: true, force: true });
|
||||
await rm(home, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
const hasGap = (result, titleRe) =>
|
||||
result.findings.some(f => f.scanner === 'GAP' && titleRe.test(f.title || ''));
|
||||
|
||||
it('a nested examples/ settings.json does NOT satisfy the outputStyle check (settings-key)', async () => {
|
||||
const result = await runGap({
|
||||
projectFiles: {
|
||||
'.claude/CLAUDE.md': '# proj',
|
||||
'examples/optimal-setup/.claude/settings.json': JSON.stringify({ outputStyle: 'Explanatory' }),
|
||||
},
|
||||
});
|
||||
assert.ok(
|
||||
hasGap(result, /output style/i),
|
||||
`example settings.json must not mask the outputStyle gap; got: ${result.findings.map(f => f.title).join(' | ')}`,
|
||||
);
|
||||
});
|
||||
|
||||
it('a nested examples/ keybindings.json does NOT satisfy the keybindings check (file-type)', async () => {
|
||||
const result = await runGap({
|
||||
projectFiles: {
|
||||
'.claude/CLAUDE.md': '# proj',
|
||||
'examples/optimal-setup/.claude/keybindings.json': JSON.stringify({}),
|
||||
},
|
||||
});
|
||||
assert.ok(
|
||||
hasGap(result, /keybinding/i),
|
||||
`example keybindings.json must not mask the keybindings gap; got: ${result.findings.map(f => f.title).join(' | ')}`,
|
||||
);
|
||||
});
|
||||
|
||||
it('a nested tests/fixtures/ settings.json does NOT satisfy the model check', async () => {
|
||||
const result = await runGap({
|
||||
projectFiles: {
|
||||
'.claude/CLAUDE.md': '# proj',
|
||||
'tests/fixtures/demo/.claude/settings.json': JSON.stringify({ model: 'opus' }),
|
||||
},
|
||||
});
|
||||
assert.ok(
|
||||
hasGap(result, /model config/i),
|
||||
`fixture settings.json must not mask the model gap; got: ${result.findings.map(f => f.title).join(' | ')}`,
|
||||
);
|
||||
});
|
||||
|
||||
it("the project's OWN real .claude/settings.json IS counted (not over-excluded)", async () => {
|
||||
const result = await runGap({
|
||||
projectFiles: {
|
||||
'.claude/CLAUDE.md': '# proj',
|
||||
'.claude/settings.json': JSON.stringify({ outputStyle: 'Explanatory' }),
|
||||
},
|
||||
});
|
||||
assert.ok(
|
||||
!hasGap(result, /output style/i),
|
||||
`real project settings.json must satisfy outputStyle; got: ${result.findings.map(f => f.title).join(' | ')}`,
|
||||
);
|
||||
});
|
||||
|
||||
it('the real ~/.claude/settings.json IS seen for settings-key checks (gotcha fix end-to-end)', async () => {
|
||||
const result = await runGap({
|
||||
homeSettings: { statusLine: { type: 'command', command: 'x' } },
|
||||
});
|
||||
assert.ok(
|
||||
!hasGap(result, /status line/i),
|
||||
`~/.claude/settings.json statusLine must be discovered; got: ${result.findings.map(f => f.title).join(' | ')}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue