@rigour-labs/core 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.
- package/dist/inference/cloud-provider.js +12 -2
- package/package.json +11 -8
- package/dist/brief/briefing.test.d.ts +0 -1
- package/dist/brief/briefing.test.js +0 -107
- package/dist/context/automatic-index-cache.test.d.ts +0 -1
- package/dist/context/automatic-index-cache.test.js +0 -30
- package/dist/context/automatic-index.test.d.ts +0 -1
- package/dist/context/automatic-index.test.js +0 -45
- package/dist/context/cache-engine.test.d.ts +0 -1
- package/dist/context/cache-engine.test.js +0 -99
- package/dist/context/dependency-graph.test.d.ts +0 -1
- package/dist/context/dependency-graph.test.js +0 -13
- package/dist/context/index-status.test.d.ts +0 -1
- package/dist/context/index-status.test.js +0 -26
- package/dist/context.test.d.ts +0 -1
- package/dist/context.test.js +0 -228
- package/dist/deep/agent-review.test.d.ts +0 -1
- package/dist/deep/agent-review.test.js +0 -53
- package/dist/deep/code-context.test.d.ts +0 -1
- package/dist/deep/code-context.test.js +0 -48
- package/dist/deep/code-pass.test.d.ts +0 -1
- package/dist/deep/code-pass.test.js +0 -112
- package/dist/deep/code-review-prompt.test.d.ts +0 -1
- package/dist/deep/code-review-prompt.test.js +0 -20
- package/dist/deep/code-verifier.test.d.ts +0 -1
- package/dist/deep/code-verifier.test.js +0 -54
- package/dist/deep/diff-tests/calls.test.d.ts +0 -1
- package/dist/deep/diff-tests/calls.test.js +0 -33
- package/dist/deep/diff-tests/run.test.d.ts +0 -1
- package/dist/deep/diff-tests/run.test.js +0 -58
- package/dist/deep/fact-extractor.test.d.ts +0 -1
- package/dist/deep/fact-extractor.test.js +0 -581
- package/dist/deep/parse-findings.test.d.ts +0 -1
- package/dist/deep/parse-findings.test.js +0 -25
- package/dist/deep/pr-review.test.d.ts +0 -1
- package/dist/deep/pr-review.test.js +0 -80
- package/dist/deep/prompts.test.d.ts +0 -1
- package/dist/deep/prompts.test.js +0 -235
- package/dist/deep/reference-pack.test.d.ts +0 -1
- package/dist/deep/reference-pack.test.js +0 -46
- package/dist/deep/related-changes.test.d.ts +0 -1
- package/dist/deep/related-changes.test.js +0 -33
- package/dist/deep/review-context-export.test.d.ts +0 -1
- package/dist/deep/review-context-export.test.js +0 -32
- package/dist/deep/review-tools.test.d.ts +0 -1
- package/dist/deep/review-tools.test.js +0 -39
- package/dist/deep/risk.test.d.ts +0 -1
- package/dist/deep/risk.test.js +0 -101
- package/dist/deep/verifier.test.d.ts +0 -1
- package/dist/deep/verifier.test.js +0 -635
- package/dist/discovery.test.d.ts +0 -1
- package/dist/discovery.test.js +0 -93
- package/dist/environment.test.d.ts +0 -1
- package/dist/environment.test.js +0 -94
- package/dist/firewall/firewall.test.d.ts +0 -1
- package/dist/firewall/firewall.test.js +0 -117
- package/dist/firewall/trust-boundaries.test.d.ts +0 -1
- package/dist/firewall/trust-boundaries.test.js +0 -64
- package/dist/firewall/trusted-control.test.d.ts +0 -1
- package/dist/firewall/trusted-control.test.js +0 -188
- package/dist/gates/agent-team.test.d.ts +0 -1
- package/dist/gates/agent-team.test.js +0 -113
- package/dist/gates/ast.test.d.ts +0 -1
- package/dist/gates/ast.test.js +0 -112
- package/dist/gates/checkpoint.test.d.ts +0 -1
- package/dist/gates/checkpoint.test.js +0 -105
- package/dist/gates/content.test.d.ts +0 -1
- package/dist/gates/content.test.js +0 -73
- package/dist/gates/coverage.test.d.ts +0 -1
- package/dist/gates/coverage.test.js +0 -53
- package/dist/gates/dedupe-failures.test.d.ts +0 -1
- package/dist/gates/dedupe-failures.test.js +0 -12
- package/dist/gates/deep-analysis.test.d.ts +0 -1
- package/dist/gates/deep-analysis.test.js +0 -86
- package/dist/gates/deep-intent.test.d.ts +0 -1
- package/dist/gates/deep-intent.test.js +0 -51
- package/dist/gates/deep-timeout.test.d.ts +0 -1
- package/dist/gates/deep-timeout.test.js +0 -10
- package/dist/gates/deprecated-apis.test.d.ts +0 -1
- package/dist/gates/deprecated-apis.test.js +0 -318
- package/dist/gates/deprecated-dependencies.test.d.ts +0 -1
- package/dist/gates/deprecated-dependencies.test.js +0 -55
- package/dist/gates/frontend-secret-exposure.test.d.ts +0 -1
- package/dist/gates/frontend-secret-exposure.test.js +0 -148
- package/dist/gates/hallucinated-imports/framework-modules-nuxt.test.d.ts +0 -1
- package/dist/gates/hallucinated-imports/framework-modules-nuxt.test.js +0 -27
- package/dist/gates/hallucinated-imports/js-resolver-types.test.d.ts +0 -1
- package/dist/gates/hallucinated-imports/js-resolver-types.test.js +0 -14
- package/dist/gates/hallucinated-imports-sveltekit.test.d.ts +0 -1
- package/dist/gates/hallucinated-imports-sveltekit.test.js +0 -132
- package/dist/gates/hallucinated-imports.test.d.ts +0 -1
- package/dist/gates/hallucinated-imports.test.js +0 -1206
- package/dist/gates/js-style-context.test.d.ts +0 -1
- package/dist/gates/js-style-context.test.js +0 -35
- package/dist/gates/logic-drift.test.d.ts +0 -1
- package/dist/gates/logic-drift.test.js +0 -52
- package/dist/gates/phantom-apis.test.d.ts +0 -1
- package/dist/gates/phantom-apis.test.js +0 -396
- package/dist/gates/promise-safety.test.d.ts +0 -1
- package/dist/gates/promise-safety.test.js +0 -34
- package/dist/gates/runner.test.d.ts +0 -1
- package/dist/gates/runner.test.js +0 -77
- package/dist/gates/scoped-gates.test.d.ts +0 -1
- package/dist/gates/scoped-gates.test.js +0 -52
- package/dist/gates/security-patterns-owasp.test.d.ts +0 -1
- package/dist/gates/security-patterns-owasp.test.js +0 -186
- package/dist/gates/security-patterns.test.d.ts +0 -1
- package/dist/gates/security-patterns.test.js +0 -194
- package/dist/gates/semantic-bugs.test.d.ts +0 -1
- package/dist/gates/semantic-bugs.test.js +0 -76
- package/dist/gates/side-effect-analysis.test.d.ts +0 -1
- package/dist/gates/side-effect-analysis.test.js +0 -162
- package/dist/gates/style-drift.test.d.ts +0 -1
- package/dist/gates/style-drift.test.js +0 -26
- package/dist/gates/test-quality.test.d.ts +0 -1
- package/dist/gates/test-quality.test.js +0 -325
- package/dist/gates/trusted-reviews.test.d.ts +0 -1
- package/dist/gates/trusted-reviews.test.js +0 -16
- package/dist/gates/unindexed-reads/queries.test.d.ts +0 -1
- package/dist/gates/unindexed-reads/queries.test.js +0 -54
- package/dist/gates/unindexed-reads/schema.test.d.ts +0 -1
- package/dist/gates/unindexed-reads/schema.test.js +0 -81
- package/dist/gates/unindexed-reads/unindexed-reads.test.d.ts +0 -1
- package/dist/gates/unindexed-reads/unindexed-reads.test.js +0 -90
- package/dist/hooks/checker.test.d.ts +0 -1
- package/dist/hooks/checker.test.js +0 -159
- package/dist/hooks/dlp-confidence.test.d.ts +0 -1
- package/dist/hooks/dlp-confidence.test.js +0 -51
- package/dist/hooks/dlp-feedback.test.d.ts +0 -1
- package/dist/hooks/dlp-feedback.test.js +0 -131
- package/dist/hooks/input-validator.test.d.ts +0 -1
- package/dist/hooks/input-validator.test.js +0 -329
- package/dist/hooks/templates.test.d.ts +0 -1
- package/dist/hooks/templates.test.js +0 -27
- package/dist/inference/brain-placeholder.test.d.ts +0 -1
- package/dist/inference/brain-placeholder.test.js +0 -28
- package/dist/inference/cloud-provider.test.d.ts +0 -1
- package/dist/inference/cloud-provider.test.js +0 -139
- package/dist/inference/executable.test.d.ts +0 -1
- package/dist/inference/executable.test.js +0 -41
- package/dist/inference/http-download.test.d.ts +0 -1
- package/dist/inference/http-download.test.js +0 -109
- package/dist/inference/llama-engine-checksum.test.d.ts +0 -1
- package/dist/inference/llama-engine-checksum.test.js +0 -27
- package/dist/inference/llama-engine.test.d.ts +0 -1
- package/dist/inference/llama-engine.test.js +0 -51
- package/dist/inference/llama-process.test.d.ts +0 -1
- package/dist/inference/llama-process.test.js +0 -61
- package/dist/inference/local-model.test.d.ts +0 -1
- package/dist/inference/local-model.test.js +0 -23
- package/dist/inference/model-download.test.d.ts +0 -1
- package/dist/inference/model-download.test.js +0 -125
- package/dist/inference/model-manager.test.d.ts +0 -1
- package/dist/inference/model-manager.test.js +0 -24
- package/dist/inference/types.test.d.ts +0 -1
- package/dist/inference/types.test.js +0 -19
- package/dist/memory/recall.test.d.ts +0 -1
- package/dist/memory/recall.test.js +0 -36
- package/dist/pattern-index/indexer.test.d.ts +0 -6
- package/dist/pattern-index/indexer.test.js +0 -197
- package/dist/pattern-index/matcher.test.d.ts +0 -6
- package/dist/pattern-index/matcher.test.js +0 -238
- package/dist/pattern-index/pattern-reuse.test.d.ts +0 -1
- package/dist/pattern-index/pattern-reuse.test.js +0 -76
- package/dist/pattern-index/semantic-runtime.test.d.ts +0 -1
- package/dist/pattern-index/semantic-runtime.test.js +0 -31
- package/dist/pattern-index/staleness.test.d.ts +0 -6
- package/dist/pattern-index/staleness.test.js +0 -211
- package/dist/review/agent-fixes.test.d.ts +0 -1
- package/dist/review/agent-fixes.test.js +0 -32
- package/dist/review/backtest-init.test.d.ts +0 -1
- package/dist/review/backtest-init.test.js +0 -136
- package/dist/review/backtest-judges.test.d.ts +0 -1
- package/dist/review/backtest-judges.test.js +0 -32
- package/dist/review/backtest-last.test.d.ts +0 -1
- package/dist/review/backtest-last.test.js +0 -109
- package/dist/review/backtest.test.d.ts +0 -1
- package/dist/review/backtest.test.js +0 -183
- package/dist/review/baseline.test.d.ts +0 -1
- package/dist/review/baseline.test.js +0 -22
- package/dist/review/branch-checks.test.d.ts +0 -1
- package/dist/review/branch-checks.test.js +0 -48
- package/dist/review/check-outcomes.test.d.ts +0 -1
- package/dist/review/check-outcomes.test.js +0 -56
- package/dist/review/code-patterns.test.d.ts +0 -1
- package/dist/review/code-patterns.test.js +0 -142
- package/dist/review/dead-code.test.d.ts +0 -1
- package/dist/review/dead-code.test.js +0 -154
- package/dist/review/deep-runs.test.d.ts +0 -1
- package/dist/review/deep-runs.test.js +0 -23
- package/dist/review/effectiveness.test.d.ts +0 -1
- package/dist/review/effectiveness.test.js +0 -42
- package/dist/review/fix-scope.test.d.ts +0 -1
- package/dist/review/fix-scope.test.js +0 -104
- package/dist/review/generated-files.test.d.ts +0 -1
- package/dist/review/generated-files.test.js +0 -25
- package/dist/review/migration-order.test.d.ts +0 -1
- package/dist/review/migration-order.test.js +0 -62
- package/dist/review/quiet.test.d.ts +0 -1
- package/dist/review/quiet.test.js +0 -38
- package/dist/review/receipt.test.d.ts +0 -1
- package/dist/review/receipt.test.js +0 -60
- package/dist/review/review-task.test.d.ts +0 -1
- package/dist/review/review-task.test.js +0 -89
- package/dist/review/review.test.d.ts +0 -1
- package/dist/review/review.test.js +0 -203
- package/dist/review/reviewer/adapters.test.d.ts +0 -1
- package/dist/review/reviewer/adapters.test.js +0 -100
- package/dist/review/reviewer/api-judge.test.d.ts +0 -1
- package/dist/review/reviewer/api-judge.test.js +0 -108
- package/dist/review/reviewer/background.test.d.ts +0 -1
- package/dist/review/reviewer/background.test.js +0 -118
- package/dist/review/reviewer/context.test.d.ts +0 -1
- package/dist/review/reviewer/context.test.js +0 -45
- package/dist/review/reviewer/exec.test.d.ts +0 -1
- package/dist/review/reviewer/exec.test.js +0 -59
- package/dist/review/reviewer/inputs.test.d.ts +0 -1
- package/dist/review/reviewer/inputs.test.js +0 -52
- package/dist/review/reviewer/panel.test.d.ts +0 -1
- package/dist/review/reviewer/panel.test.js +0 -110
- package/dist/review/reviewer/record.test.d.ts +0 -1
- package/dist/review/reviewer/record.test.js +0 -40
- package/dist/review/reviewer/rule-writer.test.d.ts +0 -1
- package/dist/review/reviewer/rule-writer.test.js +0 -52
- package/dist/review/reviewer/settings.test.d.ts +0 -1
- package/dist/review/reviewer/settings.test.js +0 -57
- package/dist/review/reviewer/usage.test.d.ts +0 -1
- package/dist/review/reviewer/usage.test.js +0 -14
- package/dist/review/reviewer.test.d.ts +0 -1
- package/dist/review/reviewer.test.js +0 -792
- package/dist/review/stories.test.d.ts +0 -1
- package/dist/review/stories.test.js +0 -58
- package/dist/review/toolchain.test.d.ts +0 -1
- package/dist/review/toolchain.test.js +0 -108
- package/dist/review/typed/redundancy.test.d.ts +0 -1
- package/dist/review/typed/redundancy.test.js +0 -315
- package/dist/review/typed/schema-nullability.test.d.ts +0 -1
- package/dist/review/typed/schema-nullability.test.js +0 -63
- package/dist/review-learning/human-edits.test.d.ts +0 -1
- package/dist/review-learning/human-edits.test.js +0 -47
- package/dist/review-learning/repo-rules.test.d.ts +0 -1
- package/dist/review-learning/repo-rules.test.js +0 -93
- package/dist/review-learning/review-learning.test.d.ts +0 -1
- package/dist/review-learning/review-learning.test.js +0 -249
- package/dist/safety.test.d.ts +0 -1
- package/dist/safety.test.js +0 -42
- package/dist/semantic/benchmark.test.d.ts +0 -1
- package/dist/semantic/benchmark.test.js +0 -22
- package/dist/semantic/intent/intent.test.d.ts +0 -1
- package/dist/semantic/intent/intent.test.js +0 -46
- package/dist/semantic/learn/learn.test.d.ts +0 -1
- package/dist/semantic/learn/learn.test.js +0 -67
- package/dist/semantic/origins.test.d.ts +0 -1
- package/dist/semantic/origins.test.js +0 -77
- package/dist/semantic/project-facts.test.d.ts +0 -1
- package/dist/semantic/project-facts.test.js +0 -45
- package/dist/semantic/sites/call-sites.test.d.ts +0 -1
- package/dist/semantic/sites/call-sites.test.js +0 -46
- package/dist/services/adaptive-thresholds.test.d.ts +0 -1
- package/dist/services/adaptive-thresholds.test.js +0 -53
- package/dist/services/agent-history.test.d.ts +0 -1
- package/dist/services/agent-history.test.js +0 -69
- package/dist/services/context-scope-summary.test.d.ts +0 -1
- package/dist/services/context-scope-summary.test.js +0 -17
- package/dist/services/context-telemetry-service.test.d.ts +0 -1
- package/dist/services/context-telemetry-service.test.js +0 -182
- package/dist/services/cursor-usage-sync.test.d.ts +0 -1
- package/dist/services/cursor-usage-sync.test.js +0 -173
- package/dist/services/engineering-knowledge-graph.test.d.ts +0 -1
- package/dist/services/engineering-knowledge-graph.test.js +0 -76
- package/dist/services/model-pricing.test.d.ts +0 -1
- package/dist/services/model-pricing.test.js +0 -44
- package/dist/services/observed-savings.test.d.ts +0 -1
- package/dist/services/observed-savings.test.js +0 -37
- package/dist/services/score-history.test.d.ts +0 -1
- package/dist/services/score-history.test.js +0 -61
- package/dist/smoke.test.d.ts +0 -1
- package/dist/smoke.test.js +0 -17
- package/dist/storage/cache-cleanup.test.d.ts +0 -1
- package/dist/storage/cache-cleanup.test.js +0 -54
- package/dist/storage/context-telemetry.test.d.ts +0 -1
- package/dist/storage/context-telemetry.test.js +0 -80
- package/dist/storage/db.test.d.ts +0 -1
- package/dist/storage/db.test.js +0 -46
- package/dist/storage/fix-lessons.test.d.ts +0 -1
- package/dist/storage/fix-lessons.test.js +0 -61
- package/dist/storage/lessons.test.d.ts +0 -1
- package/dist/storage/lessons.test.js +0 -81
- package/dist/storage/local-encryption.test.d.ts +0 -1
- package/dist/storage/local-encryption.test.js +0 -34
- package/dist/storage/local-memory.test.d.ts +0 -1
- package/dist/storage/local-memory.test.js +0 -55
- package/dist/storage/share-memory.test.d.ts +0 -1
- package/dist/storage/share-memory.test.js +0 -34
- package/dist/storage/team-diagnostics.test.d.ts +0 -1
- package/dist/storage/team-diagnostics.test.js +0 -51
- package/dist/storage/team-scope.test.d.ts +0 -1
- package/dist/storage/team-scope.test.js +0 -22
- package/dist/storage/team-store.test.d.ts +0 -1
- package/dist/storage/team-store.test.js +0 -11
- package/dist/storage/team-sync-scope.test.d.ts +0 -1
- package/dist/storage/team-sync-scope.test.js +0 -100
- package/dist/storage/team-vector-store.test.d.ts +0 -1
- package/dist/storage/team-vector-store.test.js +0 -56
- package/dist/storage/telemetry-scope.test.d.ts +0 -1
- package/dist/storage/telemetry-scope.test.js +0 -38
- package/dist/task/thread.test.d.ts +0 -1
- package/dist/task/thread.test.js +0 -133
- package/dist/telemetry/telemetry.test.d.ts +0 -1
- package/dist/telemetry/telemetry.test.js +0 -64
- package/dist/types/index.test.d.ts +0 -1
- package/dist/types/index.test.js +0 -33
- package/dist/utils/command-line.test.d.ts +0 -1
- package/dist/utils/command-line.test.js +0 -8
- package/dist/utils/diff-removed.test.d.ts +0 -1
- package/dist/utils/diff-removed.test.js +0 -29
- package/dist/utils/diff.test.d.ts +0 -1
- package/dist/utils/diff.test.js +0 -38
- package/dist/utils/glob-paths.test.d.ts +0 -1
- package/dist/utils/glob-paths.test.js +0 -40
- package/dist/utils/profile.test.d.ts +0 -1
- package/dist/utils/profile.test.js +0 -68
- package/dist/utils/scanner.test.d.ts +0 -1
- package/dist/utils/scanner.test.js +0 -48
- package/dist/utils/scope.test.d.ts +0 -1
- package/dist/utils/scope.test.js +0 -38
|
@@ -1,792 +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 { ConfigSchema } from '../types/index.js';
|
|
7
|
-
import { reviewerBlocks, runReviewer } from './reviewer.js';
|
|
8
|
-
import { dismissReviewerFinding } from './reviewer/context.js';
|
|
9
|
-
import { reviewStatus } from './reviewer/background.js';
|
|
10
|
-
import { selectReviewers, vendorsOf } from './reviewer/adapters.js';
|
|
11
|
-
import { account, attachServedRules, carryResolved, changedLinesOf, checkoutSearch, checkoutVerifier, mergeVerdicts, parseVerdict } from './reviewer/verdict.js';
|
|
12
|
-
import { recordIntact, recordLines } from './reviewer/record.js';
|
|
13
|
-
import { readThread } from '../task/thread.js';
|
|
14
|
-
let repo;
|
|
15
|
-
const config = ConfigSchema.parse({ version: 1, review: { github_account: 'reviewer-account', reviewer: { enabled: true, reviewers: ['claude', 'cursor'] } } });
|
|
16
|
-
const git = (...args) => execFileSync('git', ['-C', repo, ...args], { encoding: 'utf8' }).trim();
|
|
17
|
-
const PR = { number: 42, state: 'OPEN', isDraft: false, author: { login: 'author' }, body: 'Every read is bounded at both ends.' };
|
|
18
|
-
const REVIEWS = [
|
|
19
|
-
{ id: 1, user: { login: 'ci-bot', type: 'Bot' }, body: 'automated', state: 'COMMENTED', submitted_at: '2026-10-01', commit_id: 'aaaaaaaaa' },
|
|
20
|
-
{ id: 2, user: { login: 'author', type: 'User' }, body: 'self note', state: 'COMMENTED', submitted_at: '2026-10-02', commit_id: 'aaaaaaaaa' },
|
|
21
|
-
{ id: 3, user: { login: 'senior', type: 'User' }, body: 'Two blocking points.', state: 'CHANGES_REQUESTED', submitted_at: '2026-10-03', commit_id: 'aaaaaaaaa' },
|
|
22
|
-
];
|
|
23
|
-
const INLINE = [{ id: 7, user: { login: 'senior', type: 'User' }, path: 'src/job.ts', line: 12, body: 'Check the lock before the first read.', created_at: '2026-10-03', updated_at: '2026-10-03' }];
|
|
24
|
-
const EMPTY = { prior_points: [{ point: 'lock before read', severity: 'blocking', resolved: true, evidence: 'a.ts:1' }], redundant: [], reads: [], scans: [], merge_impact: [], findings: [], carried: [], resolved_previous: [] };
|
|
25
|
-
/** Real git; scripted gh; agent CLIs that record what they were shown and answer `answer` (a function of the reviewer's name). */
|
|
26
|
-
function fakes(answer, seen, pr = PR) {
|
|
27
|
-
return async (command, args, options) => {
|
|
28
|
-
if (command === 'git') {
|
|
29
|
-
try {
|
|
30
|
-
return { exitCode: 0, stdout: execFileSync('git', args, { cwd: options.cwd, encoding: 'utf8' }), stderr: '' };
|
|
31
|
-
}
|
|
32
|
-
catch (error) {
|
|
33
|
-
return { exitCode: 1, stdout: '', stderr: String(error.message) };
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
if (command === 'gh') {
|
|
37
|
-
seen.ghArgs.push(args);
|
|
38
|
-
if (args[0] === 'auth')
|
|
39
|
-
return { exitCode: 0, stdout: 'token-for-account\n', stderr: '' };
|
|
40
|
-
seen.ghToken = options.env?.GH_TOKEN;
|
|
41
|
-
if (args[0] === 'pr')
|
|
42
|
-
return pr ? { exitCode: 0, stdout: JSON.stringify(pr), stderr: '' } : { exitCode: 1, stdout: '', stderr: 'no pull requests found for branch "feature"' };
|
|
43
|
-
if (args[1].endsWith('/reviews'))
|
|
44
|
-
return { exitCode: 0, stdout: JSON.stringify(REVIEWS), stderr: '' };
|
|
45
|
-
if (args[1].endsWith('/comments'))
|
|
46
|
-
return { exitCode: 0, stdout: JSON.stringify(INLINE), stderr: '' };
|
|
47
|
-
return { exitCode: 1, stdout: '', stderr: 'unexpected gh call' };
|
|
48
|
-
}
|
|
49
|
-
const binary = path.basename(command).replace(/\.(cmd|exe)$/, '');
|
|
50
|
-
if (args[0] === '--version')
|
|
51
|
-
return (seen.installed ?? ['claude', 'cursor-agent']).includes(binary) ? { exitCode: 0, stdout: `${seen.versions?.[command] ?? '1.0.0'}\n`, stderr: '' } : { exitCode: 127, stdout: '', stderr: 'not found' };
|
|
52
|
-
seen.ran.push(command);
|
|
53
|
-
(seen.args ??= []).push(args);
|
|
54
|
-
(seen.unset ??= []).push(options.unset);
|
|
55
|
-
(seen.env ??= []).push(options.env);
|
|
56
|
-
const name = binary === 'claude' ? 'claude' : binary === 'cursor-agent' ? 'cursor' : 'codex';
|
|
57
|
-
const prompt = binary === 'claude' ? args[args.indexOf('-p') + 1] : args[args.length - 1];
|
|
58
|
-
seen.prompts.push(prompt);
|
|
59
|
-
// Paths as the prompt names them, on either separator (Windows writes `D:\...`).
|
|
60
|
-
for (const match of prompt.matchAll(/(\S+(?:previous-reviews\.md|pr-description\.md|full\.diff|hints\.txt|previous-open\.json|delta\.diff|previous-resolved\.json|team-knowledge\.md))/g)) {
|
|
61
|
-
seen.files[path.basename(match[1])] = fs.readFileSync(match[1], 'utf8');
|
|
62
|
-
}
|
|
63
|
-
const reply = answer(name);
|
|
64
|
-
if (typeof reply !== 'string')
|
|
65
|
-
return reply;
|
|
66
|
-
const stdout = binary === 'claude' ? JSON.stringify({ result: reply, total_cost_usd: 1.5 }) : binary === 'codex' ? JSON.stringify({ type: 'item.completed', item: { text: reply } }) : JSON.stringify({ result: reply });
|
|
67
|
-
return { exitCode: 0, stdout, stderr: '' };
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
const seenNow = () => ({ prompts: [], files: {}, ghArgs: [], ran: [] });
|
|
71
|
-
/** A PATH of our own, so the test sees only the agent CLIs it creates (the fake exec answers for them by name). */
|
|
72
|
-
let bins;
|
|
73
|
-
const originalPath = process.env.PATH;
|
|
74
|
-
function installFake(dir, name) {
|
|
75
|
-
const file = path.join(dir, process.platform === 'win32' ? `${name}.cmd` : name);
|
|
76
|
-
fs.writeFileSync(file, '#!/bin/sh\nexit 0\n', { mode: 0o755 });
|
|
77
|
-
return file;
|
|
78
|
-
}
|
|
79
|
-
beforeEach(() => {
|
|
80
|
-
bins = [fs.mkdtempSync(path.join(os.tmpdir(), 'bin-a-')), fs.mkdtempSync(path.join(os.tmpdir(), 'bin-b-'))];
|
|
81
|
-
for (const name of ['claude', 'cursor-agent'])
|
|
82
|
-
installFake(bins[0], name);
|
|
83
|
-
process.env.PATH = [...bins, originalPath ?? ''].join(path.delimiter);
|
|
84
|
-
repo = fs.mkdtempSync(path.join(os.tmpdir(), 'reviewer-'));
|
|
85
|
-
git('init', '-q', '-b', 'main');
|
|
86
|
-
git('config', 'user.email', 't@example.com');
|
|
87
|
-
git('config', 'user.name', 't');
|
|
88
|
-
git('config', 'commit.gpgsign', 'false');
|
|
89
|
-
fs.writeFileSync(path.join(repo, 'a.ts'), 'export const a = 1;\n');
|
|
90
|
-
git('add', '-A');
|
|
91
|
-
git('commit', '-qm', 'init');
|
|
92
|
-
git('checkout', '-qb', 'feature');
|
|
93
|
-
fs.mkdirSync(path.join(repo, 'src'));
|
|
94
|
-
fs.writeFileSync(path.join(repo, 'src/job.ts'), 'export function job() {\n return 1;\n}\n');
|
|
95
|
-
git('add', '-A');
|
|
96
|
-
git('commit', '-qm', 'job');
|
|
97
|
-
});
|
|
98
|
-
afterEach(() => {
|
|
99
|
-
process.env.PATH = originalPath;
|
|
100
|
-
for (const dir of [repo, ...bins])
|
|
101
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
102
|
-
});
|
|
103
|
-
describe('the reviewer', () => {
|
|
104
|
-
it('works from every human review with inline comments and the description, written to files, and blocks on an open point', async () => {
|
|
105
|
-
const seen = seenNow();
|
|
106
|
-
const answer = JSON.stringify({ ...EMPTY, prior_points: [{ point: 'lock before read', severity: 'blocking', resolved: false, evidence: 'src/job.ts:2', file: 'src/job.ts', line: 2, quote: 'export function job() {' }] });
|
|
107
|
-
const result = await runReviewer(repo, 'main', config, fakes(() => answer, seen), () => undefined);
|
|
108
|
-
expect(seen.files['previous-reviews.md']).toContain('Review by senior');
|
|
109
|
-
expect(seen.files['previous-reviews.md']).toContain('- 2026-10-03 src/job.ts:12: Check the lock before the first read.');
|
|
110
|
-
expect(seen.files['previous-reviews.md']).not.toContain('automated');
|
|
111
|
-
expect(seen.files['previous-reviews.md']).not.toContain('self note');
|
|
112
|
-
expect(seen.files['pr-description.md']).toBe('Every read is bounded at both ends.');
|
|
113
|
-
expect(seen.files['full.diff']).toContain('+export function job()');
|
|
114
|
-
expect(seen.ghToken).toBe('token-for-account');
|
|
115
|
-
expect(seen.ghArgs[1].slice(0, 3)).toEqual(['pr', 'view', 'feature']); // by branch: the commit is not on the forge yet
|
|
116
|
-
expect(result).toMatchObject({ outcome: 'findings', reviewers: ['claude'], scope: 'full', cached: false, previousReview: 'senior, 2026-10-03 (1 review)', pr: 42, costUsd: 1.5 });
|
|
117
|
-
expect(result.items).toEqual([expect.objectContaining({ kind: 'prior', issue: 'lock before read', evidence: 'src/job.ts:2', reviewer: 'claude' })]);
|
|
118
|
-
expect(reviewerBlocks(result)).toBe(true);
|
|
119
|
-
});
|
|
120
|
-
it('caches the verdict per commit and inputs, so the same push is free, and the next commit gets a delta review that carries what is not accounted for', async () => {
|
|
121
|
-
const seen = seenNow();
|
|
122
|
-
const open = JSON.stringify({ ...EMPTY, findings: [{ class: 'correctness', file: 'src/job.ts', line: 2, issue: 'returns before the lock', quote: 'export function job() {', consequence: 'a second run reads stale rows', why: 'x' }] });
|
|
123
|
-
const first = await runReviewer(repo, 'main', config, fakes(() => open, seen), () => undefined, { checks: ['src/job.ts:1 Unused export `job`'] });
|
|
124
|
-
expect(first.items).toHaveLength(1);
|
|
125
|
-
const again = await runReviewer(repo, 'main', config, fakes(() => open, seen), () => undefined, { checks: ['src/job.ts:1 Unused export `job`'] });
|
|
126
|
-
expect(again.cached).toBe(true);
|
|
127
|
-
expect(seen.prompts).toHaveLength(1);
|
|
128
|
-
expect(fs.readdirSync(repo).sort()).toEqual(['.git', 'a.ts', 'src']); // nothing written to the working tree
|
|
129
|
-
fs.writeFileSync(path.join(repo, 'src/job.ts'), 'export function job() {\n return 2;\n}\n');
|
|
130
|
-
git('commit', '-qam', 'tweak');
|
|
131
|
-
// What the checks found changed with the commit: the context changes, the instructions do not, so it is still a delta.
|
|
132
|
-
const delta = await runReviewer(repo, 'main', config, fakes(() => JSON.stringify({ ...EMPTY, prior_points: [], carried: [], resolved_previous: [] }), seen), () => undefined, { checks: ['src/job.ts:2 Unused export `other`'] });
|
|
133
|
-
expect(delta.scope).toBe('delta');
|
|
134
|
-
expect(seen.prompts[1]).toContain('DELTA MODE');
|
|
135
|
-
expect(seen.files['delta.diff']).toContain('- return 1;');
|
|
136
|
-
expect(JSON.parse(seen.files['previous-open.json'])).toHaveLength(1);
|
|
137
|
-
expect(delta.items).toEqual([expect.objectContaining({ issue: 'returns before the lock', status: 'not accounted for' })]);
|
|
138
|
-
expect(delta.outcome).toBe('findings');
|
|
139
|
-
// The human point the previous verdict resolved (a.ts untouched) was carried, not judged again.
|
|
140
|
-
expect(JSON.parse(seen.files['previous-resolved.json'])).toEqual([expect.objectContaining({ point: 'lock before read', resolved: true })]);
|
|
141
|
-
expect(delta.answerInReply).toEqual([]);
|
|
142
|
-
});
|
|
143
|
-
it('resolves a previous item only with evidence, and never blocks on an item that names code the checkout does not have', async () => {
|
|
144
|
-
const seen = seenNow();
|
|
145
|
-
const first = await runReviewer(repo, 'main', config, fakes(() => JSON.stringify({ ...EMPTY, findings: [{ class: 'correctness', file: 'src/job.ts', line: 2, issue: 'returns before the lock', quote: 'export function job() {', consequence: 'a second run reads stale rows' }, { class: 'dead-code', file: 'src/ghost.ts', line: 1, issue: 'unused', consequence: 'a second run reads stale rows' }, { class: 'dead-code', file: '', issue: 'somewhere, no file named', consequence: 'a second run reads stale rows' }] }), seen), () => undefined);
|
|
146
|
-
expect(first.items.map(i => i.file)).toEqual(['src/job.ts']);
|
|
147
|
-
expect(first.unverified.map(i => i.file)).toEqual(['src/ghost.ts', '']); // a slip and a finding with no place to check: shown, never a block
|
|
148
|
-
const id = first.items[0].id;
|
|
149
|
-
fs.writeFileSync(path.join(repo, 'src/job.ts'), 'export function job() {\n return 2;\n}\n');
|
|
150
|
-
git('commit', '-qam', 'fix');
|
|
151
|
-
const delta = await runReviewer(repo, 'main', config, fakes(() => JSON.stringify({ ...EMPTY, prior_points: [], resolved_previous: [{ id, evidence: 'src/job.ts:2 the lock comes first now' }] }), seen), () => undefined);
|
|
152
|
-
expect(delta.outcome).toBe('passed');
|
|
153
|
-
expect(delta.resolved).toEqual([{ item: expect.objectContaining({ id }), evidence: 'src/job.ts:2 the lock comes first now' }]);
|
|
154
|
-
});
|
|
155
|
-
it('at push, asks a model only when someone will read the push: an open, non-draft pull request', async () => {
|
|
156
|
-
const seen = seenNow();
|
|
157
|
-
const none = await runReviewer(repo, 'main', config, fakes(() => JSON.stringify(EMPTY), seen, null), () => undefined, { trigger: 'push' });
|
|
158
|
-
expect(none).toMatchObject({ outcome: 'skipped', reason: expect.stringContaining('no pull request for feature') });
|
|
159
|
-
expect(reviewerBlocks(none)).toBe(false);
|
|
160
|
-
expect(seen.prompts).toHaveLength(0);
|
|
161
|
-
const draft = await runReviewer(repo, 'main', config, fakes(() => JSON.stringify(EMPTY), seen, { ...PR, isDraft: true }), () => undefined, { trigger: 'push' });
|
|
162
|
-
expect(draft.reason).toContain('is a draft');
|
|
163
|
-
const asked = await runReviewer(repo, 'main', config, fakes(() => JSON.stringify(EMPTY), seen, null), () => undefined, { trigger: 'review' });
|
|
164
|
-
expect(asked.outcome).toBe('passed'); // on request it reviews without a pull request
|
|
165
|
-
expect(seen.prompts).toHaveLength(1);
|
|
166
|
-
});
|
|
167
|
-
it('for a backtest, reads the named pull request and hides every review and comment from the reviewed moment on', async () => {
|
|
168
|
-
const seen = seenNow();
|
|
169
|
-
const hidden = await runReviewer(repo, 'main', config, fakes(() => JSON.stringify({ ...EMPTY, prior_points: [] }), seen), () => undefined, { pr: 42, reviewsBefore: '2026-10-03', force: true });
|
|
170
|
-
expect(seen.ghArgs.find(a => a[0] === 'pr')?.slice(0, 3)).toEqual(['pr', 'view', '42']);
|
|
171
|
-
expect(seen.files['previous-reviews.md']).toBe('none\n');
|
|
172
|
-
expect(hidden.outcome).toBe('passed');
|
|
173
|
-
const shown = await runReviewer(repo, 'main', config, fakes(() => JSON.stringify({ ...EMPTY, prior_points: [] }), seen), () => undefined, { pr: 42, reviewsBefore: '2026-10-04', force: true });
|
|
174
|
-
expect(seen.files['previous-reviews.md']).toContain('Review by senior');
|
|
175
|
-
expect(shown).toMatchObject({ outcome: 'unavailable', reason: expect.stringContaining('did not report on the human reviews') });
|
|
176
|
-
});
|
|
177
|
-
it('for a backtest, gives the description as it read at the review, never a later edit', async () => {
|
|
178
|
-
const versions = { lastEditedAt: '2026-10-06T00:00:00Z', body: 'today: refunds are issued by the nightly job', userContentEdits: { totalCount: 3, nodes: [
|
|
179
|
-
{ editedAt: '2026-10-06T00:00:00Z', diff: 'today: refunds are issued by the nightly job' },
|
|
180
|
-
{ editedAt: '2026-10-02T00:00:00Z', diff: 'then: refunds are issued on request' },
|
|
181
|
-
{ editedAt: '2026-09-30T00:00:00Z', diff: 'first draft' },
|
|
182
|
-
] } };
|
|
183
|
-
const withEdits = (answer, seen, graphql) => {
|
|
184
|
-
const base = fakes(answer, seen);
|
|
185
|
-
return async (command, args, options) => command === 'gh' && args[0] === 'api' && args[1] === 'graphql'
|
|
186
|
-
? { exitCode: graphql ? 0 : 1, stdout: JSON.stringify({ data: { repository: { pullRequest: graphql } } }), stderr: '' }
|
|
187
|
-
: base(command, args, options);
|
|
188
|
-
};
|
|
189
|
-
const reply = () => JSON.stringify({ ...EMPTY, prior_points: [] });
|
|
190
|
-
const seen = seenNow();
|
|
191
|
-
await runReviewer(repo, 'main', config, withEdits(reply, seen, versions), () => undefined, { pr: 42, reviewsBefore: '2026-10-03T00:00:00Z', force: true });
|
|
192
|
-
expect(seen.files['pr-description.md']).toBe('then: refunds are issued on request');
|
|
193
|
-
await runReviewer(repo, 'main', config, withEdits(reply, seen, null), () => undefined, { pr: 42, reviewsBefore: '2026-10-03T00:00:00Z', force: true });
|
|
194
|
-
expect(seen.files['pr-description.md']).toContain('could not be recovered');
|
|
195
|
-
});
|
|
196
|
-
it('blind, reviews the commit alone and never asks GitHub', async () => {
|
|
197
|
-
const seen = seenNow();
|
|
198
|
-
const result = await runReviewer(repo, 'main', config, fakes(() => JSON.stringify({ ...EMPTY, prior_points: [] }), seen), () => undefined, { blind: true, trigger: 'backtest', force: true });
|
|
199
|
-
expect(result.outcome).toBe('passed');
|
|
200
|
-
expect(seen.ghArgs).toEqual([]);
|
|
201
|
-
expect(seen.files['previous-reviews.md']).toBe('none\n');
|
|
202
|
-
});
|
|
203
|
-
it('never passes without a verdict: a crash, a malformed answer, an unreadable pull request or no installed reviewer', async () => {
|
|
204
|
-
const crashed = await runReviewer(repo, 'main', config, fakes(() => ({ exitCode: 1, stdout: '', stderr: 'API error' }), seenNow()), () => undefined);
|
|
205
|
-
expect(crashed).toMatchObject({ outcome: 'unavailable', reason: expect.stringContaining('cursor: no answer (exit 1)'), mode: { degraded: expect.stringContaining('claude gave no verdict, cursor judged instead') } }); // asked twice, then the spare judge, which failed too
|
|
206
|
-
const prose = await runReviewer(repo, 'main', config, fakes(() => 'Looks good to me!', seenNow()), () => undefined);
|
|
207
|
-
expect(prose).toMatchObject({ outcome: 'unavailable', reason: expect.stringContaining('no valid verdict') });
|
|
208
|
-
const broken = async (command, args, options) => command === 'gh' && args[0] === 'pr' ? { exitCode: 1, stdout: '', stderr: 'HTTP 500' } : fakes(() => '', seenNow())(command, args, options);
|
|
209
|
-
expect(await runReviewer(repo, 'main', config, broken, () => undefined)).toMatchObject({ outcome: 'unavailable', reason: expect.stringContaining('could not read the pull request') });
|
|
210
|
-
const seen = { ...seenNow(), installed: [] };
|
|
211
|
-
expect(await runReviewer(repo, 'main', config, fakes(() => '', seen), () => undefined)).toMatchObject({ outcome: 'unavailable', reason: expect.stringContaining('no reviewer installed') });
|
|
212
|
-
for (const result of [crashed, prose])
|
|
213
|
-
expect(reviewerBlocks(result)).toBe(true);
|
|
214
|
-
});
|
|
215
|
-
it('keeps where each judge run spent its tokens and what it read, labelled, in the local verdict', async () => {
|
|
216
|
-
const seen = seenNow();
|
|
217
|
-
const base = fakes(() => JSON.stringify(EMPTY), seen);
|
|
218
|
-
const streaming = async (command, args, options) => {
|
|
219
|
-
if (path.basename(command).replace(/\.(cmd|exe)$/, '') !== 'claude' || args[0] === '--version')
|
|
220
|
-
return base(command, args, options); // claude.cmd on Windows
|
|
221
|
-
const prompt = args[args.indexOf('-p') + 1];
|
|
222
|
-
const diff = /(\S+full\.diff)/.exec(prompt)[1];
|
|
223
|
-
const call = (id, name, input) => ({ type: 'assistant', message: { id: `m-${id}`, usage: { input_tokens: 1, cache_read_input_tokens: 100, cache_creation_input_tokens: 10, output_tokens: 5 }, content: [{ type: 'tool_use', id, name, input }] } });
|
|
224
|
-
const events = [
|
|
225
|
-
call('1', 'Read', { file_path: diff }), call('2', 'Read', { file_path: path.join(repo, 'src/job.ts') }), call('3', 'Read', { file_path: path.join(repo, 'a.ts') }),
|
|
226
|
-
call('4', 'Bash', { command: 'git log -3' }), call('5', 'Grep', { pattern: 'job' }),
|
|
227
|
-
{ type: 'result', result: JSON.stringify(EMPTY), total_cost_usd: 0.2, usage: { input_tokens: 5, output_tokens: 25 } },
|
|
228
|
-
];
|
|
229
|
-
return { exitCode: 0, stdout: events.map(e => JSON.stringify(e)).join('\n'), stderr: '' };
|
|
230
|
-
};
|
|
231
|
-
await runReviewer(repo, 'main', config, streaming, () => undefined, { force: true });
|
|
232
|
-
const store = path.join(repo, '.git', 'rigour-reviewer');
|
|
233
|
-
const verdict = fs.readdirSync(store).filter(f => /^[0-9a-f]{40}\.[0-9a-f]{8}\.json$/.test(f)).map(f => JSON.parse(fs.readFileSync(path.join(store, f), 'utf8')))[0];
|
|
234
|
-
expect(verdict.reviewers[0].trace).toMatchObject({ turns: 5, usage: { input: 5, cacheRead: 500, cacheWrite: 50, output: 25 } });
|
|
235
|
-
expect(verdict.reviewers[0].trace.calls.map((c) => c.category)).toEqual(['rigour-input', 'changed-file', 'other-file', 'git', 'search']);
|
|
236
|
-
});
|
|
237
|
-
it('serves the repository\'s own rules to the judge with ids, and blocks on a requirement the judge shows broken', async () => {
|
|
238
|
-
fs.writeFileSync(path.join(repo, 'AGENTS.md'), '# Rules\n\n- `src/job.ts` must take the lock before its first read.\n- Prefer early returns.\n');
|
|
239
|
-
const seen = seenNow();
|
|
240
|
-
const answer = () => {
|
|
241
|
-
const id = /- \[([0-9a-f]{10})\] \(AGENTS\.md, requirement\)/.exec(seen.files['team-knowledge.md'] ?? '')?.[1];
|
|
242
|
-
return JSON.stringify({ ...EMPTY, rules: [{ id, status: 'broken', file: 'src/job.ts', line: 2, quote: 'return 1;', evidence: 'reads before any lock' }] });
|
|
243
|
-
};
|
|
244
|
-
const result = await runReviewer(repo, 'main', config, fakes(answer, seen), () => undefined, { force: true });
|
|
245
|
-
expect(seen.files['team-knowledge.md']).toContain('(AGENTS.md, requirement) `src/job.ts` must take the lock before its first read.');
|
|
246
|
-
expect(result.items.map(i => [i.class, i.file, i.line])).toEqual([['repo-rule', 'src/job.ts', 2]]);
|
|
247
|
-
expect(result.rules).toEqual({ checked: 1, followed: 0, broken: 1, notApplicable: 0 });
|
|
248
|
-
});
|
|
249
|
-
it('writes the record of the review beside the verdict, intact, and returns the same record on a cached read', async () => {
|
|
250
|
-
const seen = seenNow();
|
|
251
|
-
const answer = JSON.stringify({ ...EMPTY, findings: [{ class: 'correctness', file: 'src/job.ts', line: 2, issue: 'returns before the lock', input: 'two runs', consequence: 'two emails', quote: 'return 1;', severity: 'blocking' }] });
|
|
252
|
-
const first = await runReviewer(repo, 'main', config, fakes(() => answer, seen), () => undefined);
|
|
253
|
-
expect(first.record).toMatchObject({ scope: 'full', verified: { blocking: [expect.objectContaining({ issue: 'returns before the lock' })], should_fix: [] }, reported: { human_reviews: 1 }, judges: [expect.objectContaining({ reviewer: 'claude', cost_usd: 1.5 })] });
|
|
254
|
-
expect(first.recordPath).toMatch(/\.record\.json$/);
|
|
255
|
-
const onDisk = JSON.parse(fs.readFileSync(first.recordPath, 'utf8'));
|
|
256
|
-
expect(recordIntact(onDisk)).toBe(true);
|
|
257
|
-
const again = await runReviewer(repo, 'main', config, fakes(() => { throw new Error('a cached read never runs a judge'); }, seen), () => undefined);
|
|
258
|
-
expect(again.cached).toBe(true);
|
|
259
|
-
expect(again.record?.integrity).toBe(first.record?.integrity);
|
|
260
|
-
});
|
|
261
|
-
it('reviews through the API judge when the team configured one and its key is set, with the same prompt and accounting', async () => {
|
|
262
|
-
const seen = seenNow();
|
|
263
|
-
const calls = [];
|
|
264
|
-
const fetchImpl = (async (_url, init) => {
|
|
265
|
-
const body = JSON.parse(init.body);
|
|
266
|
-
calls.push(body);
|
|
267
|
-
const last = body.messages.at(-1);
|
|
268
|
-
const message = last.role === 'user'
|
|
269
|
-
? { role: 'assistant', content: null, tool_calls: [{ id: 't1', type: 'function', function: { name: 'read_file', arguments: JSON.stringify({ path: /(\S+full\.diff)/.exec(last.content)[1] }) } }] }
|
|
270
|
-
: { role: 'assistant', content: JSON.stringify({ ...EMPTY, findings: [{ class: 'correctness', file: 'src/job.ts', line: 2, issue: 'returns before the lock', input: 'two runs', consequence: 'two emails', quote: 'return 1;', severity: 'blocking' }] }) };
|
|
271
|
-
return new Response(JSON.stringify({ choices: [{ message }], usage: { prompt_tokens: 100, completion_tokens: 20, cost: 0.05 } }), { status: 200 });
|
|
272
|
-
});
|
|
273
|
-
const apiConfig = ConfigSchema.parse({ version: 1, review: { reviewer: { enabled: true, reviewers: ['api'], api: { url: 'https://example.test/v1', model: 'qwen3-coder', key_env: 'TEST_JUDGE_KEY' }, reasoning: { api: 'low' } } } });
|
|
274
|
-
const without = await runReviewer(repo, 'main', apiConfig, fakes(() => '', seen), () => undefined, { fetch: fetchImpl });
|
|
275
|
-
expect(without).toMatchObject({ outcome: 'unavailable', reason: expect.stringContaining('no reviewer installed') }); // the key is not set
|
|
276
|
-
process.env.TEST_JUDGE_KEY = 'secret';
|
|
277
|
-
try {
|
|
278
|
-
const result = await runReviewer(repo, 'main', apiConfig, fakes(() => '', seen), () => undefined, { fetch: fetchImpl, force: true });
|
|
279
|
-
expect(result).toMatchObject({ outcome: 'findings', reviewers: ['api'], costUsd: 0.1, items: [expect.objectContaining({ issue: 'returns before the lock', reviewer: 'api' })] });
|
|
280
|
-
expect(calls[0].reasoning_effort).toBe('low');
|
|
281
|
-
expect(calls[0].messages[1].content).toContain('full.diff'); // the same prompt a CLI judge gets
|
|
282
|
-
expect(result.record?.judges).toEqual([{ reviewer: 'api', version: 'qwen3-coder', cost_usd: 0.1, turns: 2 }]);
|
|
283
|
-
}
|
|
284
|
-
finally {
|
|
285
|
-
delete process.env.TEST_JUDGE_KEY;
|
|
286
|
-
}
|
|
287
|
-
});
|
|
288
|
-
it('replaces a judge that gives nothing with the next one installed, and says so', async () => {
|
|
289
|
-
const seen = seenNow();
|
|
290
|
-
const silent = (async () => new Response(JSON.stringify({ choices: [{ message: { role: 'assistant', content: '' }, finish_reason: 'stop' }], usage: { prompt_tokens: 5, completion_tokens: 0 } }), { status: 200 }));
|
|
291
|
-
const twoJudges = ConfigSchema.parse({ version: 1, review: { reviewer: { enabled: true, reviewers: ['api', 'claude'], api: { url: 'https://example.test/v1', model: 'silent-model', key_env: 'TEST_JUDGE_KEY' } } } });
|
|
292
|
-
process.env.TEST_JUDGE_KEY = 'secret';
|
|
293
|
-
try {
|
|
294
|
-
const result = await runReviewer(repo, 'main', twoJudges, fakes(() => JSON.stringify(EMPTY), seen), () => undefined, { fetch: silent, force: true });
|
|
295
|
-
expect(result).toMatchObject({ outcome: 'passed', reviewers: ['claude'], mode: { degraded: expect.stringContaining('api gave no verdict, claude judged instead') } });
|
|
296
|
-
expect(seen.prompts).toHaveLength(1); // claude ran once, after the api judge's two empty answers
|
|
297
|
-
}
|
|
298
|
-
finally {
|
|
299
|
-
delete process.env.TEST_JUDGE_KEY;
|
|
300
|
-
}
|
|
301
|
-
});
|
|
302
|
-
it('asks a judge once more after an answer that is not a verdict, and is unavailable only when the second is not one either', async () => {
|
|
303
|
-
const seen = seenNow();
|
|
304
|
-
let calls = 0;
|
|
305
|
-
const slipOnce = await runReviewer(repo, 'main', config, fakes(() => (++calls === 1 ? '{"prior_points":[], "findings":[{"class"' : JSON.stringify(EMPTY)), seen), () => undefined, { force: true });
|
|
306
|
-
expect(slipOnce.outcome).toBe('passed');
|
|
307
|
-
expect(seen.prompts).toHaveLength(2);
|
|
308
|
-
let crashes = 0;
|
|
309
|
-
const crashOnce = seenNow();
|
|
310
|
-
const recovered = await runReviewer(repo, 'main', config, fakes(() => (++crashes === 1 ? { exitCode: 1, stdout: '', stderr: 'API error' } : JSON.stringify(EMPTY)), crashOnce), () => undefined, { force: true });
|
|
311
|
-
expect(recovered.outcome).toBe('passed'); // a run that died is asked once more too
|
|
312
|
-
const twice = seenNow();
|
|
313
|
-
const slipTwice = await runReviewer(repo, 'main', config, fakes(() => 'not json', twice), () => undefined, { force: true });
|
|
314
|
-
expect(slipTwice).toMatchObject({ outcome: 'unavailable', reason: expect.stringContaining('no valid verdict') });
|
|
315
|
-
expect(twice.prompts).toHaveLength(3); // once more, then the spare judge once: never a loop
|
|
316
|
-
});
|
|
317
|
-
it('says what was asked and that nothing ran when a review ends early, with why a judge is missing', async () => {
|
|
318
|
-
const seen = { ...seenNow(), installed: ['claude'] }; // cursor is listed but not installed
|
|
319
|
-
const unreadable = async (command, args, options) => command === 'gh' && args[0] === 'pr' ? { exitCode: 1, stdout: '', stderr: 'gh auth login required' } : fakes(() => '', seen)(command, args, options);
|
|
320
|
-
const result = await runReviewer(repo, 'main', config, unreadable, () => undefined, { choice: { mode: 'full', panel: true } });
|
|
321
|
-
expect(result).toMatchObject({ outcome: 'unavailable', reviewers: ['claude'] });
|
|
322
|
-
expect(result.mode).toMatchObject({ asked: 'panel', ran: 'none', source: 'flag', degraded: expect.stringContaining('not installed: cursor-agent') });
|
|
323
|
-
const early = await runReviewer(repo, 'main', config, fakes(() => '', { ...seenNow(), installed: [] }), () => undefined, { choice: { mode: 'full', panel: true } });
|
|
324
|
-
expect(early).toMatchObject({ outcome: 'unavailable', mode: { asked: 'panel', ran: 'none', source: 'flag' } }); // before any judge was found
|
|
325
|
-
});
|
|
326
|
-
it('in full mode runs two vendors and resolves a human point only when both say so', async () => {
|
|
327
|
-
const seen = seenNow();
|
|
328
|
-
const by = (name) => JSON.stringify({ ...EMPTY, prior_points: [{ point: 'lock before read', severity: 'blocking', resolved: name === 'claude', evidence: 'src/job.ts:2', file: 'src/job.ts', line: 2, quote: 'export function job() {' }], findings: name === 'cursor' ? [{ class: 'dead-code', file: 'a.ts', line: 1, issue: 'a is unused', consequence: 'a second run reads stale rows', quote: 'export const a = 1;' }] : [] });
|
|
329
|
-
const result = await runReviewer(repo, 'main', config, fakes(by, seen), () => undefined, { full: true });
|
|
330
|
-
expect(result.reviewers).toEqual(['claude', 'cursor']);
|
|
331
|
-
expect(result.items.map(i => [i.kind, i.reviewer])).toEqual([['prior', 'cursor']]);
|
|
332
|
-
expect(result.notes.map(i => [i.kind, i.file, i.reviewer])).toEqual([['finding', 'a.ts', 'cursor']]); // a.ts is not in the change: what the code already had
|
|
333
|
-
expect(seen.prompts).toHaveLength(2);
|
|
334
|
-
});
|
|
335
|
-
});
|
|
336
|
-
describe('choosing reviewers', () => {
|
|
337
|
-
it('runs the newest installed copy of a CLI, not the first on PATH', async () => {
|
|
338
|
-
const seen = seenNow();
|
|
339
|
-
const older = installFake(bins[0], 'claude');
|
|
340
|
-
const newer = installFake(bins[1], 'claude');
|
|
341
|
-
seen.versions = { [older]: '2.0.34 (Claude Code)', [newer]: '2.1.289 (Claude Code)' };
|
|
342
|
-
const result = await runReviewer(repo, 'main', config, fakes(() => JSON.stringify(EMPTY), seen), () => undefined);
|
|
343
|
-
expect(result.outcome).toBe('passed');
|
|
344
|
-
expect(seen.ran).toEqual([newer]);
|
|
345
|
-
});
|
|
346
|
-
it('reads the vendors on the trailers, prefers another one in cross mode, and pairs two vendors in full mode', () => {
|
|
347
|
-
const authors = vendorsOf('Claude Opus <noreply@example.com>\n');
|
|
348
|
-
expect([...authors]).toEqual(['anthropic']);
|
|
349
|
-
const installed = new Set(['claude', 'cursor', 'codex']);
|
|
350
|
-
expect(selectReviewers(['claude', 'cursor', 'codex'], 'single', authors, installed)).toEqual(['claude']);
|
|
351
|
-
expect(selectReviewers(['claude', 'cursor', 'codex'], 'cross', authors, installed)).toEqual(['cursor']);
|
|
352
|
-
expect(selectReviewers(['claude', 'cursor', 'codex'], 'full', authors, installed)).toEqual(['cursor', 'claude']);
|
|
353
|
-
expect(selectReviewers(['claude', 'cursor'], 'cross', authors, new Set(['claude']))).toEqual(['claude']); // falls back to what is installed
|
|
354
|
-
expect(selectReviewers(['claude'], 'full', new Set(), new Set())).toEqual([]);
|
|
355
|
-
});
|
|
356
|
-
});
|
|
357
|
-
describe('verdicts', () => {
|
|
358
|
-
it('finds the verdict when the reviewer wraps it in a summary or a code fence, and refuses prose', () => {
|
|
359
|
-
const verdict = JSON.stringify(EMPTY);
|
|
360
|
-
for (const text of [`## Summary\nAll checked.\n\n\`\`\`json\n${verdict}\n\`\`\`\nDone.`, `Notes first.\n${verdict}\nThat is all {see above}.`]) {
|
|
361
|
-
expect(parseVerdict(text, true, 'claude', {})).toMatchObject({ verdict: { prior_points: [{ point: 'lock before read' }], reviewer: 'claude' } });
|
|
362
|
-
}
|
|
363
|
-
expect(parseVerdict('Looks good to me!', false, 'claude', {})).toMatchObject({ error: expect.stringContaining('no valid verdict') });
|
|
364
|
-
expect(parseVerdict(JSON.stringify({ ...EMPTY, prior_points: [] }), true, 'claude', {})).toEqual({ error: 'claude did not report on the human reviews' });
|
|
365
|
-
});
|
|
366
|
-
it('keeps the journey, sibling parity and claims as working notes, never blocks, and reads a verdict cached before they existed', () => {
|
|
367
|
-
const verdict = {
|
|
368
|
-
...EMPTY,
|
|
369
|
-
journey: [
|
|
370
|
-
{ file: 'src/w.ts', line: 4, what: 'stamps sent_at', cleared_by: null, retry_safe: false, overlap_safe: true, can_move_back: true, keys: [{ name: 'dedupeKey', inputs: 'updated_at', stable_under_edit: false }] },
|
|
371
|
-
{ file: 'src/w.ts', line: 9, what: 'caches the token', cleared_by: 'ttl', retry_safe: true, overlap_safe: true, can_move_back: null },
|
|
372
|
-
],
|
|
373
|
-
siblings: [
|
|
374
|
-
{ changed: 'src/a/run.ts:3', sibling: 'src/b/run.ts:7', needs_same_change: true, has_it: false, why: 'same lock' },
|
|
375
|
-
{ changed: 'src/a/run.ts:3', sibling: 'src/c/run.ts:2', needs_same_change: true, has_it: true },
|
|
376
|
-
{ changed: 'src/a/run.ts:3', sibling: 'src/d/run.ts:2', needs_same_change: false, has_it: false },
|
|
377
|
-
],
|
|
378
|
-
claims: [
|
|
379
|
-
{ source: 'description', claim: 'at worst one email', file: 'src/w.ts', line: 12, holds: false, evidence: 'loop sends per row' },
|
|
380
|
-
{ source: 'comment', claim: 'runs daily', file: 'src/cron.ts', line: 1, holds: true },
|
|
381
|
-
],
|
|
382
|
-
};
|
|
383
|
-
const { open, notes } = account(verdict, undefined, () => true);
|
|
384
|
-
expect(open).toEqual([]);
|
|
385
|
-
expect(notes.map(i => `${i.kind} ${i.class} ${i.file}:${i.line}`)).toEqual([
|
|
386
|
-
'journey correctness src/w.ts:4', 'journey correctness src/w.ts:4', 'journey correctness src/w.ts:4',
|
|
387
|
-
'sibling correctness src/b/run.ts:7', 'claim stale-claim src/w.ts:12',
|
|
388
|
-
]);
|
|
389
|
-
expect(notes[3].issue).toContain('needs the same change as src/a/run.ts:3: same lock');
|
|
390
|
-
const cached = { ...EMPTY };
|
|
391
|
-
delete cached.journey;
|
|
392
|
-
delete cached.siblings;
|
|
393
|
-
delete cached.claims;
|
|
394
|
-
expect(account(cached, undefined, () => true).open).toEqual([]);
|
|
395
|
-
expect(mergeVerdicts([cached, { ...verdict, reviewer: 'codex' }]).claims).toHaveLength(2);
|
|
396
|
-
});
|
|
397
|
-
it('blocks on a finding only when the code it quotes is at the line it names, and never carries an old working note as a block', () => {
|
|
398
|
-
const verify = checkoutVerifier(repo);
|
|
399
|
-
const at = (quote, line = 2) => account({ ...EMPTY, prior_points: [], findings: [{ class: 'correctness', file: 'src/job.ts', line, issue: 'returns before the lock', input: 'two runs at once', consequence: 'two emails', ...(quote === undefined ? {} : { quote }) }] }, undefined, verify);
|
|
400
|
-
expect(at(' return 1;').open).toHaveLength(1);
|
|
401
|
-
expect(at('return 1;').open).toHaveLength(1); // whitespace aside
|
|
402
|
-
expect(at('return 1;', 9)).toMatchObject({ open: [], unverified: [expect.objectContaining({ issue: 'returns before the lock' })] }); // past the end
|
|
403
|
-
expect(at('return 99;')).toMatchObject({ open: [], unverified: [expect.anything()] }); // not in the file
|
|
404
|
-
expect(at()).toMatchObject({ open: [], unverified: [expect.anything()] }); // no quote at all
|
|
405
|
-
const old = { id: 'r1', kind: 'read', class: 'production-cost', file: 'src/q.ts', line: 3, issue: 'window not bounded' };
|
|
406
|
-
const carried = account({ ...EMPTY, prior_points: [] }, [old], verify);
|
|
407
|
-
expect(carried).toMatchObject({ open: [], notes: [expect.objectContaining({ id: 'r1' })] });
|
|
408
|
-
});
|
|
409
|
-
it('shows a team lesson the change repeats as a note, never a block on its own', () => {
|
|
410
|
-
const verdict = { ...EMPTY, lessons: [
|
|
411
|
-
{ lesson: 'Regenerate the API client after changing the schema.', applies: true, file: 'src/schema.ts', line: 3, evidence: 'schema.ts changed, client not' },
|
|
412
|
-
{ lesson: 'Paginate with keyset.', applies: false, file: 'src/scan.ts', line: 9 },
|
|
413
|
-
] };
|
|
414
|
-
const { open, notes } = account(verdict, undefined, () => true);
|
|
415
|
-
expect(open).toEqual([]);
|
|
416
|
-
expect(notes.map(n => [n.kind, n.class, n.issue])).toEqual([['lesson', 'team-lesson', 'repeats a team lesson: Regenerate the API client after changing the schema.']]);
|
|
417
|
-
});
|
|
418
|
-
it('blocks only on what it can show: a quoted open human point, a blocking finding, never a missing thing that is there or a point a human accepted', () => {
|
|
419
|
-
const verify = checkoutVerifier(repo);
|
|
420
|
-
const decide = (v) => account({ ...EMPTY, prior_points: [], ...v }, undefined, verify);
|
|
421
|
-
const open = { point: 'take the lock before the first read', severity: 'blocking', resolved: false };
|
|
422
|
-
expect(decide({ prior_points: [{ ...open, file: 'src/job.ts', line: 2, quote: 'return 1;' }] }).open).toHaveLength(1);
|
|
423
|
-
expect(decide({ prior_points: [open] })).toMatchObject({ open: [], unverified: [expect.objectContaining({ kind: 'prior' })] }); // a later commit may have done it: no quote, no block
|
|
424
|
-
const finding = { class: 'correctness', file: 'src/job.ts', line: 2, issue: 'job never closes the connection', input: 'every run', consequence: 'one connection leaks per run', quote: 'return 1;' };
|
|
425
|
-
expect(decide({ findings: [{ ...finding, absent: 'return 1' }] })).toMatchObject({ open: [], unverified: [expect.anything()] }); // "missing", but the file has it
|
|
426
|
-
expect(decide({ findings: [{ ...finding, absent: 'conn.close(' }] }).open).toHaveLength(1);
|
|
427
|
-
expect(decide({ findings: [{ ...finding, severity: 'should' }] })).toMatchObject({ open: [], advisory: [expect.anything()], unverified: [] }); // a verified should-fix: shown
|
|
428
|
-
expect(decide({ findings: [{ ...finding, severity: 'should', quote: 'return 99;' }] })).toMatchObject({ open: [], advisory: [], unverified: [expect.anything()] }); // a should-fix it cannot show: not a claim worth time
|
|
429
|
-
const accepted = { point: 'job never closes the connection after the read', severity: 'non-blocking', resolved: false };
|
|
430
|
-
expect(decide({ prior_points: [accepted], findings: [finding] })).toMatchObject({ open: [], advisory: [expect.anything()] }); // a human raised it and accepted it
|
|
431
|
-
});
|
|
432
|
-
it('blocks on a broken requirement rule only with its quote, shows broken guidance, and takes the rule\'s words from what Rigour served', () => {
|
|
433
|
-
const verify = checkoutVerifier(repo);
|
|
434
|
-
const served = [
|
|
435
|
-
{ id: 'r1', source: 'AGENTS.md', text: 'Every job must take the lock before its first read.', requirement: true },
|
|
436
|
-
{ id: 'r2', source: 'AGENTS.md', text: 'Prefer small functions.', requirement: false },
|
|
437
|
-
];
|
|
438
|
-
const judged = (answers) => {
|
|
439
|
-
const verdict = { ...EMPTY, prior_points: [], rules: answers };
|
|
440
|
-
attachServedRules(verdict, served);
|
|
441
|
-
return { verdict, ...account(verdict, undefined, verify) };
|
|
442
|
-
};
|
|
443
|
-
const broken = judged([{ id: 'r1', status: 'broken', file: 'src/job.ts', line: 2, quote: 'return 1;', evidence: 'no lock before the read' }]);
|
|
444
|
-
expect(broken.open.map(i => [i.kind, i.class, i.issue, i.evidence])).toEqual([['rule', 'repo-rule', 'Every job must take the lock before its first read.', 'breaks a rule this repository wrote for itself (AGENTS.md): no lock before the read']]);
|
|
445
|
-
expect(judged([{ id: 'r1', status: 'broken', file: 'src/job.ts', line: 2 }])).toMatchObject({ open: [], unverified: [expect.objectContaining({ kind: 'rule' })] }); // no quote: not shown as a block
|
|
446
|
-
expect(judged([{ id: 'r2', status: 'broken', file: 'src/job.ts', line: 2, quote: 'return 1;' }])).toMatchObject({ open: [], advisory: [expect.objectContaining({ class: 'repo-rule' })] }); // guidance: shown, never a block
|
|
447
|
-
expect(judged([{ id: 'r1', status: 'followed' }, { id: 'r1', status: 'not-applicable' }])).toMatchObject({ open: [], notes: [], advisory: [], unverified: [] });
|
|
448
|
-
const unknown = judged([{ id: 'made-up', status: 'broken', file: 'src/job.ts', line: 2, quote: 'return 1;', rule: 'a rule the judge invented', requirement: true }]);
|
|
449
|
-
expect(unknown.verdict.rules).toEqual([]); // an answer naming no served rule is dropped, whatever it claims
|
|
450
|
-
expect(unknown.open).toEqual([]);
|
|
451
|
-
});
|
|
452
|
-
it("shows the same point found in several places as one item with every location; a judge item on a human point's lines folds into it, and human points never merge", () => {
|
|
453
|
-
const scan = (file, line, quote) => ({ class: 'production-cost', file, line, issue: `the ${file.split('/').pop()} scan has no upper bound on updated_at`, input: 'a week of rows', consequence: 'rows read grow with time', quote });
|
|
454
|
-
const verdict = { ...EMPTY, prior_points: [
|
|
455
|
-
{ point: 'the scan has no upper bound on updated_at', severity: 'blocking', resolved: false, file: 'src/job.ts', line: 1, quote: 'export function job() {' },
|
|
456
|
-
{ point: 'bound the window again', severity: 'blocking', resolved: false, file: 'src/job.ts', line: 1, quote: 'export function job() {' },
|
|
457
|
-
], findings: [scan('src/job.ts', 1, 'export function job() {'), scan('a.ts', 1, 'export const a = 1;'), { ...scan('src/job.ts', 2, 'return 1;'), class: 'correctness' }] };
|
|
458
|
-
const { open } = account(verdict, undefined, checkoutVerifier(repo));
|
|
459
|
-
expect(open.map(i => [i.kind, i.class, i.locations ?? []])).toEqual([
|
|
460
|
-
// The judge's scan on the human's lines, in like words, is the human's point found again, and so is the same point said as another class on the next line.
|
|
461
|
-
// The same scan in another file is not on the human's lines: its own item.
|
|
462
|
-
['prior', 'prior point', [{ file: 'src/job.ts', line: 1 }, { file: 'src/job.ts', line: 2 }]], ['prior', 'prior point', []], ['finding', 'production-cost', []],
|
|
463
|
-
]);
|
|
464
|
-
// On other lines than any human point: the same point in another file, and said as another class on the next line, is one item, every place.
|
|
465
|
-
const apart = account({ ...verdict, prior_points: [] }, undefined, checkoutVerifier(repo));
|
|
466
|
-
expect(apart.open.map(i => [i.kind, i.class, i.locations ?? []])).toEqual([['finding', 'production-cost', [{ file: 'a.ts', line: 1 }, { file: 'src/job.ts', line: 2 }]]]);
|
|
467
|
-
// On a human point's lines: a rule break in like words is that point found again; a different rule, or a finding in other
|
|
468
|
-
// words, stays its own item, so fixing the human's point does not leave it for the next round.
|
|
469
|
-
const human = { point: 'null guards on columns the query makes non-null are dead fallbacks', severity: 'blocking', resolved: false, file: 'src/job.ts', line: 2, quote: 'return 1;' };
|
|
470
|
-
const onHuman = account({ ...EMPTY, prior_points: [human],
|
|
471
|
-
findings: [{ class: 'correctness', file: 'src/job.ts', line: 3, issue: 'the retry re-sends the email', input: 'a timeout', consequence: 'two emails', quote: '}' }],
|
|
472
|
-
rules: [
|
|
473
|
-
{ id: 'r1', status: 'broken', file: 'src/job.ts', line: 2, quote: 'return 1;', rule: 'No dead fallbacks or null guards on non-null columns.', source: 'AGENTS.md', requirement: true },
|
|
474
|
-
{ id: 'r2', status: 'broken', file: 'src/job.ts', line: 3, quote: '}', rule: 'Import the JOBS_TABLE constant; do not inline the raw table name.', source: 'AGENTS.md', requirement: true },
|
|
475
|
-
] }, undefined, checkoutVerifier(repo));
|
|
476
|
-
expect(onHuman.open.map(i => [i.kind, i.locations ?? []])).toEqual([['prior', [{ file: 'src/job.ts', line: 2 }]], ['rule', []], ['finding', []]]);
|
|
477
|
-
expect(onHuman.open[1].issue).toContain('JOBS_TABLE');
|
|
478
|
-
// A rule break and the finding it caused, on the same lines and in like words, are one item.
|
|
479
|
-
const twice = account({ ...EMPTY, prior_points: [], findings: [{ class: 'correctness', file: 'src/job.ts', line: 2, issue: 'the raw table name is inlined instead of the JOBS_TABLE constant', input: 'any run', consequence: 'a rename misses it', quote: 'return 1;' }],
|
|
480
|
-
rules: [{ id: 'r', status: 'broken', file: 'src/job.ts', line: 2, quote: 'return 1;', rule: 'Import the JOBS_TABLE constant; do not inline the raw table name again.', source: 'AGENTS.md', requirement: true }] }, undefined, checkoutVerifier(repo));
|
|
481
|
-
expect(twice.open.map(i => i.class)).toEqual(['repo-rule']);
|
|
482
|
-
expect(twice.open[0].locations).toEqual([{ file: 'src/job.ts', line: 2 }]);
|
|
483
|
-
});
|
|
484
|
-
it('keeps reads, scans, redundancy and merge impact as notes with stable ids, and answers non-blocking points in the reply', () => {
|
|
485
|
-
const verdict = {
|
|
486
|
-
...EMPTY,
|
|
487
|
-
prior_points: [{ point: 'nit: rename', severity: 'non-blocking', resolved: false }],
|
|
488
|
-
reads: [{ file: 'src/q.ts', line: 10, read: 'select attempts', rules: [{ rule: 'flag off', known_before_read: true, applied_before_read: false }], narrower_source: 'attempt.updated_at', keys: [{ name: 'eventId', inputs: 'answered_at', stable_under_edit: false }], window_bounded: false, keyset: null }],
|
|
489
|
-
scans: [{ file: 'src/s.ts', line: 3, function: 'later', outer: 'attempts', inner: 'answers', fix: 'index by attempt' }],
|
|
490
|
-
redundant: [{ file: 'src/r.ts', line: 5, what: 'null guard', made_redundant_by: 'src/r.ts:2', removed: false }, { file: 'src/r.ts', line: 9, what: 'removed guard', removed: true }],
|
|
491
|
-
merge_impact: [{ symbol: 'parseId', main_file: 'src/id.ts', call_site: 'src/link.ts:4', holds: false, why: 'band dropped' }],
|
|
492
|
-
};
|
|
493
|
-
const { open, notes, answerInReply } = account(verdict, undefined, () => true);
|
|
494
|
-
expect(open).toEqual([]);
|
|
495
|
-
expect(notes.map(i => `${i.class} ${i.file}:${i.line}`)).toEqual([
|
|
496
|
-
'dead-code src/r.ts:5', 'production-cost src/q.ts:10', 'production-cost src/q.ts:10', 'production-cost src/q.ts:10', 'correctness src/q.ts:10', 'production-cost src/s.ts:3', 'correctness src/link.ts:4',
|
|
497
|
-
]);
|
|
498
|
-
expect(new Set(notes.map(i => i.id)).size).toBe(notes.length);
|
|
499
|
-
expect(account(verdict, undefined, () => true).notes.map(i => i.id)).toEqual(notes.map(i => i.id)); // stable
|
|
500
|
-
expect(answerInReply).toEqual([expect.objectContaining({ point: 'nit: rename' })]);
|
|
501
|
-
});
|
|
502
|
-
it('merges two verdicts: a point is resolved only when every reviewer resolves it', () => {
|
|
503
|
-
const a = { ...EMPTY, reviewer: 'claude', prior_points: [{ point: 'Lock before read', severity: 'blocking', resolved: true }], resolved_previous: [{ id: 'x', evidence: 'e' }, { id: 'y', evidence: 'e' }] };
|
|
504
|
-
const b = { ...EMPTY, reviewer: 'cursor', prior_points: [{ point: 'lock before read.', severity: 'blocking', resolved: false }], resolved_previous: [{ id: 'x', evidence: 'e' }] };
|
|
505
|
-
const merged = mergeVerdicts([a, b]);
|
|
506
|
-
expect(merged.prior_points).toEqual([expect.objectContaining({ resolved: false, reviewer: 'cursor' })]);
|
|
507
|
-
expect(merged.resolved_previous.map(r => r.id)).toEqual(['x']);
|
|
508
|
-
expect(merged.reviewers?.map(r => r.reviewer)).toEqual(['claude', 'cursor']);
|
|
509
|
-
});
|
|
510
|
-
it('carries a resolved human point into a delta verdict unless the new commits touch its evidence', () => {
|
|
511
|
-
const previous = { ...EMPTY, prior_points: [{ point: 'lock before read', resolved: true, evidence: 'src/job.ts:2' }, { point: 'rename it', resolved: true, evidence: 'src/name.ts:9' }] };
|
|
512
|
-
const fresh = { ...EMPTY, prior_points: [] };
|
|
513
|
-
expect(carryResolved(fresh, previous, new Set(['src/name.ts'])).prior_points.map(p => p.point)).toEqual(['lock before read']);
|
|
514
|
-
expect(carryResolved({ ...EMPTY, prior_points: [{ point: 'Lock before read', resolved: false }] }, previous, new Set()).prior_points).toHaveLength(2);
|
|
515
|
-
});
|
|
516
|
-
});
|
|
517
|
-
describe('a panel of judges', () => {
|
|
518
|
-
const panelConfig = (reviewer) => ConfigSchema.parse({ version: 1, review: { github_account: 'reviewer-account', reviewer: { enabled: true, reviewers: ['claude', 'cursor', 'codex'], mode: 'full', panel: 'on', ...reviewer } } });
|
|
519
|
-
const LOCK = { class: 'correctness', file: 'src/job.ts', line: 2, issue: 'returns before the lock is taken', quote: 'export function job() {', consequence: 'two runs send the same email' };
|
|
520
|
-
const LONE = { class: 'dead-code', file: 'src/job.ts', line: 1, issue: 'job is exported and never called', quote: 'export function job() {', consequence: 'a reader treats it as the contract' };
|
|
521
|
-
const OPINION = { class: 'duplication', file: 'src/job.ts', line: 2, issue: 'could be one line shorter', quote: 'export function job() {', consequence: '' };
|
|
522
|
-
it("keeps Rigour's own key from every judge, and a key the team names from that judge, in reviews and cross-examinations alike", async () => {
|
|
523
|
-
installFake(bins[0], 'codex');
|
|
524
|
-
const seen = { ...seenNow(), installed: ['claude', 'cursor-agent', 'codex'] };
|
|
525
|
-
const reply = (name) => {
|
|
526
|
-
const prompt = seen.prompts.at(-1) ?? '';
|
|
527
|
-
if (prompt.includes('The other\nreviewer raised')) {
|
|
528
|
-
const ids = [...prompt.matchAll(/"id": "([0-9a-f]+)"/g)].map(m => m[1]);
|
|
529
|
-
return JSON.stringify({ answers: ids.map(id => ({ id, call: 'refute', evidence: `src/job.ts:1 ${name}: job is imported by the runner` })) });
|
|
530
|
-
}
|
|
531
|
-
return JSON.stringify({ ...EMPTY, findings: name === 'codex' ? [LONE] : [] });
|
|
532
|
-
};
|
|
533
|
-
await runReviewer(repo, 'main', panelConfig({ judges: 3, judge_env: { codex: { unset: ['OPENAI_API_KEY'] } } }), fakes(reply, seen), () => undefined);
|
|
534
|
-
const runs = seen.ran.map((command, i) => ({ judge: path.basename(command).replace(/\.(cmd|exe)$/, ''), unset: seen.unset?.[i] ?? [] }));
|
|
535
|
-
expect(runs.length).toBe(5); // three reviews and two cross-examinations
|
|
536
|
-
for (const run of runs)
|
|
537
|
-
expect(run.unset).toContain('RIGOUR_API_KEY');
|
|
538
|
-
expect(runs.filter(r => r.unset.includes('OPENAI_API_KEY')).map(r => r.judge)).toEqual(['codex']);
|
|
539
|
-
});
|
|
540
|
-
it('confirms what a majority raised, drops what the others refute with evidence, and never blocks on an opinion', async () => {
|
|
541
|
-
installFake(bins[0], 'codex');
|
|
542
|
-
const seen = { ...seenNow(), installed: ['claude', 'cursor-agent', 'codex'] };
|
|
543
|
-
const reply = (name) => {
|
|
544
|
-
const prompt = seen.prompts.at(-1) ?? '';
|
|
545
|
-
if (prompt.includes('The other\nreviewer raised')) {
|
|
546
|
-
const ids = [...prompt.matchAll(/"id": "([0-9a-f]+)"/g)].map(m => m[1]);
|
|
547
|
-
return JSON.stringify({ answers: ids.map(id => ({ id, call: 'refute', evidence: `src/job.ts:1 ${name}: job is imported by the runner` })) });
|
|
548
|
-
}
|
|
549
|
-
if (name === 'codex')
|
|
550
|
-
return JSON.stringify({ ...EMPTY, findings: [LONE] });
|
|
551
|
-
return JSON.stringify({ ...EMPTY, findings: name === 'claude' ? [LOCK, OPINION] : [{ ...LOCK, line: 3, issue: 'the lock is taken only after it returns' }] });
|
|
552
|
-
};
|
|
553
|
-
const result = await runReviewer(repo, 'main', panelConfig({ judges: 3, cross_models: { claude: 'claude-haiku-4-5' } }), fakes(reply, seen), () => undefined);
|
|
554
|
-
expect(result.reviewers).toEqual(['claude', 'cursor', 'codex']);
|
|
555
|
-
expect(result.mode).toMatchObject({ asked: 'panel', ran: 'panel', source: 'team' });
|
|
556
|
-
expect(result.items.map(i => [i.issue, i.reviewer])).toEqual([[LOCK.issue, 'claude+cursor']]);
|
|
557
|
-
expect(result.dropped.map(i => i.issue)).toEqual([LONE.issue]);
|
|
558
|
-
expect(result.notes.map(i => i.issue)).toEqual([OPINION.issue]);
|
|
559
|
-
expect(seen.prompts).toHaveLength(5); // three blind reviews, then claude and cursor each cross-examine codex's lone finding once
|
|
560
|
-
expect(result.panel?.find(d => d.item.issue === LONE.issue)).toMatchObject({ judges: ['codex'], calls: { codex: 'raised', claude: 'refute', cursor: 'refute' }, status: 'dropped' });
|
|
561
|
-
expect(result.costUsd).toBe(3); // claude's review and claude's cross-examination
|
|
562
|
-
const claudeCalls = (seen.args ?? []).filter((_, i) => path.basename(seen.ran[i]).startsWith('claude'));
|
|
563
|
-
expect(claudeCalls.map(a => a.includes('claude-haiku-4-5'))).toEqual([false, true]); // the cheaper model for the cross-examination only
|
|
564
|
-
});
|
|
565
|
-
it('runs one judge and says why when only one vendor is installed, and is unavailable when the team requires the panel', async () => {
|
|
566
|
-
const seen = seenNow();
|
|
567
|
-
const reply = () => JSON.stringify({ ...EMPTY, findings: [LOCK] });
|
|
568
|
-
const fallback = await runReviewer(repo, 'main', panelConfig({ reviewers: ['claude', 'codex'] }), fakes(reply, seen), () => undefined);
|
|
569
|
-
expect(fallback.mode).toMatchObject({ asked: 'panel', ran: 'single', degraded: expect.stringContaining('not installed: codex') });
|
|
570
|
-
expect(fallback.items.map(i => i.issue)).toEqual([LOCK.issue]);
|
|
571
|
-
const required = await runReviewer(repo, 'main', panelConfig({ reviewers: ['claude', 'codex'], panel: 'required' }), fakes(reply, seenNow()), () => undefined);
|
|
572
|
-
expect(required.outcome).toBe('unavailable');
|
|
573
|
-
expect(required.reason).toContain('rigour.yml requires two reviewers');
|
|
574
|
-
});
|
|
575
|
-
it('keeps to the daily caps: a review past the run cap is skipped, or unavailable when the team requires the reviewer', async () => {
|
|
576
|
-
const reply = () => JSON.stringify({ ...EMPTY, findings: [LOCK] });
|
|
577
|
-
const skipped = await runReviewer(repo, 'main', panelConfig({ max_runs_per_day: 1 }), fakes(reply, seenNow()), () => undefined);
|
|
578
|
-
expect(skipped.outcome).toBe('skipped');
|
|
579
|
-
expect(skipped.reason).toContain('the daily run cap is reached: 0 of 1 agent runs used today in this repository, and this needs 2 more');
|
|
580
|
-
const required = await runReviewer(repo, 'main', panelConfig({ max_runs_per_day: 1, panel: 'required' }), fakes(reply, seenNow()), () => undefined);
|
|
581
|
-
expect(required.outcome).toBe('unavailable');
|
|
582
|
-
});
|
|
583
|
-
it('counts every run, stops new reviews at the cost cap, and leaves a cross-examination past the run cap disputed', async () => {
|
|
584
|
-
const seen = seenNow();
|
|
585
|
-
const lone = { class: 'dead-code', file: 'src/job.ts', line: 1, issue: 'job is exported and never called', quote: 'export function job() {', consequence: 'a reader treats it as the contract' };
|
|
586
|
-
const reply = (name) => JSON.stringify({ ...EMPTY, findings: name === 'claude' ? [LOCK] : [{ ...LOCK, line: 3, issue: 'the lock is taken only after it returns' }, lone] });
|
|
587
|
-
// Two judges fit in a cap of 2; the cross-examination of cursor's lone finding would be a third run.
|
|
588
|
-
const result = await runReviewer(repo, 'main', panelConfig({ max_runs_per_day: 2 }), fakes(reply, seen), () => undefined);
|
|
589
|
-
expect(seen.prompts).toHaveLength(2);
|
|
590
|
-
expect(result.items.map(i => i.issue)).toEqual([LOCK.issue]);
|
|
591
|
-
expect(result.panel?.find(d => d.item.issue === lone.issue)).toMatchObject({ status: 'disputed', note: expect.stringContaining('the daily run cap is reached') });
|
|
592
|
-
// claude reported $1.50: a cost cap of $1 lets no new review start today.
|
|
593
|
-
const capped = await runReviewer(repo, 'main', panelConfig({ max_usd_per_day: 1 }), fakes(reply, seenNow()), () => undefined, { force: true });
|
|
594
|
-
expect(capped).toMatchObject({ outcome: 'skipped', reason: expect.stringContaining('the daily cost cap is reached: $1.50 of $1.00') });
|
|
595
|
-
});
|
|
596
|
-
it('escalates on risk: one judge for a change with no risky function and no human review', async () => {
|
|
597
|
-
const seen = seenNow();
|
|
598
|
-
const result = await runReviewer(repo, 'main', panelConfig({ escalate: 'risk' }), fakes(() => JSON.stringify(EMPTY), seen, null), () => undefined);
|
|
599
|
-
expect(result.mode).toMatchObject({ asked: 'panel', ran: 'single', escalation: expect.stringContaining('no risky changed function') });
|
|
600
|
-
expect(seen.prompts).toHaveLength(1);
|
|
601
|
-
const full = await runReviewer(repo, 'main', panelConfig({ escalate: 'risk' }), fakes(() => JSON.stringify(EMPTY), seenNow(), null), () => undefined, { full: true, force: true });
|
|
602
|
-
expect(full.mode?.ran).toBe('panel'); // the --full hard stop always gets every judge
|
|
603
|
-
});
|
|
604
|
-
});
|
|
605
|
-
describe('what the team already knows', () => {
|
|
606
|
-
const allowing = ConfigSchema.parse({ version: 1, review: { github_account: 'reviewer-account', reviewer: { enabled: true, reviewers: ['claude', 'cursor'], dismissals: true } } });
|
|
607
|
-
it('refuses a dismissal unless the team allows them: fix the code, or the reviewer', async () => {
|
|
608
|
-
const first = await runReviewer(repo, 'main', allowing, fakes(() => JSON.stringify({ ...EMPTY, findings: [{ class: 'correctness', file: 'src/job.ts', line: 2, issue: 'returns before the lock is taken', quote: 'export function job() {', consequence: 'two runs send the same email' }] }), seenNow()), () => undefined);
|
|
609
|
-
expect((await dismissReviewerFinding(repo, first.items[0].id, 'the runner holds a lock', false)).error).toContain('this team does not dismiss reviewer findings');
|
|
610
|
-
expect(fs.existsSync(path.join(repo, '.rigour/dismissed-review-items.json'))).toBe(false);
|
|
611
|
-
});
|
|
612
|
-
it('a dismissed finding reaches the next judge as settled, and a re-worded repeat never blocks', async () => {
|
|
613
|
-
fs.mkdirSync(path.join(repo, 'docs'));
|
|
614
|
-
fs.writeFileSync(path.join(repo, 'docs/jobs.md'), 'The job runner (src/job.ts) takes the lock first.\n');
|
|
615
|
-
git('add', '-A');
|
|
616
|
-
git('commit', '-qm', 'docs');
|
|
617
|
-
const finding = { class: 'correctness', file: 'src/job.ts', line: 2, issue: 'returns before the lock is taken', quote: 'export function job() {', consequence: 'two runs send the same email' };
|
|
618
|
-
const first = await runReviewer(repo, 'main', allowing, fakes(() => JSON.stringify({ ...EMPTY, findings: [finding] }), seenNow()), () => undefined);
|
|
619
|
-
expect(first.outcome).toBe('findings');
|
|
620
|
-
expect(await dismissReviewerFinding(repo, 'abcdef0123', 'not one of ours', true)).toEqual({ error: 'no open reviewer finding abcdef0123 on feature: run `rigour review --reviewer` and copy the id it shows' });
|
|
621
|
-
expect((await dismissReviewerFinding(repo, first.items[0].id, 'the runner holds a lock one level up', true)).item?.issue).toBe('returns before the lock is taken');
|
|
622
|
-
expect((await reviewStatus(repo, 'feature'))?.last?.open).toEqual([]); // not work any more, right away
|
|
623
|
-
const seen = seenNow();
|
|
624
|
-
// The same commit again: no new run; the stored decision is reused, with the dismissal applied.
|
|
625
|
-
const reused = await runReviewer(repo, 'main', allowing, fakes(() => JSON.stringify(EMPTY), seenNow()), () => undefined);
|
|
626
|
-
expect(reused).toMatchObject({ cached: true, outcome: 'passed' });
|
|
627
|
-
expect(reused.dismissed.map(i => i.issue)).toEqual(['returns before the lock is taken']);
|
|
628
|
-
// A fresh review: the judge is told it is settled, and a re-worded repeat does not block either.
|
|
629
|
-
const again = await runReviewer(repo, 'main', allowing, fakes(() => JSON.stringify({ ...EMPTY, findings: [{ ...finding, issue: 'returns before the lock is taken, so it races' }] }), seen), () => undefined, { force: true });
|
|
630
|
-
expect(again.cached).toBe(false);
|
|
631
|
-
expect(again.outcome).toBe('passed');
|
|
632
|
-
expect(again.dismissed.map(i => i.issue)).toEqual(['returns before the lock is taken, so it races']);
|
|
633
|
-
expect(seen.files['team-knowledge.md']).toContain('dismissed as not a bug by t@example.com: src/job.ts:2 [correctness] returns before the lock is taken (reason: the runner holds a lock one level up)');
|
|
634
|
-
expect(seen.files['team-knowledge.md']).toContain('docs/jobs.md (names src/job.ts');
|
|
635
|
-
const told = seenNow();
|
|
636
|
-
await runReviewer(repo, 'main', allowing, fakes(() => JSON.stringify(EMPTY), told), () => undefined, { force: true, checks: ['src/job.ts:1 Unused export `job`'] });
|
|
637
|
-
expect(told.files['team-knowledge.md']).toContain("## Already found by Rigour's checks: they block on their own, so do not report them again\n- src/job.ts:1 Unused export `job`");
|
|
638
|
-
});
|
|
639
|
-
it('fails closed: a finding whose judge left out the consequence still blocks', async () => {
|
|
640
|
-
const result = await runReviewer(repo, 'main', config, fakes(() => JSON.stringify({ ...EMPTY, findings: [{ class: 'correctness', file: 'src/job.ts', line: 2, issue: 'returns before the lock', quote: 'export function job() {' }] }), seenNow()), () => undefined);
|
|
641
|
-
expect(result.items.map(i => i.issue)).toEqual(['returns before the lock']);
|
|
642
|
-
expect(result.notes).toEqual([]);
|
|
643
|
-
});
|
|
644
|
-
});
|
|
645
|
-
describe("a human's prior point", () => {
|
|
646
|
-
const point = (over) => ({ point: 'keep a separate case for a visitor with no account', review: 'senior 2026-09-25T18:09:11Z', severity: 'blocking', resolved: false, evidence: 'tests/e2e/gate.ts:137', file: 'tests/e2e/gate.ts', line: 137, quote: 'expect(href).toMatch(/account_id=/)', ...over });
|
|
647
|
-
const verdict = (p) => ({ ...EMPTY, prior_points: [p] });
|
|
648
|
-
const approvals = [{ login: 'senior', at: '2026-09-28T15:12:53Z' }];
|
|
649
|
-
it('blocks where the judge quotes the code that keeps it open and no one approved since', () => {
|
|
650
|
-
const { open } = account(verdict(point({})), undefined, () => true, { approvals: [], inCheckout: () => undefined });
|
|
651
|
-
expect(open.map(i => i.issue)).toEqual(['keep a separate case for a visitor with no account']);
|
|
652
|
-
});
|
|
653
|
-
it('is settled by its own reviewer approving after raising it: a note, never a block', () => {
|
|
654
|
-
const { open, notes } = account(verdict(point({})), undefined, () => true, { approvals, inCheckout: () => undefined });
|
|
655
|
-
expect(open).toEqual([]);
|
|
656
|
-
expect(notes).toMatchObject([{ kind: 'prior', issue: 'keep a separate case for a visitor with no account', evidence: 'senior approved on 2026-09-28T15:12:53Z, after raising it: settled' }]);
|
|
657
|
-
});
|
|
658
|
-
it('is not settled by an approval before it, by another person, or when the judge names no reviewer', () => {
|
|
659
|
-
const before = account(verdict(point({})), undefined, () => true, { approvals: [{ login: 'senior', at: '2026-09-20T00:00:00Z' }], inCheckout: () => undefined });
|
|
660
|
-
const other = account(verdict(point({})), undefined, () => true, { approvals: [{ login: 'peer', at: '2026-09-28T15:12:53Z' }], inCheckout: () => undefined });
|
|
661
|
-
const unnamed = account(verdict(point({ review: undefined })), undefined, () => true, { approvals, inCheckout: () => undefined });
|
|
662
|
-
for (const result of [before, other, unnamed])
|
|
663
|
-
expect(result.open).toHaveLength(1);
|
|
664
|
-
const undated = account(verdict(point({ review: 'senior' })), undefined, () => true, { approvals, inCheckout: () => undefined });
|
|
665
|
-
expect(undated.open).toEqual([]); // the reviewer named and approved: settled
|
|
666
|
-
});
|
|
667
|
-
it('that calls something missing is unverified when the checkout has it elsewhere', () => {
|
|
668
|
-
const searched = [];
|
|
669
|
-
const found = account(verdict(point({ absent: 'origin=native&returnTo=' })), undefined, () => true, { approvals: [], inCheckout: text => (searched.push(text), 'src/lib/Upsell.test.ts:128') });
|
|
670
|
-
expect(searched).toEqual(['origin=native&returnTo=']);
|
|
671
|
-
expect(found.open).toEqual([]);
|
|
672
|
-
expect(found.unverified).toMatchObject([{ kind: 'prior', evidence: 'says "origin=native&returnTo=" is missing, and the checkout has it at src/lib/Upsell.test.ts:128' }]);
|
|
673
|
-
const missing = account(verdict(point({ absent: 'origin=native&returnTo=' })), undefined, () => true, { approvals: [], inCheckout: () => undefined });
|
|
674
|
-
expect(missing.open).toHaveLength(1); // searched, not there: the point stands on its quote
|
|
675
|
-
});
|
|
676
|
-
});
|
|
677
|
-
describe('searching the checkout for what a point calls missing', () => {
|
|
678
|
-
it('finds the first line of the text anywhere in the tracked tree, and nothing untracked', () => {
|
|
679
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'rigour-search-'));
|
|
680
|
-
execFileSync('git', ['-C', dir, 'init', '-q']);
|
|
681
|
-
fs.mkdirSync(path.join(dir, 'src'));
|
|
682
|
-
fs.writeFileSync(path.join(dir, 'src', 'a.test.ts'), 'it("no account", () => {\n expect(href).toBe("/checkout?origin=native");\n});\n');
|
|
683
|
-
fs.writeFileSync(path.join(dir, 'untracked.ts'), 'const ghost = 1;\n');
|
|
684
|
-
execFileSync('git', ['-C', dir, 'add', 'src']);
|
|
685
|
-
const search = checkoutSearch(dir);
|
|
686
|
-
expect(search(' expect(href).toBe("/checkout?origin=native");\n more')).toBe('src/a.test.ts:2');
|
|
687
|
-
expect(search('const ghost = 1;')).toBeUndefined();
|
|
688
|
-
expect(search(' \n')).toBeUndefined();
|
|
689
|
-
});
|
|
690
|
-
});
|
|
691
|
-
describe('a block sits on a line the change touched', () => {
|
|
692
|
-
const diff = [
|
|
693
|
-
'diff --git a/src/player.ts b/src/player.ts', '--- a/src/player.ts', '+++ b/src/player.ts',
|
|
694
|
-
'@@ -10,4 +10,5 @@ function resume() {', ' const a = 1;', '- old();', '+ report(a);', '+ report(b);', ' return a;', ' }',
|
|
695
|
-
'diff --git a/src/gone.ts b/src/gone.ts', '--- a/src/gone.ts', '+++ /dev/null', '@@ -1,2 +0,0 @@', '-export const x = 1;', '-export const y = 2;',
|
|
696
|
-
'diff --git a/src/new.ts b/src/new.ts', '--- /dev/null', '+++ b/src/new.ts', '@@ -0,0 +1,2 @@', '+export const z = 1;', '+export const w = 2;', '',
|
|
697
|
-
].join('\n');
|
|
698
|
-
const changed = changedLinesOf(diff);
|
|
699
|
-
const rule = (file, line) => ({ ...EMPTY, prior_points: [], rules: [{ id: 'r1', status: 'broken', rule: 'wrap every navigation target in resolve()', source: 'AGENTS.md', requirement: true, file, line, quote: 'preloadCode(target)' }] });
|
|
700
|
-
const checks = { approvals: [], inCheckout: () => undefined, changed };
|
|
701
|
-
it('reads the touched lines of a diff: added lines, the place of a deletion, nothing for a deleted file', () => {
|
|
702
|
-
expect([...changed.get('src/player.ts')].sort((a, b) => a - b)).toEqual([11, 12]); // the deletion's place, then the two added lines (11 is both)
|
|
703
|
-
expect([...changed.get('src/new.ts')]).toEqual([1, 2]);
|
|
704
|
-
expect(changed.has('src/gone.ts')).toBe(false);
|
|
705
|
-
});
|
|
706
|
-
it('blocks a verified rule break near a touched line, and notes one on lines the change did not touch, or with no line', () => {
|
|
707
|
-
expect(account(rule('src/player.ts', 14), undefined, () => true, checks).open).toHaveLength(1); // within the window of line 12
|
|
708
|
-
const far = account(rule('src/player.ts', 1819), undefined, () => true, checks);
|
|
709
|
-
expect(far.open).toEqual([]);
|
|
710
|
-
expect(far.notes).toMatchObject([{ kind: 'rule', line: 1819, evidence: expect.stringContaining('on a line this change did not touch: what the code already had, never a block on this change') }]);
|
|
711
|
-
const unplaced = account(rule('src/player.ts', undefined), undefined, () => true, checks);
|
|
712
|
-
expect(unplaced.open).toEqual([]);
|
|
713
|
-
expect(unplaced.notes[0].evidence).toContain('names no line');
|
|
714
|
-
expect(account(rule('src/other.ts', 3), undefined, () => true, checks).open).toEqual([]); // a file the change did not touch at all
|
|
715
|
-
});
|
|
716
|
-
it("leaves a human's point, and every item when the diff is unknown, as before", () => {
|
|
717
|
-
const point = { ...EMPTY, prior_points: [{ point: 'wrap the target', review: 'senior 2026-10-01', severity: 'blocking', resolved: false, file: 'src/player.ts', line: 1819, quote: 'preloadCode(target)' }] };
|
|
718
|
-
expect(account(point, undefined, () => true, checks).open).toHaveLength(1);
|
|
719
|
-
expect(account(rule('src/player.ts', 1819), undefined, () => true, { approvals: [], inCheckout: () => undefined }).open).toHaveLength(1);
|
|
720
|
-
});
|
|
721
|
-
});
|
|
722
|
-
describe("a human's should-fix point", () => {
|
|
723
|
-
it('is shown with its quote and never blocks, like a should-fix finding', () => {
|
|
724
|
-
const verdict = { ...EMPTY, prior_points: [{ point: 'a reopened deck reads as a return every day', review: 'senior 2026-10-01', severity: 'should-fix', resolved: false, file: 'src/job.ts', line: 2, quote: 'return 1;' }] };
|
|
725
|
-
const { open, advisory, unverified } = account(verdict, undefined, checkoutVerifier(repo));
|
|
726
|
-
expect(open).toEqual([]);
|
|
727
|
-
expect(advisory.map(i => [i.kind, i.issue])).toEqual([['prior', 'a reopened deck reads as a return every day']]);
|
|
728
|
-
expect(unverified).toEqual([]);
|
|
729
|
-
const unplaced = account({ ...verdict, prior_points: [{ ...verdict.prior_points[0], quote: 'not in the file' }] }, undefined, checkoutVerifier(repo));
|
|
730
|
-
expect(unplaced.advisory).toEqual([]); // a should-fix the judge cannot show is not worth a person's time
|
|
731
|
-
expect(unplaced.unverified).toHaveLength(1);
|
|
732
|
-
});
|
|
733
|
-
});
|
|
734
|
-
describe("the reviewer's own severity label", () => {
|
|
735
|
-
const at = '2026-10-01T10:00:00Z';
|
|
736
|
-
const labels = [
|
|
737
|
-
{ login: 'senior', at, severity: 'blocking', text: 'The kill switch is read after every query: check it first.' },
|
|
738
|
-
{ login: 'senior', at, severity: 'should-fix', text: 'The comment on the window still says daily.' },
|
|
739
|
-
];
|
|
740
|
-
const point = (over) => ({ ...EMPTY, prior_points: [{ point: 'the kill switch is read after every query', review: 'senior 2026-10-01T10:00:00Z', severity: 'should-fix', resolved: false, file: 'src/job.ts', line: 2, quote: 'return 1;', ...over }] });
|
|
741
|
-
it('wins over the judge: a blocker the judge read as a should-fix blocks, and the disagreement is said', () => {
|
|
742
|
-
const { open, advisory } = account(point({}), undefined, checkoutVerifier(repo), { approvals: [], inCheckout: () => undefined, labels });
|
|
743
|
-
expect(advisory).toEqual([]);
|
|
744
|
-
expect(open).toMatchObject([{ kind: 'prior', evidence: 'the review labels it blocking; the judge read should-fix' }]);
|
|
745
|
-
});
|
|
746
|
-
it('matches the review by its date however the judge writes the time, and falls back to the reviewer\'s latest labelled review', () => {
|
|
747
|
-
const later = { login: 'senior', at: '2026-10-03T09:00:00Z', severity: 'should-fix', text: 'The kill switch is read after every query: check it first.' };
|
|
748
|
-
for (const review of ['senior 2026-10-01', 'senior 2026-10-01 10:00', 'senior']) {
|
|
749
|
-
const { open, labels: counted } = account(point({ review }), undefined, checkoutVerifier(repo), { approvals: [], inCheckout: () => undefined, labels });
|
|
750
|
-
expect(open).toHaveLength(1);
|
|
751
|
-
expect(counted).toEqual({ served: 2, taken: 1, disagreed: 1 });
|
|
752
|
-
}
|
|
753
|
-
// No review of that reviewer on the judge's date: the reviewer's latest labelled review decides (here, a should-fix).
|
|
754
|
-
const { open, advisory } = account(point({ review: 'senior 2026-09-30', severity: 'blocking' }), undefined, checkoutVerifier(repo), { approvals: [], inCheckout: () => undefined, labels: [...labels, later] });
|
|
755
|
-
expect([open.length, advisory.length]).toEqual([0, 1]);
|
|
756
|
-
});
|
|
757
|
-
it('applies only to the same reviewer, and only to a point that reads like the labelled line; the counts say so', () => {
|
|
758
|
-
for (const over of [{ review: 'peer 2026-10-01T10:00:00Z' }, { point: 'the email retry sends twice' }]) {
|
|
759
|
-
const { open, advisory, labels: counted } = account(point(over), undefined, checkoutVerifier(repo), { approvals: [], inCheckout: () => undefined, labels });
|
|
760
|
-
expect([open.length, advisory.length]).toEqual([0, 1]); // the judge's should-fix stands
|
|
761
|
-
expect(counted).toEqual({ served: 2, taken: 0, disagreed: 0 });
|
|
762
|
-
}
|
|
763
|
-
});
|
|
764
|
-
});
|
|
765
|
-
describe('the judge Rigour launches', () => {
|
|
766
|
-
it('runs claude with every memory file switched off, and records the isolation as unverified below the version it was verified in', async () => {
|
|
767
|
-
const seen = seenNow();
|
|
768
|
-
const result = await runReviewer(repo, 'main', ConfigSchema.parse({ version: 1, review: { reviewer: { enabled: true, reviewers: ['claude'] } } }), fakes(() => JSON.stringify(EMPTY), seen, null), () => undefined, { trigger: 'review' });
|
|
769
|
-
expect(result.outcome).toBe('passed');
|
|
770
|
-
const claude = seen.ran.findIndex(command => path.basename(command).startsWith('claude'));
|
|
771
|
-
expect(seen.env?.[claude]).toEqual({ CLAUDE_CODE_DISABLE_CLAUDE_MDS: '1', CLAUDE_CODE_DISABLE_AUTO_MEMORY: '1' });
|
|
772
|
-
// The fake reports version 1.0.0, older than the one the switches were verified in: the record says so.
|
|
773
|
-
expect(result.record?.judges.map(j => j.outside_repo)).toEqual(['claude 1.0.0: memory isolation unverified (needs 2.1.285 or later)']);
|
|
774
|
-
expect(recordLines(result.record).join('\n')).toContain('[claude 1.0.0: memory isolation unverified (needs 2.1.285 or later)]');
|
|
775
|
-
const current = seenNow();
|
|
776
|
-
// The installed fake is claude on Unix and claude.cmd on Windows: name both.
|
|
777
|
-
current.versions = { [path.join(bins[0], 'claude')]: '2.1.285 (Claude Code)', [path.join(bins[0], 'claude.cmd')]: '2.1.285 (Claude Code)' };
|
|
778
|
-
const verified = await runReviewer(repo, 'main', ConfigSchema.parse({ version: 1, review: { reviewer: { enabled: true, reviewers: ['claude'] } } }), fakes(() => JSON.stringify(EMPTY), current, null), () => undefined, { trigger: 'review', force: true });
|
|
779
|
-
expect(verified.record?.judges.map(j => j.outside_repo)).toEqual([undefined]);
|
|
780
|
-
});
|
|
781
|
-
});
|
|
782
|
-
describe("the review on the task's thread", () => {
|
|
783
|
-
it('appends each review of a branch to its task, and never a backtest replaying history', async () => {
|
|
784
|
-
const seen = seenNow();
|
|
785
|
-
await runReviewer(repo, 'main', ConfigSchema.parse({ version: 1, review: { reviewer: { enabled: true, reviewers: ['claude'] } } }), fakes(() => JSON.stringify(EMPTY), seen, null), () => undefined, { trigger: 'review' });
|
|
786
|
-
const thread = readThread(repo, 'feature');
|
|
787
|
-
expect(thread?.events.map(e => [e.kind, e.trigger, e.outcome, e.blocking])).toEqual([['review', 'review', 'passed', 0]]);
|
|
788
|
-
expect(thread?.events[0].integrity).toEqual(expect.any(String));
|
|
789
|
-
await runReviewer(repo, 'main', config, fakes(() => JSON.stringify({ ...EMPTY, prior_points: [] }), seenNow()), () => undefined, { pr: 42, reviewsBefore: '2026-10-03', force: true });
|
|
790
|
-
expect(readThread(repo, 'feature')?.events).toHaveLength(1);
|
|
791
|
-
});
|
|
792
|
-
});
|