@rigour-labs/cli 6.8.0 → 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.
Files changed (99) hide show
  1. package/package.json +7 -4
  2. package/dist/cli-help.test.d.ts +0 -1
  3. package/dist/cli-help.test.js +0 -27
  4. package/dist/commands/brief.test.d.ts +0 -1
  5. package/dist/commands/brief.test.js +0 -134
  6. package/dist/commands/deep-provider.test.d.ts +0 -1
  7. package/dist/commands/deep-provider.test.js +0 -19
  8. package/dist/commands/deep.test.d.ts +0 -1
  9. package/dist/commands/deep.test.js +0 -46
  10. package/dist/commands/demo.test.d.ts +0 -1
  11. package/dist/commands/demo.test.js +0 -59
  12. package/dist/commands/doctor.test.d.ts +0 -1
  13. package/dist/commands/doctor.test.js +0 -20
  14. package/dist/commands/exit-codes.test.d.ts +0 -1
  15. package/dist/commands/exit-codes.test.js +0 -20
  16. package/dist/commands/export-training-sites.test.d.ts +0 -1
  17. package/dist/commands/export-training-sites.test.js +0 -25
  18. package/dist/commands/hooks-check-repos.test.d.ts +0 -1
  19. package/dist/commands/hooks-check-repos.test.js +0 -33
  20. package/dist/commands/hooks-git.test.d.ts +0 -1
  21. package/dist/commands/hooks-git.test.js +0 -64
  22. package/dist/commands/hooks-push.test.d.ts +0 -1
  23. package/dist/commands/hooks-push.test.js +0 -89
  24. package/dist/commands/hooks-stop.test.d.ts +0 -1
  25. package/dist/commands/hooks-stop.test.js +0 -167
  26. package/dist/commands/hooks.test.d.ts +0 -1
  27. package/dist/commands/hooks.test.js +0 -256
  28. package/dist/commands/init-mcp-spec.test.d.ts +0 -1
  29. package/dist/commands/init-mcp-spec.test.js +0 -10
  30. package/dist/commands/init.test.d.ts +0 -1
  31. package/dist/commands/init.test.js +0 -107
  32. package/dist/commands/learn.test.d.ts +0 -1
  33. package/dist/commands/learn.test.js +0 -50
  34. package/dist/commands/personal.test.d.ts +0 -1
  35. package/dist/commands/personal.test.js +0 -191
  36. package/dist/commands/repo-setup.test.d.ts +0 -1
  37. package/dist/commands/repo-setup.test.js +0 -47
  38. package/dist/commands/review-checked.test.d.ts +0 -1
  39. package/dist/commands/review-checked.test.js +0 -50
  40. package/dist/commands/review-diff-tests.test.d.ts +0 -1
  41. package/dist/commands/review-diff-tests.test.js +0 -20
  42. package/dist/commands/review-human.test.d.ts +0 -1
  43. package/dist/commands/review-human.test.js +0 -94
  44. package/dist/commands/review-post.test.d.ts +0 -1
  45. package/dist/commands/review-post.test.js +0 -157
  46. package/dist/commands/review-pr-body.test.d.ts +0 -1
  47. package/dist/commands/review-pr-body.test.js +0 -19
  48. package/dist/commands/review-receipt.test.d.ts +0 -1
  49. package/dist/commands/review-receipt.test.js +0 -16
  50. package/dist/commands/review-reviewer.test.d.ts +0 -1
  51. package/dist/commands/review-reviewer.test.js +0 -68
  52. package/dist/commands/review-scope.test.d.ts +0 -1
  53. package/dist/commands/review-scope.test.js +0 -43
  54. package/dist/commands/review-stats.test.d.ts +0 -1
  55. package/dist/commands/review-stats.test.js +0 -19
  56. package/dist/commands/review-stdin.test.d.ts +0 -1
  57. package/dist/commands/review-stdin.test.js +0 -21
  58. package/dist/commands/review-summary.test.d.ts +0 -1
  59. package/dist/commands/review-summary.test.js +0 -46
  60. package/dist/commands/review-task.test.d.ts +0 -1
  61. package/dist/commands/review-task.test.js +0 -47
  62. package/dist/commands/scan-rules.test.d.ts +0 -1
  63. package/dist/commands/scan-rules.test.js +0 -29
  64. package/dist/commands/scan.test.d.ts +0 -1
  65. package/dist/commands/scan.test.js +0 -64
  66. package/dist/commands/skills.test.d.ts +0 -1
  67. package/dist/commands/skills.test.js +0 -56
  68. package/dist/commands/studio-activity.test.d.ts +0 -1
  69. package/dist/commands/studio-activity.test.js +0 -47
  70. package/dist/commands/studio-checkouts.test.d.ts +0 -1
  71. package/dist/commands/studio-checkouts.test.js +0 -23
  72. package/dist/commands/studio-context.test.d.ts +0 -1
  73. package/dist/commands/studio-context.test.js +0 -26
  74. package/dist/commands/studio-contracts.test.d.ts +0 -1
  75. package/dist/commands/studio-contracts.test.js +0 -8
  76. package/dist/commands/studio-guard.test.d.ts +0 -1
  77. package/dist/commands/studio-guard.test.js +0 -24
  78. package/dist/commands/studio-learned-rules.test.d.ts +0 -1
  79. package/dist/commands/studio-learned-rules.test.js +0 -31
  80. package/dist/commands/studio-learning.test.d.ts +0 -1
  81. package/dist/commands/studio-learning.test.js +0 -41
  82. package/dist/commands/studio-progress.test.d.ts +0 -1
  83. package/dist/commands/studio-progress.test.js +0 -41
  84. package/dist/commands/studio-reviewer.test.d.ts +0 -1
  85. package/dist/commands/studio-reviewer.test.js +0 -92
  86. package/dist/commands/studio-week.test.d.ts +0 -1
  87. package/dist/commands/studio-week.test.js +0 -32
  88. package/dist/commands/thread.test.d.ts +0 -1
  89. package/dist/commands/thread.test.js +0 -32
  90. package/dist/commands/uninstall.test.d.ts +0 -1
  91. package/dist/commands/uninstall.test.js +0 -104
  92. package/dist/init-rules.test.d.ts +0 -1
  93. package/dist/init-rules.test.js +0 -55
  94. package/dist/smoke.test.d.ts +0 -1
  95. package/dist/smoke.test.js +0 -82
  96. package/dist/utils/cli-version.test.d.ts +0 -1
  97. package/dist/utils/cli-version.test.js +0 -9
  98. package/dist/utils/version.test.d.ts +0 -1
  99. package/dist/utils/version.test.js +0 -22
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rigour-labs/cli",
3
- "version": "6.8.0",
3
+ "version": "6.8.1-rc.1",
4
4
  "description": "Code review that happens while your agent writes: deterministic checks on the lines it changed, quiet unless it can prove a defect, and learning from your team's fixes and dismissals. Works with Claude Code, Cursor, Codex, Cline and Windsurf.",
