Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions biome.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
// being linted in-place (the name maps into node_modules, which is not the working
// tree here), so the relative path is the correct seam for self-linting. Consumers
// still extend the bare subpath — see biome/base.jsonc's header.
"$schema": "https://biomejs.dev/schemas/2.5.0/schema.json",
"$schema": "https://biomejs.dev/schemas/2.5.6/schema.json",
"extends": ["./biome/base.jsonc"],
"files": {
"includes": [
Expand All @@ -28,7 +28,12 @@
// templates/ holds literal emitted configs (verbatim fixtures) that may not
// match devkit's own formatting — exclude.
"!templates",
"!bun.lock"
"!bun.lock",
// dist/ is build output — already unlinted (not in the includes allowlist), but a
// single "!" still lets the LSP scanner index it for the module graph, and it is
// rewritten wholesale on every build. Force-ignore ("!!") keeps the scanner out
// entirely, per Biome's own guidance for output folders.
"!!dist"
]
},
"overrides": [
Expand Down
20 changes: 10 additions & 10 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

215 changes: 116 additions & 99 deletions cli/__tests__/checklist-scripts.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -73,72 +73,74 @@ describe('skill checklist script (spawned source)', () => {
expect(state.items.length).toBeGreaterThan(0);
});

it.each(
REVIEW_ROOT_CASES,
)('%s consumes the exact review-mode roots injected by the gate', (skill, envName, stateName) => {
const repo = mkdtempSync(join(tmpdir(), 'checklist-review-roots-'));
dirs.push(repo);
const git = (args) => execFileSync('git', args, { cwd: repo, encoding: 'utf8' });
git(['init', '-q']);
writeFileSync(
join(repo, 'guard.config.json'),
JSON.stringify({ review: { backendRoots: [], frontendRoots: [] } }),
);
mkdirSync(join(repo, 'apps', 'web'), { recursive: true });
mkdirSync(join(repo, 'outside'), { recursive: true });
writeFileSync(
join(repo, 'apps', 'web', 'changed.tsx'),
'export const login = (password) => fetch("/api", { body: password });\n',
);
writeFileSync(join(repo, 'outside', 'ignored.tsx'), 'export const unrelated = true;\n');
git(['add', '.']);
const script = fileURLToPath(
new URL(`../../skills/${skill}/scripts/checklist.mjs`, import.meta.url),
);
const r = spawnSync('node', [script, 'generate'], {
cwd: repo,
encoding: 'utf8',
env: {
...process.env,
DEVKIT_RUN_MODE: 'review',
[envName]: JSON.stringify([' apps/web ']),
},
});
expect(r.status, r.stderr).toBe(0);
const state = JSON.parse(readFileSync(join(repo, '.claude', stateName), 'utf8'));
expect(state.files ?? state.items).not.toHaveLength(0);
expect(JSON.stringify(state)).not.toContain('outside/ignored.tsx');
});
it.each(REVIEW_ROOT_CASES)(
'%s consumes the exact review-mode roots injected by the gate',
(skill, envName, stateName) => {
const repo = mkdtempSync(join(tmpdir(), 'checklist-review-roots-'));
dirs.push(repo);
const git = (args) => execFileSync('git', args, { cwd: repo, encoding: 'utf8' });
git(['init', '-q']);
writeFileSync(
join(repo, 'guard.config.json'),
JSON.stringify({ review: { backendRoots: [], frontendRoots: [] } }),
);
mkdirSync(join(repo, 'apps', 'web'), { recursive: true });
mkdirSync(join(repo, 'outside'), { recursive: true });
writeFileSync(
join(repo, 'apps', 'web', 'changed.tsx'),
'export const login = (password) => fetch("/api", { body: password });\n',
);
writeFileSync(join(repo, 'outside', 'ignored.tsx'), 'export const unrelated = true;\n');
git(['add', '.']);
const script = fileURLToPath(
new URL(`../../skills/${skill}/scripts/checklist.mjs`, import.meta.url),
);
const r = spawnSync('node', [script, 'generate'], {
cwd: repo,
encoding: 'utf8',
env: {
...process.env,
DEVKIT_RUN_MODE: 'review',
[envName]: JSON.stringify([' apps/web ']),
},
});
expect(r.status, r.stderr).toBe(0);
const state = JSON.parse(readFileSync(join(repo, '.claude', stateName), 'utf8'));
expect(state.files ?? state.items).not.toHaveLength(0);
expect(JSON.stringify(state)).not.toContain('outside/ignored.tsx');
},
);

it.each(
CHECKLIST_CASES,
)('%s preserves its artifact for independent review-mode verification', (skill, stateName) => {
const repo = mkdtempSync(join(tmpdir(), 'checklist-review-cleanup-'));
dirs.push(repo);
const stateDir = join(repo, '.claude');
const stateFile = join(stateDir, stateName);
mkdirSync(stateDir, { recursive: true });
writeFileSync(stateFile, '{}');
const script = fileURLToPath(
new URL(`../../skills/${skill}/scripts/checklist.mjs`, import.meta.url),
);
it.each(CHECKLIST_CASES)(
'%s preserves its artifact for independent review-mode verification',
(skill, stateName) => {
const repo = mkdtempSync(join(tmpdir(), 'checklist-review-cleanup-'));
dirs.push(repo);
const stateDir = join(repo, '.claude');
const stateFile = join(stateDir, stateName);
mkdirSync(stateDir, { recursive: true });
writeFileSync(stateFile, '{}');
const script = fileURLToPath(
new URL(`../../skills/${skill}/scripts/checklist.mjs`, import.meta.url),
);

const reviewCleanup = spawnSync('node', [script, 'cleanup'], {
cwd: repo,
encoding: 'utf8',
env: { ...process.env, DEVKIT_RUN_MODE: 'review' },
});
expect(reviewCleanup.status, reviewCleanup.stderr).toBe(0);
expect(existsSync(stateFile)).toBe(true);
const reviewCleanup = spawnSync('node', [script, 'cleanup'], {
cwd: repo,
encoding: 'utf8',
env: { ...process.env, DEVKIT_RUN_MODE: 'review' },
});
expect(reviewCleanup.status, reviewCleanup.stderr).toBe(0);
expect(existsSync(stateFile)).toBe(true);

const normalCleanup = spawnSync('node', [script, 'cleanup'], {
cwd: repo,
encoding: 'utf8',
env: { ...process.env, DEVKIT_RUN_MODE: 'commit' },
});
expect(normalCleanup.status, normalCleanup.stderr).toBe(0);
expect(existsSync(stateFile)).toBe(false);
});
const normalCleanup = spawnSync('node', [script, 'cleanup'], {
cwd: repo,
encoding: 'utf8',
env: { ...process.env, DEVKIT_RUN_MODE: 'commit' },
});
expect(normalCleanup.status, normalCleanup.stderr).toBe(0);
expect(existsSync(stateFile)).toBe(false);
},
);

it('correctness unions scanRoots with injected domain roots outside the static topology', () => {
const repo = mkdtempSync(join(tmpdir(), 'checklist-correctness-review-roots-'));
Expand Down Expand Up @@ -179,45 +181,60 @@ describe('skill checklist script (spawned source)', () => {
expect(JSON.stringify(state)).not.toContain('static-api/excluded.ts');
});

it.each(
REVIEW_ROOT_CASES,
)('%s rejects unsafe injected roots before constructing a Git pathspec', (skill, envName) => {
const repo = repoWithCraftedFile();
const script = fileURLToPath(
new URL(`../../skills/${skill}/scripts/checklist.mjs`, import.meta.url),
);
for (const roots of [
[],
[''],
[' '],
['/outside'],
['../outside'],
['src/../outside'],
['C:\\outside'],
[':(exclude)**'],
['./:(exclude)**'],
[3],
]) {
const r = spawnSync('node', [script, 'generate'], {
it.each(REVIEW_ROOT_CASES)(
'%s rejects unsafe injected roots before constructing a Git pathspec',
(skill, envName, stateName) => {
const repo = repoWithCraftedFile();
// Point the CONFIGURED roots away from the crafted file. Without this, the final
// `' . '` case's `status === 0` also passes when the script ignores the env var
// entirely and falls back to the fixture's backendRoots: ['src'] — so it would
// prove the root was accepted, not that it actually drove the scan.
writeFileSync(
join(repo, 'guard.config.json'),
JSON.stringify({
review: { backendRoots: ['no-such-root'], frontendRoots: ['no-such-root'] },
}),
);
const script = fileURLToPath(
new URL(`../../skills/${skill}/scripts/checklist.mjs`, import.meta.url),
);
for (const roots of [
[],
[''],
[' '],
['/outside'],
['../outside'],
['src/../outside'],
['C:\\outside'],
[':(exclude)**'],
['./:(exclude)**'],
[3],
]) {
const r = spawnSync('node', [script, 'generate'], {
cwd: repo,
encoding: 'utf8',
env: { ...process.env, DEVKIT_RUN_MODE: 'review', [envName]: JSON.stringify(roots) },
});
expect(r.status, `${JSON.stringify(roots)}\n${r.stderr}`).not.toBe(0);
expect(r.stderr).toContain(envName);
}

const dot = spawnSync('node', [script, 'generate'], {
cwd: repo,
encoding: 'utf8',
env: { ...process.env, DEVKIT_RUN_MODE: 'review', [envName]: JSON.stringify(roots) },
env: {
...process.env,
DEVKIT_RUN_MODE: 'review',
[envName]: JSON.stringify([' . ']),
},
});
expect(r.status, `${JSON.stringify(roots)}\n${r.stderr}`).not.toBe(0);
expect(r.stderr).toContain(envName);
}

const dot = spawnSync('node', [script, 'generate'], {
cwd: repo,
encoding: 'utf8',
env: {
...process.env,
DEVKIT_RUN_MODE: 'review',
[envName]: JSON.stringify([' . ']),
},
});
expect(dot.status, dot.stderr).toBe(0);
});
expect(dot.status, dot.stderr).toBe(0);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
// ...and the injected root must be what got scanned: ' . ' normalises to the repo
// root, so the crafted file is reached despite no configured root covering it.
const state = JSON.parse(readFileSync(join(repo, '.claude', stateName), 'utf8'));
expect(JSON.stringify(state)).toContain('src/auth$(touch INJECTED).ts');
},
);

it('ignores review-only injected roots outside review mode', () => {
const repo = repoWithCraftedFile();
Expand Down
20 changes: 9 additions & 11 deletions cli/__tests__/decision-edit-guard.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,15 @@ const payload = (toolName: string, filePath: string, cursor = false) => ({
});

describe('decision-edit-guard path policy', () => {
it.each([
'Edit',
'Write',
'MultiEdit',
'Delete',
])('blocks %s inside the default decisions directory', (toolName) => {
const reason = decide(payload(toolName, join(root, 'docs/decisions/axis.md')), root);
expect(reason).toContain('docs/decisions');
expect(reason).toContain('guard-decisions add');
expect(reason).toContain('guard-decisions amend');
});
it.each(['Edit', 'Write', 'MultiEdit', 'Delete'])(
'blocks %s inside the default decisions directory',
(toolName) => {
const reason = decide(payload(toolName, join(root, 'docs/decisions/axis.md')), root);
expect(reason).toContain('docs/decisions');
expect(reason).toContain('guard-decisions add');
expect(reason).toContain('guard-decisions amend');
},
);

it('honours a custom decisionsDir and Cursor path-shaped input', () => {
writeFileSync(
Expand Down
18 changes: 8 additions & 10 deletions cli/__tests__/knip-check-hook.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -54,16 +54,14 @@ const run = (dir, { stopHookActive = false, edits = ['KNIP_RAN.ts'] } = {}) => {
describe.skipIf(!HAS_BUN)('knip-check.sh gate behaviour', () => {
// Forms the ORIGINAL hook missed (it checked only knip.json/.jsonc/.ts/.config.ts). Parametrised
// so dropping any arm of the detection loop regresses a test — the bug was an INCOMPLETE list.
it.each([
'.knip.json',
'.knip.jsonc',
'knip.js',
'knip.config.js',
])('Defect C: runs knip for a %s config (a newly-supported form)', (configFile) => {
const r = run(fixture({ [configFile]: '{}', 'package.json': withKnipScript() }));
expect(r.status).toBe(2);
expect(r.stderr).toContain('KNIP_RAN');
});
it.each(['.knip.json', '.knip.jsonc', 'knip.js', 'knip.config.js'])(
'Defect C: runs knip for a %s config (a newly-supported form)',
(configFile) => {
const r = run(fixture({ [configFile]: '{}', 'package.json': withKnipScript() }));
expect(r.status).toBe(2);
expect(r.stderr).toContain('KNIP_RAN');
},
);

it('Defect C: runs knip for a package.json#knip config key (no separate config file)', () => {
const r = run(fixture({ 'package.json': withKnipScript({ knip: {} }) }));
Expand Down
4 changes: 2 additions & 2 deletions cli/__tests__/reconcile-detect-merged.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ describe('detectMerged — DEVKIT_RECONCILE_DEBUG stderr seam', () => {
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
mockExec.mockReturnValue('MERGED\n');
expect(detectMerged({ repo: 'o/r', prNumber: 1, branch: 'feat' })).toBe('MERGED');
expect((mockExec.mock.calls[0]?.[2] as { stdio?: unknown }).stdio).toEqual([
expect((mockExec.mock.calls[0]?.[2] as { stdio?: unknown } | undefined)?.stdio).toEqual([
'ignore',
'pipe',
'ignore',
Expand All @@ -98,7 +98,7 @@ describe('detectMerged — DEVKIT_RECONCILE_DEBUG stderr seam', () => {
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
mockExec.mockReturnValue('MERGED\n');
expect(detectMerged({ repo: 'o/r', prNumber: 1, branch: 'feat' })).toBe('MERGED');
expect((mockExec.mock.calls[0]?.[2] as { stdio?: unknown }).stdio).toEqual([
expect((mockExec.mock.calls[0]?.[2] as { stdio?: unknown } | undefined)?.stdio).toEqual([
'ignore',
'pipe',
'pipe',
Expand Down
Loading
Loading