@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,325 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
const mockFindFiles = vi.hoisted(() => vi.fn());
|
|
3
|
-
const mockReadFile = vi.hoisted(() => vi.fn());
|
|
4
|
-
vi.mock('../utils/scanner.js', () => ({
|
|
5
|
-
FileScanner: { findFiles: mockFindFiles },
|
|
6
|
-
}));
|
|
7
|
-
vi.mock('fs-extra', () => ({
|
|
8
|
-
default: {
|
|
9
|
-
readFile: mockReadFile,
|
|
10
|
-
pathExists: vi.fn().mockResolvedValue(false),
|
|
11
|
-
pathExistsSync: vi.fn().mockReturnValue(false),
|
|
12
|
-
readFileSync: vi.fn().mockReturnValue(''),
|
|
13
|
-
readJson: vi.fn().mockResolvedValue(null),
|
|
14
|
-
readdirSync: vi.fn().mockReturnValue([]),
|
|
15
|
-
},
|
|
16
|
-
}));
|
|
17
|
-
import { TestQualityGate } from './test-quality.js';
|
|
18
|
-
describe('TestQualityGate — JS/TS tests', () => {
|
|
19
|
-
let gate;
|
|
20
|
-
beforeEach(() => {
|
|
21
|
-
gate = new TestQualityGate();
|
|
22
|
-
vi.clearAllMocks();
|
|
23
|
-
});
|
|
24
|
-
it('should flag empty test bodies', async () => {
|
|
25
|
-
mockFindFiles.mockResolvedValue(['src/utils.test.ts']);
|
|
26
|
-
mockReadFile.mockResolvedValue(`
|
|
27
|
-
describe('Utils', () => {
|
|
28
|
-
it('should do something', () => {
|
|
29
|
-
});
|
|
30
|
-
});
|
|
31
|
-
`);
|
|
32
|
-
const failures = await gate.run({ cwd: '/project' });
|
|
33
|
-
expect(failures).toHaveLength(1);
|
|
34
|
-
expect(failures[0].details).toContain('empty-test');
|
|
35
|
-
});
|
|
36
|
-
it('should flag tests with no assertions', async () => {
|
|
37
|
-
mockFindFiles.mockResolvedValue(['src/api.test.ts']);
|
|
38
|
-
mockReadFile.mockResolvedValue(`
|
|
39
|
-
describe('API', () => {
|
|
40
|
-
it('should fetch data', () => {
|
|
41
|
-
const data = fetchData();
|
|
42
|
-
console.log(data);
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
`);
|
|
46
|
-
const failures = await gate.run({ cwd: '/project' });
|
|
47
|
-
expect(failures).toHaveLength(1);
|
|
48
|
-
expect(failures[0].details).toContain('no-assertion');
|
|
49
|
-
});
|
|
50
|
-
it('should not flag type-level tests, where the compiler is the assertion', async () => {
|
|
51
|
-
mockFindFiles.mockResolvedValue(['src/types.test.ts']);
|
|
52
|
-
mockReadFile.mockResolvedValue(`
|
|
53
|
-
it('our document type accepts the published one', () => {
|
|
54
|
-
const _published: Document = null as unknown as Published.Document;
|
|
55
|
-
});
|
|
56
|
-
it('infers the output', () => {
|
|
57
|
-
expectTypeOf(output).toEqualTypeOf<string>();
|
|
58
|
-
});
|
|
59
|
-
`);
|
|
60
|
-
const failures = await gate.run({ cwd: '/project' });
|
|
61
|
-
expect(failures).toHaveLength(0);
|
|
62
|
-
});
|
|
63
|
-
it('should flag tautological assertions', async () => {
|
|
64
|
-
mockFindFiles.mockResolvedValue(['src/basic.test.ts']);
|
|
65
|
-
mockReadFile.mockResolvedValue(`
|
|
66
|
-
describe('Basic', () => {
|
|
67
|
-
it('should pass', () => {
|
|
68
|
-
expect(true).toBe(true);
|
|
69
|
-
});
|
|
70
|
-
});
|
|
71
|
-
`);
|
|
72
|
-
const failures = await gate.run({ cwd: '/project' });
|
|
73
|
-
expect(failures).toHaveLength(1);
|
|
74
|
-
expect(failures[0].details).toContain('tautological');
|
|
75
|
-
});
|
|
76
|
-
it('should flag mock-heavy tests', async () => {
|
|
77
|
-
mockFindFiles.mockResolvedValue(['src/service.test.ts']);
|
|
78
|
-
mockReadFile.mockResolvedValue(`
|
|
79
|
-
describe('Service', () => {
|
|
80
|
-
it('should process', () => {
|
|
81
|
-
const mock1 = vi.fn();
|
|
82
|
-
const mock2 = vi.fn();
|
|
83
|
-
const mock3 = vi.fn();
|
|
84
|
-
const mock4 = vi.fn();
|
|
85
|
-
const mock5 = vi.fn();
|
|
86
|
-
const mock6 = vi.fn();
|
|
87
|
-
expect(mock1).toBeDefined();
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
`);
|
|
91
|
-
const failures = await gate.run({ cwd: '/project' });
|
|
92
|
-
expect(failures).toHaveLength(1);
|
|
93
|
-
expect(failures[0].details).toContain('mock-heavy');
|
|
94
|
-
});
|
|
95
|
-
it('should NOT flag well-written tests', async () => {
|
|
96
|
-
mockFindFiles.mockResolvedValue(['src/good.test.ts']);
|
|
97
|
-
mockReadFile.mockResolvedValue(`
|
|
98
|
-
describe('Calculator', () => {
|
|
99
|
-
it('should add two numbers', () => {
|
|
100
|
-
const result = add(2, 3);
|
|
101
|
-
expect(result).toBe(5);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it('should handle negative numbers', () => {
|
|
105
|
-
const result = add(-1, -2);
|
|
106
|
-
expect(result).toBe(-3);
|
|
107
|
-
});
|
|
108
|
-
});
|
|
109
|
-
`);
|
|
110
|
-
const failures = await gate.run({ cwd: '/project' });
|
|
111
|
-
expect(failures).toHaveLength(0);
|
|
112
|
-
});
|
|
113
|
-
it('should NOT flag tests with expect().toEqual()', async () => {
|
|
114
|
-
mockFindFiles.mockResolvedValue(['src/valid.test.ts']);
|
|
115
|
-
mockReadFile.mockResolvedValue(`
|
|
116
|
-
describe('Parser', () => {
|
|
117
|
-
it('should parse JSON', () => {
|
|
118
|
-
const result = parseConfig('{"key": "value"}');
|
|
119
|
-
expect(result).toEqual({ key: 'value' });
|
|
120
|
-
});
|
|
121
|
-
});
|
|
122
|
-
`);
|
|
123
|
-
const failures = await gate.run({ cwd: '/project' });
|
|
124
|
-
expect(failures).toHaveLength(0);
|
|
125
|
-
});
|
|
126
|
-
it('should not scan when disabled', async () => {
|
|
127
|
-
const disabled = new TestQualityGate({ enabled: false });
|
|
128
|
-
const failures = await disabled.run({ cwd: '/project' });
|
|
129
|
-
expect(failures).toHaveLength(0);
|
|
130
|
-
expect(mockFindFiles).not.toHaveBeenCalled();
|
|
131
|
-
});
|
|
132
|
-
});
|
|
133
|
-
describe('TestQualityGate — Python tests', () => {
|
|
134
|
-
let gate;
|
|
135
|
-
beforeEach(() => {
|
|
136
|
-
gate = new TestQualityGate();
|
|
137
|
-
vi.clearAllMocks();
|
|
138
|
-
});
|
|
139
|
-
it('should flag empty test functions', async () => {
|
|
140
|
-
mockFindFiles.mockResolvedValue(['test_utils.py']);
|
|
141
|
-
mockReadFile.mockResolvedValue(`
|
|
142
|
-
def test_something():
|
|
143
|
-
pass
|
|
144
|
-
`);
|
|
145
|
-
const failures = await gate.run({ cwd: '/project' });
|
|
146
|
-
expect(failures).toHaveLength(1);
|
|
147
|
-
expect(failures[0].details).toContain('empty-test');
|
|
148
|
-
});
|
|
149
|
-
it('should flag tests with no assertions', async () => {
|
|
150
|
-
mockFindFiles.mockResolvedValue(['test_api.py']);
|
|
151
|
-
mockReadFile.mockResolvedValue(`
|
|
152
|
-
def test_fetch_data():
|
|
153
|
-
data = fetch_data()
|
|
154
|
-
print(data)
|
|
155
|
-
result = process(data)
|
|
156
|
-
`);
|
|
157
|
-
const failures = await gate.run({ cwd: '/project' });
|
|
158
|
-
expect(failures).toHaveLength(1);
|
|
159
|
-
expect(failures[0].details).toContain('no-assertion');
|
|
160
|
-
});
|
|
161
|
-
it('should flag tautological Python assertions', async () => {
|
|
162
|
-
mockFindFiles.mockResolvedValue(['test_basic.py']);
|
|
163
|
-
mockReadFile.mockResolvedValue(`
|
|
164
|
-
def test_always_passes():
|
|
165
|
-
assert True
|
|
166
|
-
`);
|
|
167
|
-
const failures = await gate.run({ cwd: '/project' });
|
|
168
|
-
expect(failures).toHaveLength(1);
|
|
169
|
-
expect(failures[0].details).toContain('tautological');
|
|
170
|
-
});
|
|
171
|
-
it('should NOT flag Python tests with real assertions', async () => {
|
|
172
|
-
mockFindFiles.mockResolvedValue(['test_calc.py']);
|
|
173
|
-
mockReadFile.mockResolvedValue(`
|
|
174
|
-
def test_addition():
|
|
175
|
-
result = add(2, 3)
|
|
176
|
-
assert result == 5
|
|
177
|
-
|
|
178
|
-
def test_subtraction():
|
|
179
|
-
result = subtract(5, 3)
|
|
180
|
-
assert result == 2
|
|
181
|
-
`);
|
|
182
|
-
const failures = await gate.run({ cwd: '/project' });
|
|
183
|
-
expect(failures).toHaveLength(0);
|
|
184
|
-
});
|
|
185
|
-
it('should NOT flag tests using pytest.raises', async () => {
|
|
186
|
-
mockFindFiles.mockResolvedValue(['test_errors.py']);
|
|
187
|
-
mockReadFile.mockResolvedValue(`
|
|
188
|
-
def test_invalid_input():
|
|
189
|
-
with pytest.raises(ValueError):
|
|
190
|
-
parse_int("not a number")
|
|
191
|
-
`);
|
|
192
|
-
const failures = await gate.run({ cwd: '/project' });
|
|
193
|
-
expect(failures).toHaveLength(0);
|
|
194
|
-
});
|
|
195
|
-
});
|
|
196
|
-
describe('TestQualityGate — Go tests', () => {
|
|
197
|
-
let gate;
|
|
198
|
-
beforeEach(() => {
|
|
199
|
-
gate = new TestQualityGate();
|
|
200
|
-
vi.clearAllMocks();
|
|
201
|
-
});
|
|
202
|
-
it('should flag empty Go test functions', async () => {
|
|
203
|
-
mockFindFiles.mockResolvedValue(['utils_test.go']);
|
|
204
|
-
mockReadFile.mockResolvedValue(`
|
|
205
|
-
package utils
|
|
206
|
-
|
|
207
|
-
import "testing"
|
|
208
|
-
|
|
209
|
-
func TestSomething(t *testing.T) {
|
|
210
|
-
}
|
|
211
|
-
`);
|
|
212
|
-
const failures = await gate.run({ cwd: '/project' });
|
|
213
|
-
expect(failures).toHaveLength(1);
|
|
214
|
-
expect(failures[0].details).toContain('empty-test');
|
|
215
|
-
});
|
|
216
|
-
it('should flag Go tests without assertions', async () => {
|
|
217
|
-
mockFindFiles.mockResolvedValue(['calc_test.go']);
|
|
218
|
-
mockReadFile.mockResolvedValue(`
|
|
219
|
-
package calc
|
|
220
|
-
|
|
221
|
-
import "testing"
|
|
222
|
-
|
|
223
|
-
func TestAdd(t *testing.T) {
|
|
224
|
-
result := Add(2, 3)
|
|
225
|
-
_ = result
|
|
226
|
-
}
|
|
227
|
-
`);
|
|
228
|
-
const failures = await gate.run({ cwd: '/project' });
|
|
229
|
-
expect(failures).toHaveLength(1);
|
|
230
|
-
expect(failures[0].details).toContain('no-assertion');
|
|
231
|
-
});
|
|
232
|
-
it('should NOT flag Go tests with t.Error assertions', async () => {
|
|
233
|
-
mockFindFiles.mockResolvedValue(['good_test.go']);
|
|
234
|
-
mockReadFile.mockResolvedValue(`
|
|
235
|
-
package calc
|
|
236
|
-
|
|
237
|
-
import "testing"
|
|
238
|
-
|
|
239
|
-
func TestAdd(t *testing.T) {
|
|
240
|
-
result := Add(2, 3)
|
|
241
|
-
if result != 5 {
|
|
242
|
-
t.Errorf("expected 5, got %d", result)
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
`);
|
|
246
|
-
const failures = await gate.run({ cwd: '/project' });
|
|
247
|
-
expect(failures).toHaveLength(0);
|
|
248
|
-
});
|
|
249
|
-
it('should NOT flag Go tests with testify assertions', async () => {
|
|
250
|
-
mockFindFiles.mockResolvedValue(['testify_test.go']);
|
|
251
|
-
mockReadFile.mockResolvedValue(`
|
|
252
|
-
package main
|
|
253
|
-
|
|
254
|
-
import (
|
|
255
|
-
"testing"
|
|
256
|
-
"github.com/stretchr/testify/assert"
|
|
257
|
-
)
|
|
258
|
-
|
|
259
|
-
func TestAdd(t *testing.T) {
|
|
260
|
-
result := Add(2, 3)
|
|
261
|
-
assert.Equal(t, 5, result)
|
|
262
|
-
}
|
|
263
|
-
`);
|
|
264
|
-
const failures = await gate.run({ cwd: '/project' });
|
|
265
|
-
expect(failures).toHaveLength(0);
|
|
266
|
-
});
|
|
267
|
-
});
|
|
268
|
-
describe('TestQualityGate — Java tests', () => {
|
|
269
|
-
let gate;
|
|
270
|
-
beforeEach(() => {
|
|
271
|
-
gate = new TestQualityGate();
|
|
272
|
-
vi.clearAllMocks();
|
|
273
|
-
});
|
|
274
|
-
it('should flag Java tests without assertions', async () => {
|
|
275
|
-
mockFindFiles.mockResolvedValue(['CalcTest.java']);
|
|
276
|
-
mockReadFile.mockResolvedValue(`
|
|
277
|
-
import org.junit.jupiter.api.Test;
|
|
278
|
-
|
|
279
|
-
class CalcTest {
|
|
280
|
-
@Test
|
|
281
|
-
void testAdd() {
|
|
282
|
-
int result = Calculator.add(2, 3);
|
|
283
|
-
System.out.println(result);
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
`);
|
|
287
|
-
const failures = await gate.run({ cwd: '/project' });
|
|
288
|
-
expect(failures).toHaveLength(1);
|
|
289
|
-
expect(failures[0].details).toContain('no-assertion');
|
|
290
|
-
});
|
|
291
|
-
it('should NOT flag Java tests with assertions', async () => {
|
|
292
|
-
mockFindFiles.mockResolvedValue(['GoodTest.java']);
|
|
293
|
-
mockReadFile.mockResolvedValue(`
|
|
294
|
-
import org.junit.jupiter.api.Test;
|
|
295
|
-
import static org.junit.jupiter.api.Assertions.*;
|
|
296
|
-
|
|
297
|
-
class GoodTest {
|
|
298
|
-
@Test
|
|
299
|
-
void testAdd() {
|
|
300
|
-
int result = Calculator.add(2, 3);
|
|
301
|
-
assertEquals(5, result);
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
`);
|
|
305
|
-
const failures = await gate.run({ cwd: '/project' });
|
|
306
|
-
expect(failures).toHaveLength(0);
|
|
307
|
-
});
|
|
308
|
-
it('should flag tautological Java assertions', async () => {
|
|
309
|
-
mockFindFiles.mockResolvedValue(['TautTest.java']);
|
|
310
|
-
mockReadFile.mockResolvedValue(`
|
|
311
|
-
import org.junit.jupiter.api.Test;
|
|
312
|
-
import static org.junit.jupiter.api.Assertions.*;
|
|
313
|
-
|
|
314
|
-
class TautTest {
|
|
315
|
-
@Test
|
|
316
|
-
void testAlwaysPasses() {
|
|
317
|
-
assertTrue(true);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
`);
|
|
321
|
-
const failures = await gate.run({ cwd: '/project' });
|
|
322
|
-
expect(failures.length).toBeGreaterThanOrEqual(1);
|
|
323
|
-
expect(failures[0].details).toContain('tautological');
|
|
324
|
-
});
|
|
325
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import os from 'os';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import { afterEach, describe, expect, it } from 'vitest';
|
|
5
|
-
import { trustedReviews } from './deep-analysis.js';
|
|
6
|
-
let cwd;
|
|
7
|
-
afterEach(() => fs.rmSync(cwd, { recursive: true, force: true }));
|
|
8
|
-
describe('trustedReviews', () => {
|
|
9
|
-
it('lets a normal run skip what agents recorded as reviewed, and an independent run skip nothing', () => {
|
|
10
|
-
cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'trusted-reviews-'));
|
|
11
|
-
fs.mkdirSync(path.join(cwd, '.rigour'));
|
|
12
|
-
fs.writeFileSync(path.join(cwd, '.rigour', 'reviewed.json'), JSON.stringify({ version: 1, entries: [{ file: 'src/a.ts', function: 'f', hash: 'h', reviewer: 'agent', verdict: 'no_issue' }] }));
|
|
13
|
-
expect(trustedReviews(cwd, {}).map(r => r.function)).toEqual(['f']);
|
|
14
|
-
expect(trustedReviews(cwd, { independent: true })).toEqual([]);
|
|
15
|
-
});
|
|
16
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { findReads } from './queries.js';
|
|
3
|
-
const reads = (code) => findReads('a.ts', code);
|
|
4
|
-
describe('findReads', () => {
|
|
5
|
-
it('models a literal supabase-js read: filters, first order column, line', () => {
|
|
6
|
-
const [read] = reads(`
|
|
7
|
-
const page = await supabase
|
|
8
|
-
.from('orders')
|
|
9
|
-
.select('id, placed_at')
|
|
10
|
-
.eq('status', 'open')
|
|
11
|
-
.not('customer_id', 'is', null)
|
|
12
|
-
.gt('placed_at', since)
|
|
13
|
-
.lte('placed_at', until)
|
|
14
|
-
.order('placed_at')
|
|
15
|
-
.order('id')
|
|
16
|
-
.limit(100);`);
|
|
17
|
-
expect(read).toEqual({
|
|
18
|
-
table: 'orders', line: 3, uncertain: false, orderBy: 'placed_at',
|
|
19
|
-
filters: [
|
|
20
|
-
{ column: 'status', op: 'eq', value: 'open' },
|
|
21
|
-
{ column: 'customer_id', op: 'notnull' },
|
|
22
|
-
{ column: 'placed_at', op: 'range' },
|
|
23
|
-
{ column: 'placed_at', op: 'range' },
|
|
24
|
-
],
|
|
25
|
-
});
|
|
26
|
-
});
|
|
27
|
-
it('reads .in, .is(null), .match and .filter, and qualifies a non-public schema', () => {
|
|
28
|
-
const [read] = reads(`db.schema('billing').from('invoices').select('*').in('id', ids).is('paid_at', null).match({ kind: 'card' }).filter('total', 'gte', 10);`);
|
|
29
|
-
expect(read.table).toBe('billing.invoices');
|
|
30
|
-
expect(read.filters).toEqual([
|
|
31
|
-
{ column: 'id', op: 'in' },
|
|
32
|
-
{ column: 'paid_at', op: 'isnull' },
|
|
33
|
-
{ column: 'kind', op: 'eq', value: 'card' },
|
|
34
|
-
{ column: 'total', op: 'range' },
|
|
35
|
-
]);
|
|
36
|
-
expect(read.uncertain).toBe(false);
|
|
37
|
-
});
|
|
38
|
-
it('is uncertain when anything is computed, OR-ed, embedded or unknown', () => {
|
|
39
|
-
const uncertain = [
|
|
40
|
-
`db.from(table).select('*').eq('a', 1);`,
|
|
41
|
-
`db.from('t').select('*').eq(column, 1);`,
|
|
42
|
-
`db.from('t').select('*').or('a.eq.1,b.eq.2');`,
|
|
43
|
-
`db.from('t').select('*, u!inner(*)').eq('u.id', 1);`,
|
|
44
|
-
`db.from('t').select('*').someNewFilter('a', 1);`,
|
|
45
|
-
`db.schema(name).from('t').select('*').eq('a', 1);`,
|
|
46
|
-
];
|
|
47
|
-
expect(uncertain.map(code => reads(code)[0].uncertain)).toEqual(uncertain.map(() => true));
|
|
48
|
-
});
|
|
49
|
-
it('ignores writes and calls that are not reads', () => {
|
|
50
|
-
expect(reads(`db.from('t').insert({ a: 1 }).select();`)).toEqual([]);
|
|
51
|
-
expect(reads(`db.from('t').update({ a: 1 }).eq('id', 1);`)).toEqual([]);
|
|
52
|
-
expect(reads(`Array.from(items).filter(Boolean);`)).toEqual([]);
|
|
53
|
-
});
|
|
54
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { policyColumns, policyFilters, replayMigrations } from './schema.js';
|
|
3
|
-
import { splitStatements } from './sql.js';
|
|
4
|
-
describe('splitStatements', () => {
|
|
5
|
-
it('keeps comments, quotes and dollar bodies from splitting a statement', () => {
|
|
6
|
-
const sql = `-- a; comment\ncreate table a (x text default ';');\n/* b; */ do $$ begin perform 1; end $$;\nselect 'it''s; fine';`;
|
|
7
|
-
expect(splitStatements(sql)).toEqual(["create table a (x text default ';')", 'do $$ begin perform 1; end $$', "select 'it''s; fine'"]);
|
|
8
|
-
});
|
|
9
|
-
});
|
|
10
|
-
describe('replayMigrations', () => {
|
|
11
|
-
it('collects primary keys, unique columns and table constraints as indexes', () => {
|
|
12
|
-
const schema = replayMigrations([`
|
|
13
|
-
create table public.orders (
|
|
14
|
-
id uuid primary key default gen_random_uuid(),
|
|
15
|
-
code text not null unique,
|
|
16
|
-
customer_id uuid not null references customers(id),
|
|
17
|
-
placed_at timestamptz,
|
|
18
|
-
constraint orders_customer_code unique (customer_id, code)
|
|
19
|
-
);`]);
|
|
20
|
-
expect(schema.get('orders').indexes.map(i => i.leading)).toEqual(['id', 'code', 'customer_id']);
|
|
21
|
-
expect(schema.get('orders').uncertain).toBe(false);
|
|
22
|
-
});
|
|
23
|
-
it('applies indexes, partial predicates, drops and renames in order', () => {
|
|
24
|
-
const schema = replayMigrations([
|
|
25
|
-
'create table orders (id uuid primary key, status text, placed_at timestamptz, customer_id uuid);',
|
|
26
|
-
`create index concurrently orders_open_idx on public.orders (placed_at desc, id) where status = 'open' and customer_id is not null;`,
|
|
27
|
-
'create index orders_customer_idx on orders using btree (customer_id);',
|
|
28
|
-
'create index orders_lower_idx on orders (lower(status));',
|
|
29
|
-
'drop index if exists public.orders_customer_idx;',
|
|
30
|
-
'alter index orders_open_idx rename to orders_open_v2_idx;',
|
|
31
|
-
]);
|
|
32
|
-
const indexes = schema.get('orders').indexes;
|
|
33
|
-
expect(indexes.map(i => [i.name, i.leading])).toEqual([[undefined, 'id'], ['orders_open_v2_idx', 'placed_at'], ['orders_lower_idx', null]]);
|
|
34
|
-
expect(indexes[1].predicate).toEqual([{ kind: 'eq', column: 'status', value: 'open' }, { kind: 'notnull', column: 'customer_id' }]);
|
|
35
|
-
});
|
|
36
|
-
it('follows ALTER TABLE: added keys, table renames, dropped tables', () => {
|
|
37
|
-
const schema = replayMigrations([
|
|
38
|
-
'create table events (id bigint, kind text);',
|
|
39
|
-
'alter table events add constraint events_pkey primary key (id);',
|
|
40
|
-
'alter table events add column ref text unique;',
|
|
41
|
-
'alter table events rename to audit_events;',
|
|
42
|
-
'create table scratch (x int);',
|
|
43
|
-
'drop table scratch;',
|
|
44
|
-
]);
|
|
45
|
-
expect(schema.get('audit_events').indexes.map(i => i.leading)).toEqual(['id', 'ref']);
|
|
46
|
-
expect([...schema.keys()]).toEqual(['audit_events']);
|
|
47
|
-
});
|
|
48
|
-
it('marks a table uncertain when DDL is beyond what it models', () => {
|
|
49
|
-
const schema = replayMigrations([
|
|
50
|
-
'create table a (id int, x int); create table b (id int); create table c (like a including indexes); create table d (id int) partition by range (id);',
|
|
51
|
-
'alter table a rename column x to y;',
|
|
52
|
-
`create or replace function f() returns void language plpgsql as $$ begin create index on b (id); end $$;`,
|
|
53
|
-
]);
|
|
54
|
-
expect(['a', 'b', 'c', 'd'].map(t => schema.get(t).uncertain)).toEqual([true, true, true, true]);
|
|
55
|
-
});
|
|
56
|
-
it('treats an OR or an unreadable predicate as unknown', () => {
|
|
57
|
-
const schema = replayMigrations([
|
|
58
|
-
'create table t (a int, b int);',
|
|
59
|
-
`create index t_or on t (a) where a = 1 or b = 2;`,
|
|
60
|
-
`create index t_fn on t (a) where coalesce(b, 0) > 0;`,
|
|
61
|
-
]);
|
|
62
|
-
expect(schema.get('t').indexes.map(i => i.predicate)).toEqual([[{ kind: 'unknown' }], [{ kind: 'unknown' }]]);
|
|
63
|
-
});
|
|
64
|
-
it('keeps SELECT and ALL policies as the columns they compare to the user', () => {
|
|
65
|
-
const schema = replayMigrations([
|
|
66
|
-
'create table subs (id text primary key, user_id uuid, status text);',
|
|
67
|
-
`create policy "own" on subs for select using (auth.uid() = user_id);`,
|
|
68
|
-
`create policy "writes" on subs for insert with check (auth.uid() = user_id);`,
|
|
69
|
-
`create policy "all" on subs to authenticated using (true);`,
|
|
70
|
-
]);
|
|
71
|
-
expect(policyFilters(schema.get('subs'))).toEqual({ columns: ['user_id'], unknown: false });
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
describe('policyColumns', () => {
|
|
75
|
-
it('reads comparisons to the current user and gives up on anything else', () => {
|
|
76
|
-
expect(policyColumns('(select auth.uid()) = owner_id')).toEqual(['owner_id']);
|
|
77
|
-
expect(policyColumns('(owner_id = auth.uid()) or (editor_id = auth.uid())')).toEqual(['owner_id', 'editor_id']);
|
|
78
|
-
expect(policyColumns('true')).toEqual([]);
|
|
79
|
-
expect(policyColumns('exists (select 1 from members m where m.org_id = org_id)')).toBeNull();
|
|
80
|
-
});
|
|
81
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import os from 'os';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
5
|
-
import { UnindexedReadsGate } from './index.js';
|
|
6
|
-
let repo;
|
|
7
|
-
const write = (rel, body) => {
|
|
8
|
-
fs.mkdirSync(path.dirname(path.join(repo, rel)), { recursive: true });
|
|
9
|
-
fs.writeFileSync(path.join(repo, rel), body);
|
|
10
|
-
};
|
|
11
|
-
const run = (config = { enabled: true }) => new UnindexedReadsGate(config).run({ cwd: repo, patterns: ['src/**/*.ts'] });
|
|
12
|
-
const TABLE = `create table orders (
|
|
13
|
-
id uuid primary key,
|
|
14
|
-
customer_id uuid,
|
|
15
|
-
status text not null,
|
|
16
|
-
placed_at timestamptz,
|
|
17
|
-
campaign_id uuid
|
|
18
|
-
);
|
|
19
|
-
create index orders_customer_idx on orders (customer_id, status, placed_at desc);
|
|
20
|
-
create index orders_campaign_window_idx on orders (placed_at, id) where status = 'open' and campaign_id is not null;
|
|
21
|
-
`;
|
|
22
|
-
const WINDOW_READ = `
|
|
23
|
-
export async function openOrders(db: Db, since: string, until: string) {
|
|
24
|
-
return db
|
|
25
|
-
.from('orders')
|
|
26
|
-
.select('id, placed_at')
|
|
27
|
-
.eq('status', 'open')
|
|
28
|
-
.not('customer_id', 'is', null)
|
|
29
|
-
.gt('placed_at', since)
|
|
30
|
-
.lte('placed_at', until)
|
|
31
|
-
.order('placed_at')
|
|
32
|
-
.order('id');
|
|
33
|
-
}
|
|
34
|
-
`;
|
|
35
|
-
beforeEach(() => { repo = fs.mkdtempSync(path.join(os.tmpdir(), 'unindexed-')); });
|
|
36
|
-
afterEach(() => fs.rmSync(repo, { recursive: true, force: true }));
|
|
37
|
-
describe('UnindexedReadsGate', () => {
|
|
38
|
-
it('flags a window read whose only candidate index is partial on a filter the read does not have', async () => {
|
|
39
|
-
write('supabase/migrations/20260101000000_orders.sql', TABLE);
|
|
40
|
-
write('src/orders.ts', WINDOW_READ);
|
|
41
|
-
const failures = await run();
|
|
42
|
-
expect(failures).toHaveLength(1);
|
|
43
|
-
expect(failures[0]).toMatchObject({ id: 'unindexed-reads', files: ['src/orders.ts'], line: 4 });
|
|
44
|
-
expect(failures[0].details).toContain('`orders`');
|
|
45
|
-
expect(failures[0].details).toContain('`placed_at`');
|
|
46
|
-
});
|
|
47
|
-
it('is silent once a later migration adds an index that serves the read', async () => {
|
|
48
|
-
write('supabase/migrations/20260101000000_orders.sql', TABLE);
|
|
49
|
-
write('supabase/migrations/20260102000000_orders_open.sql', `create index concurrently orders_open_window_idx on public.orders (placed_at, id) where status = 'open' and customer_id is not null;`);
|
|
50
|
-
write('src/orders.ts', WINDOW_READ);
|
|
51
|
-
expect(await run()).toEqual([]);
|
|
52
|
-
});
|
|
53
|
-
it('is silent for a primary-key lookup, a table outside the migrations, and an uncertain table', async () => {
|
|
54
|
-
write('db/migrations/001_orders.sql', TABLE + 'create table notes (id int, body text);\nalter table notes rename column body to text;\n');
|
|
55
|
-
write('src/reads.ts', `
|
|
56
|
-
db.from('orders').select('*').eq('id', id);
|
|
57
|
-
db.from('auth_users').select('*').eq('email', email);
|
|
58
|
-
db.from('notes').select('*').eq('text', q);
|
|
59
|
-
`);
|
|
60
|
-
expect(await run()).toEqual([]);
|
|
61
|
-
});
|
|
62
|
-
it('is silent unless enabled, and for a read that narrows nothing', async () => {
|
|
63
|
-
write('supabase/migrations/20260101000000_orders.sql', TABLE);
|
|
64
|
-
write('src/orders.ts', WINDOW_READ + `\ndb.from('orders').select('*').limit(10);\n`);
|
|
65
|
-
expect(await run({ enabled: false })).toEqual([]);
|
|
66
|
-
expect(await new UnindexedReadsGate({ enabled: true }).run({ cwd: repo, patterns: ['src/**/*.ts'] })).toHaveLength(1);
|
|
67
|
-
});
|
|
68
|
-
it('needs every migration set that defines the table to lack a serving index', async () => {
|
|
69
|
-
write('apps/a/supabase/migrations/1_orders.sql', TABLE);
|
|
70
|
-
write('apps/b/supabase/migrations/1_orders.sql', TABLE + `create index orders_status_idx on orders (status);`);
|
|
71
|
-
write('src/orders.ts', WINDOW_READ);
|
|
72
|
-
expect(await run()).toEqual([]);
|
|
73
|
-
});
|
|
74
|
-
it('counts a row-level policy as a filter: names its column, and is silent once that column is indexed', async () => {
|
|
75
|
-
const subs = `create table subs (id text primary key, user_id uuid not null, status text);
|
|
76
|
-
create policy "own subs" on subs for select using (auth.uid() = user_id);`;
|
|
77
|
-
write('supabase/migrations/1_subs.sql', subs);
|
|
78
|
-
write('src/subs.ts', `export const active = (db: Db) => db.from('subs').select('*').in('status', ['active']).maybeSingle();\n`);
|
|
79
|
-
const [finding] = await run();
|
|
80
|
-
expect(finding.details).toContain('`user_id`');
|
|
81
|
-
write('supabase/migrations/2_subs_user.sql', 'create index subs_user_idx on subs (user_id);');
|
|
82
|
-
expect(await run()).toEqual([]);
|
|
83
|
-
});
|
|
84
|
-
it('is silent for a table whose policy it cannot read', async () => {
|
|
85
|
-
write('supabase/migrations/1_docs.sql', `create table docs (id int primary key, org_id int, title text);
|
|
86
|
-
create policy "members" on docs for select using (exists (select 1 from members m where m.org_id = docs.org_id));`);
|
|
87
|
-
write('src/docs.ts', `db.from('docs').select('*').eq('title', t);\n`);
|
|
88
|
-
expect(await run()).toEqual([]);
|
|
89
|
-
});
|
|
90
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|