5
5
  "engines": {
6
6
  "node": ">=22.13"
@@ -36,7 +36,10 @@
36
36
  "files": [
37
37
  "dist",
38
38
  "studio-dist",
39
- "templates"
39
+ "templates",
40
+ "!dist/**/*.test.js",
41
+ "!dist/**/*.test.d.ts",
42
+ "!dist/**/*.test.js.map"
40
43
  ],
41
44
  "repository": {
42
45
  "type": "git",
@@ -56,13 +59,13 @@
56
59
  "inquirer": "9.2.16",
57
60
  "ora": "^8.0.1",
58
61
  "yaml": "^2.8.2",
59
- "@rigour-labs/core": "6.8.0"
62
+ "@rigour-labs/core": "6.8.1-rc.1"
60
63
  },
61
64
  "devDependencies": {
62
65
  "@types/fs-extra": "^11.0.4",
63
66
  "@types/inquirer": "9.0.7",
64
67
  "@types/node": "^25.0.3",
65
- "@rigour-labs/studio": "6.8.0"
68
+ "@rigour-labs/studio": "6.8.1-rc.1"
66
69
  },
67
70
  "scripts": {
68
71
  "build": "tsc && pnpm bundle-studio",
@@ -1 +0,0 @@
1
- export {};
@@ -1,27 +0,0 @@
1
- import { Command } from 'commander';
2
- import { describe, expect, it } from 'vitest';
3
- import { configureHelp } from './cli-help.js';
4
- function program() {
5
- const p = new Command('rigour');
6
- for (const name of ['setup', 'review', 'studio', 'doctor', 'check', 'scan', 'learn'])
7
- p.command(name).description(name);
8
- return p;
9
- }
10
- describe('rigour help', () => {
11
- it('lists the four everyday commands and says how to see the rest', () => {
12
- const p = program();
13
- configureHelp(p, ['node', 'rigour', '--help']);
14
- const text = p.helpInformation() + '\n';
15
- expect(text).toMatch(/setup[\s\S]*review[\s\S]*studio[\s\S]*doctor/);
16
- expect(text).not.toMatch(/\n\s+check\b/);
17
- });
18
- it('lists every command with --all, and drops the flag before parsing', () => {
19
- const p = program();
20
- const argv = configureHelp(p, ['node', 'rigour', 'help', '--all']);
21
- expect(argv).toEqual(['node', 'rigour', 'help']);
22
- expect(p.helpInformation()).toMatch(/\n\s+check\b/);
23
- });
24
- it('leaves --all alone for any other command', () => {
25
- expect(configureHelp(program(), ['node', 'rigour', 'review', '--all'])).toEqual(['node', 'rigour', 'review', '--all']);
26
- });
27
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,134 +0,0 @@
1
- import { execFileSync } 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 { readThread } from '@rigour-labs/core';
7
- import { briefCommand, hooksBriefCommand, hooksBriefFileCommand } from './brief.js';
8
- import { hooksCheckCommand, hooksInitCommand } from './hooks.js';
9
- let repo;
10
- const write = (file, text) => {
11
- fs.mkdirSync(path.dirname(path.join(repo, file)), { recursive: true });
12
- fs.writeFileSync(path.join(repo, file), text);
13
- };
14
- beforeEach(() => {
15
- repo = fs.mkdtempSync(path.join(os.tmpdir(), 'brief-cli-'));
16
- execFileSync('git', ['-C', repo, 'init', '-q', '-b', 'feat/retry']);
17
- write('AGENTS.md', '- Every job in `src/jobs/` must take `withLock()` before its first read; a job that reads first double-sends.\n');
18
- write('src/jobs/retry.ts', 'export async function retryJob() {}\n');
19
- execFileSync('git', ['-C', repo, 'add', '-A']);
20
- delete process.env.RIGOUR_BRIEF;
21
- });
22
- afterEach(() => {
23
- vi.restoreAllMocks();
24
- delete process.env.RIGOUR_BRIEF;
25
- fs.rmSync(repo, { recursive: true, force: true });
26
- });
27
- describe('the prompt hook', () => {
28
- const payload = (session, prompt = 'make the jobs retry with backoff') => JSON.stringify({ cwd: repo, session_id: session, prompt, hook_event_name: 'UserPromptSubmit' });
29
- it("briefs from the session's first prompt, once per session, as context Claude Code adds", async () => {
30
- const reply = JSON.parse(await hooksBriefCommand(payload('s1'), '/'));
31
- expect(reply.hookSpecificOutput.hookEventName).toBe('UserPromptSubmit');
32
- expect(reply.hookSpecificOutput.additionalContext).toContain('1. [must] Every job in `src/jobs/` must take `withLock()`');
33
- expect(await hooksBriefCommand(payload('s1', 'now add tests'), '/')).toBe(''); // never twice in a session
34
- expect(await hooksBriefCommand(payload('s2'), '/')).not.toBe(''); // a new session is briefed
35
- expect(readThread(repo)?.events.map(e => [e.kind, e.session, e.agent])).toEqual([['brief', 's1', 'claude'], ['brief', 's2', 'claude']]);
36
- });
37
- it('says nothing when the team or the person switched briefings off, or the payload is not a session', async () => {
38
- write('rigour.yml', 'version: 1\nbrief:\n enabled: false\n');
39
- expect(await hooksBriefCommand(payload('s1'), '/')).toBe('');
40
- fs.rmSync(path.join(repo, 'rigour.yml'));
41
- process.env.RIGOUR_BRIEF = '0';
42
- expect(await hooksBriefCommand(payload('s2'), '/')).toBe('');
43
- delete process.env.RIGOUR_BRIEF;
44
- expect(await hooksBriefCommand('not json', '/')).toBe('');
45
- expect(await hooksBriefCommand(JSON.stringify({ cwd: repo, prompt: 'x' }), '/')).toBe('');
46
- expect(readThread(repo)?.events ?? []).toEqual([]);
47
- });
48
- it('caps the briefing at the max_items the team set', async () => {
49
- write('AGENTS.md', Array.from({ length: 8 }, (_, i) => `- Every job in \`src/jobs/\` must call \`step${i}()\` before \`retryJob\` reads.\n`).join('\n'));
50
- write('rigour.yml', 'version: 1\nbrief:\n max_items: 3\n');
51
- const text = JSON.parse(await hooksBriefCommand(payload('s3', 'retry job'), '/')).hookSpecificOutput.additionalContext;
52
- expect(text.split('\n').filter(l => /^\d+\. /.test(l))).toHaveLength(3);
53
- });
54
- });
55
- describe('the edit hook', () => {
56
- const edit = (session, file) => JSON.stringify({ cwd: repo, session_id: session, hook_event_name: 'PreToolUse', tool_name: 'Edit', tool_input: { file_path: path.join(repo, file) } });
57
- it("gives the team's word on a file the first time a session edits it, once per file per session", async () => {
58
- const first = JSON.parse(await hooksBriefFileCommand(edit('s1', 'src/jobs/retry.ts'), '/'));
59
- expect(first.hookSpecificOutput.hookEventName).toBe('PreToolUse');
60
- expect(first.hookSpecificOutput.additionalContext).toContain('Rigour, before you edit src/jobs/retry.ts');
61
- expect(first.hookSpecificOutput.additionalContext).toContain('1. [must] Every job in `src/jobs/` must take `withLock()`');
62
- expect(await hooksBriefFileCommand(edit('s1', 'src/jobs/retry.ts'), '/')).toBe(''); // the same file again
63
- expect(await hooksBriefFileCommand(edit('s2', 'src/jobs/retry.ts'), '/')).not.toBe(''); // another session
64
- expect(await hooksBriefFileCommand(edit('s1', 'README.md'), '/')).toBe(''); // nothing applies: nothing said
65
- expect(readThread(repo)?.events.map(e => [e.kind, e.file, e.session, e.items])).toEqual([['brief', 'src/jobs/retry.ts', 's1', 1], ['brief', 'src/jobs/retry.ts', 's2', 1], ['brief', 'README.md', 's1', 0]]);
66
- });
67
- it('places a file being created in a new folder, and says nothing for a file of another repository inside this one', async () => {
68
- const created = JSON.parse(await hooksBriefFileCommand(edit('n1', 'src/jobs/new/sweep.ts'), '/'));
69
- expect(created.hookSpecificOutput.additionalContext).toContain('Rigour, before you edit src/jobs/new/sweep.ts');
70
- fs.mkdirSync(path.join(repo, 'vendor/other'), { recursive: true });
71
- execFileSync('git', ['-C', path.join(repo, 'vendor/other'), 'init', '-q']);
72
- expect(await hooksBriefFileCommand(edit('n1', 'vendor/other/src/jobs/x.ts'), '/')).toBe('');
73
- });
74
- it('says nothing for a file outside the repository, a bad payload, or when briefings are switched off', async () => {
75
- expect(await hooksBriefFileCommand(JSON.stringify({ cwd: repo, session_id: 's1', tool_input: { file_path: '/etc/hosts' } }), '/')).toBe('');
76
- expect(await hooksBriefFileCommand('not json', '/')).toBe('');
77
- expect(await hooksBriefFileCommand(JSON.stringify({ cwd: repo, tool_input: { file_path: 'src/jobs/retry.ts' } }), '/')).toBe('');
78
- process.env.RIGOUR_BRIEF = 'off';
79
- expect(await hooksBriefFileCommand(edit('s3', 'src/jobs/retry.ts'), '/')).toBe('');
80
- delete process.env.RIGOUR_BRIEF;
81
- write('rigour.yml', 'version: 1\nbrief:\n enabled: false\n');
82
- expect(await hooksBriefFileCommand(edit('s4', 'src/jobs/retry.ts'), '/')).toBe('');
83
- expect(readThread(repo)?.events ?? []).toEqual([]);
84
- });
85
- });
86
- describe('the DLP pre-tool hook with --brief', () => {
87
- it("adds the file's briefing to its own answer on a first edit, and nothing on other tools or later edits", async () => {
88
- const out = [];
89
- vi.spyOn(process.stdout, 'write').mockImplementation((chunk) => (out.push(String(chunk)), true));
90
- vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
91
- const run = async (tool, session) => {
92
- out.length = 0;
93
- await hooksCheckCommand(repo, { mode: 'dlp', brief: true, files: JSON.stringify({ cwd: repo, session_id: session, tool_name: tool, tool_input: { file_path: path.join(repo, 'src/jobs/retry.ts') } }) });
94
- return JSON.parse(out.join(''));
95
- };
96
- const first = await run('Edit', 'd1');
97
- expect(first.status).toBe('clean'); // the DLP answer is unchanged
98
- expect(first.hookSpecificOutput).toMatchObject({ hookEventName: 'PreToolUse', additionalContext: expect.stringContaining('Rigour, before you edit src/jobs/retry.ts') });
99
- expect((await run('Edit', 'd1')).hookSpecificOutput).toBeUndefined(); // briefed once in the session
100
- expect((await run('Read', 'd2')).hookSpecificOutput).toBeUndefined(); // not an edit
101
- out.length = 0;
102
- await hooksCheckCommand(repo, { mode: 'dlp', files: JSON.stringify({ cwd: repo, session_id: 'd3', tool_name: 'Edit', tool_input: { file_path: path.join(repo, 'src/jobs/retry.ts') } }) });
103
- expect(JSON.parse(out.join('')).hookSpecificOutput).toBeUndefined(); // without --brief, DLP only
104
- });
105
- });
106
- describe('rigour brief', () => {
107
- it('prints the briefing as JSON for a goal and files', async () => {
108
- const out = [];
109
- vi.spyOn(console, 'log').mockImplementation((line) => void out.push(String(line)));
110
- expect(await briefCommand(repo, 'retry', { files: 'src/jobs/retry.ts', json: true })).toBe(0);
111
- expect(JSON.parse(out.join('\n'))).toMatchObject({ goal: 'retry', files: ['src/jobs/retry.ts'], items: [{ kind: 'rule', requirement: true, cite: 'AGENTS.md' }] });
112
- });
113
- });
114
- describe('rigour hooks init --brief', () => {
115
- it('installs the prompt hook only when asked', async () => {
116
- vi.spyOn(console, 'log').mockImplementation(() => undefined);
117
- await hooksInitCommand(repo, { tool: 'claude' });
118
- const plain = JSON.parse(fs.readFileSync(path.join(repo, '.claude/settings.json'), 'utf8')).hooks;
119
- expect(plain.UserPromptSubmit).toBeUndefined();
120
- expect(plain.PreToolUse.some((h) => h.matcher === 'Write|Edit|MultiEdit')).toBe(false);
121
- await hooksInitCommand(repo, { tool: 'claude', brief: true, force: true });
122
- const hook = JSON.parse(fs.readFileSync(path.join(repo, '.claude/settings.json'), 'utf8')).hooks.UserPromptSubmit;
123
- expect(hook[0].hooks[0].command).toMatch(/ brief$/);
124
- expect(hook[0].hooks[0].timeout).toBe(20);
125
- // DLP on (the default): the DLP hook, which already runs before every tool, briefs too; no second process per edit.
126
- const pre = JSON.parse(fs.readFileSync(path.join(repo, '.claude/settings.json'), 'utf8')).hooks.PreToolUse;
127
- expect(pre.some((h) => h.matcher === 'Write|Edit|MultiEdit')).toBe(false);
128
- expect(pre.find((h) => h.matcher === '.*').hooks[0].command).toMatch(/--mode dlp --stdin --brief$/);
129
- // DLP off: an edit hook of its own.
130
- await hooksInitCommand(repo, { tool: 'claude', brief: true, dlp: false, force: true });
131
- const own = JSON.parse(fs.readFileSync(path.join(repo, '.claude/settings.json'), 'utf8')).hooks.PreToolUse.find((h) => h.matcher === 'Write|Edit|MultiEdit');
132
- expect(own.hooks[0].command).toMatch(/ brief-file$/);
133
- });
134
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,19 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { deepProvider } from './deep-provider.js';
3
- import { UsageError } from './review-config.js';
4
- describe('deepProvider', () => {
5
- it('uses the resolved provider when there is a key', () => {
6
- expect(deepProvider('openrouter', 'openrouter', 'k')).toEqual({ provider: 'openrouter', apiKey: 'k' });
7
- expect(deepProvider(undefined, undefined, 'k')).toEqual({ provider: 'claude', apiKey: 'k' });
8
- });
9
- it('refuses a named cloud provider with no key instead of switching to the local model', () => {
10
- expect(() => deepProvider('openrouter', 'openrouter', undefined)).toThrow(UsageError);
11
- expect(() => deepProvider('openrouter', 'openrouter', undefined)).toThrow(/RIGOUR_API_KEY/);
12
- });
13
- it('runs the built-in model when no provider was named, and a model server on this machine when one was', () => {
14
- expect(deepProvider(undefined, 'anthropic', undefined)).toEqual({ provider: 'local' });
15
- expect(deepProvider('local', 'local', undefined)).toEqual({ provider: 'local' });
16
- expect(deepProvider('Ollama', 'ollama', undefined)).toEqual({ provider: 'ollama', apiKey: 'ollama' });
17
- expect(deepProvider('lmstudio', 'lmstudio', undefined)).toEqual({ provider: 'lmstudio', apiKey: 'lmstudio' });
18
- });
19
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,46 +0,0 @@
1
- import { afterEach, describe, expect, it, vi } from 'vitest';
2
- const core = vi.hoisted(() => ({
3
- prepare: vi.fn(),
4
- findEngine: vi.fn(),
5
- getCachedModel: vi.fn(),
6
- tiers: [],
7
- }));
8
- vi.mock('@rigour-labs/core', () => ({
9
- SidecarProvider: class {
10
- constructor(tier) { core.tiers.push(tier); }
11
- prepare = core.prepare;
12
- findEngine = core.findEngine;
13
- },
14
- getCachedModel: core.getCachedModel,
15
- }));
16
- const { deepPullCommand } = await import('./deep.js');
17
- describe('rigour deep pull', () => {
18
- afterEach(() => {
19
- vi.restoreAllMocks();
20
- core.tiers.length = 0;
21
- });
22
- it('prepares the lite tier by default and reports what it installed', async () => {
23
- core.prepare.mockResolvedValue(undefined);
24
- core.findEngine.mockResolvedValue({ path: '/home/.rigour/bin/llama-b5604/llama-cli', version: '5604' });
25
- core.getCachedModel.mockResolvedValue({ info: { name: 'Qwen2.5-Coder-0.5B-Instruct (stock)' } });
26
- const log = vi.spyOn(console, 'log').mockImplementation(() => undefined);
27
- expect(await deepPullCommand(false)).toBe(0);
28
- expect(core.tiers).toEqual(['lite']);
29
- expect(log.mock.calls.flat().join('\n')).toContain('llama.cpp 5604');
30
- expect(log.mock.calls.flat().join('\n')).toContain('Qwen2.5-Coder-0.5B-Instruct (stock)');
31
- });
32
- it('prepares the deep tier with --pro', async () => {
33
- core.prepare.mockResolvedValue(undefined);
34
- core.findEngine.mockResolvedValue(null);
35
- core.getCachedModel.mockResolvedValue(null);
36
- vi.spyOn(console, 'log').mockImplementation(() => undefined);
37
- await deepPullCommand(true);
38
- expect(core.tiers).toEqual(['deep']);
39
- });
40
- it('exits 3 with the reason when the engine or model cannot be installed', async () => {
41
- core.prepare.mockRejectedValue(new Error('HTTP 404 for https://github.com/...'));
42
- const error = vi.spyOn(console, 'error').mockImplementation(() => undefined);
43
- expect(await deepPullCommand(false)).toBe(3);
44
- expect(error.mock.calls.flat().join('\n')).toContain('HTTP 404');
45
- });
46
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,59 +0,0 @@
1
- /**
2
- * Tests for the demo command — all modes: default, --hooks, --cinematic.
3
- */
4
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
5
- import { demoCommand } from './demo.js';
6
- describe('demoCommand', () => {
7
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
8
- let consoleSpy;
9
- beforeEach(() => {
10
- consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
11
- vi.spyOn(console, 'error').mockImplementation(() => { });
12
- vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
13
- });
14
- afterEach(() => {
15
- vi.restoreAllMocks();
16
- });
17
- it('should run default demo without errors', async () => {
18
- await expect(demoCommand({})).resolves.not.toThrow();
19
- }, 30_000);
20
- it('should run with --hooks flag', async () => {
21
- await expect(demoCommand({ hooks: true, speed: 'fast' })).resolves.not.toThrow();
22
- // Should show hooks simulation
23
- const allOutput = consoleSpy.mock.calls.map((c) => c.join(' ')).join('\n');
24
- expect(allOutput).toContain('hooks');
25
- }, 30_000);
26
- it('should run with --cinematic flag at fast speed', async () => {
27
- await expect(demoCommand({ cinematic: true, speed: 'fast' })).resolves.not.toThrow();
28
- // Cinematic mode should show the before/after flow
29
- const allOutput = consoleSpy.mock.calls.map((c) => c.join(' ')).join('\n');
30
- expect(allOutput).toContain('fix');
31
- }, 60_000);
32
- it('should produce FAIL status on demo project', async () => {
33
- await demoCommand({ speed: 'fast' });
34
- const allOutput = consoleSpy.mock.calls.map((c) => c.join(' ')).join('\n');
35
- expect(allOutput).toContain('FAIL');
36
- }, 30_000);
37
- it('should generate fix packet and audit report', async () => {
38
- await demoCommand({ speed: 'fast' });
39
- const allOutput = consoleSpy.mock.calls.map((c) => c.join(' ')).join('\n');
40
- expect(allOutput).toContain('Fix packet generated');
41
- expect(allOutput).toContain('Audit report exported');
42
- }, 30_000);
43
- it('should show score bars in output', async () => {
44
- await demoCommand({ speed: 'fast' });
45
- const allOutput = consoleSpy.mock.calls.map((c) => c.join(' ')).join('\n');
46
- // Score bars use block characters
47
- expect(allOutput).toContain('/100');
48
- }, 30_000);
49
- it('should show whitepaper link in cinematic closing', async () => {
50
- await demoCommand({ cinematic: true, speed: 'fast' });
51
- const allOutput = consoleSpy.mock.calls.map((c) => c.join(' ')).join('\n');
52
- expect(allOutput).toContain('zenodo.org');
53
- }, 60_000);
54
- it('should show hooks init command in closing', async () => {
55
- await demoCommand({ speed: 'fast' });
56
- const allOutput = consoleSpy.mock.calls.map((c) => c.join(' ')).join('\n');
57
- expect(allOutput).toContain('hooks init');
58
- }, 30_000);
59
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,20 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { detectInstallKind, hasVersionShadowing } from './doctor.js';
3
- describe('doctor helpers', () => {
4
- it('classifies Homebrew paths', () => {
5
- expect(detectInstallKind('/opt/homebrew/bin/rigour')).toBe('homebrew');
6
- expect(detectInstallKind('/usr/local/bin/rigour')).toBe('homebrew');
7
- expect(detectInstallKind('/opt/homebrew/Cellar/rigour/4.0.5/bin/rigour')).toBe('homebrew');
8
- });
9
- it('classifies npm/global node_modules paths', () => {
10
- expect(detectInstallKind('/opt/homebrew/lib/node_modules/@rigour-labs/cli/dist/cli.js')).toBe('npm');
11
- expect(detectInstallKind('/Users/test/.nvm/versions/node/v22.0.0/lib/node_modules/@rigour-labs/cli/dist/cli.js')).toBe('npm');
12
- });
13
- it('returns unknown for unrelated paths', () => {
14
- expect(detectInstallKind('/usr/bin/rigour')).toBe('unknown');
15
- });
16
- it('detects version shadowing only when versions differ', () => {
17
- expect(hasVersionShadowing(['4.0.5', '4.0.5'])).toBe(false);
18
- expect(hasVersionShadowing(['4.0.5', '2.0.0'])).toBe(true);
19
- });
20
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,20 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { exitCodeFor, EXIT_FAIL, EXIT_INTERNAL_ERROR, EXIT_PASS } from './exit-codes.js';
3
- const report = (status, deepStatus) => ({
4
- status,
5
- stats: { duration_ms: 1, ...(deepStatus ? { deep: { enabled: true, status: deepStatus } } : {}) },
6
- });
7
- describe('exitCodeFor', () => {
8
- it('maps PASS and FAIL to 0 and 1', () => {
9
- expect(exitCodeFor(report('PASS'))).toBe(EXIT_PASS);
10
- expect(exitCodeFor(report('FAIL'))).toBe(EXIT_FAIL);
11
- });
12
- it('exits 3 when deep analysis was requested but did not run', () => {
13
- expect(exitCodeFor(report('FAIL', 'error'))).toBe(EXIT_INTERNAL_ERROR);
14
- expect(exitCodeFor(report('PASS', 'error'))).toBe(EXIT_INTERNAL_ERROR);
15
- });
16
- it('keeps the normal code for completed or partial deep runs', () => {
17
- expect(exitCodeFor(report('PASS', 'ok'))).toBe(EXIT_PASS);
18
- expect(exitCodeFor(report('FAIL', 'partial'))).toBe(EXIT_FAIL);
19
- });
20
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,25 +0,0 @@
1
- import { execFileSync } from 'child_process';
2
- import fs from 'fs';
3
- import os from 'os';
4
- import path from 'path';
5
- import { afterEach, describe, expect, it, vi } from 'vitest';
6
- import { exportTrainingSitesCommand } from './export-training-sites.js';
7
- describe('rigour export-training-sites', () => {
8
- let repo;
9
- afterEach(() => { if (repo)
10
- fs.rmSync(repo, { recursive: true, force: true }); repo = undefined; vi.restoreAllMocks(); });
11
- it('writes one JSON line per awaited call in tracked source files, skipping tests', () => {
12
- repo = fs.mkdtempSync(path.join(os.tmpdir(), 'sites-cli-'));
13
- execFileSync('git', ['init', '-q'], { cwd: repo });
14
- fs.mkdirSync(path.join(repo, 'src'));
15
- fs.writeFileSync(path.join(repo, 'src/load.ts'), 'declare function get(): Promise<number>;\nexport async function load() { return await get(); }\n');
16
- fs.writeFileSync(path.join(repo, 'src/load.test.ts'), 'declare function get(): Promise<number>;\nexport async function t() { return await get(); }\n');
17
- execFileSync('git', ['add', '-A'], { cwd: repo });
18
- const out = [];
19
- vi.spyOn(process.stdout, 'write').mockImplementation((chunk) => { out.push(String(chunk)); return true; });
20
- exportTrainingSitesCommand(repo, []);
21
- const sites = out.join('').trim().split('\n').map(line => JSON.parse(line));
22
- expect(sites).toEqual([expect.objectContaining({ file: 'src/load.ts', function: 'load', callee: 'get', ordinal: 0, handledBy: null, line: 2 })]);
23
- expect(sites[0].source).toContain('return await get()');
24
- });
25
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,33 +0,0 @@
1
- import { execFileSync } 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 } from 'vitest';
6
- import { groupFilesByRepo, hookFindings } from './hooks-check-repos.js';
7
- let base;
8
- const repo = (name) => {
9
- const dir = path.join(base, name);
10
- fs.mkdirSync(path.join(dir, 'src'), { recursive: true });
11
- execFileSync('git', ['init', '-q', dir]);
12
- return fs.realpathSync.native(dir);
13
- };
14
- beforeEach(() => { base = fs.mkdtempSync(path.join(os.tmpdir(), 'hook-repos-')); });
15
- afterEach(() => fs.rmSync(base, { recursive: true, force: true }));
16
- describe('groupFilesByRepo', () => {
17
- it("checks each file in its own repository, not the session's", () => {
18
- const app = repo('app');
19
- const lib = repo('lib');
20
- const groups = groupFilesByRepo(app, ['src/a.ts', path.join(lib, 'src/b.ts')]);
21
- expect(groups).toEqual([{ root: app, files: ['src/a.ts'] }, { root: lib, files: ['src/b.ts'] }]);
22
- });
23
- });
24
- describe('hookFindings', () => {
25
- it('keeps code findings and drops findings about the write itself', () => {
26
- const result = { status: 'fail', duration_ms: 1, failures: [
27
- { gate: 'security-patterns', file: 'src/a.ts', message: 'Hard-coded secret', severity: 'critical', line: 3 },
28
- { gate: 'file-guard', file: '.rigour/dismissed.json', message: 'BLOCKED', severity: 'critical' },
29
- { gate: 'file-size', file: 'src/big.ts', message: 'Too long', severity: 'low' },
30
- ] };
31
- expect(hookFindings(result)).toEqual([expect.objectContaining({ id: 'security-patterns', title: 'Hard-coded secret', files: ['src/a.ts'], line: 3 })]);
32
- });
33
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,64 +0,0 @@
1
- import { execFileSync } from 'child_process';
2
- import fs from 'fs';
3
- import os from 'os';
4
- import path from 'path';
5
- import { fileURLToPath } from 'url';
6
- import { afterEach, beforeEach, describe, expect, it } from 'vitest';
7
- import { gitPushGateCommand, installGitPushHook, selfTestCommand, selfTestGitPushHook } from './hooks-git.js';
8
- let repo;
9
- const git = (...args) => execFileSync('git', ['-C', repo, ...args], { encoding: 'utf8' }).trim();
10
- const bin = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../dist/bin.js');
11
- beforeEach(() => {
12
- repo = fs.mkdtempSync(path.join(os.tmpdir(), 'git-hook-'));
13
- git('init', '-q', '-b', 'main');
14
- git('config', 'user.email', 't@example.com');
15
- git('config', 'user.name', 't');
16
- git('config', 'commit.gpgsign', 'false');
17
- git('config', 'core.hooksPath', '.git/hooks'); // a machine-wide hooks path must not reach the test
18
- fs.writeFileSync(path.join(repo, 'a.ts'), 'export const a = 1;\n');
19
- fs.writeFileSync(path.join(repo, 'rigour.yml'), 'version: 1\ngates:\n unused_exports:\n block: true\n'); // this team blocks on dead code
20
- git('add', '-A');
21
- git('commit', '-qm', 'init');
22
- });
23
- afterEach(() => { fs.rmSync(repo, { recursive: true, force: true }); });
24
- describe('the git pre-push hook', () => {
25
- it('is installed where git looks, once, and completes a hook another tool owns instead of replacing it', () => {
26
- const first = installGitPushHook(repo, 'npx --yes @rigour-labs/cli@9.9.9');
27
- expect(first.action).toBe('installed');
28
- const text = fs.readFileSync(first.path, 'utf8');
29
- expect(text).toContain('exec npx --yes @rigour-labs/cli@9.9.9 hooks push --git "$@"');
30
- if (process.platform !== 'win32')
31
- expect(fs.statSync(first.path).mode & 0o111).toBeTruthy(); // no execute bit on Windows
32
- expect(installGitPushHook(repo, 'npx --yes @rigour-labs/cli@9.9.9').action).toBe('present');
33
- git('config', 'core.hooksPath', '.husky');
34
- fs.mkdirSync(path.join(repo, '.husky'));
35
- fs.writeFileSync(path.join(repo, '.husky/pre-push'), '#!/bin/sh\nnpm test\n');
36
- const appended = installGitPushHook(repo, 'rigour');
37
- expect(appended).toMatchObject({ action: 'appended', path: path.join(repo, '.husky/pre-push') });
38
- expect(fs.readFileSync(appended.path, 'utf8')).toBe('#!/bin/sh\nnpm test\n\n# Rigour push gate (rigour hooks init): the same gate for every tool and the terminal.\nrigour hooks push --git "$@" || exit $?\n');
39
- expect(installGitPushHook(repo, 'rigour').action).toBe('present');
40
- expect(installGitPushHook(path.join(os.tmpdir()), 'rigour').action).toBe('no repository');
41
- // A hooks directory outside the repository belongs to whoever set it: named, never written.
42
- const elsewhere = fs.mkdtempSync(path.join(os.tmpdir(), 'global-hooks-'));
43
- fs.writeFileSync(path.join(elsewhere, 'pre-push'), '#!/bin/sh\nexit 0\n');
44
- git('config', 'core.hooksPath', elsewhere);
45
- expect(installGitPushHook(repo, 'rigour')).toEqual({ path: path.join(elsewhere, 'pre-push'), action: 'managed elsewhere' });
46
- expect(fs.readFileSync(path.join(elsewhere, 'pre-push'), 'utf8')).toBe('#!/bin/sh\nexit 0\n');
47
- fs.rmSync(elsewhere, { recursive: true, force: true });
48
- });
49
- it('refuses a push from a tree with uncommitted changes to tracked files, and lets a branch deletion through', async () => {
50
- const refs = `refs/heads/main ${git('rev-parse', 'HEAD')} refs/heads/main 0000000000000000000000000000000000000000\n`;
51
- fs.writeFileSync(path.join(repo, 'a.ts'), 'export const a = 2;\n');
52
- const dirty = await gitPushGateCommand(refs, repo);
53
- expect(dirty.exitCode).toBe(1);
54
- expect(dirty.message).toContain('uncommitted changes to tracked files');
55
- expect(dirty.message).toContain(' M a.ts');
56
- const deletion = await gitPushGateCommand('(delete) 0000000000000000000000000000000000000000 refs/heads/old 1234567890123456789012345678901234567890\n', repo);
57
- expect(deletion).toEqual({ exitCode: 0, message: '' });
58
- });
59
- it('holds under a real git push: a change the gate must refuse is refused, the fix lands, read from the remote', async () => {
60
- const result = await selfTestGitPushHook(selfTestCommand(bin));
61
- expect(result.steps.join('\n')).toContain('was refused');
62
- expect(result).toMatchObject({ ok: true });
63
- }, 120_000);
64
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,89 +0,0 @@
1
- import { execFileSync } 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 } from 'vitest';
6
- import { readThread } from '@rigour-labs/core';
7
- import { hooksPushCommand, isPush, pushTarget } from './hooks-push.js';
8
- describe('push command parsing', () => {
9
- it('acts on a real push only', () => {
10
- expect(isPush('git push -u origin feature')).toBe(true);
11
- expect(isPush('cd app && git -C . push')).toBe(true);
12
- expect(isPush('git push --dry-run')).toBe(false);
13
- expect(isPush('git status && npm test')).toBe(false);
14
- expect(isPush('echo "do not git pushx"')).toBe(false);
15
- expect(isPush('claude -p review --disallowedTools "Bash(git push:*)"')).toBe(false); // text inside an argument
16
- expect(isPush('npm test && git push origin main')).toBe(true);
17
- expect(isPush('git -C "/work/my app" push')).toBe(true);
18
- });
19
- it('finds the repository the push runs in', () => {
20
- expect(pushTarget('cd /work/app && git push')).toBe('/work/app');
21
- expect(pushTarget('git -C "/work/my app" push')).toBe('/work/my app');
22
- expect(pushTarget('git push')).toBeUndefined();
23
- });
24
- });
25
- describe('rigour hooks push', () => {
26
- let repo;
27
- const git = (...args) => execFileSync('git', ['-C', repo, ...args], { encoding: 'utf8' });
28
- const write = (rel, body) => {
29
- fs.mkdirSync(path.dirname(path.join(repo, rel)), { recursive: true });
30
- fs.writeFileSync(path.join(repo, rel), body);
31
- };
32
- const push = (command = 'git push') => hooksPushCommand(JSON.stringify({ cwd: repo, tool_input: { command } }), '/');
33
- beforeEach(() => {
34
- repo = fs.mkdtempSync(path.join(os.tmpdir(), 'push-gate-'));
35
- git('init', '-q', '-b', 'main');
36
- git('config', 'user.email', 't@example.com');
37
- git('config', 'user.name', 't');
38
- git('config', 'commit.gpgsign', 'false');
39
- write('.gitignore', '.rigour/\nnode_modules/\n');
40
- write('src/main.ts', "import { used } from './util';\nconsole.log(used);\n");
41
- write('src/util.ts', 'export const used = 1;\n');
42
- write('package.json', '{"scripts":{"start":"node src/main.ts"}}\n');
43
- write('rigour.yml', 'version: 1\ngates:\n unused_exports:\n block: true\n'); // this team blocks on dead code
44
- git('add', '-A');
45
- git('commit', '-qm', 'init');
46
- git('checkout', '-q', '-b', 'feature');
47
- });
48
- afterEach(() => { fs.rmSync(repo, { recursive: true, force: true }); });
49
- it('lets any other command, and a clean branch, through', async () => {
50
- expect(await push('npm test')).toEqual({ exitCode: 0, message: '' });
51
- write('src/util.ts', 'export const used = 2;\n');
52
- git('commit', '-qam', 'tweak');
53
- expect(await push()).toEqual({ exitCode: 0, message: '' });
54
- });
55
- it('blocks a push when a check could not run: a TypeScript project whose program cannot be built', async () => {
56
- write('tsconfig.json', '{"extends":"./.generated/tsconfig.json"}\n');
57
- write('src/util.ts', 'export const used = 2;\n');
58
- git('add', '-A');
59
- git('commit', '-qm', 'typed change, broken config');
60
- const result = await push();
61
- expect(result.exitCode).toBe(2);
62
- expect(result.message).toContain('- typed-checks-unavailable could not run: ');
63
- expect(result.message).toContain('.generated/tsconfig.json');
64
- // The task's thread keeps the blocked push.
65
- expect(readThread(repo, 'feature')?.events.map(e => [e.kind, e.passed, e.failed])).toEqual([['push', false, 1]]);
66
- });
67
- it('blocks a push that adds an export nothing uses, saying where', async () => {
68
- write('src/util.ts', 'export const used = 1;\nexport const forgotten = 2;\n');
69
- git('commit', '-qam', 'add forgotten');
70
- const result = await push();
71
- expect(result.exitCode).toBe(2);
72
- expect(result.message).toContain('Push blocked');
73
- expect(result.message).toContain('src/util.ts:2 Unused export');
74
- });
75
- it("blocks a push the project's own formatter rejects, and keeps the output in a log", async () => {
76
- const bin = path.join(repo, 'node_modules', '.bin');
77
- fs.mkdirSync(bin, { recursive: true });
78
- fs.writeFileSync(path.join(bin, 'prettier.js'), "console.log('[warn] src/util.ts'); process.exit(1);\n");
79
- fs.writeFileSync(path.join(bin, 'prettier'), '#!/bin/sh\nexec node "$(dirname "$0")/prettier.js" "$@"\n', { mode: 0o755 });
80
- fs.writeFileSync(path.join(bin, 'prettier.cmd'), '@node "%~dp0\\prettier.js" %*\r\n');
81
- write('src/util.ts', 'export const used = 3;\n');
82
- git('commit', '-qam', 'unformatted');
83
- const result = await push();
84
- expect(result.exitCode).toBe(2);
85
- expect(result.message).toContain('- format failed: prettier --check');
86
- const log = result.message.match(/full output: (\S+)\)/)[1];
87
- expect(fs.readFileSync(log, 'utf8')).toContain('[warn] src/util.ts');
88
- });
89
- });
@@ -1 +0,0 @@
1
- export {};