@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,47 +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 { checkRepoSetup } from './repo-setup.js';
|
|
7
|
-
let cwd;
|
|
8
|
-
const write = (rel, body) => { fs.mkdirSync(path.dirname(path.join(cwd, rel)), { recursive: true }); fs.writeFileSync(path.join(cwd, rel), body); };
|
|
9
|
-
beforeEach(() => { cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'setup-')); });
|
|
10
|
-
afterEach(() => fs.rmSync(cwd, { recursive: true, force: true }));
|
|
11
|
-
const now = new Date('2026-10-09T12:00:00Z');
|
|
12
|
-
const byId = async (id) => (await checkRepoSetup(cwd, now, [])).find(c => c.id === id);
|
|
13
|
-
describe('checkRepoSetup', () => {
|
|
14
|
-
it('says working only when the hook fired this week, not because a file exists', async () => {
|
|
15
|
-
write('.claude/settings.json', JSON.stringify({ hooks: { PostToolUse: [{ hooks: [{ command: 'rigour hooks check --stdin' }] }] } }));
|
|
16
|
-
expect(await byId('edit')).toMatchObject({ state: 'set up', detail: 'Configured; has not fired here yet' });
|
|
17
|
-
write('.rigour/events.jsonl', JSON.stringify({ type: 'hook_check', timestamp: '2026-10-09T11:50:00Z' }) + '\n');
|
|
18
|
-
expect(await byId('edit')).toMatchObject({ state: 'working', detail: '1 edit check this week · last 10 min ago' });
|
|
19
|
-
});
|
|
20
|
-
it('calls out the old hook that checked nothing', async () => {
|
|
21
|
-
write('.claude/settings.json', 'rigour hooks check --files "$TOOL_INPUT_file_path"');
|
|
22
|
-
expect(await byId('edit')).toMatchObject({ state: 'broken', fix: 'rigour hooks init --force' });
|
|
23
|
-
});
|
|
24
|
-
it('finds the PR workflow and reports what is missing', async () => {
|
|
25
|
-
expect((await byId('pr')).state).toBe('missing');
|
|
26
|
-
expect((await byId('config')).state).toBe('missing');
|
|
27
|
-
write('.github/workflows/review.yml', 'steps:\n - uses: rigour-labs/rigour@v6\n');
|
|
28
|
-
expect(await byId('pr')).toMatchObject({ state: 'set up', detail: '.github/workflows/review.yml' });
|
|
29
|
-
});
|
|
30
|
-
it('counts what agents did in every worktree of the repository', async () => {
|
|
31
|
-
const git = (...args) => execFileSync('git', ['-C', cwd, ...args], { encoding: 'utf8' });
|
|
32
|
-
git('init', '-q', '-b', 'main');
|
|
33
|
-
git('-c', 'user.email=t@example.com', '-c', 'user.name=t', 'commit', '-q', '--allow-empty', '-m', 'init');
|
|
34
|
-
const tree = path.join(cwd, 'wt');
|
|
35
|
-
git('worktree', 'add', '-q', tree);
|
|
36
|
-
write('.claude/settings.json', 'rigour hooks check --stdin; rigour hooks stop --tool claude');
|
|
37
|
-
fs.mkdirSync(path.join(tree, '.rigour'));
|
|
38
|
-
fs.writeFileSync(path.join(tree, '.rigour', 'events.jsonl'), JSON.stringify({ type: 'stop_review', timestamp: '2026-10-09T11:00:00Z' }) + '\n');
|
|
39
|
-
expect(await byId('stop')).toMatchObject({ state: 'working', detail: '1 finish check this week · last 1 h ago' });
|
|
40
|
-
});
|
|
41
|
-
it("counts the MCP server's tool calls, not the CLI's own events", async () => {
|
|
42
|
-
write('.rigour/events.jsonl', JSON.stringify({ type: 'tool_call', tool: 'rigour_init', timestamp: '2026-10-09T11:00:00Z' }) + '\n');
|
|
43
|
-
expect((await checkRepoSetup(cwd, now, [])).find(c => c.id === 'mcp').state).toBe('missing');
|
|
44
|
-
const call = { toolName: 'rigour_review', cacheStatus: 'none', candidateTokens: 0, returnedTokens: 0, createdAt: Date.parse('2026-10-09T11:30:00Z') };
|
|
45
|
-
expect((await checkRepoSetup(cwd, now, [call])).find(c => c.id === 'mcp')).toMatchObject({ state: 'working', detail: '1 tool call this week · last 30 min ago' });
|
|
46
|
-
});
|
|
47
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,50 +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 { whatWasChecked } from './review-checked.js';
|
|
7
|
-
let repo;
|
|
8
|
-
const git = (...args) => execFileSync('git', ['-C', repo, ...args], { encoding: 'utf8' }).trim();
|
|
9
|
-
const result = { report: { summary: { 'hallucinated-imports': 'PASS', 'unused-exports': 'FAIL' } } };
|
|
10
|
-
beforeEach(() => {
|
|
11
|
-
repo = fs.mkdtempSync(path.join(os.tmpdir(), 'checked-'));
|
|
12
|
-
git('init', '-q', '-b', 'main');
|
|
13
|
-
git('config', 'user.email', 't@example.com');
|
|
14
|
-
git('config', 'user.name', 't');
|
|
15
|
-
git('config', 'commit.gpgsign', 'false');
|
|
16
|
-
fs.writeFileSync(path.join(repo, 'a.ts'), 'export const a = 1;\n');
|
|
17
|
-
git('add', '-A');
|
|
18
|
-
git('commit', '-qm', 'init');
|
|
19
|
-
});
|
|
20
|
-
afterEach(() => { fs.rmSync(repo, { recursive: true, force: true }); });
|
|
21
|
-
describe('what a review checked', () => {
|
|
22
|
-
it('names the commits compared, whether uncommitted work was in it, the settings and every check', () => {
|
|
23
|
-
const base = git('rev-parse', 'HEAD');
|
|
24
|
-
git('checkout', '-q', '-b', 'feature');
|
|
25
|
-
fs.writeFileSync(path.join(repo, 'a.ts'), 'export const a = 2;\n');
|
|
26
|
-
git('commit', '-qam', 'change');
|
|
27
|
-
fs.writeFileSync(path.join(repo, 'a.ts'), 'export const a = 3;\n'); // not committed
|
|
28
|
-
expect(whatWasChecked(repo, { base: 'main' }, undefined, result)).toMatchObject({
|
|
29
|
-
rigour_version: expect.stringMatching(/^\d+\.\d+\.\d+/),
|
|
30
|
-
base: 'main',
|
|
31
|
-
base_sha: base,
|
|
32
|
-
head_sha: git('rev-parse', 'HEAD'),
|
|
33
|
-
uncommitted: true,
|
|
34
|
-
config: 'defaults',
|
|
35
|
-
checks: { 'hallucinated-imports': 'PASS', 'unused-exports': 'FAIL' },
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
it('says which rigour.yml it read: the working tree, the base for an independent review, or a file named by -c', () => {
|
|
39
|
-
fs.writeFileSync(path.join(repo, 'rigour.yml'), 'gates: {}\n');
|
|
40
|
-
expect(whatWasChecked(repo, {}, undefined, result)).toMatchObject({ base: null, base_sha: git('rev-parse', 'HEAD'), uncommitted: false, config: 'rigour.yml' });
|
|
41
|
-
const base = git('rev-parse', 'HEAD'); // rigour.yml is not committed there
|
|
42
|
-
expect(whatWasChecked(repo, { base: 'main' }, base, result).config).toBe('defaults');
|
|
43
|
-
git('add', '-A');
|
|
44
|
-
git('commit', '-qm', 'config');
|
|
45
|
-
expect(whatWasChecked(repo, { base: 'main' }, git('rev-parse', 'HEAD'), result).config).toBe(`rigour.yml at ${git('rev-parse', 'HEAD')}`);
|
|
46
|
-
fs.mkdirSync(path.join(repo, 'ci'));
|
|
47
|
-
fs.writeFileSync(path.join(repo, 'ci/strict.yml'), 'gates: {}\n');
|
|
48
|
-
expect(whatWasChecked(repo, { config: 'ci/strict.yml' }, undefined, result).config).toBe('ci/strict.yml');
|
|
49
|
-
});
|
|
50
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import os from 'os';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
-
import { reviewCommand } from './review.js';
|
|
6
|
-
describe('rigour review --diff-tests', () => {
|
|
7
|
-
let dir;
|
|
8
|
-
afterEach(() => { if (dir)
|
|
9
|
-
fs.rmSync(dir, { recursive: true, force: true }); dir = undefined; vi.restoreAllMocks(); });
|
|
10
|
-
it('refuses to run without a model that can propose test inputs', async () => {
|
|
11
|
-
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'diff-tests-cli-'));
|
|
12
|
-
fs.writeFileSync(path.join(dir, 'change.diff'), '');
|
|
13
|
-
const out = [];
|
|
14
|
-
vi.spyOn(console, 'log').mockImplementation((line) => { out.push(line); });
|
|
15
|
-
const exit = vi.spyOn(process, 'exit').mockImplementation(((code) => { throw new Error(`exit ${code}`); }));
|
|
16
|
-
await expect(reviewCommand(dir, { json: true, diff: 'change.diff', deep: true, diffTests: true })).rejects.toThrow('exit 2');
|
|
17
|
-
expect(exit).toHaveBeenCalledWith(2);
|
|
18
|
-
expect(JSON.parse(out[0])).toEqual({ error: 'INPUT_ERROR', message: expect.stringContaining('--diff-tests needs a model') });
|
|
19
|
-
});
|
|
20
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,94 +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 { printHuman } from './review-human.js';
|
|
6
|
-
let dir;
|
|
7
|
-
let out;
|
|
8
|
-
beforeEach(() => {
|
|
9
|
-
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'rigour-human-'));
|
|
10
|
-
out = [];
|
|
11
|
-
vi.spyOn(console, 'log').mockImplementation((...args) => void out.push(args.join(' ')));
|
|
12
|
-
});
|
|
13
|
-
afterEach(() => {
|
|
14
|
-
vi.restoreAllMocks();
|
|
15
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
16
|
-
});
|
|
17
|
-
const finding = (n, over = {}) => ({
|
|
18
|
-
id: 'unused-export', title: `export f${n} is used nowhere`, details: '', files: ['src/a.ts'], line: n, hint: 'Remove the export.', ...over,
|
|
19
|
-
});
|
|
20
|
-
const result = (over = {}) => ({
|
|
21
|
-
status: 'PASS', findings: [], fileFindings: [], contextFindings: [], advisory: [], muted: 0, dismissed: 0, dismissedByGate: {},
|
|
22
|
-
unlocated: 0, excludedOutsideChangedLines: 0, preexisting: 0, changedLines: { 'src/a.ts': new Set([1]), 'src/b.ts': new Set([2]) },
|
|
23
|
-
report: { status: 'PASS', failures: [] }, gateErrors: [], controlFilesChanged: [], hints: [], ...over,
|
|
24
|
-
});
|
|
25
|
-
const receipt = (over = {}) => ({
|
|
26
|
-
functions: 3, reviewed: 0, changedSinceReview: 0, lowRisk: 1, notCovered: [{ file: 'src/a.ts', function: 'f', start: 1, score: 4 }],
|
|
27
|
-
reviewers: {}, otherFiles: 0, setAside: 0, ...over,
|
|
28
|
-
});
|
|
29
|
-
const show = (r, over = {}) => {
|
|
30
|
-
printHuman(r, { cwd: dir, scope: 'this branch against main', commits: 10, ms: 3200, receipt: null, ...over });
|
|
31
|
-
return out.join('\n');
|
|
32
|
-
};
|
|
33
|
-
describe('rigour review, as a person reads it', () => {
|
|
34
|
-
it('says what it reviewed, then the verdict', () => {
|
|
35
|
-
const text = show(result());
|
|
36
|
-
expect(text).toContain('Rigour reviewed this branch against main: 10 commits, 2 files, 3 s.');
|
|
37
|
-
expect(text).toContain('✔ Nothing to fix in what this changed.');
|
|
38
|
-
});
|
|
39
|
-
it('lists at most five findings with where, what and the fix, and points at the rest', () => {
|
|
40
|
-
const text = show(result({ status: 'FAIL', findings: [1, 2, 3, 4, 5, 6, 7].map(n => finding(n)) }));
|
|
41
|
-
expect(text).toContain('✘ 7 things to fix before this is ready');
|
|
42
|
-
expect(text).toContain('src/a.ts:5 export f5 is used nowhere');
|
|
43
|
-
expect(text).not.toContain('export f6');
|
|
44
|
-
expect(text).toContain('→ Remove the export.');
|
|
45
|
-
expect(text).toContain('rigour dismiss');
|
|
46
|
-
expect(text).toContain('…and 2 more: rigour review --all');
|
|
47
|
-
});
|
|
48
|
-
it('lists every finding with --all', () => {
|
|
49
|
-
const text = show(result({ status: 'FAIL', findings: [1, 2, 3, 4, 5, 6, 7].map(n => finding(n)) }), { all: true });
|
|
50
|
-
expect(text).toContain('export f7 is used nowhere');
|
|
51
|
-
expect(text).not.toContain('--all');
|
|
52
|
-
});
|
|
53
|
-
it('is not finished, with the project\'s own install command, when the type checks need dependencies', () => {
|
|
54
|
-
fs.writeFileSync(path.join(dir, 'pnpm-lock.yaml'), '');
|
|
55
|
-
const text = show(result({ status: 'ERROR', gateErrors: ['typed-checks-unavailable'], typedError: "tsconfig.json cannot be loaded: File 'x' not found." }));
|
|
56
|
-
expect(text).toContain('⚠ Not finished: the type checks need your dependencies installed');
|
|
57
|
-
expect(text).toContain('Run pnpm install, then rigour review again. Everything else found nothing.');
|
|
58
|
-
expect(text).not.toContain('Nothing to fix');
|
|
59
|
-
});
|
|
60
|
-
it('names a check that could not run for another reason', () => {
|
|
61
|
-
fs.mkdirSync(path.join(dir, 'node_modules'));
|
|
62
|
-
const text = show(result({ status: 'ERROR', gateErrors: ['typed-checks-unavailable'], typedError: 'tsconfig.json cannot be loaded: bad JSON' }));
|
|
63
|
-
expect(text).toContain('⚠ Not finished: typed-checks-unavailable could not run (tsconfig.json cannot be loaded: bad JSON)');
|
|
64
|
-
});
|
|
65
|
-
it('keeps notes and old-code issues to one quiet line each', () => {
|
|
66
|
-
const text = show(result({ advisory: [finding(1)], fileFindings: [finding(2)], preexisting: 12, excludedOutsideChangedLines: 3 }));
|
|
67
|
-
expect(text).toContain('Also seen, never blocking: 2 notes (rigour review --notes)');
|
|
68
|
-
expect(text).toContain('Not shown: 15 issues the code already had before this change');
|
|
69
|
-
expect(text).not.toContain('export f1');
|
|
70
|
-
expect(show(result({ advisory: [finding(1)] }), { notes: true })).toContain('src/a.ts:1 export f1 is used nowhere');
|
|
71
|
-
});
|
|
72
|
-
it('counts the hints a person should confirm, and lists them with --notes', () => {
|
|
73
|
-
const hint = 'nested-scan src/a.ts:9: match() scans its `rows` argument and is called inside a loop; confirm the sizes or index the inner collection once';
|
|
74
|
-
expect(show(result({ hints: [hint] }))).toContain('To confirm by hand: 1 hint (rigour review --notes)');
|
|
75
|
-
out = [];
|
|
76
|
-
expect(show(result({ hints: [hint] }), { notes: true })).toContain(` ${hint}`);
|
|
77
|
-
});
|
|
78
|
-
it('hides the receipt until agents have reviewed something, unless asked', () => {
|
|
79
|
-
expect(show(result(), { receipt: receipt() })).not.toContain('Quality receipt');
|
|
80
|
-
out = [];
|
|
81
|
-
expect(show(result(), { receipt: receipt(), showReceipt: true })).toContain('Quality receipt');
|
|
82
|
-
out = [];
|
|
83
|
-
expect(show(result(), { receipt: receipt({ reviewed: 1 }) })).toContain('Quality receipt');
|
|
84
|
-
});
|
|
85
|
-
it('offers setup once, only where Rigour is not set up', () => {
|
|
86
|
-
expect(show(result())).toContain('Next: rigour setup checks this while your agent works and before every push.');
|
|
87
|
-
out = [];
|
|
88
|
-
fs.writeFileSync(path.join(dir, 'rigour.yml'), 'version: 1\n');
|
|
89
|
-
expect(show(result())).not.toContain('Next:');
|
|
90
|
-
});
|
|
91
|
-
it('says so when there is nothing to review', () => {
|
|
92
|
-
expect(show(result({ report: null }))).toBe('No changes to review.');
|
|
93
|
-
});
|
|
94
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
import { buildRecord } from '@rigour-labs/core';
|
|
2
|
-
import { describe, expect, it } from 'vitest';
|
|
3
|
-
import { findingKey, postReview, rankFindings, receiptLines, summaryBody } from './review-post.js';
|
|
4
|
-
const target = { token: 't', repo: 'acme/app', pr: 7, sha: 'abc123' };
|
|
5
|
-
const BOT = { login: 'github-actions[bot]' };
|
|
6
|
-
function finding(over) {
|
|
7
|
-
return { id: 'x', gate: 'Gate', severity: 'medium', provenance: 'traditional', message: 'm', file: 'src/a.ts', line: 10, ...over };
|
|
8
|
-
}
|
|
9
|
-
/** A GitHub that remembers comments, and refuses inline reviews when told to. */
|
|
10
|
-
function fakeGitHub(state, refuseInline = false) {
|
|
11
|
-
const calls = [];
|
|
12
|
-
const fetchImpl = async (url, init = {}) => {
|
|
13
|
-
const path = url.replace('https://api.github.com/repos/acme/app', '').replace(/\?.*$/, '');
|
|
14
|
-
const method = init.method ?? 'GET';
|
|
15
|
-
const body = init.body ? JSON.parse(init.body) : undefined;
|
|
16
|
-
calls.push({ method, path, body });
|
|
17
|
-
const reply = (ok, json = {}) => ({ ok, status: ok ? 200 : 422, json: async () => json, text: async () => '' });
|
|
18
|
-
const query = new URL(url).searchParams;
|
|
19
|
-
const page = (items) => {
|
|
20
|
-
const size = Number(query.get('per_page') ?? 30);
|
|
21
|
-
const from = (Number(query.get('page') ?? 1) - 1) * size;
|
|
22
|
-
return items.slice(from, from + size);
|
|
23
|
-
};
|
|
24
|
-
if (method === 'GET' && path === '/pulls/7/comments')
|
|
25
|
-
return reply(true, page(state.review));
|
|
26
|
-
if (method === 'GET' && path === '/issues/7/comments')
|
|
27
|
-
return reply(true, page(state.issue));
|
|
28
|
-
if (method === 'POST' && path === '/pulls/7/reviews') {
|
|
29
|
-
if (refuseInline)
|
|
30
|
-
return reply(false);
|
|
31
|
-
state.review.push(...body.comments.map((c) => ({ body: c.body, user: BOT })));
|
|
32
|
-
return reply(true);
|
|
33
|
-
}
|
|
34
|
-
if (method === 'POST' && path === '/issues/7/comments') {
|
|
35
|
-
state.issue.push({ id: state.issue.length + 1, body: body.body, user: BOT });
|
|
36
|
-
return reply(true);
|
|
37
|
-
}
|
|
38
|
-
const patched = method === 'PATCH' && state.issue.find(c => path === `/issues/comments/${c.id}`);
|
|
39
|
-
if (patched) {
|
|
40
|
-
patched.body = body.body;
|
|
41
|
-
return reply(true);
|
|
42
|
-
}
|
|
43
|
-
return reply(false);
|
|
44
|
-
};
|
|
45
|
-
return { fetchImpl, calls };
|
|
46
|
-
}
|
|
47
|
-
const report = {
|
|
48
|
-
status: 'FAIL',
|
|
49
|
-
failures: [
|
|
50
|
-
finding({ gate: 'Style', severity: 'low', message: 'naming' }),
|
|
51
|
-
finding({ gate: 'Race', severity: 'high', provenance: 'deep-analysis', message: 'check-then-act', line: 30, anchor_line: 32 }),
|
|
52
|
-
finding({ gate: 'Leak', severity: 'high', provenance: 'security', message: 'token in log' }),
|
|
53
|
-
finding({ gate: 'NoLine', line: null }),
|
|
54
|
-
],
|
|
55
|
-
deep: { model: 'anthropic/claude-sonnet-5.5', cost_usd: 0.0421, router: { routed: 3, functions: 11, already_reviewed: 2 } },
|
|
56
|
-
};
|
|
57
|
-
describe('the summary with a review record', () => {
|
|
58
|
-
it('carries the record: blocks, should-fixes, counts, judges and the hash', () => {
|
|
59
|
-
const record = buildRecord({ head: 'abcdef0123456789', base: '0123456789abcdef', scope: 'full', verdict: { prior_points: [], redundant: [], reads: [], scans: [], merge_impact: [], findings: [], carried: [], resolved_previous: [] },
|
|
60
|
-
accounted: { open: [{ id: 'i1', kind: 'finding', class: 'correctness', file: 'src/job.ts', line: 2, issue: 'returns before the lock' }], advisory: [], unverified: [], notes: [], resolved: [], answerInReply: [], disputed: [], dismissed: [] },
|
|
61
|
-
judges: [{ reviewer: 'claude', cost_usd: 0.5 }], lessonsServed: 0, humanReviews: 0 });
|
|
62
|
-
const body = summaryBody({ ...report, reviewer: { record } }, 1, 1, 0);
|
|
63
|
-
expect(body).toContain('**Review record** · 1 blocking · 0 should-fix');
|
|
64
|
-
expect(body).toContain('- **Blocking** `src/job.ts:2` returns before the lock');
|
|
65
|
-
expect(body).toContain(`Integrity \`${record.integrity.slice(0, 16)}\``);
|
|
66
|
-
expect(summaryBody(report, 1, 1, 0)).not.toContain('Review record');
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
describe('rankFindings', () => {
|
|
70
|
-
it('puts severity first, then the most provable source', () => {
|
|
71
|
-
expect(rankFindings(report.failures).map(f => f.gate)).toEqual(['Leak', 'Race', 'NoLine', 'Style']);
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
describe('postReview', () => {
|
|
75
|
-
it('posts at most two inline comments on their anchor lines, and one summary', async () => {
|
|
76
|
-
const state = { review: [], issue: [] };
|
|
77
|
-
const { fetchImpl, calls } = fakeGitHub(state);
|
|
78
|
-
const result = await postReview(report, target, 2, fetchImpl);
|
|
79
|
-
const review = calls.find(c => c.path === '/pulls/7/reviews').body;
|
|
80
|
-
expect(review.commit_id).toBe('abc123');
|
|
81
|
-
expect(review.comments.map((c) => [c.path, c.line])).toEqual([['src/a.ts', 10], ['src/a.ts', 32]]);
|
|
82
|
-
expect(review.comments[1].body).toContain('root cause at line 30');
|
|
83
|
-
expect(result).toMatchObject({ inline: 2, summaryUpdated: true });
|
|
84
|
-
expect(state.issue[0].body).toContain('2 already reviewed before the PR');
|
|
85
|
-
expect(state.issue[0].body).toContain('cost $0.042');
|
|
86
|
-
});
|
|
87
|
-
it('never re-posts a finding on the next push, and edits the summary instead of adding one', async () => {
|
|
88
|
-
const state = { review: [], issue: [] };
|
|
89
|
-
await postReview(report, target, 2, fakeGitHub(state).fetchImpl);
|
|
90
|
-
const { fetchImpl, calls } = fakeGitHub(state);
|
|
91
|
-
const second = await postReview(report, target, 2, fetchImpl);
|
|
92
|
-
const reposted = calls.find(c => c.path === '/pulls/7/reviews').body.comments;
|
|
93
|
-
// Leak and Race went out on the first push; only Style is new.
|
|
94
|
-
expect(reposted).toHaveLength(1);
|
|
95
|
-
expect(reposted[0].body).toContain(findingKey(report.failures[0]));
|
|
96
|
-
expect(second.skippedAlreadyPosted).toBe(2);
|
|
97
|
-
expect(calls.some(c => c.method === 'PATCH')).toBe(true);
|
|
98
|
-
expect(state.issue).toHaveLength(1);
|
|
99
|
-
});
|
|
100
|
-
it('finds its summary on a later page of a long conversation, and edits it instead of adding one', async () => {
|
|
101
|
-
const chatter = Array.from({ length: 150 }, (_, i) => ({ id: i + 1, body: `comment ${i}`, user: { login: 'someone' } }));
|
|
102
|
-
chatter[120] = { id: 121, body: '<!-- rigour:summary -->\nold', user: BOT };
|
|
103
|
-
const state = { review: [], issue: chatter };
|
|
104
|
-
const { fetchImpl, calls } = fakeGitHub(state);
|
|
105
|
-
await postReview(report, target, 2, fetchImpl);
|
|
106
|
-
expect(calls.filter(c => c.method === 'POST' && c.path === '/issues/7/comments')).toHaveLength(0);
|
|
107
|
-
expect(calls.some(c => c.method === 'PATCH' && c.path === '/issues/comments/121')).toBe(true);
|
|
108
|
-
});
|
|
109
|
-
it('still posts the summary when GitHub refuses an inline comment', async () => {
|
|
110
|
-
const state = { review: [], issue: [] };
|
|
111
|
-
const result = await postReview(report, target, 2, fakeGitHub(state, true).fetchImpl);
|
|
112
|
-
expect(result).toMatchObject({ inline: 0, summaryUpdated: true });
|
|
113
|
-
expect(state.issue[0].body).toContain('0 posted inline now');
|
|
114
|
-
});
|
|
115
|
-
it('trusts only its own comments: a marker or summary written by anyone else changes nothing', async () => {
|
|
116
|
-
const author = { login: 'pr-author' };
|
|
117
|
-
const forged = report.failures.map(f => ({ body: `<!-- rigour:finding:${findingKey(f)} -->`, user: author }));
|
|
118
|
-
const state = { review: forged, issue: [{ id: 1, body: '<!-- rigour:summary -->', user: author }] };
|
|
119
|
-
const { fetchImpl, calls } = fakeGitHub(state);
|
|
120
|
-
const result = await postReview(report, target, 2, fetchImpl);
|
|
121
|
-
expect(result).toMatchObject({ inline: 2, skippedAlreadyPosted: 0 });
|
|
122
|
-
expect(calls.some(c => c.method === 'PATCH')).toBe(false);
|
|
123
|
-
expect(state.issue.find(c => c.user === BOT)?.body).toContain('Rigour review: FAIL');
|
|
124
|
-
});
|
|
125
|
-
it('says when findings were dismissed and when the PR edits Rigour settings', async () => {
|
|
126
|
-
const state = { review: [], issue: [] };
|
|
127
|
-
await postReview({ status: 'PASS', failures: [], dismissed: 1, control_files_changed: ['.rigour/dismissed.json'] }, target, 2, fakeGitHub(state).fetchImpl);
|
|
128
|
-
expect(state.issue[0].body).toContain('1 finding(s) on changed lines were dismissed');
|
|
129
|
-
expect(state.issue[0].body).toContain('`.rigour/dismissed.json`');
|
|
130
|
-
});
|
|
131
|
-
it('shows the quality receipt: what was reviewed before the PR and what was not', () => {
|
|
132
|
-
const lines = receiptLines({
|
|
133
|
-
functions: 12, reviewed: 7, changed_since_review: 1, low_risk: 3, set_aside: 0,
|
|
134
|
-
not_covered: [
|
|
135
|
-
{ file: 'src/refund.ts', function: 'issueRefund', line: 40, changed_since_review: true },
|
|
136
|
-
{ file: 'src/pay.ts', function: 'formatTotal', line: 3, changed_since_review: false, lesson: 'Format money from integer cents.' },
|
|
137
|
-
],
|
|
138
|
-
});
|
|
139
|
-
expect(lines[0]).toBe('**Quality receipt:** 12 changed functions · 7 reviewed before this PR · 1 changed after review · 3 low risk · **2 not covered**');
|
|
140
|
-
expect(lines.slice(1)).toEqual([
|
|
141
|
-
'- `issueRefund` in `src/refund.ts:40`: changed after its review',
|
|
142
|
-
'- `formatTotal` in `src/pay.ts:3`: matches a team lesson: Format money from integer cents.',
|
|
143
|
-
]);
|
|
144
|
-
expect(receiptLines({ functions: 2, reviewed: 0, changed_since_review: 0, low_risk: 0, set_aside: 2, not_covered: [] })[1]).toContain('not counted: this check is independent');
|
|
145
|
-
});
|
|
146
|
-
});
|
|
147
|
-
describe('reviewPostCommand', () => {
|
|
148
|
-
it('says why when the review did not run, and posts nothing', async () => {
|
|
149
|
-
const { reviewPostCommand } = await import('./review-post.js');
|
|
150
|
-
const fs = await import('fs');
|
|
151
|
-
const os = await import('os');
|
|
152
|
-
const path = await import('path');
|
|
153
|
-
const file = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'post-')), 'report.json');
|
|
154
|
-
fs.writeFileSync(file, JSON.stringify({ error: 'rigour.yml: gates.ast.complexity: Expected number' }));
|
|
155
|
-
await expect(reviewPostCommand({ report: file })).rejects.toThrow('The review did not run, so there is nothing to post: rigour.yml: gates.ast.complexity');
|
|
156
|
-
});
|
|
157
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import os from 'os';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import { afterEach, describe, expect, it } from 'vitest';
|
|
5
|
-
import { readPrBody } from './review.js';
|
|
6
|
-
let dir;
|
|
7
|
-
afterEach(() => { if (dir)
|
|
8
|
-
fs.rmSync(dir, { recursive: true, force: true }); dir = undefined; });
|
|
9
|
-
describe('readPrBody', () => {
|
|
10
|
-
it('prefers --pr-body, else reads the pull request of a GitHub Actions event, else nothing', () => {
|
|
11
|
-
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'pr-body-'));
|
|
12
|
-
fs.writeFileSync(path.join(dir, 'body.md'), 'Fix the redirect loop.');
|
|
13
|
-
fs.writeFileSync(path.join(dir, 'event.json'), JSON.stringify({ pull_request: { title: 'fix: encode spaces', body: 'Paths with spaces looped.' } }));
|
|
14
|
-
const event = { GITHUB_EVENT_PATH: path.join(dir, 'event.json') };
|
|
15
|
-
expect(readPrBody(dir, { prBody: 'body.md' }, event)).toBe('Fix the redirect loop.');
|
|
16
|
-
expect(readPrBody(dir, {}, event)).toBe('fix: encode spaces\n\nPaths with spaces looped.');
|
|
17
|
-
expect(readPrBody(dir, {}, {})).toBeUndefined();
|
|
18
|
-
});
|
|
19
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { receiptFor, receiptSummary } from './review-receipt.js';
|
|
3
|
-
const receipt = (over) => ({
|
|
4
|
-
functions: 4, reviewed: 1, changedSinceReview: 0, lowRisk: 2, notCovered: [], reviewers: {}, otherFiles: 0, setAside: 0, ...over,
|
|
5
|
-
});
|
|
6
|
-
describe('quality receipt line', () => {
|
|
7
|
-
it('counts what is known, and mentions changes after review only when there are some', () => {
|
|
8
|
-
expect(receiptSummary(receipt({}))).toBe('4 changed functions · 1 reviewed before this · 2 low risk · 0 not covered');
|
|
9
|
-
const gap = { file: 'a.ts', function: 'f', start: 3, score: 4, changedSinceReview: true };
|
|
10
|
-
expect(receiptSummary(receipt({ functions: 1, reviewed: 0, lowRisk: 0, changedSinceReview: 1, notCovered: [gap] })))
|
|
11
|
-
.toBe('1 changed function · 0 reviewed before this · 1 changed after review · 0 low risk · 1 not covered');
|
|
12
|
-
});
|
|
13
|
-
it('has no receipt without a diff', () => {
|
|
14
|
-
expect(receiptFor(process.cwd(), undefined, { gates: {} }, false)).toBeNull();
|
|
15
|
-
});
|
|
16
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { printReviewer, reviewerJson } from './review-reviewer.js';
|
|
3
|
-
let out;
|
|
4
|
-
beforeEach(() => {
|
|
5
|
-
out = [];
|
|
6
|
-
vi.spyOn(console, 'log').mockImplementation((...args) => void out.push(args.join(' ')));
|
|
7
|
-
});
|
|
8
|
-
afterEach(() => {
|
|
9
|
-
vi.restoreAllMocks();
|
|
10
|
-
});
|
|
11
|
-
const LOCK = { id: 'abcdef0123', kind: 'finding', class: 'correctness', file: 'src/job.ts', line: 2, issue: 'returns before the lock', consequence: 'two runs send the same email', reviewer: 'claude+codex' };
|
|
12
|
-
const base = { outcome: 'findings', items: [LOCK], unverified: [], resolved: [], answerInReply: [], notes: [{ ...LOCK, id: 'n1', issue: 'could be shorter', consequence: undefined }], advisory: [], disputed: [{ ...LOCK, id: 'd1', issue: 'maybe slow' }], dropped: [{ ...LOCK, id: 'x1' }], dismissed: [], reviewers: ['claude', 'codex'], scope: 'full', cached: false, dismissable: true, costUsd: 0.42, tokens: { input: 26565, output: 24 } };
|
|
13
|
-
describe('the reviewer as the CLI prints it', () => {
|
|
14
|
-
it('shows what ran against what was asked, why, what was refused, and only confirmed findings as open', () => {
|
|
15
|
-
printReviewer({ ...base, mode: { asked: 'panel', ran: 'single', source: 'user', degraded: '3 judges asked, claude could run (not installed: codex)', refused: ['mode single (flag) refused: rigour.yml sets review.reviewer.panel: required'] } });
|
|
16
|
-
const text = out.join('\n');
|
|
17
|
-
expect(text).toContain('panel asked (user), single ran: 3 judges asked, claude could run (not installed: codex)');
|
|
18
|
-
expect(text).toContain('mode single (flag) refused');
|
|
19
|
-
expect(text).toContain('OPEN');
|
|
20
|
-
expect(text).toContain('consequence: two runs send the same email');
|
|
21
|
-
expect(text).toContain('not a bug? rigour dismiss abcdef0123');
|
|
22
|
-
expect(text).not.toContain('disputed, never blocks'); // folded: what is shown gets the discipline of what blocks
|
|
23
|
-
expect(text).toMatch(/Also seen, never blocking: .*1 working note.*1 disputed.*1 refuted by the other judges.*--notes lists them/);
|
|
24
|
-
expect(text).toContain('1 open item(s), $0.42, 26,589 tokens'); // a Codex judge reports tokens, not dollars
|
|
25
|
-
});
|
|
26
|
-
it('shows at most five verified should-fixes, folds the rest, and lists everything with --notes', () => {
|
|
27
|
-
const advisory = Array.from({ length: 6 }, (_, i) => ({ ...LOCK, id: `s${i}`, issue: `should fix ${i}` }));
|
|
28
|
-
printReviewer({ ...base, advisory });
|
|
29
|
-
const text = out.join('\n');
|
|
30
|
-
expect(text.match(/should fix \(verified, never blocks\)/g)).toHaveLength(5);
|
|
31
|
-
expect(text).toContain('1 more should-fix');
|
|
32
|
-
out = [];
|
|
33
|
-
printReviewer({ ...base, advisory }, { notes: true });
|
|
34
|
-
const all = out.join('\n');
|
|
35
|
-
expect(all.match(/should fix \(verified, never blocks\)/g)).toHaveLength(6);
|
|
36
|
-
expect(all).toContain('disputed, never blocks (no majority)');
|
|
37
|
-
expect(all).toContain('note, never blocks');
|
|
38
|
-
expect(all).not.toContain('Also seen, never blocking');
|
|
39
|
-
});
|
|
40
|
-
it('returns every decision in JSON', () => {
|
|
41
|
-
expect(reviewerJson({ ...base, mode: { asked: 'panel', ran: 'panel', source: 'team' } })).toMatchObject({ blocks: true, mode: { ran: 'panel' }, disputed: [{ id: 'd1' }], notes: [{ id: 'n1' }], shown: { blocking: 1, should_fix: 0 }, dropped: [{ id: 'x1' }] });
|
|
42
|
-
});
|
|
43
|
-
it('offers "not a bug" only where the team allows dismissals', () => {
|
|
44
|
-
printReviewer({ ...base, dismissable: false });
|
|
45
|
-
expect(out.join('\n')).not.toContain('rigour dismiss');
|
|
46
|
-
});
|
|
47
|
-
});
|
|
48
|
-
describe('rigour review --status', () => {
|
|
49
|
-
it('runs in a repository with no verdict yet, instead of crashing on the branch lookup', async () => {
|
|
50
|
-
const { execFileSync } = await import('child_process');
|
|
51
|
-
const fs = await import('fs');
|
|
52
|
-
const os = await import('os');
|
|
53
|
-
const path = await import('path');
|
|
54
|
-
const { printStatus } = await import('./review-reviewer.js');
|
|
55
|
-
const repo = fs.mkdtempSync(path.join(os.tmpdir(), 'status-'));
|
|
56
|
-
execFileSync('git', ['init', '-q', '-b', 'main'], { cwd: repo });
|
|
57
|
-
execFileSync('git', ['-c', 'user.email=t@example.com', '-c', 'user.name=t', '-c', 'commit.gpgsign=false', 'commit', '-q', '--allow-empty', '-m', 'init'], { cwd: repo });
|
|
58
|
-
const log = vi.spyOn(console, 'log').mockImplementation(() => undefined);
|
|
59
|
-
try {
|
|
60
|
-
expect(await printStatus(repo, true)).toBe(0);
|
|
61
|
-
expect(JSON.parse(String(log.mock.calls[0][0]))).toMatchObject({ branch: 'main' });
|
|
62
|
-
}
|
|
63
|
-
finally {
|
|
64
|
-
log.mockRestore();
|
|
65
|
-
fs.rmSync(repo, { recursive: true, force: true });
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,43 +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
|
-
const fixScope = vi.fn();
|
|
6
|
-
vi.mock('@rigour-labs/core', async (original) => ({ ...(await original()), fixScope }));
|
|
7
|
-
const { printScope } = await import('./review-scope.js');
|
|
8
|
-
let dir;
|
|
9
|
-
let out;
|
|
10
|
-
const SCOPE = { pr: 42, review: { id: 4, login: 'senior', submittedAt: '2026-10-03', commit: 'abcdef0123456789' }, cited: ['src/job.ts'], changed: ['src/job.ts', 'src/other.ts'], extra: ['src/other.ts'] };
|
|
11
|
-
beforeEach(() => {
|
|
12
|
-
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'rigour-scope-'));
|
|
13
|
-
out = [];
|
|
14
|
-
vi.spyOn(console, 'log').mockImplementation((...args) => void out.push(args.join(' ')));
|
|
15
|
-
fixScope.mockReset();
|
|
16
|
-
});
|
|
17
|
-
afterEach(() => {
|
|
18
|
-
vi.restoreAllMocks();
|
|
19
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
20
|
-
});
|
|
21
|
-
describe('rigour review --scope', () => {
|
|
22
|
-
it('names the files no point cited and fails', async () => {
|
|
23
|
-
fixScope.mockResolvedValue({ scope: SCOPE });
|
|
24
|
-
expect(await printScope(dir, { scope: '42', scopeReview: '4' })).toBe(1);
|
|
25
|
-
expect(fixScope).toHaveBeenCalledWith(dir, expect.objectContaining({ pr: 42, review: 4 }));
|
|
26
|
-
const text = out.join('\n');
|
|
27
|
-
expect(text).toContain("Fix round since senior's review of abcdef012 (pull request 42): 2 files changed, 1 file cited.");
|
|
28
|
-
expect(text).toContain('✘ 1 file no point of the review cited');
|
|
29
|
-
expect(text).toContain(' src/other.ts');
|
|
30
|
-
});
|
|
31
|
-
it('passes when the round stayed in scope, and answers in JSON', async () => {
|
|
32
|
-
fixScope.mockResolvedValue({ scope: { ...SCOPE, extra: [] } });
|
|
33
|
-
expect(await printScope(dir, { scope: true, json: true })).toBe(0);
|
|
34
|
-
expect(fixScope).toHaveBeenCalledWith(dir, expect.objectContaining({ pr: undefined, review: undefined }));
|
|
35
|
-
expect(JSON.parse(out.join('\n'))).toMatchObject({ status: 'PASS', pr: 42, extra: [] });
|
|
36
|
-
});
|
|
37
|
-
it('is not a pass when it could not tell, and refuses a pull request that is not a number', async () => {
|
|
38
|
-
fixScope.mockResolvedValue({ error: 'pull request 42 has no review by a person yet' });
|
|
39
|
-
expect(await printScope(dir, { scope: true })).toBe(3);
|
|
40
|
-
expect(out.join('\n')).toContain("⚠ Could not check the fix round's scope: pull request 42 has no review by a person yet");
|
|
41
|
-
await expect(printScope(dir, { scope: 'feature' })).rejects.toThrow('--scope takes a number, not "feature"');
|
|
42
|
-
});
|
|
43
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import os from 'os';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
-
import { appendAgentEvent } from '@rigour-labs/core';
|
|
6
|
-
import { reviewStatsCommand } from './review-stats.js';
|
|
7
|
-
describe('rigour review-stats', () => {
|
|
8
|
-
let dir;
|
|
9
|
-
afterEach(() => { if (dir)
|
|
10
|
-
fs.rmSync(dir, { recursive: true, force: true }); dir = undefined; vi.restoreAllMocks(); });
|
|
11
|
-
it('reports the loop from the local event log as JSON', () => {
|
|
12
|
-
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'review-stats-'));
|
|
13
|
-
appendAgentEvent(dir, { type: 'stop_review', session: 's', blocked: true });
|
|
14
|
-
appendAgentEvent(dir, { type: 'stop_review', session: 's', blocked: false });
|
|
15
|
-
const log = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
16
|
-
reviewStatsCommand(dir, { json: true });
|
|
17
|
-
expect(JSON.parse(String(log.mock.calls[0][0]))).toMatchObject({ stopChecks: 2, stopBlocks: 1, blockedStopsFollowedThrough: 1 });
|
|
18
|
-
});
|
|
19
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { readsStdinDiff } from './review.js';
|
|
3
|
-
const stat = (kind) => ({
|
|
4
|
-
isFIFO: () => kind === 'fifo',
|
|
5
|
-
isFile: () => kind === 'file',
|
|
6
|
-
});
|
|
7
|
-
describe('readsStdinDiff', () => {
|
|
8
|
-
it('reads a piped or redirected diff when no other source is named', () => {
|
|
9
|
-
expect(readsStdinDiff({}, stat('fifo'))).toBe(true); // git diff | rigour review
|
|
10
|
-
expect(readsStdinDiff({}, stat('file'))).toBe(true); // rigour review < change.patch
|
|
11
|
-
});
|
|
12
|
-
it('never waits on a socket or terminal an agent left open', () => {
|
|
13
|
-
expect(readsStdinDiff({}, stat('socket'))).toBe(false);
|
|
14
|
-
expect(readsStdinDiff({}, stat('tty'))).toBe(false);
|
|
15
|
-
expect(readsStdinDiff({}, undefined)).toBe(false);
|
|
16
|
-
});
|
|
17
|
-
it('ignores stdin once --base or --files names the change', () => {
|
|
18
|
-
expect(readsStdinDiff({ base: 'origin/main' }, stat('fifo'))).toBe(false);
|
|
19
|
-
expect(readsStdinDiff({ files: 'src/a.ts' }, stat('file'))).toBe(false);
|
|
20
|
-
});
|
|
21
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|