@rigour-labs/cli 6.8.0 → 6.8.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/package.json +7 -4
- package/dist/cli-help.test.d.ts +0 -1
- package/dist/cli-help.test.js +0 -27
- package/dist/commands/brief.test.d.ts +0 -1
- package/dist/commands/brief.test.js +0 -134
- 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 -89
- package/dist/commands/hooks-stop.test.d.ts +0 -1
- package/dist/commands/hooks-stop.test.js +0 -167
- package/dist/commands/hooks.test.d.ts +0 -1
- package/dist/commands/hooks.test.js +0 -256
- 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/thread.test.d.ts +0 -1
- package/dist/commands/thread.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,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 {};
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { splitByChangedLines } from '@rigour-labs/core';
|
|
3
|
-
import { buildCiReviewSummary, renderGithubSummary } from './review-summary.js';
|
|
4
|
-
function failure(id, severity, file, line, details) {
|
|
5
|
-
return { id, title: id, details, hint: details, severity, provenance: 'traditional', files: [file], line };
|
|
6
|
-
}
|
|
7
|
-
describe('GitHub change review summary', () => {
|
|
8
|
-
it('prioritizes severe findings and never includes raw secret-bearing messages', () => {
|
|
9
|
-
const failures = [
|
|
10
|
-
failure('style-drift', 'low', 'src/a.ts', 3, 'ordinary style'),
|
|
11
|
-
failure('security-patterns', 'critical', 'src/shell.ts', 8, 'API_KEY=secret-value-that-must-not-appear'),
|
|
12
|
-
failure('logic-drift', 'medium', 'src/logic.ts', 5, 'changed operator'),
|
|
13
|
-
];
|
|
14
|
-
const summary = buildCiReviewSummary(failures, 5, { 'src/a.ts': new Set([3]), 'src/shell.ts': new Set([8]) });
|
|
15
|
-
const markdown = renderGithubSummary(summary);
|
|
16
|
-
expect(summary.findings.map(item => item.rule)).toEqual(['security-patterns', 'logic-drift', 'style-drift']);
|
|
17
|
-
expect(summary.severity).toEqual({ critical: 1, high: 0, medium: 1, low: 1 });
|
|
18
|
-
expect(summary.excluded_outside_changed_lines).toBe(2);
|
|
19
|
-
expect(summary.changed_lines).toBe(2);
|
|
20
|
-
expect(markdown).toContain('Trace the input and sink');
|
|
21
|
-
expect(markdown).not.toContain('secret-value-that-must-not-appear');
|
|
22
|
-
});
|
|
23
|
-
it('caps output and escapes untrusted path text', () => {
|
|
24
|
-
const failures = Array.from({ length: 8 }, (_, i) => failure('style-drift', 'low', `src/bad\`\n${i}.ts`, i + 1, 'untrusted'));
|
|
25
|
-
const summary = buildCiReviewSummary(failures, 8, {});
|
|
26
|
-
const markdown = renderGithubSummary(summary);
|
|
27
|
-
expect(summary.findings).toHaveLength(5);
|
|
28
|
-
expect(summary.truncated).toBe(3);
|
|
29
|
-
expect(markdown).not.toContain('bad`');
|
|
30
|
-
expect(markdown).toContain('3 more finding(s)');
|
|
31
|
-
});
|
|
32
|
-
it('does not imply a clean repository when changed lines pass', () => {
|
|
33
|
-
const markdown = renderGithubSummary(buildCiReviewSummary([], 20, {}));
|
|
34
|
-
expect(markdown).toContain('No findings on changed lines');
|
|
35
|
-
expect(markdown).toContain('does not assess the full repository');
|
|
36
|
-
});
|
|
37
|
-
it('abstains from attributing file-level findings to changed line one', () => {
|
|
38
|
-
const withLine = failure('security-patterns', 'high', 'src/a.ts', 5, 'specific line');
|
|
39
|
-
const fileLevel = { ...failure('AST_COMPLEXITY', 'medium', 'src/a.ts', 1, 'whole file'), line: undefined };
|
|
40
|
-
const split = splitByChangedLines([withLine, fileLevel], { 'src/a.ts': new Set([1, 5]) });
|
|
41
|
-
expect(split.findings).toEqual([withLine]);
|
|
42
|
-
expect(split.fileFindings).toEqual([fileLevel]);
|
|
43
|
-
const summary = buildCiReviewSummary(split.findings, 2, { 'src/a.ts': new Set([1, 5]) }, split.fileFindings.length + split.unlocated);
|
|
44
|
-
expect(renderGithubSummary(summary)).toContain('file-level finding(s) lacked a changed-line location');
|
|
45
|
-
});
|
|
46
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -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, vi } from 'vitest';
|
|
6
|
-
import { reviewAckCommand, reviewExportCommand, reviewTaskCommand } from './review-task.js';
|
|
7
|
-
let repo;
|
|
8
|
-
let out;
|
|
9
|
-
const git = (...args) => execFileSync('git', ['-C', repo, ...args], { encoding: 'utf8' });
|
|
10
|
-
beforeEach(() => {
|
|
11
|
-
repo = fs.mkdtempSync(path.join(os.tmpdir(), 'cli-review-task-'));
|
|
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, 'README.md'), 'x\n');
|
|
17
|
-
git('add', '-A');
|
|
18
|
-
git('commit', '-qm', 'init');
|
|
19
|
-
fs.writeFileSync(path.join(repo, 'sync.ts'), "export async function syncOrders(db, rows) {\n await db.from('orders').upsert(rows);\n}\n");
|
|
20
|
-
out = [];
|
|
21
|
-
vi.spyOn(console, 'log').mockImplementation((...args) => { out.push(args.join(' ')); });
|
|
22
|
-
vi.spyOn(console, 'error').mockImplementation((...args) => { out.push(args.join(' ')); });
|
|
23
|
-
});
|
|
24
|
-
afterEach(() => {
|
|
25
|
-
vi.restoreAllMocks();
|
|
26
|
-
process.exitCode = undefined;
|
|
27
|
-
fs.rmSync(repo, { recursive: true, force: true });
|
|
28
|
-
});
|
|
29
|
-
describe('review-task, review-ack, review-export', () => {
|
|
30
|
-
it('lists the risky function, records a human review, and exports it', async () => {
|
|
31
|
-
await reviewTaskCommand(repo, { json: true });
|
|
32
|
-
expect(JSON.parse(out.join('\n')).items).toEqual([expect.objectContaining({ file: 'sync.ts', function: 'syncOrders' })]);
|
|
33
|
-
reviewAckCommand(repo, 'sync.ts', 'syncOrders', { verdict: 'no_issue', note: 'rows are one page; key is the order id' });
|
|
34
|
-
expect(out.at(-1)).toContain('Recorded: sync.ts syncOrders (no_issue)');
|
|
35
|
-
out = [];
|
|
36
|
-
await reviewTaskCommand(repo, {});
|
|
37
|
-
expect(out.join('\n')).toContain('No risky changed function waiting for review (1 already reviewed)');
|
|
38
|
-
reviewExportCommand(repo);
|
|
39
|
-
const exported = JSON.parse(fs.readFileSync(path.join(repo, '.rigour', 'reviewed.json'), 'utf8'));
|
|
40
|
-
expect(exported.entries).toEqual([expect.objectContaining({ file: 'sync.ts', function: 'syncOrders', reviewer: 'human', verdict: 'no_issue' })]);
|
|
41
|
-
});
|
|
42
|
-
it('rejects an acknowledgement without a real note, with a failing exit code', () => {
|
|
43
|
-
reviewAckCommand(repo, 'sync.ts', 'syncOrders', { verdict: 'no_issue', note: 'ok' });
|
|
44
|
-
expect(out.at(-1)).toContain('Not recorded: note must say what was checked');
|
|
45
|
-
expect(process.exitCode).toBe(1);
|
|
46
|
-
});
|
|
47
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,29 +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 { scanRulesCommand } from './scan-rules.js';
|
|
6
|
-
describe('rigour scan-rules', () => {
|
|
7
|
-
let dir;
|
|
8
|
-
afterEach(() => { if (dir)
|
|
9
|
-
fs.rmSync(dir, { recursive: true, force: true }); dir = undefined; vi.restoreAllMocks(); });
|
|
10
|
-
function scan(files, rules) {
|
|
11
|
-
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'scan-rules-'));
|
|
12
|
-
for (const [name, text] of Object.entries(files))
|
|
13
|
-
fs.writeFileSync(path.join(dir, name), text);
|
|
14
|
-
const out = [];
|
|
15
|
-
vi.spyOn(process.stdout, 'write').mockImplementation((chunk) => { out.push(String(chunk)); return true; });
|
|
16
|
-
scanRulesCommand(dir, Object.keys(files).filter(f => f.endsWith('.ts')), { rules });
|
|
17
|
-
return out.join('').trim().split('\n').filter(Boolean).map(line => JSON.parse(line));
|
|
18
|
-
}
|
|
19
|
-
it('runs only the named rules and prints one JSON line per finding', () => {
|
|
20
|
-
const code = "export function setup(fn: () => void) {\n window.addEventListener('a', fn);\n addEventListener('b', fn);\n}\n"
|
|
21
|
-
+ "// @ts-expect-error internal\nexport const r = (e: any) => e[Symbol.for('lib.internal.x')];\n";
|
|
22
|
-
expect(scan({ 'a.ts': code }, 'env/bare-browser-global')).toEqual([
|
|
23
|
-
{ rule: 'env/bare-browser-global', file: 'a.ts', line: 3, severity: 'medium', message: expect.stringContaining('addEventListener') },
|
|
24
|
-
]);
|
|
25
|
-
});
|
|
26
|
-
it('rejects an unknown rule id rather than scanning nothing', () => {
|
|
27
|
-
expect(() => scan({ 'a.ts': 'export {};\n' }, 'no/such-rule')).toThrow('Unknown rule(s): no/such-rule');
|
|
28
|
-
});
|
|
29
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
-
import fs from 'fs-extra';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import os from 'os';
|
|
5
|
-
import { scanCommand } from './scan.js';
|
|
6
|
-
describe('scanCommand', () => {
|
|
7
|
-
let testDir;
|
|
8
|
-
beforeEach(async () => {
|
|
9
|
-
testDir = await fs.mkdtemp(path.join(os.tmpdir(), 'rigour-scan-test-'));
|
|
10
|
-
vi.spyOn(process, 'exit').mockImplementation(() => undefined);
|
|
11
|
-
vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
12
|
-
vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
13
|
-
});
|
|
14
|
-
afterEach(async () => {
|
|
15
|
-
await fs.remove(testDir);
|
|
16
|
-
vi.restoreAllMocks();
|
|
17
|
-
});
|
|
18
|
-
it('runs in zero-config mode without rigour.yml', async () => {
|
|
19
|
-
await fs.writeJson(path.join(testDir, 'package.json'), { name: 'scan-zero-config-test' });
|
|
20
|
-
await fs.writeFile(path.join(testDir, 'index.js'), "import fake from 'totally-fake-package';\nconsole.log(fake);\n");
|
|
21
|
-
await expect(scanCommand(testDir, [], {})).resolves.not.toThrow();
|
|
22
|
-
expect(process.exit).toHaveBeenCalled();
|
|
23
|
-
expect(await fs.pathExists(path.join(testDir, 'rigour-report.json'))).toBe(true);
|
|
24
|
-
}, 30_000);
|
|
25
|
-
it('uses provided config path when passed', async () => {
|
|
26
|
-
await fs.writeFile(path.join(testDir, 'app.js'), "export const ok = 42;\n");
|
|
27
|
-
await fs.writeFile(path.join(testDir, 'scan-config.yml'), `
|
|
28
|
-
version: 1
|
|
29
|
-
gates:
|
|
30
|
-
required_files: []
|
|
31
|
-
forbid_todos: false
|
|
32
|
-
forbid_fixme: false
|
|
33
|
-
context:
|
|
34
|
-
enabled: false
|
|
35
|
-
environment:
|
|
36
|
-
enabled: false
|
|
37
|
-
retry_loop_breaker:
|
|
38
|
-
enabled: false
|
|
39
|
-
security:
|
|
40
|
-
enabled: false
|
|
41
|
-
duplication_drift:
|
|
42
|
-
enabled: false
|
|
43
|
-
hallucinated_imports:
|
|
44
|
-
enabled: false
|
|
45
|
-
inconsistent_error_handling:
|
|
46
|
-
enabled: false
|
|
47
|
-
context_window_artifacts:
|
|
48
|
-
enabled: false
|
|
49
|
-
promise_safety:
|
|
50
|
-
enabled: false
|
|
51
|
-
phantom_apis:
|
|
52
|
-
enabled: false
|
|
53
|
-
deprecated_apis:
|
|
54
|
-
enabled: false
|
|
55
|
-
test_quality:
|
|
56
|
-
enabled: false
|
|
57
|
-
output:
|
|
58
|
-
report_path: scan-report.json
|
|
59
|
-
`);
|
|
60
|
-
await expect(scanCommand(testDir, [], { config: 'scan-config.yml' })).resolves.not.toThrow();
|
|
61
|
-
expect(process.exit).toHaveBeenCalledWith(0);
|
|
62
|
-
expect(await fs.pathExists(path.join(testDir, 'scan-report.json'))).toBe(true);
|
|
63
|
-
}, 30_000);
|
|
64
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import fs from 'fs';
|
|
3
|
-
import os from 'os';
|
|
4
|
-
import path from 'path';
|
|
5
|
-
import { installSkills, skillsDoctor } from './skills.js';
|
|
6
|
-
describe('Rigour skills', () => {
|
|
7
|
-
let testDir;
|
|
8
|
-
beforeEach(() => {
|
|
9
|
-
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rigour-skills-'));
|
|
10
|
-
vi.spyOn(console, 'log').mockImplementation(() => undefined);
|
|
11
|
-
});
|
|
12
|
-
afterEach(() => {
|
|
13
|
-
fs.rmSync(testDir, { recursive: true, force: true });
|
|
14
|
-
vi.restoreAllMocks();
|
|
15
|
-
});
|
|
16
|
-
it('installs native Codex skills with valid discovery metadata', async () => {
|
|
17
|
-
await installSkills(testDir, ['context'], { target: 'codex' });
|
|
18
|
-
const filePath = path.join(testDir, '.agents', 'skills', 'rigour-context', 'SKILL.md');
|
|
19
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
20
|
-
expect(content).toContain('name: rigour-context');
|
|
21
|
-
expect(content).toContain('rigour_context_scope');
|
|
22
|
-
expect(content).toContain('repository-wide scan');
|
|
23
|
-
});
|
|
24
|
-
it('installs focused Cursor commands instead of a global rule', async () => {
|
|
25
|
-
await installSkills(testDir, ['verify'], { target: 'cursor' });
|
|
26
|
-
const filePath = path.join(testDir, '.cursor', 'commands', 'rigour-verify.md');
|
|
27
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
28
|
-
expect(content).toContain('rigour_check');
|
|
29
|
-
expect(content).toContain('rigour_get_fix_packet');
|
|
30
|
-
expect(content).not.toContain('alwaysApply');
|
|
31
|
-
});
|
|
32
|
-
it('keeps an existing playbook unless force is requested', async () => {
|
|
33
|
-
const filePath = path.join(testDir, '.cursor', 'commands', 'rigour-context.md');
|
|
34
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
35
|
-
fs.writeFileSync(filePath, 'team customisation');
|
|
36
|
-
await installSkills(testDir, ['context'], { target: 'cursor' });
|
|
37
|
-
expect(fs.readFileSync(filePath, 'utf-8')).toBe('team customisation');
|
|
38
|
-
await installSkills(testDir, ['context'], { target: 'cursor', force: true });
|
|
39
|
-
expect(fs.readFileSync(filePath, 'utf-8')).toContain('rigour_context_scope');
|
|
40
|
-
});
|
|
41
|
-
it('supports a no-write preview and portable playbooks', async () => {
|
|
42
|
-
await installSkills(testDir, ['handoff'], { target: 'universal', dryRun: true });
|
|
43
|
-
expect(fs.existsSync(path.join(testDir, 'docs', 'rigour-skills', 'rigour-handoff.md'))).toBe(false);
|
|
44
|
-
await installSkills(testDir, ['handoff'], { target: 'universal' });
|
|
45
|
-
const content = fs.readFileSync(path.join(testDir, 'docs', 'rigour-skills', 'rigour-handoff.md'), 'utf-8');
|
|
46
|
-
expect(content).toContain('portable Rigour playbook');
|
|
47
|
-
expect(content).toContain('rigour_handoff_accept');
|
|
48
|
-
});
|
|
49
|
-
it('reports missing and installed targets through doctor', async () => {
|
|
50
|
-
await skillsDoctor(testDir);
|
|
51
|
-
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('missing'));
|
|
52
|
-
await installSkills(testDir, ['all'], { target: 'all' });
|
|
53
|
-
await skillsDoctor(testDir);
|
|
54
|
-
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('All Rigour playbooks are installed'));
|
|
55
|
-
});
|
|
56
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { aboutAnotherRepository, buildActivity, groupSessions, readableFunction } from './studio-activity.js';
|
|
3
|
-
describe('buildActivity', () => {
|
|
4
|
-
it('turns the record into sentences, newest first, and leaves raw tool traffic out', () => {
|
|
5
|
-
const items = buildActivity({
|
|
6
|
-
events: [
|
|
7
|
-
{ type: 'hook_check', timestamp: '2026-10-09T10:00:00Z', files: ['src/pay.ts'], findings: [{ message: 'Stripe API key detected in code' }] },
|
|
8
|
-
{ type: 'tool_call', timestamp: '2026-10-09T10:01:00Z', tool: 'rigour_recall' },
|
|
9
|
-
{ type: 'stop_review', timestamp: '2026-10-09T10:03:00Z', blocked: true, blocking: 1 },
|
|
10
|
-
{ type: 'lessons_served', timestamp: '2026-10-09T10:04:00Z', via: 'recall', lessons: ['Amounts are cents'] },
|
|
11
|
-
],
|
|
12
|
-
ledger: [{ file: 'src/refund.ts', function: 'issueRefund', hash: 'h', reviewer: 'agent', verdict: 'fixed', note: 'Read the row under lock.', at: '2026-10-09T10:05:00Z' }],
|
|
13
|
-
stories: [{ id: 's', at: '2026-10-09T10:02:00Z', stage: 'edit', file: 'src/pay.ts', rule: 'security-patterns', title: 'Stripe API key detected in code', diff: [] }],
|
|
14
|
-
});
|
|
15
|
-
expect(items.map(i => i.text)).toEqual([
|
|
16
|
-
'Fixed `issueRefund` in src/refund.ts',
|
|
17
|
-
'Told the agent 1 lesson before it wrote',
|
|
18
|
-
'Kept the agent working: 1 problem left when it tried to finish',
|
|
19
|
-
'Fixed: Stripe API key detected in code',
|
|
20
|
-
'Stopped an edit to src/pay.ts: Stripe API key detected in code',
|
|
21
|
-
]);
|
|
22
|
-
});
|
|
23
|
-
it('names unnamed functions the way people do', () => {
|
|
24
|
-
expect(readableFunction('<anonymous>@17', 'src/proxy.test.ts')).toBe('the test at line 17');
|
|
25
|
-
expect(readableFunction('<anonymous>@4', 'src/a.ts')).toBe('the function at line 4');
|
|
26
|
-
expect(readableFunction('loadDiagnosis', 'src/a.ts')).toBe('`loadDiagnosis`');
|
|
27
|
-
});
|
|
28
|
-
it('groups activity into sessions, with fixes up front and routine items folded', () => {
|
|
29
|
-
const item = (at, kind, text = 'x') => ({ at, kind, text });
|
|
30
|
-
const sessions = groupSessions([
|
|
31
|
-
item('2026-10-09T12:00:00Z', 'reviewed', 'Reviewed `a` in a.ts: no issue'),
|
|
32
|
-
item('2026-10-09T11:50:00Z', 'reviewed', 'Fixed `b` in b.ts'),
|
|
33
|
-
item('2026-10-09T11:40:00Z', 'checked'),
|
|
34
|
-
item('2026-10-09T08:00:00Z', 'stopped'),
|
|
35
|
-
]);
|
|
36
|
-
expect(sessions).toHaveLength(2);
|
|
37
|
-
expect(sessions[0].counts).toMatchObject({ reviewed: 2, reviewedFixed: 1, checked: 1 });
|
|
38
|
-
expect(sessions[0].highlights.map(i => i.text)).toEqual(['Fixed `b` in b.ts']);
|
|
39
|
-
expect(sessions[0].rest).toHaveLength(2);
|
|
40
|
-
expect(sessions[1].highlights).toHaveLength(1);
|
|
41
|
-
});
|
|
42
|
-
it("leaves out old edit events that were about another repository's files", () => {
|
|
43
|
-
expect(aboutAnotherRepository({ type: 'hook_check', files: ['/work/other/src/a.ts'] }, ['/work/app'])).toBe(true);
|
|
44
|
-
expect(aboutAnotherRepository({ type: 'hook_check', files: ['/work/app/src/a.ts'] }, ['/work/app'])).toBe(false);
|
|
45
|
-
expect(aboutAnotherRepository({ type: 'hook_check', files: ['src/a.ts'] }, ['/work/app'])).toBe(false);
|
|
46
|
-
});
|
|
47
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,23 +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 { checkoutRoots } from './studio-checkouts.js';
|
|
7
|
-
let repo;
|
|
8
|
-
beforeEach(() => {
|
|
9
|
-
repo = fs.realpathSync.native(fs.mkdtempSync(path.join(os.tmpdir(), 'checkouts-')));
|
|
10
|
-
execFileSync('git', ['init', '-q', repo]);
|
|
11
|
-
execFileSync('git', ['-C', repo, '-c', 'user.email=t@example.com', '-c', 'user.name=t', 'commit', '-q', '--allow-empty', '-m', 'init']);
|
|
12
|
-
fs.mkdirSync(path.join(repo, '.rigour'));
|
|
13
|
-
});
|
|
14
|
-
afterEach(() => fs.rmSync(repo, { recursive: true, force: true }));
|
|
15
|
-
describe('checkoutRoots', () => {
|
|
16
|
-
it('lists each checkout once, however the folder is spelled', () => {
|
|
17
|
-
const tree = path.join(repo, 'wt');
|
|
18
|
-
execFileSync('git', ['-C', repo, 'worktree', 'add', '-q', tree]);
|
|
19
|
-
fs.mkdirSync(path.join(tree, '.rigour'));
|
|
20
|
-
// git reports these folders with forward slashes; started from a trailing-slash path
|
|
21
|
-
expect(checkoutRoots(repo + path.sep)).toEqual([repo, fs.realpathSync.native(tree)]);
|
|
22
|
-
});
|
|
23
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { buildAgentContext } from './studio-context.js';
|
|
3
|
-
const now = new Date('2026-10-09T12:00:00Z');
|
|
4
|
-
const t = (iso) => Date.parse(iso);
|
|
5
|
-
describe('buildAgentContext', () => {
|
|
6
|
-
it('sums what agents were given this week: focused files, recalls, lessons, reuse', () => {
|
|
7
|
-
const ctx = buildAgentContext({
|
|
8
|
-
now,
|
|
9
|
-
context: [
|
|
10
|
-
{ toolName: 'rigour_context_scope', cacheStatus: 'none', candidateFiles: 180, returnedFiles: 6, candidateTokens: 90000, returnedTokens: 4000, createdAt: t('2026-10-08T00:00:00Z') },
|
|
11
|
-
{ toolName: 'rigour_context_scope', cacheStatus: 'none', candidateFiles: 180, returnedFiles: 4, candidateTokens: 90000, returnedTokens: 3000, createdAt: t('2026-09-01T00:00:00Z') },
|
|
12
|
-
{ toolName: 'rigour_recall', cacheStatus: 'none', candidateTokens: 0, returnedTokens: 200, createdAt: t('2026-10-07T00:00:00Z') },
|
|
13
|
-
],
|
|
14
|
-
events: [
|
|
15
|
-
{ type: 'lessons_served', timestamp: '2026-10-07T00:00:00Z', lessons: ['a', 'b'] },
|
|
16
|
-
{ type: 'reuse_suggested', timestamp: '2026-10-08T00:00:00Z', planned: 'formatMoney', existing: 'formatCents in src/lib/money.ts:12', action: 'BLOCK' },
|
|
17
|
-
],
|
|
18
|
-
});
|
|
19
|
-
expect(ctx.week).toEqual({
|
|
20
|
-
scopes: 1, filesConsidered: 180, filesReturned: 6, tokensConsidered: 90000, tokensReturned: 4000, recalls: 1, lessonsTold: 2,
|
|
21
|
-
reuse: [{ at: '2026-10-08T00:00:00Z', planned: 'formatMoney', existing: 'formatCents in src/lib/money.ts:12', action: 'BLOCK' }],
|
|
22
|
-
});
|
|
23
|
-
expect(ctx.weeks.at(-1)).toMatchObject({ scopes: 1, filesReturned: 6, filesConsidered: 180, recalls: 1, lessonsTold: 2, reuse: 1 });
|
|
24
|
-
expect(ctx.weeks.reduce((n, w) => n + w.scopes, 0)).toBe(2); // September 1 is inside the 8 weeks
|
|
25
|
-
});
|
|
26
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { resolveStudioVersion } from './studio-contracts.js';
|
|
3
|
-
describe('resolveStudioVersion', () => {
|
|
4
|
-
it('prefers the released CLI version used to serve Studio', () => {
|
|
5
|
-
expect(resolveStudioVersion('6.2.1', '5.4.0')).toBe('6.2.1');
|
|
6
|
-
expect(resolveStudioVersion(undefined, '6.2.0')).toBe('6.2.0');
|
|
7
|
-
});
|
|
8
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { createStudioGuard, refuseStudioRequest, STUDIO_KEY_HEADER, studioLaunchUrl } from './studio-guard.js';
|
|
3
|
-
const guard = createStudioGuard([3000, 3001]);
|
|
4
|
-
const req = (method, headers) => ({ method, headers });
|
|
5
|
-
describe('Studio request guard', () => {
|
|
6
|
-
it('serves reads only to its own loopback host, so a rebound DNS name cannot read files', () => {
|
|
7
|
-
expect(refuseStudioRequest(req('GET', { host: '127.0.0.1:3000' }), guard)).toBeNull();
|
|
8
|
-
expect(refuseStudioRequest(req('GET', { host: 'localhost:3001' }), guard)).toBeNull();
|
|
9
|
-
expect(refuseStudioRequest(req('GET', { host: 'attacker.example:3000' }), guard)).toBe('unknown host');
|
|
10
|
-
expect(refuseStudioRequest(req('GET', {}), guard)).toBe('unknown host');
|
|
11
|
-
});
|
|
12
|
-
it('accepts a write only with the launch key and no foreign origin', () => {
|
|
13
|
-
const host = { host: '127.0.0.1:3000' };
|
|
14
|
-
expect(refuseStudioRequest(req('POST', { ...host, [STUDIO_KEY_HEADER]: guard.launchKey }), guard)).toBeNull();
|
|
15
|
-
expect(refuseStudioRequest(req('POST', { ...host, origin: 'http://127.0.0.1:3000', [STUDIO_KEY_HEADER]: guard.launchKey }), guard)).toBeNull();
|
|
16
|
-
expect(refuseStudioRequest(req('POST', host), guard)).toContain('missing studio key');
|
|
17
|
-
expect(refuseStudioRequest(req('DELETE', { ...host, [STUDIO_KEY_HEADER]: 'x'.repeat(48) }), guard)).toContain('wrong studio key');
|
|
18
|
-
expect(refuseStudioRequest(req('POST', { ...host, origin: 'https://evil.example', [STUDIO_KEY_HEADER]: guard.launchKey }), guard)).toBe('cross-origin write');
|
|
19
|
-
});
|
|
20
|
-
it('puts the key in the fragment, which the browser never sends to a server', () => {
|
|
21
|
-
expect(studioLaunchUrl('http://127.0.0.1:3000', guard)).toBe(`http://127.0.0.1:3000/#key=${guard.launchKey}`);
|
|
22
|
-
expect(createStudioGuard([3000]).launchKey).not.toBe(guard.launchKey);
|
|
23
|
-
});
|
|
24
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,31 +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 { saveLearnedRule } from '@rigour-labs/core';
|
|
6
|
-
import { loadStudioLearnedRules } from './studio-learned-rules.js';
|
|
7
|
-
const base = {
|
|
8
|
-
version: 1, message: 'm', severity: 'medium',
|
|
9
|
-
source: { commit: 'abc12345', file: 'src/http.ts', function: 'post' },
|
|
10
|
-
validation: { firesBefore: 1, firesAfter: 0, maxHits: 3, repoHits: ['src/other.ts:4'] },
|
|
11
|
-
};
|
|
12
|
-
describe('loadStudioLearnedRules', () => {
|
|
13
|
-
let cwd;
|
|
14
|
-
afterEach(() => { if (cwd)
|
|
15
|
-
fs.rmSync(cwd, { recursive: true, force: true }); cwd = undefined; });
|
|
16
|
-
it('describes each rule by what it requires and where it applies', () => {
|
|
17
|
-
cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'studio-learned-'));
|
|
18
|
-
saveLearnedRule(cwd, { ...base, id: 'require-option-redirect-aaaa1111', pattern: { template: 'require-option', callee: { level: 'declaration', key: 'src/http.ts#Deps.fetch' }, argIndex: 1, property: 'redirect' } });
|
|
19
|
-
saveLearnedRule(cwd, { ...base, id: 'require-guard-cache-control-bbbb2222', pattern: { template: 'require-guard', callee: { level: 'text', key: 'Response.json' }, property: 'cache-control', guard: 'stepsAvailable', appliesWhenArgsMention: false, scope: { file: 'src/route.ts', fn: 'GET>respond' } } });
|
|
20
|
-
const rules = loadStudioLearnedRules(cwd);
|
|
21
|
-
expect(rules.map(r => [r.requirement, r.appliesTo])).toEqual([
|
|
22
|
-
['`cache-control` conditional on `stepsAvailable`', 'regression guard for GET>respond in src/route.ts'],
|
|
23
|
-
['`redirect` in argument 2', 'declaration src/http.ts#Deps.fetch'],
|
|
24
|
-
]);
|
|
25
|
-
expect(rules[1].validation.repoHits).toEqual(['src/other.ts:4']);
|
|
26
|
-
});
|
|
27
|
-
it('is empty for a repository without learned rules', () => {
|
|
28
|
-
cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'studio-learned-'));
|
|
29
|
-
expect(loadStudioLearnedRules(cwd)).toEqual([]);
|
|
30
|
-
});
|
|
31
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|