@rigour-labs/cli 6.7.10 → 6.8.1-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +43 -0
- package/dist/commands/brief.d.ts +29 -0
- package/dist/commands/brief.js +169 -0
- package/dist/commands/hooks-push.js +2 -1
- package/dist/commands/hooks-stop.js +2 -1
- package/dist/commands/hooks.d.ts +4 -0
- package/dist/commands/hooks.js +32 -7
- package/dist/commands/thread.d.ts +3 -0
- package/dist/commands/thread.js +26 -0
- package/package.json +7 -4
- package/dist/cli-help.test.d.ts +0 -1
- package/dist/cli-help.test.js +0 -27
- package/dist/commands/deep-provider.test.d.ts +0 -1
- package/dist/commands/deep-provider.test.js +0 -19
- package/dist/commands/deep.test.d.ts +0 -1
- package/dist/commands/deep.test.js +0 -46
- package/dist/commands/demo.test.d.ts +0 -1
- package/dist/commands/demo.test.js +0 -59
- package/dist/commands/doctor.test.d.ts +0 -1
- package/dist/commands/doctor.test.js +0 -20
- package/dist/commands/exit-codes.test.d.ts +0 -1
- package/dist/commands/exit-codes.test.js +0 -20
- package/dist/commands/export-training-sites.test.d.ts +0 -1
- package/dist/commands/export-training-sites.test.js +0 -25
- package/dist/commands/hooks-check-repos.test.d.ts +0 -1
- package/dist/commands/hooks-check-repos.test.js +0 -33
- package/dist/commands/hooks-git.test.d.ts +0 -1
- package/dist/commands/hooks-git.test.js +0 -64
- package/dist/commands/hooks-push.test.d.ts +0 -1
- package/dist/commands/hooks-push.test.js +0 -86
- package/dist/commands/hooks-stop.test.d.ts +0 -1
- package/dist/commands/hooks-stop.test.js +0 -165
- package/dist/commands/hooks.test.d.ts +0 -1
- package/dist/commands/hooks.test.js +0 -247
- package/dist/commands/init-mcp-spec.test.d.ts +0 -1
- package/dist/commands/init-mcp-spec.test.js +0 -10
- package/dist/commands/init.test.d.ts +0 -1
- package/dist/commands/init.test.js +0 -107
- package/dist/commands/learn.test.d.ts +0 -1
- package/dist/commands/learn.test.js +0 -50
- package/dist/commands/personal.test.d.ts +0 -1
- package/dist/commands/personal.test.js +0 -191
- package/dist/commands/repo-setup.test.d.ts +0 -1
- package/dist/commands/repo-setup.test.js +0 -47
- package/dist/commands/review-checked.test.d.ts +0 -1
- package/dist/commands/review-checked.test.js +0 -50
- package/dist/commands/review-diff-tests.test.d.ts +0 -1
- package/dist/commands/review-diff-tests.test.js +0 -20
- package/dist/commands/review-human.test.d.ts +0 -1
- package/dist/commands/review-human.test.js +0 -94
- package/dist/commands/review-post.test.d.ts +0 -1
- package/dist/commands/review-post.test.js +0 -157
- package/dist/commands/review-pr-body.test.d.ts +0 -1
- package/dist/commands/review-pr-body.test.js +0 -19
- package/dist/commands/review-receipt.test.d.ts +0 -1
- package/dist/commands/review-receipt.test.js +0 -16
- package/dist/commands/review-reviewer.test.d.ts +0 -1
- package/dist/commands/review-reviewer.test.js +0 -68
- package/dist/commands/review-scope.test.d.ts +0 -1
- package/dist/commands/review-scope.test.js +0 -43
- package/dist/commands/review-stats.test.d.ts +0 -1
- package/dist/commands/review-stats.test.js +0 -19
- package/dist/commands/review-stdin.test.d.ts +0 -1
- package/dist/commands/review-stdin.test.js +0 -21
- package/dist/commands/review-summary.test.d.ts +0 -1
- package/dist/commands/review-summary.test.js +0 -46
- package/dist/commands/review-task.test.d.ts +0 -1
- package/dist/commands/review-task.test.js +0 -47
- package/dist/commands/scan-rules.test.d.ts +0 -1
- package/dist/commands/scan-rules.test.js +0 -29
- package/dist/commands/scan.test.d.ts +0 -1
- package/dist/commands/scan.test.js +0 -64
- package/dist/commands/skills.test.d.ts +0 -1
- package/dist/commands/skills.test.js +0 -56
- package/dist/commands/studio-activity.test.d.ts +0 -1
- package/dist/commands/studio-activity.test.js +0 -47
- package/dist/commands/studio-checkouts.test.d.ts +0 -1
- package/dist/commands/studio-checkouts.test.js +0 -23
- package/dist/commands/studio-context.test.d.ts +0 -1
- package/dist/commands/studio-context.test.js +0 -26
- package/dist/commands/studio-contracts.test.d.ts +0 -1
- package/dist/commands/studio-contracts.test.js +0 -8
- package/dist/commands/studio-guard.test.d.ts +0 -1
- package/dist/commands/studio-guard.test.js +0 -24
- package/dist/commands/studio-learned-rules.test.d.ts +0 -1
- package/dist/commands/studio-learned-rules.test.js +0 -31
- package/dist/commands/studio-learning.test.d.ts +0 -1
- package/dist/commands/studio-learning.test.js +0 -41
- package/dist/commands/studio-progress.test.d.ts +0 -1
- package/dist/commands/studio-progress.test.js +0 -41
- package/dist/commands/studio-reviewer.test.d.ts +0 -1
- package/dist/commands/studio-reviewer.test.js +0 -92
- package/dist/commands/studio-week.test.d.ts +0 -1
- package/dist/commands/studio-week.test.js +0 -32
- package/dist/commands/uninstall.test.d.ts +0 -1
- package/dist/commands/uninstall.test.js +0 -104
- package/dist/init-rules.test.d.ts +0 -1
- package/dist/init-rules.test.js +0 -55
- package/dist/smoke.test.d.ts +0 -1
- package/dist/smoke.test.js +0 -82
- package/dist/utils/cli-version.test.d.ts +0 -1
- package/dist/utils/cli-version.test.js +0 -9
- package/dist/utils/version.test.d.ts +0 -1
- package/dist/utils/version.test.js +0 -22
|
@@ -1,247 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for hooks init command.
|
|
3
|
-
*/
|
|
4
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
5
|
-
import { hooksInitCommand, hooksCheckCommand, parseStdinFiles } from './hooks.js';
|
|
6
|
-
import * as fs from 'fs';
|
|
7
|
-
import * as path from 'path';
|
|
8
|
-
import * as os from 'os';
|
|
9
|
-
import yaml from 'yaml';
|
|
10
|
-
vi.mock('@rigour-labs/core', async (importOriginal) => {
|
|
11
|
-
const actual = await importOriginal();
|
|
12
|
-
return {
|
|
13
|
-
...actual,
|
|
14
|
-
recordInteractionEvidence: vi.fn().mockResolvedValue(undefined),
|
|
15
|
-
updateAutomaticIndexForFiles: vi.fn().mockResolvedValue(undefined),
|
|
16
|
-
};
|
|
17
|
-
});
|
|
18
|
-
describe('hooksInitCommand', () => {
|
|
19
|
-
let testDir;
|
|
20
|
-
beforeEach(() => {
|
|
21
|
-
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hooks-test-'));
|
|
22
|
-
// Write minimal rigour.yml
|
|
23
|
-
fs.writeFileSync(path.join(testDir, 'rigour.yml'), yaml.stringify({
|
|
24
|
-
version: 1,
|
|
25
|
-
gates: { max_file_lines: 500 },
|
|
26
|
-
}));
|
|
27
|
-
vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
28
|
-
vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
29
|
-
});
|
|
30
|
-
afterEach(() => {
|
|
31
|
-
fs.rmSync(testDir, { recursive: true, force: true });
|
|
32
|
-
vi.restoreAllMocks();
|
|
33
|
-
});
|
|
34
|
-
it('should generate Claude hooks', async () => {
|
|
35
|
-
await hooksInitCommand(testDir, { tool: 'claude' });
|
|
36
|
-
const settingsPath = path.join(testDir, '.claude', 'settings.json');
|
|
37
|
-
expect(fs.existsSync(settingsPath)).toBe(true);
|
|
38
|
-
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
39
|
-
expect(settings.hooks).toBeDefined();
|
|
40
|
-
expect(settings.hooks.PostToolUse).toBeDefined();
|
|
41
|
-
expect(settings.hooks.PostToolUse[0].hooks[0].command).toContain('hooks check');
|
|
42
|
-
// Claude Code sends the edited path as JSON on stdin; it exports no TOOL_INPUT_* variable.
|
|
43
|
-
expect(settings.hooks.PostToolUse[0].hooks[0].command).toContain('--stdin');
|
|
44
|
-
expect(settings.hooks.PostToolUse[0].hooks[0].command).not.toContain('TOOL_INPUT');
|
|
45
|
-
});
|
|
46
|
-
it('reads the edited file from the JSON Claude Code sends on stdin', () => {
|
|
47
|
-
expect(parseStdinFiles(JSON.stringify({ hook_event_name: 'PostToolUse', tool_name: 'Write', tool_input: { file_path: 'src/a.ts', content: 'x' } }))).toEqual(['src/a.ts']);
|
|
48
|
-
});
|
|
49
|
-
it('should generate Cursor hooks', async () => {
|
|
50
|
-
await hooksInitCommand(testDir, { tool: 'cursor' });
|
|
51
|
-
const hooksPath = path.join(testDir, '.cursor', 'hooks.json');
|
|
52
|
-
expect(fs.existsSync(hooksPath)).toBe(true);
|
|
53
|
-
const hooks = JSON.parse(fs.readFileSync(hooksPath, 'utf-8'));
|
|
54
|
-
expect(hooks.hooks).toBeDefined();
|
|
55
|
-
});
|
|
56
|
-
it('should generate Cline hooks', async () => {
|
|
57
|
-
await hooksInitCommand(testDir, { tool: 'cline' });
|
|
58
|
-
const hookPath = path.join(testDir, '.clinerules', 'hooks', 'PostToolUse');
|
|
59
|
-
expect(fs.existsSync(hookPath)).toBe(true);
|
|
60
|
-
});
|
|
61
|
-
it('should skip a legacy .clinerules file and continue other tools', async () => {
|
|
62
|
-
fs.writeFileSync(path.join(testDir, '.clinerules'), 'legacy Cline rules');
|
|
63
|
-
await expect(hooksInitCommand(testDir, {
|
|
64
|
-
tool: 'cline,windsurf',
|
|
65
|
-
})).resolves.toBeUndefined();
|
|
66
|
-
expect(fs.readFileSync(path.join(testDir, '.clinerules'), 'utf-8'))
|
|
67
|
-
.toBe('legacy Cline rules');
|
|
68
|
-
expect(fs.existsSync(path.join(testDir, '.windsurf', 'hooks.json'))).toBe(true);
|
|
69
|
-
expect(console.error).toHaveBeenCalledWith(expect.stringContaining('SKIP .clinerules/hooks/PostToolUse'));
|
|
70
|
-
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Cline: Not configured'));
|
|
71
|
-
});
|
|
72
|
-
it('should generate Windsurf hooks', async () => {
|
|
73
|
-
await hooksInitCommand(testDir, { tool: 'windsurf' });
|
|
74
|
-
const hooksPath = path.join(testDir, '.windsurf', 'hooks.json');
|
|
75
|
-
expect(fs.existsSync(hooksPath)).toBe(true);
|
|
76
|
-
});
|
|
77
|
-
it('should support dry-run mode', async () => {
|
|
78
|
-
await hooksInitCommand(testDir, { tool: 'claude', dryRun: true });
|
|
79
|
-
// Dry run should NOT create files
|
|
80
|
-
const settingsPath = path.join(testDir, '.claude', 'settings.json');
|
|
81
|
-
expect(fs.existsSync(settingsPath)).toBe(false);
|
|
82
|
-
});
|
|
83
|
-
it('merges into a settings file the person already has, keeping their settings and hooks, and stays idempotent', async () => {
|
|
84
|
-
const claudeDir = path.join(testDir, '.claude');
|
|
85
|
-
fs.mkdirSync(claudeDir, { recursive: true });
|
|
86
|
-
const own = { permissions: { allow: ['Bash(ls)'] }, hooks: { PostToolUse: [{ matcher: 'Write', hooks: [{ type: 'command', command: 'npm run format' }] }] } };
|
|
87
|
-
fs.writeFileSync(path.join(claudeDir, 'settings.json'), JSON.stringify(own));
|
|
88
|
-
for (const force of [false, true, true])
|
|
89
|
-
await hooksInitCommand(testDir, { tool: 'claude', force });
|
|
90
|
-
const merged = JSON.parse(fs.readFileSync(path.join(claudeDir, 'settings.json'), 'utf-8'));
|
|
91
|
-
expect(merged.permissions).toEqual(own.permissions);
|
|
92
|
-
const postToolUse = merged.hooks.PostToolUse.flatMap((group) => group.hooks.map((h) => h.command));
|
|
93
|
-
expect(postToolUse[0]).toBe('npm run format');
|
|
94
|
-
expect(postToolUse.filter((c) => /hooks check/.test(c))).toHaveLength(1); // three runs, one Rigour entry
|
|
95
|
-
expect(merged.hooks.Stop).toHaveLength(1);
|
|
96
|
-
});
|
|
97
|
-
it('leaves a settings file that is not valid JSON alone', async () => {
|
|
98
|
-
const claudeDir = path.join(testDir, '.claude');
|
|
99
|
-
fs.mkdirSync(claudeDir, { recursive: true });
|
|
100
|
-
fs.writeFileSync(path.join(claudeDir, 'settings.json'), '{ not json');
|
|
101
|
-
await hooksInitCommand(testDir, { tool: 'claude', force: true });
|
|
102
|
-
expect(fs.readFileSync(path.join(claudeDir, 'settings.json'), 'utf-8')).toBe('{ not json');
|
|
103
|
-
});
|
|
104
|
-
it('should propagate --block to generated hook commands', async () => {
|
|
105
|
-
await hooksInitCommand(testDir, { tool: 'all', force: true, block: true });
|
|
106
|
-
const claude = JSON.parse(fs.readFileSync(path.join(testDir, '.claude', 'settings.json'), 'utf-8'));
|
|
107
|
-
const cursor = JSON.parse(fs.readFileSync(path.join(testDir, '.cursor', 'hooks.json'), 'utf-8'));
|
|
108
|
-
const windsurf = JSON.parse(fs.readFileSync(path.join(testDir, '.windsurf', 'hooks.json'), 'utf-8'));
|
|
109
|
-
const clineScript = fs.readFileSync(path.join(testDir, '.clinerules', 'hooks', 'PostToolUse'), 'utf-8');
|
|
110
|
-
expect(claude.hooks.PostToolUse[0].hooks[0].command).toContain('--block');
|
|
111
|
-
expect(cursor.hooks.afterFileEdit[0].command).toContain('--block');
|
|
112
|
-
expect(windsurf.hooks.post_write_code[0].command).toContain('--block');
|
|
113
|
-
expect(clineScript).toContain('--block');
|
|
114
|
-
});
|
|
115
|
-
});
|
|
116
|
-
describe('hooksInitCommand — DLP integration', () => {
|
|
117
|
-
let testDir;
|
|
118
|
-
beforeEach(() => {
|
|
119
|
-
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hooks-dlp-test-'));
|
|
120
|
-
fs.writeFileSync(path.join(testDir, 'rigour.yml'), yaml.stringify({
|
|
121
|
-
version: 1,
|
|
122
|
-
gates: { max_file_lines: 500 },
|
|
123
|
-
}));
|
|
124
|
-
vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
125
|
-
vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
126
|
-
});
|
|
127
|
-
afterEach(() => {
|
|
128
|
-
fs.rmSync(testDir, { recursive: true, force: true });
|
|
129
|
-
vi.restoreAllMocks();
|
|
130
|
-
});
|
|
131
|
-
it('should generate Claude hooks with DLP (PreToolUse) by default', async () => {
|
|
132
|
-
await hooksInitCommand(testDir, { tool: 'claude', force: true });
|
|
133
|
-
const settingsPath = path.join(testDir, '.claude', 'settings.json');
|
|
134
|
-
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
135
|
-
expect(settings.hooks.PostToolUse).toBeDefined();
|
|
136
|
-
const dlp = settings.hooks.PreToolUse.find((h) => h.matcher === '.*');
|
|
137
|
-
expect(dlp.hooks[0].command).toContain('--mode dlp');
|
|
138
|
-
expect(dlp.hooks[0].command).toMatch(/npx --yes @rigour-labs\/cli@\d+\.\d+\.\d+/);
|
|
139
|
-
const push = settings.hooks.PreToolUse.find((h) => h.matcher === 'Bash');
|
|
140
|
-
expect(push.hooks[0]).toMatchObject({ command: expect.stringContaining('hooks push --stdin'), timeout: 1800 });
|
|
141
|
-
expect(push.hooks[0].command).toContain('case "$payload" in *git*push*)'); // other commands never start Rigour
|
|
142
|
-
});
|
|
143
|
-
it('should generate Cursor hooks with DLP (beforeFileEdit) by default', async () => {
|
|
144
|
-
await hooksInitCommand(testDir, { tool: 'cursor', force: true });
|
|
145
|
-
const hooksPath = path.join(testDir, '.cursor', 'hooks.json');
|
|
146
|
-
const hooks = JSON.parse(fs.readFileSync(hooksPath, 'utf-8'));
|
|
147
|
-
expect(hooks.hooks.afterFileEdit).toBeDefined();
|
|
148
|
-
expect(hooks.hooks.beforeSubmitPrompt).toBeDefined();
|
|
149
|
-
});
|
|
150
|
-
it('should generate Windsurf hooks with DLP by default', async () => {
|
|
151
|
-
await hooksInitCommand(testDir, { tool: 'windsurf', force: true });
|
|
152
|
-
const hooksPath = path.join(testDir, '.windsurf', 'hooks.json');
|
|
153
|
-
const hooks = JSON.parse(fs.readFileSync(hooksPath, 'utf-8'));
|
|
154
|
-
expect(hooks.hooks.post_write_code).toBeDefined();
|
|
155
|
-
expect(hooks.hooks.pre_write_code).toBeDefined();
|
|
156
|
-
});
|
|
157
|
-
it('should skip DLP hooks when dlp: false', async () => {
|
|
158
|
-
await hooksInitCommand(testDir, { tool: 'claude', force: true, dlp: false });
|
|
159
|
-
const settingsPath = path.join(testDir, '.claude', 'settings.json');
|
|
160
|
-
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
161
|
-
expect(settings.hooks.PostToolUse).toBeDefined();
|
|
162
|
-
expect(settings.hooks.PreToolUse.map((h) => h.matcher)).toEqual(['Bash']); // the push gate stays
|
|
163
|
-
});
|
|
164
|
-
it('should generate Cline DLP warnings without default blocking', async () => {
|
|
165
|
-
await hooksInitCommand(testDir, { tool: 'cline', force: true });
|
|
166
|
-
const script = fs.readFileSync(path.join(testDir, '.clinerules', 'hooks', 'PreToolUse'), 'utf-8');
|
|
167
|
-
expect(script).toContain("result.status !== 'clean'");
|
|
168
|
-
expect(script).toContain("if (result.status === 'blocked') process.exit(2)");
|
|
169
|
-
});
|
|
170
|
-
it('should not route DLP hooks through the file-only core checker', async () => {
|
|
171
|
-
const coreChecker = path.join(testDir, 'node_modules/@rigour-labs/core/dist/hooks/standalone-checker.js');
|
|
172
|
-
fs.mkdirSync(path.dirname(coreChecker), { recursive: true });
|
|
173
|
-
fs.writeFileSync(coreChecker, '');
|
|
174
|
-
await hooksInitCommand(testDir, { tool: 'cursor', force: true });
|
|
175
|
-
const hooks = JSON.parse(fs.readFileSync(path.join(testDir, '.cursor', 'hooks.json'), 'utf-8'));
|
|
176
|
-
const command = hooks.hooks.beforeSubmitPrompt[0].command;
|
|
177
|
-
expect(command).toMatch(/npx --yes @rigour-labs\/cli@\d+\.\d+\.\d+/);
|
|
178
|
-
expect(command).not.toContain('standalone-checker');
|
|
179
|
-
});
|
|
180
|
-
});
|
|
181
|
-
describe('hooksCheckCommand', () => {
|
|
182
|
-
let testDir;
|
|
183
|
-
beforeEach(() => {
|
|
184
|
-
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hooks-check-test-'));
|
|
185
|
-
vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
186
|
-
vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
187
|
-
});
|
|
188
|
-
afterEach(() => {
|
|
189
|
-
fs.rmSync(testDir, { recursive: true, force: true });
|
|
190
|
-
vi.restoreAllMocks();
|
|
191
|
-
});
|
|
192
|
-
it('should return pass JSON when file is clean', async () => {
|
|
193
|
-
const filePath = path.join(testDir, 'ok.ts');
|
|
194
|
-
fs.writeFileSync(filePath, 'export const x = 1;\n');
|
|
195
|
-
const stdoutSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
196
|
-
await hooksCheckCommand(testDir, { files: 'ok.ts' });
|
|
197
|
-
const output = stdoutSpy.mock.calls.map(call => String(call[0])).join('');
|
|
198
|
-
expect(output).toContain('"status":"pass"');
|
|
199
|
-
});
|
|
200
|
-
it('reports skipped, not pass, when the hook named no file', async () => {
|
|
201
|
-
const stdoutSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
202
|
-
vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
203
|
-
await hooksCheckCommand(testDir, { files: '' });
|
|
204
|
-
expect(stdoutSpy.mock.calls.map(call => String(call[0])).join('')).toContain('"status":"skipped"');
|
|
205
|
-
});
|
|
206
|
-
it('should return fail JSON and set exit code 2 in block mode', async () => {
|
|
207
|
-
const filePath = path.join(testDir, 'bad.ts');
|
|
208
|
-
fs.writeFileSync(filePath, "const password = 'abcdefghijklmnopqrstuvwxyz12345';\n");
|
|
209
|
-
const stdoutSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
210
|
-
const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
211
|
-
const originalExitCode = process.exitCode;
|
|
212
|
-
await hooksCheckCommand(testDir, { files: 'bad.ts', block: true });
|
|
213
|
-
const output = stdoutSpy.mock.calls.map(call => String(call[0])).join('');
|
|
214
|
-
expect(output).toContain('"status":"fail"');
|
|
215
|
-
expect(stderrSpy).toHaveBeenCalled();
|
|
216
|
-
expect(process.exitCode).toBe(2);
|
|
217
|
-
process.exitCode = originalExitCode;
|
|
218
|
-
});
|
|
219
|
-
it('should warn and continue for DLP detections by default', async () => {
|
|
220
|
-
const stdoutSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
221
|
-
const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
222
|
-
const originalExitCode = process.exitCode;
|
|
223
|
-
await hooksCheckCommand(testDir, {
|
|
224
|
-
mode: 'dlp',
|
|
225
|
-
files: 'AKIAZ9Y8X7W6V5U4T3Q2',
|
|
226
|
-
});
|
|
227
|
-
const output = stdoutSpy.mock.calls.map(call => String(call[0])).join('');
|
|
228
|
-
expect(output).toContain('"status":"warning"');
|
|
229
|
-
expect(output).not.toContain('AKIAZ9Y8X7W6V5U4T3Q2');
|
|
230
|
-
expect(stderrSpy).toHaveBeenCalled();
|
|
231
|
-
expect(process.exitCode).toBe(originalExitCode);
|
|
232
|
-
});
|
|
233
|
-
it('should block DLP detections only with --block', async () => {
|
|
234
|
-
const stdoutSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
235
|
-
vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
236
|
-
const originalExitCode = process.exitCode;
|
|
237
|
-
await hooksCheckCommand(testDir, {
|
|
238
|
-
mode: 'dlp',
|
|
239
|
-
files: 'AKIAZ9Y8X7W6V5U4T3Q2',
|
|
240
|
-
block: true,
|
|
241
|
-
});
|
|
242
|
-
const output = stdoutSpy.mock.calls.map(call => String(call[0])).join('');
|
|
243
|
-
expect(output).toContain('"status":"blocked"');
|
|
244
|
-
expect(process.exitCode).toBe(2);
|
|
245
|
-
process.exitCode = originalExitCode;
|
|
246
|
-
});
|
|
247
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { mcpPackageSpec } from './init.js';
|
|
3
|
-
describe('mcpPackageSpec', () => {
|
|
4
|
-
it("pins the MCP server to this CLI's major, never a bare or stale exact version", () => {
|
|
5
|
-
expect(mcpPackageSpec('6.5.2')).toBe('@rigour-labs/mcp@6');
|
|
6
|
-
expect(mcpPackageSpec('7.0.0-beta.1')).toBe('@rigour-labs/mcp@7');
|
|
7
|
-
expect(mcpPackageSpec('0.0.0')).toBe('@rigour-labs/mcp@latest');
|
|
8
|
-
expect(mcpPackageSpec('unknown')).toBe('@rigour-labs/mcp@latest');
|
|
9
|
-
});
|
|
10
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for init command — IDE detection, config generation, auto-hook integration.
|
|
3
|
-
*/
|
|
4
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
5
|
-
import { initCommand } from './init.js';
|
|
6
|
-
import * as fs from 'fs';
|
|
7
|
-
import * as path from 'path';
|
|
8
|
-
import * as os from 'os';
|
|
9
|
-
describe('initCommand', () => {
|
|
10
|
-
let testDir;
|
|
11
|
-
beforeEach(() => {
|
|
12
|
-
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'init-test-'));
|
|
13
|
-
// Minimal package.json for discovery
|
|
14
|
-
fs.writeFileSync(path.join(testDir, 'package.json'), JSON.stringify({
|
|
15
|
-
name: 'test-project',
|
|
16
|
-
dependencies: { express: '^4.0.0' },
|
|
17
|
-
}));
|
|
18
|
-
vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
19
|
-
vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
20
|
-
});
|
|
21
|
-
afterEach(() => {
|
|
22
|
-
fs.rmSync(testDir, { recursive: true, force: true });
|
|
23
|
-
vi.restoreAllMocks();
|
|
24
|
-
});
|
|
25
|
-
it('should create rigour.yml', async () => {
|
|
26
|
-
await initCommand(testDir);
|
|
27
|
-
const configPath = path.join(testDir, 'rigour.yml');
|
|
28
|
-
expect(fs.existsSync(configPath)).toBe(true);
|
|
29
|
-
const content = fs.readFileSync(configPath, 'utf-8');
|
|
30
|
-
expect(content).toContain('gates:');
|
|
31
|
-
expect(content).not.toMatch(/^version:|fast_gates|^planned:|input_validation|adaptive:/m); // settings nothing reads are not written
|
|
32
|
-
});
|
|
33
|
-
it('writes no instruction files or empty documents unless asked', async () => {
|
|
34
|
-
await initCommand(testDir);
|
|
35
|
-
for (const rel of ['docs/AGENT_INSTRUCTIONS.md', 'CLAUDE.md', 'AGENTS.md', '.windsurfrules', 'docs/SPEC.md', 'docs/ARCH.md']) {
|
|
36
|
-
expect(fs.existsSync(path.join(testDir, rel))).toBe(false);
|
|
37
|
-
}
|
|
38
|
-
});
|
|
39
|
-
it('should support dry-run mode', async () => {
|
|
40
|
-
await initCommand(testDir, { dryRun: true });
|
|
41
|
-
// Dry run should NOT create rigour.yml
|
|
42
|
-
expect(fs.existsSync(path.join(testDir, 'rigour.yml'))).toBe(false);
|
|
43
|
-
});
|
|
44
|
-
it('should not overwrite without --force', async () => {
|
|
45
|
-
// Create existing rigour.yml
|
|
46
|
-
fs.writeFileSync(path.join(testDir, 'rigour.yml'), 'version: 1\nexisting: true\n');
|
|
47
|
-
await initCommand(testDir);
|
|
48
|
-
const content = fs.readFileSync(path.join(testDir, 'rigour.yml'), 'utf-8');
|
|
49
|
-
expect(content).toContain('existing');
|
|
50
|
-
});
|
|
51
|
-
it('should overwrite with --force', async () => {
|
|
52
|
-
// Create existing rigour.yml
|
|
53
|
-
fs.writeFileSync(path.join(testDir, 'rigour.yml'), 'version: 1\nexisting: true\n');
|
|
54
|
-
await initCommand(testDir, { force: true });
|
|
55
|
-
// Should create backup
|
|
56
|
-
expect(fs.existsSync(path.join(testDir, 'rigour.yml.bak'))).toBe(true);
|
|
57
|
-
// New config should not contain 'existing'
|
|
58
|
-
const content = fs.readFileSync(path.join(testDir, 'rigour.yml'), 'utf-8');
|
|
59
|
-
expect(content).not.toContain('existing: true');
|
|
60
|
-
});
|
|
61
|
-
it('should detect Claude IDE and create hooks', async () => {
|
|
62
|
-
// Create Claude marker
|
|
63
|
-
fs.mkdirSync(path.join(testDir, '.claude'), { recursive: true });
|
|
64
|
-
await initCommand(testDir);
|
|
65
|
-
// Should have created .claude/settings.json (hooks)
|
|
66
|
-
const settingsPath = path.join(testDir, '.claude', 'settings.json');
|
|
67
|
-
expect(fs.existsSync(settingsPath)).toBe(true);
|
|
68
|
-
});
|
|
69
|
-
it('should detect Cursor IDE and create hooks', async () => {
|
|
70
|
-
// Create Cursor marker
|
|
71
|
-
fs.mkdirSync(path.join(testDir, '.cursor'), { recursive: true });
|
|
72
|
-
await initCommand(testDir);
|
|
73
|
-
// Should have created .cursor/hooks.json
|
|
74
|
-
const hooksPath = path.join(testDir, '.cursor', 'hooks.json');
|
|
75
|
-
expect(fs.existsSync(hooksPath)).toBe(true);
|
|
76
|
-
});
|
|
77
|
-
it('should update .gitignore with rigour patterns', async () => {
|
|
78
|
-
await initCommand(testDir);
|
|
79
|
-
const gitignorePath = path.join(testDir, '.gitignore');
|
|
80
|
-
expect(fs.existsSync(gitignorePath)).toBe(true);
|
|
81
|
-
const content = fs.readFileSync(gitignorePath, 'utf-8');
|
|
82
|
-
expect(content).toContain('rigour-report.json');
|
|
83
|
-
expect(content).toContain('.rigour/*');
|
|
84
|
-
expect(content).toContain('!.rigour/dismissed.json');
|
|
85
|
-
expect(content).toContain('!.rigour/backtest.json');
|
|
86
|
-
});
|
|
87
|
-
it('turns an older whole-directory `.rigour/` ignore into one the shared files can escape', async () => {
|
|
88
|
-
fs.writeFileSync(path.join(testDir, '.gitignore'), 'node_modules\n.rigour/\n');
|
|
89
|
-
await initCommand(testDir);
|
|
90
|
-
const lines = fs.readFileSync(path.join(testDir, '.gitignore'), 'utf-8').split('\n');
|
|
91
|
-
expect(lines).not.toContain('.rigour/');
|
|
92
|
-
expect(lines.filter(l => l === '.rigour/*')).toHaveLength(1);
|
|
93
|
-
expect(lines).toContain('!.rigour/dismissed.json');
|
|
94
|
-
});
|
|
95
|
-
it('should create .rigour/memory.json for Studio', async () => {
|
|
96
|
-
await initCommand(testDir);
|
|
97
|
-
const memPath = path.join(testDir, '.rigour', 'memory.json');
|
|
98
|
-
expect(fs.existsSync(memPath)).toBe(true);
|
|
99
|
-
const mem = JSON.parse(fs.readFileSync(memPath, 'utf-8'));
|
|
100
|
-
expect(mem.memories.project_boot).toBeDefined();
|
|
101
|
-
});
|
|
102
|
-
it('should support --ide flag to target specific IDE', async () => {
|
|
103
|
-
await initCommand(testDir, { ide: 'windsurf' });
|
|
104
|
-
expect(fs.existsSync(path.join(testDir, '.windsurf', 'hooks.json'))).toBe(true);
|
|
105
|
-
expect(fs.existsSync(path.join(testDir, '.claude', 'settings.json'))).toBe(false);
|
|
106
|
-
});
|
|
107
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import os from 'os';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
-
import { learnCommand } from './learn.js';
|
|
6
|
-
const BEFORE = "export async function post(url: string, init: RequestInit) {\n return fetch(url, init);\n}\n";
|
|
7
|
-
const AFTER = "export async function post(url: string, init: RequestInit) {\n return fetch(url, { ...init, redirect: 'manual' });\n}\n";
|
|
8
|
-
describe('learnCommand', () => {
|
|
9
|
-
let cwd;
|
|
10
|
-
let log;
|
|
11
|
-
let error;
|
|
12
|
-
beforeEach(() => {
|
|
13
|
-
cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'learn-cli-'));
|
|
14
|
-
fs.mkdirSync(path.join(cwd, 'src'));
|
|
15
|
-
fs.writeFileSync(path.join(cwd, 'old.ts'), BEFORE);
|
|
16
|
-
fs.writeFileSync(path.join(cwd, 'src', 'http.ts'), AFTER);
|
|
17
|
-
log = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
18
|
-
error = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
19
|
-
process.exitCode = undefined;
|
|
20
|
-
});
|
|
21
|
-
afterEach(() => {
|
|
22
|
-
log.mockRestore();
|
|
23
|
-
error.mockRestore();
|
|
24
|
-
process.exitCode = undefined;
|
|
25
|
-
fs.rmSync(cwd, { recursive: true, force: true });
|
|
26
|
-
});
|
|
27
|
-
it('learns from --before/--after and saves the rule', async () => {
|
|
28
|
-
await learnCommand(cwd, undefined, { before: 'old.ts', after: 'src/http.ts' });
|
|
29
|
-
const saved = fs.readdirSync(path.join(cwd, '.rigour', 'rules'));
|
|
30
|
-
expect(saved).toHaveLength(1);
|
|
31
|
-
const rule = JSON.parse(fs.readFileSync(path.join(cwd, '.rigour', 'rules', saved[0]), 'utf8'));
|
|
32
|
-
expect(rule.pattern).toMatchObject({ template: 'require-option', property: 'redirect', value: 'manual' });
|
|
33
|
-
expect(rule.source).toEqual({ file: 'src/http.ts', function: 'post' });
|
|
34
|
-
});
|
|
35
|
-
it('saves nothing on --dry-run and reports JSON', async () => {
|
|
36
|
-
await learnCommand(cwd, undefined, { before: 'old.ts', after: 'src/http.ts', dryRun: true, json: true });
|
|
37
|
-
expect(fs.existsSync(path.join(cwd, '.rigour'))).toBe(false);
|
|
38
|
-
const report = JSON.parse(String(log.mock.calls[0][0]));
|
|
39
|
-
expect(report.learned).toHaveLength(1);
|
|
40
|
-
expect(report.saved).toEqual([]);
|
|
41
|
-
});
|
|
42
|
-
it('rejects missing or conflicting inputs with a usage exit code', async () => {
|
|
43
|
-
await learnCommand(cwd, undefined, { before: 'old.ts' });
|
|
44
|
-
expect(process.exitCode).toBe(2);
|
|
45
|
-
await learnCommand(cwd, 'abc123', { before: 'old.ts', after: 'src/http.ts' });
|
|
46
|
-
expect(error.mock.calls.flat().join('\n')).toContain('Give only one of');
|
|
47
|
-
await learnCommand(cwd, undefined, { before: 'old.ts', after: 'src/http.ts', maxHits: '-1' });
|
|
48
|
-
expect(error.mock.calls.flat().join('\n')).toContain('--max-hits');
|
|
49
|
-
});
|
|
50
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
import { execFileSync, spawnSync } from 'child_process';
|
|
2
|
-
import fs from 'fs';
|
|
3
|
-
import os from 'os';
|
|
4
|
-
import path from 'path';
|
|
5
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
-
import { asUserLevel, disableHere, enabledHere, enableHere } from './personal.js';
|
|
7
|
-
import { setupCommand } from './setup.js';
|
|
8
|
-
import { uninstall } from './uninstall.js';
|
|
9
|
-
let repo;
|
|
10
|
-
let home;
|
|
11
|
-
let bin;
|
|
12
|
-
const saved = { RIGOUR_AGENT_HOME: process.env.RIGOUR_AGENT_HOME, RIGOUR_CLAUDE_CLI: process.env.RIGOUR_CLAUDE_CLI, RIGOUR_HOME: process.env.RIGOUR_HOME };
|
|
13
|
-
const git = (...args) => execFileSync('git', ['-C', repo, ...args], { encoding: 'utf8' }).trim();
|
|
14
|
-
const atHome = (rel) => path.join(home, rel);
|
|
15
|
-
const unix = process.platform !== 'win32';
|
|
16
|
-
/** A `claude` CLI that keeps its MCP servers in a file, so add/get/remove behave like the real one. */
|
|
17
|
-
function fakeClaude() {
|
|
18
|
-
const servers = path.join(bin, 'claude-mcp.txt');
|
|
19
|
-
fs.writeFileSync(path.join(bin, 'claude'), `#!/bin/sh
|
|
20
|
-
echo "$@" >> "${path.join(bin, 'claude-calls.log')}"
|
|
21
|
-
case "$1 $2" in
|
|
22
|
-
"--version "*) echo "9.9.9 (Claude Code)"; exit 0 ;;
|
|
23
|
-
"mcp get") grep -qx "$3" "${servers}" 2>/dev/null; exit $? ;;
|
|
24
|
-
"mcp add") echo "$5" >> "${servers}"; exit 0 ;;
|
|
25
|
-
"mcp remove") : > "${servers}"; exit 0 ;;
|
|
26
|
-
esac
|
|
27
|
-
exit 1
|
|
28
|
-
`, { mode: 0o755 });
|
|
29
|
-
process.env.RIGOUR_CLAUDE_CLI = path.join(bin, 'claude');
|
|
30
|
-
return path.join(bin, 'claude-calls.log');
|
|
31
|
-
}
|
|
32
|
-
beforeEach(() => {
|
|
33
|
-
repo = fs.mkdtempSync(path.join(os.tmpdir(), 'personal-repo-'));
|
|
34
|
-
home = fs.mkdtempSync(path.join(os.tmpdir(), 'personal-home-'));
|
|
35
|
-
bin = fs.mkdtempSync(path.join(os.tmpdir(), 'personal-bin-'));
|
|
36
|
-
// The agents' home and Rigour's own, both throwaway (os.homedir() would ignore a HOME set here).
|
|
37
|
-
process.env.RIGOUR_AGENT_HOME = home;
|
|
38
|
-
process.env.RIGOUR_HOME = home;
|
|
39
|
-
git('init', '-q', '-b', 'main');
|
|
40
|
-
git('config', 'core.hooksPath', '.git/hooks');
|
|
41
|
-
git('config', 'user.email', 't@example.com');
|
|
42
|
-
git('config', 'user.name', 't');
|
|
43
|
-
git('config', 'commit.gpgsign', 'false');
|
|
44
|
-
fs.writeFileSync(path.join(repo, 'package.json'), JSON.stringify({ name: 'app' }));
|
|
45
|
-
fs.writeFileSync(path.join(repo, 'a.ts'), 'export const a = 1;\n');
|
|
46
|
-
git('add', '-A');
|
|
47
|
-
git('commit', '-qm', 'init');
|
|
48
|
-
vi.spyOn(console, 'log').mockImplementation(() => undefined);
|
|
49
|
-
vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
50
|
-
vi.spyOn(console, 'warn').mockImplementation(() => undefined);
|
|
51
|
-
});
|
|
52
|
-
afterEach(() => {
|
|
53
|
-
vi.restoreAllMocks();
|
|
54
|
-
for (const [key, value] of Object.entries(saved)) {
|
|
55
|
-
if (value === undefined)
|
|
56
|
-
delete process.env[key];
|
|
57
|
-
else
|
|
58
|
-
process.env[key] = value;
|
|
59
|
-
}
|
|
60
|
-
for (const dir of [repo, home, bin])
|
|
61
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
62
|
-
});
|
|
63
|
-
describe('the switch for a repository', () => {
|
|
64
|
-
it('lives in the git directory, shared by worktrees, and puts .rigour/ in info/exclude once', () => {
|
|
65
|
-
expect(enabledHere(repo)).toBe(false);
|
|
66
|
-
enableHere(repo);
|
|
67
|
-
enableHere(repo);
|
|
68
|
-
expect(enabledHere(repo)).toBe(true);
|
|
69
|
-
expect(fs.readFileSync(path.join(repo, '.git/info/exclude'), 'utf8').split('\n').filter(l => l === '.rigour/')).toHaveLength(1);
|
|
70
|
-
const worktree = path.join(os.tmpdir(), `personal-wt-${process.pid}`);
|
|
71
|
-
git('worktree', 'add', '-q', '--detach', worktree);
|
|
72
|
-
try {
|
|
73
|
-
expect(enabledHere(worktree)).toBe(true);
|
|
74
|
-
}
|
|
75
|
-
finally {
|
|
76
|
-
git('worktree', 'remove', '--force', worktree);
|
|
77
|
-
}
|
|
78
|
-
disableHere(repo, true);
|
|
79
|
-
expect(enabledHere(repo)).toBe(false);
|
|
80
|
-
expect(fs.readFileSync(path.join(repo, '.git/info/exclude'), 'utf8')).not.toContain('.rigour/');
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
describe.skipIf(!unix)('the guard on a machine-level hook', () => {
|
|
84
|
-
/** The command a project Stop hook becomes in the user-level Claude settings. */
|
|
85
|
-
const guarded = (command) => JSON.parse(asUserLevel({ path: '.claude/settings.json', content: JSON.stringify({ hooks: { Stop: [{ hooks: [{ type: 'command', command }] }] } }) }).content).hooks.Stop[0].hooks[0].command;
|
|
86
|
-
const run = (command) => spawnSync('sh', ['-c', command], { cwd: repo, input: 'payload', encoding: 'utf8', env: { ...process.env, CLAUDE_PROJECT_DIR: '' } });
|
|
87
|
-
it('runs the command only in a repository switched on, with stdin passed through, even for a quoted command', () => {
|
|
88
|
-
const command = guarded(`sh -c 'printf "ran:"; cat'`);
|
|
89
|
-
expect(run(command).stdout).toBe('');
|
|
90
|
-
expect(run(command).status).toBe(0);
|
|
91
|
-
enableHere(repo);
|
|
92
|
-
expect(run(command).stdout).toBe('ran:payload');
|
|
93
|
-
const outside = spawnSync('sh', ['-c', command], { cwd: os.tmpdir(), input: 'x', encoding: 'utf8' });
|
|
94
|
-
expect(outside).toMatchObject({ status: 0, stdout: '' });
|
|
95
|
-
});
|
|
96
|
-
it('turns each project hook file into its agent\'s user-level twin, every command guarded', () => {
|
|
97
|
-
const claude = asUserLevel({ path: '.claude/settings.json', content: JSON.stringify({ hooks: { Stop: [{ hooks: [{ type: 'command', command: 'npx rigour hooks stop' }] }] } }) });
|
|
98
|
-
expect(claude.path).toBe('.claude/settings.json');
|
|
99
|
-
expect(JSON.parse(claude.content).hooks.Stop[0].hooks[0].command).toMatch(/^sh -c '.*rigour-enabled.*exec npx rigour hooks stop'$/);
|
|
100
|
-
expect(asUserLevel({ path: '.windsurf/hooks.json', content: '{}' }).path).toBe('.codeium/windsurf/hooks.json');
|
|
101
|
-
const cline = asUserLevel({ path: '.clinerules/hooks/PostToolUse', content: '#!/usr/bin/env node\nprocess.stdout.write("ran");\n' });
|
|
102
|
-
expect(cline.path).toBe('Documents/Cline/Hooks/PostToolUse');
|
|
103
|
-
fs.writeFileSync(path.join(bin, 'hook.js'), cline.content);
|
|
104
|
-
expect(spawnSync(process.execPath, [path.join(bin, 'hook.js')], { cwd: repo, encoding: 'utf8' }).stdout).toBe('{}');
|
|
105
|
-
enableHere(repo);
|
|
106
|
-
expect(spawnSync(process.execPath, [path.join(bin, 'hook.js')], { cwd: repo, encoding: 'utf8' }).stdout).toBe('ran');
|
|
107
|
-
});
|
|
108
|
-
});
|
|
109
|
-
describe.skipIf(!unix)('rigour setup, personal', () => {
|
|
110
|
-
it('leaves the working tree untouched, installs machine hooks into the configs the person has, and comes back out exactly', async () => {
|
|
111
|
-
const calls = fakeClaude();
|
|
112
|
-
const ownSettings = { permissions: { allow: ['Bash(ls)'] }, hooks: { PostToolUse: [{ matcher: 'Write', hooks: [{ type: 'command', command: 'npm run format' }] }] } };
|
|
113
|
-
fs.mkdirSync(atHome('.claude'), { recursive: true });
|
|
114
|
-
fs.writeFileSync(atHome('.claude/settings.json'), JSON.stringify(ownSettings));
|
|
115
|
-
fs.mkdirSync(atHome('.cursor')); // Cursor is installed here; Windsurf and Cline are not
|
|
116
|
-
await setupCommand(repo, { semantic: false });
|
|
117
|
-
expect(git('status', '--porcelain', '--untracked-files=all')).toBe(''); // nothing in the working tree
|
|
118
|
-
expect(enabledHere(repo)).toBe(true);
|
|
119
|
-
expect(fs.readFileSync(path.join(repo, '.git/hooks/pre-push'), 'utf8')).toContain('hooks push --git');
|
|
120
|
-
const settings = JSON.parse(fs.readFileSync(atHome('.claude/settings.json'), 'utf8'));
|
|
121
|
-
expect(settings.permissions).toEqual(ownSettings.permissions);
|
|
122
|
-
const commands = JSON.stringify(settings.hooks);
|
|
123
|
-
expect(commands).toContain('npm run format');
|
|
124
|
-
expect(commands).toContain('rigour-enabled'); // every Rigour hook is guarded
|
|
125
|
-
for (const rel of ['.cursor/hooks.json', '.cursor/mcp.json'])
|
|
126
|
-
expect(fs.existsSync(atHome(rel)), rel).toBe(true);
|
|
127
|
-
for (const rel of ['.codeium', 'Documents'])
|
|
128
|
-
expect(fs.existsSync(atHome(rel)), rel).toBe(false); // nothing for an agent that is not installed
|
|
129
|
-
expect(fs.readFileSync(calls, 'utf8')).toMatch(/^mcp add --scope user rigour -- /m);
|
|
130
|
-
// Setting up a second repository reuses the machine install: one Rigour entry per event, not two.
|
|
131
|
-
await setupCommand(repo, { semantic: false });
|
|
132
|
-
const again = JSON.parse(fs.readFileSync(atHome('.claude/settings.json'), 'utf8'));
|
|
133
|
-
expect(again.hooks.Stop).toHaveLength(1);
|
|
134
|
-
// This repository off: the machine hooks stay, silent here now.
|
|
135
|
-
uninstall(repo);
|
|
136
|
-
expect(enabledHere(repo)).toBe(false);
|
|
137
|
-
expect(fs.existsSync(path.join(repo, '.git/hooks/pre-push'))).toBe(false);
|
|
138
|
-
expect(fs.existsSync(atHome('.cursor/hooks.json'))).toBe(true);
|
|
139
|
-
// Off the machine: the person's own settings are back as they were.
|
|
140
|
-
uninstall(repo, { machine: true });
|
|
141
|
-
expect(JSON.parse(fs.readFileSync(atHome('.claude/settings.json'), 'utf8'))).toEqual(ownSettings);
|
|
142
|
-
for (const rel of ['.cursor/hooks.json', '.codeium/windsurf/hooks.json', 'Documents/Cline/Hooks/PostToolUse', '.cursor/mcp.json']) {
|
|
143
|
-
expect(fs.existsSync(atHome(rel))).toBe(false);
|
|
144
|
-
}
|
|
145
|
-
expect(fs.readFileSync(calls, 'utf8')).toMatch(/^mcp remove --scope user rigour$/m);
|
|
146
|
-
expect(git('status', '--porcelain', '--untracked-files=all')).toBe('');
|
|
147
|
-
});
|
|
148
|
-
});
|
|
149
|
-
describe('rigour setup --team', () => {
|
|
150
|
-
it('commits the configuration, without instruction files or empty documents unless asked', async () => {
|
|
151
|
-
await setupCommand(repo, { semantic: false, team: true });
|
|
152
|
-
expect(fs.existsSync(path.join(repo, 'rigour.yml'))).toBe(true);
|
|
153
|
-
expect(fs.readFileSync(path.join(repo, '.claude/settings.json'), 'utf8')).toContain('hooks check');
|
|
154
|
-
for (const rel of ['CLAUDE.md', 'AGENTS.md', 'docs/AGENT_INSTRUCTIONS.md', 'docs/SPEC.md', 'docs/ARCH.md'])
|
|
155
|
-
expect(fs.existsSync(path.join(repo, rel))).toBe(false);
|
|
156
|
-
expect(enabledHere(repo)).toBe(false); // machine hooks stay silent beside the committed ones
|
|
157
|
-
});
|
|
158
|
-
it('writes instruction files where the project has none when asked', async () => {
|
|
159
|
-
await setupCommand(repo, { semantic: false, team: true, instructions: true });
|
|
160
|
-
expect(fs.existsSync(path.join(repo, 'CLAUDE.md'))).toBe(true);
|
|
161
|
-
});
|
|
162
|
-
it("leaves a teammate's clone exactly as committed, and still writes instructions when asked", async () => {
|
|
163
|
-
await setupCommand(repo, { semantic: false, team: true });
|
|
164
|
-
git('add', '-A');
|
|
165
|
-
git('commit', '-qm', 'adopt rigour');
|
|
166
|
-
await setupCommand(repo, { semantic: false }); // a teammate after cloning: rigour.yml is tracked
|
|
167
|
-
expect(git('status', '--porcelain')).toBe(''); // no committed file changed
|
|
168
|
-
expect(fs.readFileSync(path.join(repo, '.git/hooks/pre-push'), 'utf8')).toContain('hooks push --git');
|
|
169
|
-
await setupCommand(repo, { semantic: false, instructions: true });
|
|
170
|
-
expect(fs.readFileSync(path.join(repo, 'CLAUDE.md'), 'utf8')).toBe('@AGENTS.md\n');
|
|
171
|
-
});
|
|
172
|
-
it('keeps a .cursor/mcp.json that is not valid JSON', async () => {
|
|
173
|
-
fs.mkdirSync(path.join(repo, '.cursor'));
|
|
174
|
-
fs.writeFileSync(path.join(repo, '.cursor/mcp.json'), '{ not json');
|
|
175
|
-
await setupCommand(repo, { semantic: false, team: true });
|
|
176
|
-
expect(fs.readFileSync(path.join(repo, '.cursor/mcp.json'), 'utf8')).toBe('{ not json');
|
|
177
|
-
});
|
|
178
|
-
});
|
|
179
|
-
describe('rigour setup, personal, with git hooks kept in the repository', () => {
|
|
180
|
-
it('leaves a committed hooks folder (Husky) alone and says what to add', async () => {
|
|
181
|
-
fs.mkdirSync(path.join(repo, '.husky'));
|
|
182
|
-
fs.writeFileSync(path.join(repo, '.husky/pre-push'), 'npm test\n');
|
|
183
|
-
git('add', '-A');
|
|
184
|
-
git('commit', '-qm', 'husky');
|
|
185
|
-
git('config', 'core.hooksPath', '.husky');
|
|
186
|
-
await setupCommand(repo, { semantic: false });
|
|
187
|
-
expect(fs.readFileSync(path.join(repo, '.husky/pre-push'), 'utf8')).toBe('npm test\n');
|
|
188
|
-
expect(git('status', '--porcelain', '--untracked-files=all')).toBe('');
|
|
189
|
-
expect(vi.mocked(console.log).mock.calls.flat().join('\n')).toContain('hooks push --git "$@" || exit $?');
|
|
190
|
-
});
|
|
191
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|