@rigour-labs/core 6.7.10 → 6.8.1-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/brief/briefing.d.ts +59 -0
- package/dist/brief/briefing.js +126 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/inference/cloud-provider.js +12 -2
- package/dist/review/backtest-init.d.ts +8 -5
- package/dist/review/backtest-init.js +27 -9
- package/dist/review/backtest-last.js +3 -1
- package/dist/review/reviewer/adapters.d.ts +4 -0
- package/dist/review/reviewer/adapters.js +23 -0
- package/dist/review/reviewer/inputs.d.ts +4 -1
- package/dist/review/reviewer/inputs.js +36 -2
- package/dist/review/reviewer/record.d.ts +5 -0
- package/dist/review/reviewer/record.js +3 -3
- package/dist/review/reviewer/verdict.d.ts +14 -0
- package/dist/review/reviewer/verdict.js +46 -6
- package/dist/review/reviewer.d.ts +2 -0
- package/dist/review/reviewer.js +24 -5
- package/dist/review-learning/repo-rules.d.ts +14 -2
- package/dist/review-learning/repo-rules.js +59 -9
- package/dist/task/thread.d.ts +47 -0
- package/dist/task/thread.js +234 -0
- package/dist/templates/universal-config.js +4 -0
- package/dist/types/index.d.ts +21 -0
- package/dist/types/index.js +7 -0
- package/package.json +11 -8
- 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 -98
- 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 -60
- 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 -26
- 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 -32
- 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 -705
- 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 -55
- 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/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,203 +0,0 @@
|
|
|
1
|
-
import { execFileSync } from 'child_process';
|
|
2
|
-
import fs from 'fs';
|
|
3
|
-
import os from 'os';
|
|
4
|
-
import path from 'path';
|
|
5
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
-
import { GateRunner } from '../gates/runner.js';
|
|
7
|
-
import { ConfigSchema } from '../types/index.js';
|
|
8
|
-
import { parseDiff } from '../utils/diff.js';
|
|
9
|
-
import { splitByChangedLines } from './changed-lines.js';
|
|
10
|
-
import { anchorInChangedFunction, changedFunctionSpans } from './changed-function-spans.js';
|
|
11
|
-
import { diffFromGit } from './git-diff.js';
|
|
12
|
-
import { reviewChange, toReviewFinding } from './review.js';
|
|
13
|
-
import { findingKey } from './quiet.js';
|
|
14
|
-
/** These tests are about other checks; a new unreferenced fixture file is not their subject. */
|
|
15
|
-
const NO_DEAD_CODE = { unused_exports: { enabled: false }, orphan_files: { enabled: false } };
|
|
16
|
-
const LEAKY = [
|
|
17
|
-
'export async function notify(endpoint: string, signature: string) {',
|
|
18
|
-
" return fetch(endpoint, { method: 'POST', headers: { 'x-hook-signature': signature } });",
|
|
19
|
-
'}',
|
|
20
|
-
'',
|
|
21
|
-
].join('\n');
|
|
22
|
-
function failure(file, line) {
|
|
23
|
-
return { id: 'x', title: 'X', details: 'd', files: file ? [file] : [], ...(line !== undefined ? { line } : {}) };
|
|
24
|
-
}
|
|
25
|
-
describe('splitByChangedLines', () => {
|
|
26
|
-
it('separates changed-line, file-level, unlocated and outside findings', () => {
|
|
27
|
-
const changed = { 'a.ts': new Set([3]) };
|
|
28
|
-
const split = splitByChangedLines([failure('a.ts', 3), failure('a.ts', 9), failure('a.ts'), failure('b.ts'), failure(undefined)], changed);
|
|
29
|
-
expect(split.findings.map(f => f.line)).toEqual([3]);
|
|
30
|
-
expect(split.fileFindings).toHaveLength(1);
|
|
31
|
-
expect(split.unlocated).toBe(1);
|
|
32
|
-
expect(split.outside).toBe(2);
|
|
33
|
-
});
|
|
34
|
-
it('keeps a deep finding inside a changed function, anchored on the nearest changed line', () => {
|
|
35
|
-
const deep = (line) => ({ ...failure('a.ts', line), provenance: 'deep-analysis' });
|
|
36
|
-
const changed = { 'a.ts': new Set([12, 14]) };
|
|
37
|
-
const spans = { 'a.ts': [[10, 20]] };
|
|
38
|
-
const split = splitByChangedLines([deep(11), deep(30), failure('a.ts', 11)], changed, spans);
|
|
39
|
-
expect(split.findings).toEqual([expect.objectContaining({ line: 11, anchorLine: 12 })]);
|
|
40
|
-
expect(split.contextFindings.map(f => f.line)).toEqual([30]); // elsewhere in a changed file: shown, not blocking
|
|
41
|
-
expect(split.outside).toBe(1); // a rule finding on an unchanged line is pre-existing, as before
|
|
42
|
-
});
|
|
43
|
-
it('counts a multi-line finding as changed when lines inside it were deleted', () => {
|
|
44
|
-
// fetch(url, { <- line 4, unchanged
|
|
45
|
-
// redirect: ... <- deleted, so the removal sits before new line 5
|
|
46
|
-
// }) <- line 5
|
|
47
|
-
const call = { ...failure('a.ts', 4), endLine: 5 };
|
|
48
|
-
const outsideCall = { ...failure('a.ts', 9), endLine: 10 };
|
|
49
|
-
const removed = { 'a.ts': [{ line: 5, text: [" redirect: 'manual',"] }] };
|
|
50
|
-
const split = splitByChangedLines([call, outsideCall, failure('a.ts', 4)], {}, {}, removed);
|
|
51
|
-
expect(split.findings).toEqual([call]);
|
|
52
|
-
expect(split.outside).toBe(2);
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
describe('changedFunctionSpans', () => {
|
|
56
|
-
it('spans the outermost function around changed lines, and anchors to the nearest changed line in it', () => {
|
|
57
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'spans-'));
|
|
58
|
-
try {
|
|
59
|
-
fs.writeFileSync(path.join(dir, 'a.ts'), 'const x = 1;\nexport function f(a: number) {\n if (a > 0) {\n return a;\n }\n return 0;\n}\n');
|
|
60
|
-
const spans = changedFunctionSpans(dir, { 'a.ts': new Set([4]), 'missing.ts': new Set([1]), 'notes.md': new Set([1]) });
|
|
61
|
-
expect(spans).toEqual({ 'a.ts': [[2, 7]] });
|
|
62
|
-
expect(anchorInChangedFunction(6, new Set([4]), spans['a.ts'])).toBe(4);
|
|
63
|
-
expect(anchorInChangedFunction(1, new Set([4]), spans['a.ts'])).toBeUndefined();
|
|
64
|
-
}
|
|
65
|
-
finally {
|
|
66
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
describe('git-backed review', () => {
|
|
71
|
-
let repo;
|
|
72
|
-
const git = (...args) => execFileSync('git', ['-C', repo, ...args], { encoding: 'utf8' });
|
|
73
|
-
const write = (rel, body) => {
|
|
74
|
-
fs.mkdirSync(path.dirname(path.join(repo, rel)), { recursive: true });
|
|
75
|
-
fs.writeFileSync(path.join(repo, rel), body);
|
|
76
|
-
};
|
|
77
|
-
beforeEach(() => {
|
|
78
|
-
repo = fs.mkdtempSync(path.join(os.tmpdir(), 'review-core-'));
|
|
79
|
-
git('init', '-q', '-b', 'main');
|
|
80
|
-
git('config', 'user.email', 't@example.com');
|
|
81
|
-
git('config', 'user.name', 't');
|
|
82
|
-
git('config', 'commit.gpgsign', 'false');
|
|
83
|
-
});
|
|
84
|
-
afterEach(() => { fs.rmSync(repo, { recursive: true, force: true }); });
|
|
85
|
-
it('works in a repository with no commits yet', () => {
|
|
86
|
-
write('src/new.ts', 'export const a = 1;\n');
|
|
87
|
-
expect(parseDiff(diffFromGit(repo))).toEqual({ 'src/new.ts': new Set([1]) });
|
|
88
|
-
});
|
|
89
|
-
it('takes uncommitted edits and untracked files from the working tree', () => {
|
|
90
|
-
write('src/a.ts', 'export const a = 1;\nexport const b = 2;\n');
|
|
91
|
-
git('add', '-A');
|
|
92
|
-
git('commit', '-qm', 'init');
|
|
93
|
-
write('src/a.ts', 'export const a = 1;\nexport const b = 3;\n');
|
|
94
|
-
write('src/new.ts', 'export const c = 1;\nexport const d = 2;\n');
|
|
95
|
-
expect(parseDiff(diffFromGit(repo))).toEqual({ 'src/a.ts': new Set([2]), 'src/new.ts': new Set([1, 2]) });
|
|
96
|
-
});
|
|
97
|
-
it('takes a branch against its merge-base, committed and uncommitted', () => {
|
|
98
|
-
write('src/a.ts', 'export const a = 1;\n');
|
|
99
|
-
git('add', '-A');
|
|
100
|
-
git('commit', '-qm', 'init');
|
|
101
|
-
git('checkout', '-q', '-b', 'feature');
|
|
102
|
-
write('src/b.ts', 'export const b = 1;\n');
|
|
103
|
-
git('add', '-A');
|
|
104
|
-
git('commit', '-qm', 'feature');
|
|
105
|
-
write('src/a.ts', 'export const a = 1;\nexport const z = 2;\n');
|
|
106
|
-
expect(parseDiff(diffFromGit(repo, { mode: 'base', base: 'main' }))).toEqual({ 'src/a.ts': new Set([2]), 'src/b.ts': new Set([1]) });
|
|
107
|
-
});
|
|
108
|
-
it('fails a change that introduces a bug and ignores the same bug in untouched code', async () => {
|
|
109
|
-
write('src/old.ts', LEAKY);
|
|
110
|
-
git('add', '-A');
|
|
111
|
-
git('commit', '-qm', 'init');
|
|
112
|
-
write('src/notify.ts', LEAKY);
|
|
113
|
-
const config = ConfigSchema.parse({ version: 1, gates: { semantic_bugs: { enabled: true } } });
|
|
114
|
-
const result = await reviewChange({ cwd: repo, config });
|
|
115
|
-
const semantic = result.findings.filter(f => f.id === 'semantic-bugs');
|
|
116
|
-
expect(result.status).toBe('FAIL');
|
|
117
|
-
expect(semantic.map(f => [f.files?.[0], f.line])).toEqual([['src/notify.ts', 2]]);
|
|
118
|
-
expect(toReviewFinding(semantic[0])).toMatchObject({ id: 'semantic-bugs', file: 'src/notify.ts', line: 2, severity: 'high' });
|
|
119
|
-
});
|
|
120
|
-
it("names every check it ran in the summary, the review's own beside the gates", async () => {
|
|
121
|
-
write('src/a.ts', 'export const a = 1;\n');
|
|
122
|
-
git('add', '-A');
|
|
123
|
-
git('commit', '-qm', 'init');
|
|
124
|
-
write('src/b.ts', 'export const unused = 2;\n');
|
|
125
|
-
git('add', '-A');
|
|
126
|
-
const config = ConfigSchema.parse({ gates: { migration_order: { enabled: false } } });
|
|
127
|
-
const { report } = await reviewChange({ cwd: repo, config });
|
|
128
|
-
expect(report?.summary).toMatchObject({ 'unused-exports': 'FAIL', 'migration-order': 'SKIP', 'query-patterns': 'PASS', 'change-sweep': 'PASS' });
|
|
129
|
-
expect(report?.summary).not.toHaveProperty('redundancy'); // typed checks were not asked for
|
|
130
|
-
});
|
|
131
|
-
it('reports what the change introduced, not what the touched code already had', async () => {
|
|
132
|
-
const branchy = (name, extra = '') => [
|
|
133
|
-
`export function ${name}(x: number) {`,
|
|
134
|
-
...Array.from({ length: 12 }, (_, i) => ` if (x === ${i}) return ${i};`),
|
|
135
|
-
extra,
|
|
136
|
-
' return -1;',
|
|
137
|
-
'}',
|
|
138
|
-
'',
|
|
139
|
-
].join('\n');
|
|
140
|
-
write('src/old.ts', branchy('legacy'));
|
|
141
|
-
git('add', '-A');
|
|
142
|
-
git('commit', '-qm', 'init');
|
|
143
|
-
write('src/old.ts', branchy('legacy', ' if (x === 99) return 99;'));
|
|
144
|
-
write('src/fresh.ts', branchy('fresh'));
|
|
145
|
-
const config = ConfigSchema.parse({ version: 1 });
|
|
146
|
-
const result = await reviewChange({ cwd: repo, config });
|
|
147
|
-
const complexity = [...result.findings, ...result.fileFindings, ...result.advisory].filter(f => f.id === 'AST_COMPLEXITY');
|
|
148
|
-
expect(complexity.map(f => f.files?.[0])).toEqual(['src/fresh.ts']);
|
|
149
|
-
expect(result.preexisting).toBeGreaterThanOrEqual(1);
|
|
150
|
-
const all = await reviewChange({ cwd: repo, config: ConfigSchema.parse({ version: 1, review: { show_preexisting: true } }) });
|
|
151
|
-
const shown = [...all.findings, ...all.fileFindings, ...all.advisory].filter(f => f.id === 'AST_COMPLEXITY');
|
|
152
|
-
expect(shown.map(f => f.files?.[0]).sort()).toEqual(['src/fresh.ts', 'src/old.ts']);
|
|
153
|
-
expect(all.preexisting).toBe(0);
|
|
154
|
-
}, 30_000); // two whole reviews, each running the rules on the change and on the base tree
|
|
155
|
-
it('lets a change dismiss its own finding only when the review trusts the working tree', async () => {
|
|
156
|
-
write('src/old.ts', 'export const a = 1;\n');
|
|
157
|
-
write('.gitignore', '.rigour/*\n!.rigour/dismissed.json\n');
|
|
158
|
-
git('add', '-A');
|
|
159
|
-
git('commit', '-qm', 'init');
|
|
160
|
-
git('checkout', '-q', '-b', 'pr');
|
|
161
|
-
write('src/notify.ts', LEAKY);
|
|
162
|
-
const config = ConfigSchema.parse({ version: 1, gates: { semantic_bugs: { enabled: true }, ...NO_DEAD_CODE } });
|
|
163
|
-
const key = findingKey((await reviewChange({ cwd: repo, config })).findings[0]);
|
|
164
|
-
write('.rigour/dismissed.json', JSON.stringify({ version: 1, entries: [{ key, reason: 'trust me', at: 'now' }] }));
|
|
165
|
-
git('add', '-A');
|
|
166
|
-
git('commit', '-qm', 'pr');
|
|
167
|
-
const trusting = await reviewChange({ cwd: repo, config, source: { mode: 'base', base: 'main' } });
|
|
168
|
-
expect([trusting.status, trusting.dismissed]).toEqual(['PASS', 1]);
|
|
169
|
-
expect(trusting.controlFilesChanged).toEqual(['.rigour/dismissed.json']);
|
|
170
|
-
const independent = await reviewChange({ cwd: repo, config, source: { mode: 'base', base: 'main' }, trustedRef: 'main' });
|
|
171
|
-
expect([independent.status, independent.dismissed]).toEqual(['FAIL', 0]);
|
|
172
|
-
});
|
|
173
|
-
it('runs every gate in a git worktree, where .git is a file', async () => {
|
|
174
|
-
write('src/a.ts', 'export const a = 1;\n');
|
|
175
|
-
git('add', '-A');
|
|
176
|
-
git('commit', '-qm', 'init');
|
|
177
|
-
const worktree = path.join(repo, 'wt');
|
|
178
|
-
git('worktree', 'add', '-q', worktree);
|
|
179
|
-
fs.writeFileSync(path.join(worktree, 'src/a.ts'), 'export const a = 1;\nexport const b = 2;\n');
|
|
180
|
-
const config = ConfigSchema.parse({ version: 1, ignore: ['.git/**', 'wt/**'] });
|
|
181
|
-
const result = await reviewChange({ cwd: worktree, config });
|
|
182
|
-
expect(result.gateErrors).toEqual([]);
|
|
183
|
-
expect(result.status).not.toBe('ERROR');
|
|
184
|
-
});
|
|
185
|
-
it('is ERROR when a proven gate crashed, and only lists a crashed heuristic gate', async () => {
|
|
186
|
-
write('src/a.ts', 'export const a = 1;\n');
|
|
187
|
-
const config = ConfigSchema.parse({ version: 1, gates: NO_DEAD_CODE });
|
|
188
|
-
const crash = (summary) => vi.spyOn(GateRunner.prototype, 'run').mockResolvedValueOnce({ status: 'FAIL', summary, failures: [], stats: { duration_ms: 0 } });
|
|
189
|
-
crash({ 'hallucinated-imports': 'ERROR' });
|
|
190
|
-
const proven = await reviewChange({ cwd: repo, config });
|
|
191
|
-
expect([proven.status, proven.gateErrors]).toEqual(['ERROR', ['hallucinated-imports']]);
|
|
192
|
-
crash({ 'style-drift': 'ERROR', 'semantic-bugs': 'PASS' });
|
|
193
|
-
const heuristic = await reviewChange({ cwd: repo, config });
|
|
194
|
-
expect([heuristic.status, heuristic.gateErrors]).toEqual(['PASS', ['style-drift']]);
|
|
195
|
-
});
|
|
196
|
-
it('passes when nothing changed', async () => {
|
|
197
|
-
write('src/a.ts', 'export const a = 1;\n');
|
|
198
|
-
git('add', '-A');
|
|
199
|
-
git('commit', '-qm', 'init');
|
|
200
|
-
const result = await reviewChange({ cwd: repo, config: ConfigSchema.parse({ version: 1 }) });
|
|
201
|
-
expect(result).toMatchObject({ status: 'PASS', findings: [], report: null });
|
|
202
|
-
});
|
|
203
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { ADAPTERS, apiVendor, selectReviewers } from './adapters.js';
|
|
3
|
-
/** A real `codex exec --json` run (codex-cli 0.160.1), the warning's path shortened: the shape the adapter must read. */
|
|
4
|
-
const CODEX = [
|
|
5
|
-
'{"type":"thread.started","thread_id":"01a11477-f9fd-7472-8d31-5863de4cbbdb"}',
|
|
6
|
-
'{"type":"item.completed","item":{"id":"item_0","type":"error","message":"Codex is ignoring 1 unrecognized configuration setting. Check for typos or deprecated settings.\\n user (~/.codex/config.toml)"}}',
|
|
7
|
-
'{"type":"turn.started"}',
|
|
8
|
-
'{"type":"item.completed","item":{"id":"item_2","type":"agent_message","text":"{\\"answers\\":[{\\"id\\":\\"a1\\",\\"call\\":\\"unsure\\",\\"evidence\\":\\"none\\"}]}"}}',
|
|
9
|
-
'{"type":"turn.completed","usage":{"input_tokens":26565,"cached_input_tokens":13184,"cache_write_input_tokens":0,"output_tokens":24,"reasoning_output_tokens":0}}',
|
|
10
|
-
].join('\n');
|
|
11
|
-
describe('reading an agent CLI\'s answer', () => {
|
|
12
|
-
it('takes Codex\'s last agent message, never a warning, and its tokens', () => {
|
|
13
|
-
expect(ADAPTERS.codex.answer(CODEX)).toEqual({ text: '{"answers":[{"id":"a1","call":"unsure","evidence":"none"}]}', tokens: { input: 26565, output: 24 } });
|
|
14
|
-
});
|
|
15
|
-
it('takes Claude Code\'s result, its cost and its tokens', () => {
|
|
16
|
-
const out = JSON.stringify({ result: '{"prior_points":[]}', total_cost_usd: 0.42, usage: { input_tokens: 10, cache_read_input_tokens: 900, cache_creation_input_tokens: 90, output_tokens: 300 } });
|
|
17
|
-
expect(ADAPTERS.claude.answer(out)).toEqual({ text: '{"prior_points":[]}', costUsd: 0.42, tokens: { input: 1000, output: 300 } });
|
|
18
|
-
});
|
|
19
|
-
it('falls back to the raw output when it is not what the CLI usually prints', () => {
|
|
20
|
-
expect(ADAPTERS.claude.answer('not json')).toEqual({ text: 'not json' });
|
|
21
|
-
expect(ADAPTERS.codex.answer('plain text')).toEqual({ text: 'plain text' });
|
|
22
|
-
});
|
|
23
|
-
it("reads Claude Code's event stream: the answer and cost from its result, each turn's usage once, and what every tool call read", () => {
|
|
24
|
-
const events = [
|
|
25
|
-
{ type: 'system', subtype: 'init' },
|
|
26
|
-
{ type: 'assistant', message: { id: 'm1', usage: { input_tokens: 10, cache_read_input_tokens: 0, cache_creation_input_tokens: 4000, output_tokens: 50 }, content: [{ type: 'tool_use', id: 't1', name: 'Read', input: { file_path: '/work/full.diff' } }] } },
|
|
27
|
-
{ type: 'user', message: { content: [{ type: 'tool_result', tool_use_id: 't1', content: 'x'.repeat(1200) }] } },
|
|
28
|
-
{ type: 'assistant', message: { id: 'm2', usage: { input_tokens: 5, cache_read_input_tokens: 4000, cache_creation_input_tokens: 300, output_tokens: 20 }, content: [{ type: 'text', text: 'checking' }] } },
|
|
29
|
-
{ type: 'assistant', message: { id: 'm2', usage: { input_tokens: 5, cache_read_input_tokens: 4000, cache_creation_input_tokens: 300, output_tokens: 20 }, content: [{ type: 'tool_use', id: 't2', name: 'Grep', input: { pattern: 'preloadAll' } }] } },
|
|
30
|
-
{ type: 'user', message: { content: [{ type: 'tool_result', tool_use_id: 't2', content: [{ type: 'text', text: 'src/a.ts:3' }] }] } },
|
|
31
|
-
{ type: 'result', result: '{"prior_points":[]}', total_cost_usd: 0.31, usage: { input_tokens: 15, cache_read_input_tokens: 4000, cache_creation_input_tokens: 4300, output_tokens: 70 } },
|
|
32
|
-
];
|
|
33
|
-
const answer = ADAPTERS.claude.answer(events.map(e => JSON.stringify(e)).join('\n'));
|
|
34
|
-
expect(answer).toMatchObject({ text: '{"prior_points":[]}', costUsd: 0.31, tokens: { input: 8315, output: 70 } });
|
|
35
|
-
expect(answer.trace).toEqual({
|
|
36
|
-
turns: 2, // m2 arrived as two events: one turn, counted once
|
|
37
|
-
usage: { input: 15, cacheRead: 4000, cacheWrite: 4300, output: 70 },
|
|
38
|
-
calls: [
|
|
39
|
-
{ turn: 1, tool: 'Read', target: '/work/full.diff', resultChars: 1200 },
|
|
40
|
-
{ turn: 2, tool: 'Grep', target: 'preloadAll', resultChars: 10 },
|
|
41
|
-
],
|
|
42
|
-
});
|
|
43
|
-
expect(ADAPTERS.claude.args('p', undefined)).toEqual(expect.arrayContaining(['--output-format', 'stream-json', '--verbose']));
|
|
44
|
-
});
|
|
45
|
-
});
|
|
46
|
-
describe('a judge reached through an API', () => {
|
|
47
|
-
it('is told apart by its model\'s maker, so cross and full modes pair it with a different vendor', () => {
|
|
48
|
-
expect(apiVendor({ model: 'anthropic/claude-sonnet-4.5' })).toBe('anthropic');
|
|
49
|
-
expect(apiVendor({ model: 'gpt-5' })).toBe('openai');
|
|
50
|
-
expect(apiVendor({ model: 'google/gemini-2.5-pro' })).toBe('google');
|
|
51
|
-
expect(apiVendor({ model: 'qwen3-coder' })).toBe('other');
|
|
52
|
-
expect(apiVendor({ model: 'qwen3-coder', vendor: 'openai' })).toBe('openai');
|
|
53
|
-
const vendorOf = (name) => (name === 'api' ? 'openai' : ADAPTERS[name].vendor);
|
|
54
|
-
expect(selectReviewers(['claude', 'api'], 'cross', new Set(['anthropic']), new Set(['claude', 'api']), 2, vendorOf)).toEqual(['api']); // the author's vendor is skipped
|
|
55
|
-
expect(selectReviewers(['claude', 'api'], 'full', new Set(), new Set(['claude', 'api']), 2, vendorOf)).toEqual(['claude', 'api']);
|
|
56
|
-
expect(ADAPTERS.api.answer(JSON.stringify({ result: '{"prior_points":[]}', usage: { input: 10, cacheRead: 5, cacheWrite: 0, output: 3 }, cost_usd: 0.02, trace: { turns: 2, usage: {}, calls: [] } }))).toMatchObject({ text: '{"prior_points":[]}', costUsd: 0.02, tokens: { input: 15, output: 3 }, trace: { turns: 2 } });
|
|
57
|
-
expect(ADAPTERS.codex.args('p', undefined, { reasoning: 'medium' })).toContain('model_reasoning_effort=medium');
|
|
58
|
-
expect(ADAPTERS.codex.args('p', undefined)).toContain('model_reasoning_effort=high');
|
|
59
|
-
});
|
|
60
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,108 +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 { runApiJudge } from './api-judge.js';
|
|
7
|
-
let repo;
|
|
8
|
-
let work;
|
|
9
|
-
beforeEach(() => {
|
|
10
|
-
repo = fs.mkdtempSync(path.join(os.tmpdir(), 'api-judge-'));
|
|
11
|
-
work = fs.mkdtempSync(path.join(os.tmpdir(), 'api-judge-inputs-'));
|
|
12
|
-
execFileSync('git', ['-C', repo, 'init', '-q']);
|
|
13
|
-
fs.mkdirSync(path.join(repo, 'src'));
|
|
14
|
-
fs.writeFileSync(path.join(repo, 'src/job.ts'), 'export function job() {\n return 1;\n}\n');
|
|
15
|
-
execFileSync('git', ['-C', repo, 'add', '-A']);
|
|
16
|
-
execFileSync('git', ['-C', repo, '-c', 'user.email=t@x', '-c', 'user.name=t', 'commit', '-qm', 'init']);
|
|
17
|
-
fs.writeFileSync(path.join(work, 'full.diff'), '+++ b/src/job.ts\n');
|
|
18
|
-
});
|
|
19
|
-
afterEach(() => { for (const d of [repo, work])
|
|
20
|
-
fs.rmSync(d, { recursive: true, force: true }); });
|
|
21
|
-
/** A model that plays a scripted conversation: each entry is what it answers to the next request. */
|
|
22
|
-
function model(turns) {
|
|
23
|
-
const seen = [];
|
|
24
|
-
let i = 0;
|
|
25
|
-
const fetchImpl = (async (_url, init) => {
|
|
26
|
-
const body = JSON.parse(init.body);
|
|
27
|
-
seen.push(body);
|
|
28
|
-
const turn = turns[i++] ?? { text: 'done' };
|
|
29
|
-
if (turn.status)
|
|
30
|
-
return new Response('nope', { status: turn.status });
|
|
31
|
-
const message = turn.tools
|
|
32
|
-
? { role: 'assistant', content: null, tool_calls: turn.tools.map((t, k) => ({ id: `c${i}-${k}`, type: 'function', function: { name: t.name, arguments: JSON.stringify(t.args) } })) }
|
|
33
|
-
: { role: 'assistant', content: turn.text };
|
|
34
|
-
return new Response(JSON.stringify({ choices: [{ message }], usage: turn.usage ?? { prompt_tokens: 100, completion_tokens: 10, prompt_tokens_details: { cached_tokens: 60 } } }), { status: 200, headers: { 'content-type': 'application/json' } });
|
|
35
|
-
});
|
|
36
|
-
return { fetchImpl, seen };
|
|
37
|
-
}
|
|
38
|
-
const options = (fetchImpl, extra = {}) => ({ url: 'https://example.test/v1', model: 'some-model', key: 'k', maxTurns: 10, timeoutMs: 30_000, cwd: repo, roots: [repo, work], fetchImpl, ...extra });
|
|
39
|
-
describe('the API judge', () => {
|
|
40
|
-
it('runs the tools the model asks for inside the checkout and the inputs, then returns its answer with the usage and the trace', async () => {
|
|
41
|
-
const { fetchImpl, seen } = model([
|
|
42
|
-
{ tools: [{ name: 'read_file', args: { path: path.join(work, 'full.diff') } }, { name: 'read_file', args: { path: 'src/job.ts', start_line: 2, end_line: 2 } }] },
|
|
43
|
-
{ tools: [{ name: 'search', args: { pattern: 'return' } }, { name: 'git', args: { args: ['log', '-1', '--format=%s'] } }, { name: 'list_dir', args: { path: 'src' } }] },
|
|
44
|
-
{ text: '{"prior_points":[]}', usage: { prompt_tokens: 50, completion_tokens: 5, cost: 0.01 } },
|
|
45
|
-
]);
|
|
46
|
-
const run = await runApiJudge('review this', options(fetchImpl));
|
|
47
|
-
expect(run.exitCode).toBe(0);
|
|
48
|
-
const answer = JSON.parse(run.stdout);
|
|
49
|
-
expect(answer.result).toBe('{"prior_points":[]}');
|
|
50
|
-
expect(answer.usage).toEqual({ input: 130, cacheRead: 120, cacheWrite: 0, output: 25 });
|
|
51
|
-
expect(answer.cost_usd).toBe(0.01);
|
|
52
|
-
expect(answer.trace.turns).toBe(3);
|
|
53
|
-
expect(answer.trace.calls.map(c => c.tool)).toEqual(['Read', 'Read', 'Grep', 'Bash', 'Glob']);
|
|
54
|
-
const toolResults = (request) => request.messages.filter((m) => m.role === 'tool').map((m) => m.content);
|
|
55
|
-
expect(toolResults(seen[1])).toEqual(['1: +++ b/src/job.ts\n2: ', '2: return 1;']); // turn 1's reads, as the model got them
|
|
56
|
-
expect(toolResults(seen[2]).slice(2)).toEqual(['src/job.ts:2: return 1;', 'init', 'job.ts']); // turn 2's search, git and listing
|
|
57
|
-
expect(seen[0].messages[0].role).toBe('system');
|
|
58
|
-
expect(seen[0].tools.map((t) => t.function.name)).toEqual(['read_file', 'search', 'list_dir', 'git']);
|
|
59
|
-
});
|
|
60
|
-
it('refuses to read outside the roots, a symlink out included, and refuses git that writes or points elsewhere', async () => {
|
|
61
|
-
fs.symlinkSync(os.homedir(), path.join(repo, 'out'));
|
|
62
|
-
const { fetchImpl, seen } = model([
|
|
63
|
-
{ tools: [{ name: 'read_file', args: { path: '/etc/hosts' } }, { name: 'read_file', args: { path: 'out/.bashrc' } }, { name: 'git', args: { args: ['log', '--output=/tmp/x'] } }, { name: 'git', args: { args: ['push'] } }, { name: 'nope', args: {} }] },
|
|
64
|
-
{ text: 'ok' },
|
|
65
|
-
]);
|
|
66
|
-
await runApiJudge('p', options(fetchImpl));
|
|
67
|
-
const results = seen[1].messages.filter((m) => m.role === 'tool').map((m) => m.content);
|
|
68
|
-
expect(results).toEqual([
|
|
69
|
-
'refused: outside the repository and the review inputs', 'refused: outside the repository and the review inputs',
|
|
70
|
-
'refused: only read-only git commands, without options that write or point elsewhere', 'refused: only read-only git commands, without options that write or point elsewhere',
|
|
71
|
-
'refused: no tool named nope',
|
|
72
|
-
]);
|
|
73
|
-
});
|
|
74
|
-
it('fails closed on a turn cap, an HTTP error and a timeout, saying why', async () => {
|
|
75
|
-
const loop = model(Array.from({ length: 5 }, () => ({ tools: [{ name: 'list_dir', args: { path: '.' } }] })));
|
|
76
|
-
expect(await runApiJudge('p', options(loop.fetchImpl, { maxTurns: 3 }))).toMatchObject({ exitCode: 1, stderr: expect.stringContaining('no answer within 3 turns') });
|
|
77
|
-
expect(await runApiJudge('p', options(model([{ status: 429 }]).fetchImpl))).toMatchObject({ exitCode: 1, stderr: expect.stringContaining('HTTP 429') });
|
|
78
|
-
const slow = (async (_u, init) => new Promise((_r, reject) => init.signal.addEventListener('abort', () => reject(new Error('aborted')))));
|
|
79
|
-
expect(await runApiJudge('p', options(slow, { timeoutMs: 50 }))).toMatchObject({ exitCode: 1, stderr: expect.stringContaining('request failed') });
|
|
80
|
-
});
|
|
81
|
-
it('fails closed on an empty final message, saying why, and echoes back only what the API needs to continue', async () => {
|
|
82
|
-
const empty = (async () => new Response(JSON.stringify({ choices: [{ message: { role: 'assistant', content: '', reasoning: 'thinking…' }, finish_reason: 'length' }], usage: {} }), { status: 200 }));
|
|
83
|
-
expect(await runApiJudge('p', options(empty))).toMatchObject({ exitCode: 1, stderr: expect.stringContaining('an empty answer on turn 1 (finish_reason length)') });
|
|
84
|
-
const providerError = (async () => new Response(JSON.stringify({ choices: [{ message: { role: 'assistant', content: '' }, finish_reason: 'error', error: { message: 'Provider returned error', code: 502 } }], usage: {} }), { status: 200 }));
|
|
85
|
-
expect(await runApiJudge('p', options(providerError))).toMatchObject({ exitCode: 1, stderr: expect.stringContaining('the API reported an error: {"message":"Provider returned error","code":502}') });
|
|
86
|
-
const { fetchImpl, seen } = model([{ tools: [{ name: 'list_dir', args: { path: '.' } }] }, { text: 'ok' }]);
|
|
87
|
-
await runApiJudge('p', options(fetchImpl));
|
|
88
|
-
const echoed = seen[1].messages.find((m) => m.role === 'assistant');
|
|
89
|
-
expect(Object.keys(echoed).sort()).toEqual(['content', 'role', 'tool_calls']); // no reasoning prose sent back
|
|
90
|
-
expect(seen[0].max_tokens).toBe(32000);
|
|
91
|
-
});
|
|
92
|
-
it('gives the judge its inputs inline, smallest first, the diff cut with a note when the budget runs out', async () => {
|
|
93
|
-
const { fetchImpl, seen } = model([{ text: 'ok' }]);
|
|
94
|
-
const inputs = [{ path: '/w/full.diff', text: 'x'.repeat(300_000) }, { path: '/w/pr-description.md', text: 'the description' }, { path: '/w/previous-reviews.md', text: 'the reviews' }];
|
|
95
|
-
await runApiJudge('review this', options(fetchImpl, { inputs }));
|
|
96
|
-
const first = seen[0].messages[1].content;
|
|
97
|
-
expect(first.startsWith('review this\n\nThe inputs named above, inline')).toBe(true);
|
|
98
|
-
expect(first.indexOf('### /w/previous-reviews.md')).toBeLessThan(first.indexOf('### /w/full.diff')); // smallest first
|
|
99
|
-
expect(first).toContain('…[cut here: read /w/full.diff with read_file for the rest]');
|
|
100
|
-
expect(first.length).toBeLessThan(241_000);
|
|
101
|
-
});
|
|
102
|
-
it('passes the reasoning effort when asked', async () => {
|
|
103
|
-
const { fetchImpl, seen } = model([{ text: 'ok' }]);
|
|
104
|
-
await runApiJudge('p', options(fetchImpl, { reasoning: 'low' }));
|
|
105
|
-
expect(seen[0].reasoning_effort).toBe('low');
|
|
106
|
-
expect(seen[0].model).toBe('some-model');
|
|
107
|
-
});
|
|
108
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,118 +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 { backgroundReview, reviewStatus, startBackgroundReview } from './background.js';
|
|
8
|
-
import { VerdictStore } from './store.js';
|
|
9
|
-
let repo;
|
|
10
|
-
let bin;
|
|
11
|
-
const git = (...args) => execFileSync('git', ['-C', repo, ...args], { encoding: 'utf8' }).trim();
|
|
12
|
-
const config = ConfigSchema.parse({ version: 1, review: { reviewer: { enabled: true } } });
|
|
13
|
-
const originalPath = process.env.PATH;
|
|
14
|
-
let PR = { number: 7, state: 'OPEN', isDraft: false, author: { login: 'author' }, body: 'desc' };
|
|
15
|
-
const VERDICT = JSON.stringify({ prior_points: [], redundant: [], reads: [], scans: [], merge_impact: [], findings: [{ class: 'correctness', file: 'src/job.ts', line: 1, issue: 'never locks', quote: 'export const job = 1;', consequence: 'a second run reads stale rows' }], carried: [], resolved_previous: [] });
|
|
16
|
-
/** Real git; a pull request on the branch; a claude that answers one finding. */
|
|
17
|
-
const exec = async (command, args, options) => {
|
|
18
|
-
if (command === 'git') {
|
|
19
|
-
try {
|
|
20
|
-
return { exitCode: 0, stdout: execFileSync('git', args, { cwd: options.cwd, encoding: 'utf8' }), stderr: '' };
|
|
21
|
-
}
|
|
22
|
-
catch (error) {
|
|
23
|
-
return { exitCode: 1, stdout: '', stderr: String(error.message) };
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
if (command === 'gh') {
|
|
27
|
-
if (args[0] === 'pr')
|
|
28
|
-
return { exitCode: 0, stdout: JSON.stringify(PR), stderr: '' };
|
|
29
|
-
return { exitCode: 0, stdout: '[]', stderr: '' };
|
|
30
|
-
}
|
|
31
|
-
if (args[0] === '--version')
|
|
32
|
-
return { exitCode: 0, stdout: '1.0.0\n', stderr: '' };
|
|
33
|
-
return { exitCode: 0, stdout: JSON.stringify({ result: VERDICT }), stderr: '' };
|
|
34
|
-
};
|
|
35
|
-
beforeEach(() => {
|
|
36
|
-
bin = fs.mkdtempSync(path.join(os.tmpdir(), 'bin-'));
|
|
37
|
-
fs.writeFileSync(path.join(bin, process.platform === 'win32' ? 'claude.cmd' : 'claude'), '#!/bin/sh\nexit 0\n', { mode: 0o755 });
|
|
38
|
-
process.env.PATH = [bin, originalPath ?? ''].join(path.delimiter);
|
|
39
|
-
repo = fs.mkdtempSync(path.join(os.tmpdir(), 'background-'));
|
|
40
|
-
git('init', '-q', '-b', 'main');
|
|
41
|
-
git('config', 'user.email', 't@example.com');
|
|
42
|
-
git('config', 'user.name', 't');
|
|
43
|
-
git('config', 'commit.gpgsign', 'false');
|
|
44
|
-
fs.writeFileSync(path.join(repo, 'a.ts'), 'export const a = 1;\n');
|
|
45
|
-
git('add', '-A');
|
|
46
|
-
git('commit', '-qm', 'init');
|
|
47
|
-
git('checkout', '-qb', 'feature');
|
|
48
|
-
fs.mkdirSync(path.join(repo, 'src'));
|
|
49
|
-
fs.writeFileSync(path.join(repo, 'src/job.ts'), 'export const job = 1;\n');
|
|
50
|
-
git('add', '-A');
|
|
51
|
-
git('commit', '-qm', 'job');
|
|
52
|
-
});
|
|
53
|
-
afterEach(() => {
|
|
54
|
-
process.env.PATH = originalPath;
|
|
55
|
-
try {
|
|
56
|
-
git('worktree', 'prune');
|
|
57
|
-
}
|
|
58
|
-
catch { /* gone */ }
|
|
59
|
-
for (const dir of [repo, bin])
|
|
60
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
61
|
-
});
|
|
62
|
-
describe('the background review', () => {
|
|
63
|
-
it('reviews the pushed commit in a worktree of its own, records the verdict under the branch, and leaves the checkout alone', async () => {
|
|
64
|
-
const head = git('rev-parse', 'HEAD');
|
|
65
|
-
fs.writeFileSync(path.join(repo, 'src/job.ts'), 'export const job = 2; // edited after the push\n'); // must not be what the reviewer reads
|
|
66
|
-
const lines = [];
|
|
67
|
-
const result = await backgroundReview(repo, { head, branch: 'feature', base: 'main' }, config, exec, line => lines.push(line));
|
|
68
|
-
expect(result).toMatchObject({ outcome: 'findings', pr: 7 });
|
|
69
|
-
expect(result.items).toEqual([expect.objectContaining({ file: 'src/job.ts', issue: 'never locks' })]);
|
|
70
|
-
expect(lines.at(-1)).toContain('never locks');
|
|
71
|
-
expect(fs.readFileSync(path.join(repo, 'src/job.ts'), 'utf8')).toContain('edited after the push');
|
|
72
|
-
const store = (await VerdictStore.open(repo, exec));
|
|
73
|
-
expect(store.branchState('feature')).toMatchObject({ head, mode: 'full' });
|
|
74
|
-
expect(fs.existsSync(store.worktreeDir(head))).toBe(false); // removed when done
|
|
75
|
-
const status = await reviewStatus(repo, 'feature', exec);
|
|
76
|
-
expect(status).toMatchObject({ branch: 'feature', last: { head, mode: 'full', open: [expect.objectContaining({ issue: 'never locks' })] } });
|
|
77
|
-
expect(status?.running).toBeUndefined();
|
|
78
|
-
});
|
|
79
|
-
it('asks whether a review is due before checking anything out, and the status says why it was skipped', async () => {
|
|
80
|
-
PR = { ...PR, isDraft: true };
|
|
81
|
-
try {
|
|
82
|
-
const head = git('rev-parse', 'HEAD');
|
|
83
|
-
const result = await backgroundReview(repo, { head, branch: 'feature', base: 'main' }, config, exec, () => undefined);
|
|
84
|
-
expect(result).toMatchObject({ outcome: 'skipped', reason: expect.stringContaining('is a draft') });
|
|
85
|
-
const store = (await VerdictStore.open(repo, exec));
|
|
86
|
-
expect(fs.existsSync(store.worktreeDir(head))).toBe(false); // never checked out
|
|
87
|
-
expect((await reviewStatus(repo, 'feature', exec))?.attempt).toMatchObject({ head, outcome: 'skipped', reason: expect.stringContaining('is a draft') });
|
|
88
|
-
}
|
|
89
|
-
finally {
|
|
90
|
-
PR = { ...PR, isDraft: false };
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
|
-
it('starts detached with a pid the status reports, and a newer start for the branch ends the older one', async () => {
|
|
94
|
-
const head = git('rev-parse', 'HEAD');
|
|
95
|
-
const sleeper = [process.execPath, '-e', 'setTimeout(() => {}, 20000)'];
|
|
96
|
-
const log = await startBackgroundReview(repo, { head, branch: 'feature', base: 'main' }, sleeper, exec);
|
|
97
|
-
expect(log).toMatch(/feature\.log$/);
|
|
98
|
-
const first = (await reviewStatus(repo, 'feature', exec))?.running;
|
|
99
|
-
expect(first).toMatchObject({ head });
|
|
100
|
-
expect(alive(first.pid)).toBe(true);
|
|
101
|
-
await startBackgroundReview(repo, { head: 'b'.repeat(40), branch: 'feature', base: 'main' }, sleeper, exec);
|
|
102
|
-
const second = (await reviewStatus(repo, 'feature', exec))?.running;
|
|
103
|
-
expect(second?.pid).not.toBe(first.pid);
|
|
104
|
-
for (let waited = 0; alive(first.pid) && waited < 3000; waited += 50)
|
|
105
|
-
await new Promise(resolve => setTimeout(resolve, 50)); // under load the signal lands later
|
|
106
|
-
expect(alive(first.pid)).toBe(false);
|
|
107
|
-
process.kill(second.pid);
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
function alive(pid) {
|
|
111
|
-
try {
|
|
112
|
-
process.kill(pid, 0);
|
|
113
|
-
return true;
|
|
114
|
-
}
|
|
115
|
-
catch {
|
|
116
|
-
return false;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import os from 'os';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import { describe, expect, it } from 'vitest';
|
|
5
|
-
import { buildContext, dismissedAs } from './context.js';
|
|
6
|
-
const dismissal = { id: 'abcdef0123', file: 'src/x.ts', line: 40, class: 'correctness', issue: 'cache key built from user id may collide', reason: 'ids are unique per tenant', at: '2026-10-06' };
|
|
7
|
-
const finding = (over) => ({ id: 'ffff000011', kind: 'finding', class: 'correctness', file: 'src/x.ts', line: 40, issue: 'cache key built from the user id may collide', consequence: 'two users share an entry', ...over });
|
|
8
|
-
describe('a dismissal', () => {
|
|
9
|
-
it('covers the same finding re-worded nearby', () => {
|
|
10
|
-
expect(dismissedAs(finding({}), [dismissal])).toBe(dismissal);
|
|
11
|
-
expect(dismissedAs(finding({ line: 42, issue: 'the cache key built from user id may collide' }), [dismissal])).toBe(dismissal);
|
|
12
|
-
expect(dismissedAs(finding({ id: 'abcdef0123', issue: 'anything at all' }), [dismissal])).toBe(dismissal); // its own id
|
|
13
|
-
});
|
|
14
|
-
it('never hides a different bug, even on the same line and in the same class', () => {
|
|
15
|
-
expect(dismissedAs(finding({ issue: 'cache entry never invalidated when the user changes' }), [dismissal])).toBeUndefined();
|
|
16
|
-
expect(dismissedAs(finding({ line: 42, issue: 'cache entry never invalidated when user id changes' }), [dismissal])).toBeUndefined();
|
|
17
|
-
expect(dismissedAs(finding({ line: 80 }), [dismissal])).toBeUndefined(); // far away
|
|
18
|
-
expect(dismissedAs(finding({ class: 'production-cost' }), [dismissal])).toBeUndefined();
|
|
19
|
-
expect(dismissedAs({ ...finding({}), kind: 'prior' }, [dismissal])).toBeUndefined(); // a human's point is never dismissed this way
|
|
20
|
-
});
|
|
21
|
-
});
|
|
22
|
-
describe('what the judges are told the team knows', () => {
|
|
23
|
-
it('shows verified lessons by default, candidates too with all, none when off, each with the words it came from', () => {
|
|
24
|
-
const repo = fs.mkdtempSync(path.join(os.tmpdir(), 'context-'));
|
|
25
|
-
try {
|
|
26
|
-
const lesson = { file: 'src/a.ts', symbols: ['save'], createdAt: '', updatedAt: '' };
|
|
27
|
-
fs.mkdirSync(path.join(repo, '.rigour'));
|
|
28
|
-
fs.writeFileSync(path.join(repo, '.rigour', 'review-lessons.json'), JSON.stringify({ version: 1, lessons: [
|
|
29
|
-
{ ...lesson, id: 'v', text: 'Make every save idempotent on retry.', state: 'verified', evidence: [{ pr: 3, comment: 'a', author: 'p', said: 'A retry here saves twice.' }, { pr: 4, comment: 'b', author: 'p' }] },
|
|
30
|
-
{ ...lesson, id: 'c', text: 'Log the save id on failure.', state: 'candidate', evidence: [{ pr: 5, comment: 'c', author: 'p' }] },
|
|
31
|
-
] }));
|
|
32
|
-
const diff = 'diff --git a/src/a.ts b/src/a.ts\n--- a/src/a.ts\n+++ b/src/a.ts\n@@ -0,0 +1,1 @@\n+export const save = 1;\n';
|
|
33
|
-
const told = (lessons) => buildContext({ cwd: repo, stateRoot: repo, dismissals: [], diff, router: undefined, lessons, previousPanel: undefined, touched: new Set(), docs: [], checks: [] }).text;
|
|
34
|
-
expect(told()).toContain('src/a.ts: Make every save idempotent on retry. (in their words: "A retry here saves twice.") (acted on in PR #3, #4)');
|
|
35
|
-
expect(told()).not.toContain('Log the save id');
|
|
36
|
-
expect(told('all')).toContain('Log the save id on failure.');
|
|
37
|
-
expect(told('off')).not.toContain('idempotent');
|
|
38
|
-
fs.writeFileSync(path.join(repo, 'AGENTS.md'), '- Every call to `save` in `src/a.ts` must be idempotent on retry.\n');
|
|
39
|
-
expect(told()).toMatch(/## Rules this repository wrote for itself[^\n]*\n- \[[0-9a-f]{10}\] \(AGENTS\.md, requirement\) Every call to `save` in `src\/a\.ts` must be idempotent on retry\./);
|
|
40
|
-
}
|
|
41
|
-
finally {
|
|
42
|
-
fs.rmSync(repo, { recursive: true, force: true });
|
|
43
|
-
}
|
|
44
|
-
});
|
|
45
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import os from 'os';
|
|
2
|
-
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
-
import { defaultExec, githubToken } from './exec.js';
|
|
4
|
-
describe('running a command', () => {
|
|
5
|
-
it('passes the command\'s own output through', async () => {
|
|
6
|
-
const result = await defaultExec(process.execPath, ['-e', 'process.stdout.write("out"); process.stderr.write("err"); process.exit(3)'], { cwd: os.tmpdir(), timeoutMs: 30_000 });
|
|
7
|
-
expect(result).toEqual({ exitCode: 3, stdout: 'out', stderr: 'err' });
|
|
8
|
-
});
|
|
9
|
-
it('gives the command an ended stdin, so one that reads it never waits and one that exits at once never fails', async () => {
|
|
10
|
-
const reader = await defaultExec(process.execPath, ['-e', 'let n = 0; process.stdin.on("data", d => n += d.length).on("end", () => process.stdout.write(`read ${n}`))'], { cwd: os.tmpdir(), timeoutMs: 30_000 });
|
|
11
|
-
expect(reader).toEqual({ exitCode: 0, stdout: 'read 0', stderr: '' });
|
|
12
|
-
const runs = await Promise.all(Array.from({ length: 50 }, () => defaultExec(process.execPath, ['-e', 'process.stdout.write("ok")'], { cwd: os.tmpdir(), timeoutMs: 30_000 })));
|
|
13
|
-
expect(runs.every(r => r.exitCode === 0 && r.stdout === 'ok')).toBe(true);
|
|
14
|
-
});
|
|
15
|
-
it('keeps a variable the command must not see out of its environment, and passes the rest through', async () => {
|
|
16
|
-
process.env.RIGOUR_TEST_SECRET = 'gateway-key';
|
|
17
|
-
process.env.RIGOUR_TEST_KEPT = 'kept';
|
|
18
|
-
try {
|
|
19
|
-
const print = ['-e', 'process.stdout.write(JSON.stringify({ secret: process.env.RIGOUR_TEST_SECRET ?? null, kept: process.env.RIGOUR_TEST_KEPT ?? null, added: process.env.RIGOUR_TEST_ADDED ?? null }))'];
|
|
20
|
-
const hidden = await defaultExec(process.execPath, print, { cwd: os.tmpdir(), timeoutMs: 30_000, env: { RIGOUR_TEST_ADDED: 'added' }, unset: ['RIGOUR_TEST_SECRET'] });
|
|
21
|
-
expect(JSON.parse(hidden.stdout)).toEqual({ secret: null, kept: 'kept', added: 'added' });
|
|
22
|
-
const inherited = await defaultExec(process.execPath, print, { cwd: os.tmpdir(), timeoutMs: 30_000 });
|
|
23
|
-
expect(JSON.parse(inherited.stdout)).toEqual({ secret: 'gateway-key', kept: 'kept', added: null });
|
|
24
|
-
}
|
|
25
|
-
finally {
|
|
26
|
-
delete process.env.RIGOUR_TEST_SECRET;
|
|
27
|
-
delete process.env.RIGOUR_TEST_KEPT;
|
|
28
|
-
}
|
|
29
|
-
});
|
|
30
|
-
it('says why a command that never answered failed: not started, or timed out', async () => {
|
|
31
|
-
const missing = await defaultExec('rigour-no-such-command', [], { cwd: os.tmpdir(), timeoutMs: 30_000 });
|
|
32
|
-
expect(missing.exitCode).not.toBe(0);
|
|
33
|
-
expect(missing.stderr).toContain('rigour-no-such-command'); // ENOENT on Unix, "is not recognized" on Windows
|
|
34
|
-
const slow = await defaultExec(process.execPath, ['-e', 'setTimeout(() => {}, 10_000)'], { cwd: os.tmpdir(), timeoutMs: 200 });
|
|
35
|
-
expect(slow.exitCode).not.toBe(0);
|
|
36
|
-
expect(slow.stderr).toMatch(/timed out/i);
|
|
37
|
-
});
|
|
38
|
-
});
|
|
39
|
-
describe('the token GitHub is read with', () => {
|
|
40
|
-
afterEach(() => { vi.unstubAllEnvs(); });
|
|
41
|
-
const gh = (tokens, asked) => async (_command, args) => {
|
|
42
|
-
asked.push(args);
|
|
43
|
-
const user = args[args.indexOf('--user') + 1];
|
|
44
|
-
const token = args.includes('--user') ? tokens[user] : tokens.active;
|
|
45
|
-
return token ? { exitCode: 0, stdout: `${token}\n`, stderr: '' } : { exitCode: 1, stdout: '', stderr: 'no account' };
|
|
46
|
-
};
|
|
47
|
-
it('is an explicit token first, then the named account, and the active account only when none is named', async () => {
|
|
48
|
-
vi.stubEnv('GH_TOKEN', '');
|
|
49
|
-
vi.stubEnv('GITHUB_TOKEN', '');
|
|
50
|
-
const asked = [];
|
|
51
|
-
const tokens = { active: 'other-employer', work: 'work-token' };
|
|
52
|
-
expect(await githubToken(os.tmpdir(), 'work', gh(tokens, asked))).toBe('work-token');
|
|
53
|
-
expect(await githubToken(os.tmpdir(), undefined, gh(tokens, asked))).toBe('other-employer');
|
|
54
|
-
await expect(githubToken(os.tmpdir(), 'missing', gh(tokens, asked))).rejects.toThrow('GitHub account missing is named for reading');
|
|
55
|
-
expect(asked).toEqual([['auth', 'token', '--user', 'work'], ['auth', 'token'], ['auth', 'token', '--user', 'missing']]); // a named account never falls back to the active one
|
|
56
|
-
vi.stubEnv('GITHUB_TOKEN', 'ci-token');
|
|
57
|
-
expect(await githubToken(os.tmpdir(), 'work', gh(tokens, asked))).toBe('ci-token');
|
|
58
|
-
});
|
|
59
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|