@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,61 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
/** An in-memory lessons table, enough for the reads and writes fix lessons make. */
|
|
3
|
-
const db = vi.hoisted(() => ({ rows: new Map(), reset() { this.rows = new Map(); } }));
|
|
4
|
-
vi.mock('./db.js', () => {
|
|
5
|
-
const store = {
|
|
6
|
-
run: async (sql, ...p) => {
|
|
7
|
-
if (sql.includes('INSERT INTO lessons'))
|
|
8
|
-
db.rows.set(p[0], { id: p[0], state: p[4], subject: p[5], evidence_json: p[6], confidence: p[7] });
|
|
9
|
-
if (sql.includes('UPDATE lessons')) {
|
|
10
|
-
const row = db.rows.get(p[4]);
|
|
11
|
-
if (!['promoted', 'rejected'].includes(row.state))
|
|
12
|
-
row.state = p[0];
|
|
13
|
-
Object.assign(row, { evidence_json: p[1], confidence: Math.max(row.confidence, p[2]) });
|
|
14
|
-
}
|
|
15
|
-
return { changes: 1, lastID: 0 };
|
|
16
|
-
},
|
|
17
|
-
get: async (sql, ...p) => (sql.includes("kind = 'fix'") ? [...db.rows.values()].find(r => r.subject.slice(0, p[1]) === p[2]) : undefined),
|
|
18
|
-
all: async () => [],
|
|
19
|
-
exec: async () => undefined,
|
|
20
|
-
close: async () => undefined,
|
|
21
|
-
};
|
|
22
|
-
return { openDatabase: async () => store };
|
|
23
|
-
});
|
|
24
|
-
vi.mock('./local-encryption.js', () => ({ encryptLocalPayload: async (v) => JSON.stringify(v), decryptLocalPayload: async (v) => JSON.parse(v) }));
|
|
25
|
-
vi.mock('./team-store.js', () => ({ loadTeamConfiguration: async () => null }));
|
|
26
|
-
const { recordFixLessons, fixLessonSubject } = await import('./fix-lessons.js');
|
|
27
|
-
const fix = (id, file) => ({
|
|
28
|
-
id, file, rule: 'semantic-bugs', title: 'Unbounded read without paging', details: 'Rows read without a limit.', before: 'a', after: 'b', resolvedAt: '2026-10-02T00:00:00Z',
|
|
29
|
-
});
|
|
30
|
-
const lessons = () => [...db.rows.values()];
|
|
31
|
-
beforeEach(() => db.reset());
|
|
32
|
-
describe('recordFixLessons', () => {
|
|
33
|
-
it('turns a fix into a candidate lesson named after the defect it removed', async () => {
|
|
34
|
-
await recordFixLessons(process.cwd(), [fix('f1', 'src/orders.ts')]);
|
|
35
|
-
expect(lessons().map(l => [l.subject, l.state])).toEqual([['Fixed before: Unbounded read without paging (semantic-bugs). Rows read without a limit.', 'candidate']]);
|
|
36
|
-
});
|
|
37
|
-
it('validates the lesson once fixes of that kind reach a second file, and counts the same fix once', async () => {
|
|
38
|
-
await recordFixLessons(process.cwd(), [fix('f1', 'src/orders.ts')]);
|
|
39
|
-
await recordFixLessons(process.cwd(), [fix('f1', 'src/orders.ts'), fix('f2', 'src/orders.ts')]);
|
|
40
|
-
expect(lessons()[0].state).toBe('candidate');
|
|
41
|
-
await recordFixLessons(process.cwd(), [fix('f3', 'src/invoices.ts')]);
|
|
42
|
-
expect(lessons()).toHaveLength(1);
|
|
43
|
-
expect(lessons()[0].state).toBe('validated');
|
|
44
|
-
expect(JSON.parse(lessons()[0].evidence_json).files).toEqual(['src/orders.ts', 'src/invoices.ts']);
|
|
45
|
-
});
|
|
46
|
-
it("never overrides a person's decision to promote or reject", async () => {
|
|
47
|
-
await recordFixLessons(process.cwd(), [fix('f1', 'src/orders.ts')]);
|
|
48
|
-
lessons()[0].state = 'rejected';
|
|
49
|
-
await recordFixLessons(process.cwd(), [fix('f2', 'src/invoices.ts')]);
|
|
50
|
-
expect(lessons()[0].state).toBe('rejected');
|
|
51
|
-
});
|
|
52
|
-
it('names a fix whose finding had no title after its rule', () => {
|
|
53
|
-
expect(fixLessonSubject({ rule: 'hallucinated-imports' })).toBe('Fixed before: hallucinated-imports (hallucinated-imports).');
|
|
54
|
-
});
|
|
55
|
-
it('groups fixes of one kind even when their details differ', async () => {
|
|
56
|
-
await recordFixLessons(process.cwd(), [fix('f1', 'src/orders.ts')]);
|
|
57
|
-
await recordFixLessons(process.cwd(), [{ ...fix('f2', 'src/invoices.ts'), details: 'Invoices read in full to count them.' }]);
|
|
58
|
-
expect(lessons()).toHaveLength(1);
|
|
59
|
-
expect(lessons()[0].state).toBe('validated');
|
|
60
|
-
});
|
|
61
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import fs from 'fs-extra';
|
|
2
|
-
import os from 'os';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
-
import { openDatabase } from './db.js';
|
|
6
|
-
import { decryptLocalPayload, encryptLocalPayload } from './local-encryption.js';
|
|
7
|
-
import { getRepositoryId } from './lessons.js';
|
|
8
|
-
import { queueLocalLessonsForTeam } from './team-import.js';
|
|
9
|
-
const roots = [];
|
|
10
|
-
afterEach(async () => {
|
|
11
|
-
vi.unstubAllEnvs();
|
|
12
|
-
await Promise.all(roots.splice(0).map((root) => fs.remove(root)));
|
|
13
|
-
});
|
|
14
|
-
describe('getRepositoryId', () => {
|
|
15
|
-
it('uses normalized origin identity instead of a basename', async () => {
|
|
16
|
-
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'rigour-repo-id-'));
|
|
17
|
-
roots.push(root);
|
|
18
|
-
await fs.ensureDir(path.join(root, '.git'));
|
|
19
|
-
await fs.writeFile(path.join(root, '.git', 'config'), '[remote "origin"]\nurl = git@github.com:Rigour-Labs/Rigour.git\n');
|
|
20
|
-
const first = await getRepositoryId(root);
|
|
21
|
-
const second = await getRepositoryId(path.join(root, '..', path.basename(root)));
|
|
22
|
-
expect(first).toBe(second);
|
|
23
|
-
expect(first).toHaveLength(64);
|
|
24
|
-
});
|
|
25
|
-
it('uses the shared origin identity from a linked worktree', async () => {
|
|
26
|
-
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'rigour-worktree-id-'));
|
|
27
|
-
roots.push(root);
|
|
28
|
-
const common = path.join(root, 'common');
|
|
29
|
-
const linkedGit = path.join(common, 'worktrees', 'feature');
|
|
30
|
-
const worktree = path.join(root, 'checkout');
|
|
31
|
-
await fs.ensureDir(linkedGit);
|
|
32
|
-
await fs.ensureDir(worktree);
|
|
33
|
-
await fs.writeFile(path.join(worktree, '.git'), `gitdir: ${linkedGit}\n`);
|
|
34
|
-
await fs.writeFile(path.join(linkedGit, 'commondir'), '../..\n');
|
|
35
|
-
await fs.writeFile(path.join(common, 'config'), '[remote "origin"]\nurl = https://github.com/rigour-labs/rigour.git\n');
|
|
36
|
-
const regular = path.join(root, 'regular');
|
|
37
|
-
await fs.ensureDir(path.join(regular, '.git'));
|
|
38
|
-
await fs.writeFile(path.join(regular, '.git', 'config'), '[remote "origin"]\nurl = git@github.com:Rigour-Labs/Rigour.git\n');
|
|
39
|
-
expect(await getRepositoryId(worktree)).toBe(await getRepositoryId(regular));
|
|
40
|
-
expect(await getRepositoryId(worktree)).toHaveLength(64);
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
describe('local lesson team import', () => {
|
|
44
|
-
it('dry-runs, assigns legacy ownership, and queues each lesson version once', async () => {
|
|
45
|
-
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'rigour-team-import-'));
|
|
46
|
-
roots.push(root);
|
|
47
|
-
await fs.ensureDir(path.join(root, '.git'));
|
|
48
|
-
await fs.writeFile(path.join(root, '.git', 'config'), '[remote "origin"]\nurl = https://github.com/acme/service.git\n');
|
|
49
|
-
vi.stubEnv('RIGOUR_ORGANIZATION_ID', 'acme');
|
|
50
|
-
vi.stubEnv('RIGOUR_TEAM_ID', 'platform');
|
|
51
|
-
vi.stubEnv('RIGOUR_ACTOR_ID', 'alice');
|
|
52
|
-
vi.stubEnv('RIGOUR_TEAM_DATABASE_URL', 'postgresql://alice@127.0.0.1/rigour');
|
|
53
|
-
vi.stubEnv('RIGOUR_LOCAL_CACHE_KEY', Buffer.alloc(32, 7).toString('base64'));
|
|
54
|
-
const repositoryId = await getRepositoryId(root);
|
|
55
|
-
const databasePath = path.join(root, 'rigour.db');
|
|
56
|
-
const db = await openDatabase(databasePath);
|
|
57
|
-
expect(db).not.toBeNull();
|
|
58
|
-
const now = Date.now();
|
|
59
|
-
await db.run(`INSERT INTO lessons (
|
|
60
|
-
id, repository_id, actor_id, team_id, visibility, state, kind, subject,
|
|
61
|
-
evidence_json, confidence, source, created_at, updated_at
|
|
62
|
-
) VALUES (?, ?, NULL, NULL, 'personal', 'candidate', 'interaction', ?, ?, 0.3, 'mcp', ?, ?)`, 'lesson-legacy', repositoryId, 'Prefer scoped retrieval.', await encryptLocalPayload({ requestId: 'legacy-request' }), now, now);
|
|
63
|
-
await db.close();
|
|
64
|
-
const preview = await queueLocalLessonsForTeam([root], { databasePath, dryRun: true });
|
|
65
|
-
expect(preview).toMatchObject({ matched: 1, eligible: 1, wouldQueue: 1, queued: 0 });
|
|
66
|
-
const queued = await queueLocalLessonsForTeam([root], { databasePath });
|
|
67
|
-
expect(queued).toMatchObject({ matched: 1, eligible: 1, wouldQueue: 1, queued: 1 });
|
|
68
|
-
const existingOutbox = await openDatabase(databasePath);
|
|
69
|
-
await existingOutbox.run('UPDATE sync_outbox SET id = ? WHERE entity_id = ?', 'outbox-created-by-normal-learning', 'lesson-legacy');
|
|
70
|
-
await existingOutbox.close();
|
|
71
|
-
const repeated = await queueLocalLessonsForTeam([root], { databasePath });
|
|
72
|
-
expect(repeated).toMatchObject({ matched: 1, eligible: 1, wouldQueue: 0, queued: 0, alreadyQueued: 1 });
|
|
73
|
-
const verified = await openDatabase(databasePath);
|
|
74
|
-
const lesson = await verified.get('SELECT actor_id, team_id, visibility, state FROM lessons WHERE id = ?', 'lesson-legacy');
|
|
75
|
-
expect(lesson).toMatchObject({ actor_id: 'alice', team_id: 'platform', visibility: 'personal', state: 'candidate' });
|
|
76
|
-
const outbox = await verified.get('SELECT payload_json FROM sync_outbox WHERE entity_id = ?', 'lesson-legacy');
|
|
77
|
-
const payload = await decryptLocalPayload(outbox.payload_json);
|
|
78
|
-
expect(payload).toMatchObject({ id: 'lesson-legacy', actorId: 'alice', teamId: 'platform', visibility: 'personal', state: 'candidate' });
|
|
79
|
-
await verified.close();
|
|
80
|
-
});
|
|
81
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import fs from 'fs-extra';
|
|
2
|
-
import os from 'os';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import { afterEach, describe, expect, it } from 'vitest';
|
|
5
|
-
import { loadOrCreateFileKey } from './local-encryption.js';
|
|
6
|
-
const temporaryDirectories = [];
|
|
7
|
-
afterEach(async () => {
|
|
8
|
-
await Promise.all(temporaryDirectories.splice(0).map(directory => fs.remove(directory)));
|
|
9
|
-
});
|
|
10
|
-
async function temporaryKeyPath() {
|
|
11
|
-
const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'rigour-key-test-'));
|
|
12
|
-
temporaryDirectories.push(directory);
|
|
13
|
-
return path.join(directory, 'team-cache.key');
|
|
14
|
-
}
|
|
15
|
-
describe('local encryption key storage', () => {
|
|
16
|
-
it('returns one key when concurrent processes initialize the same path', async () => {
|
|
17
|
-
const keyPath = await temporaryKeyPath();
|
|
18
|
-
const keys = await Promise.all(Array.from({ length: 32 }, () => loadOrCreateFileKey(keyPath)));
|
|
19
|
-
expect(new Set(keys.map(key => key.toString('base64')))).toHaveLength(1);
|
|
20
|
-
expect(await fs.readFile(keyPath, 'utf8')).toBe(keys[0].toString('base64'));
|
|
21
|
-
});
|
|
22
|
-
it('rejects an invalid existing key without replacing it', async () => {
|
|
23
|
-
const keyPath = await temporaryKeyPath();
|
|
24
|
-
await fs.writeFile(keyPath, 'not-a-valid-key', 'utf8');
|
|
25
|
-
await expect(loadOrCreateFileKey(keyPath)).rejects.toThrow('must contain a base64-encoded 32-byte key');
|
|
26
|
-
expect(await fs.readFile(keyPath, 'utf8')).toBe('not-a-valid-key');
|
|
27
|
-
});
|
|
28
|
-
it('accepts an existing key without base64 padding for backward compatibility', async () => {
|
|
29
|
-
const keyPath = await temporaryKeyPath();
|
|
30
|
-
const encodedKey = Buffer.alloc(32, 7).toString('base64').replace(/=+$/, '');
|
|
31
|
-
await fs.writeFile(keyPath, encodedKey, 'utf8');
|
|
32
|
-
expect((await loadOrCreateFileKey(keyPath)).equals(Buffer.alloc(32, 7))).toBe(true);
|
|
33
|
-
});
|
|
34
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import os from 'os';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
-
/** An in-memory stand-in for the local SQLite store: records writes, answers the two reads under test. */
|
|
6
|
-
const db = vi.hoisted(() => ({
|
|
7
|
-
writes: [],
|
|
8
|
-
patterns: [],
|
|
9
|
-
findings: [],
|
|
10
|
-
reset() { this.writes = []; this.patterns = []; this.findings = []; },
|
|
11
|
-
}));
|
|
12
|
-
vi.mock('./db.js', () => {
|
|
13
|
-
const store = {
|
|
14
|
-
run: async (sql) => { db.writes.push(sql.trim().split(/\s+/).slice(0, 3).join(' ')); return { changes: 1, lastID: 0 }; },
|
|
15
|
-
get: async () => undefined,
|
|
16
|
-
all: async (sql) => (sql.includes('FROM patterns') ? db.patterns : sql.includes('FROM findings') ? db.findings : []),
|
|
17
|
-
exec: async () => undefined,
|
|
18
|
-
transaction: async (work) => work(store),
|
|
19
|
-
close: async () => undefined,
|
|
20
|
-
};
|
|
21
|
-
return { openDatabase: async () => store };
|
|
22
|
-
});
|
|
23
|
-
const { checkLocalPatterns, persistAndReinforce } = await import('./local-memory.js');
|
|
24
|
-
let repo;
|
|
25
|
-
beforeEach(() => {
|
|
26
|
-
db.reset();
|
|
27
|
-
repo = fs.mkdtempSync(path.join(os.tmpdir(), 'local-memory-'));
|
|
28
|
-
fs.mkdirSync(path.join(repo, 'src'));
|
|
29
|
-
});
|
|
30
|
-
afterEach(() => fs.rmSync(repo, { recursive: true, force: true }));
|
|
31
|
-
const failure = (id, line) => ({ id, title: id, details: 'x', severity: 'medium', provenance: 'traditional', files: ['src/a.ts'], line });
|
|
32
|
-
describe('persistAndReinforce', () => {
|
|
33
|
-
it('reinforces each category once per scan, however many findings it has', async () => {
|
|
34
|
-
await persistAndReinforce(repo, { status: 'FAIL', failures: [failure('content-check', 1), failure('content-check', 2), failure('content-check', 3), failure('ast', 4)], stats: {} });
|
|
35
|
-
expect(db.writes.filter(w => w.startsWith('INSERT INTO patterns'))).toHaveLength(2);
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
describe('checkLocalPatterns', () => {
|
|
39
|
-
const remembered = (scanTime) => ({ file: 'src/a.ts', line: 3, category: 'security', description: 'token logged', confidence: 0.9, scan_time: scanTime });
|
|
40
|
-
it('replays a remembered finding while its file is unchanged since that scan, once', async () => {
|
|
41
|
-
fs.writeFileSync(path.join(repo, 'src/a.ts'), 'log(token)\n');
|
|
42
|
-
const after = fs.statSync(path.join(repo, 'src/a.ts')).mtimeMs + 1000;
|
|
43
|
-
db.patterns = [{ pattern: 'security', strength: 0.9 }];
|
|
44
|
-
db.findings = [remembered(after), remembered(after)];
|
|
45
|
-
const findings = await checkLocalPatterns(repo, ['src/a.ts']);
|
|
46
|
-
expect(findings.map(f => f.description)).toEqual(['[Local Memory] token logged']);
|
|
47
|
-
});
|
|
48
|
-
it('never replays a finding for a file edited since, which may be the fix', async () => {
|
|
49
|
-
fs.writeFileSync(path.join(repo, 'src/a.ts'), 'log(redact(token))\n');
|
|
50
|
-
const before = fs.statSync(path.join(repo, 'src/a.ts')).mtimeMs - 1000;
|
|
51
|
-
db.patterns = [{ pattern: 'security', strength: 0.9 }];
|
|
52
|
-
db.findings = [remembered(before)];
|
|
53
|
-
expect(await checkLocalPatterns(repo, ['src/a.ts'])).toEqual([]);
|
|
54
|
-
});
|
|
55
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
const db = vi.hoisted(() => ({ rows: [], outbox: 0, reset() { this.rows = []; this.outbox = 0; } }));
|
|
3
|
-
vi.mock('./db.js', () => {
|
|
4
|
-
const store = {
|
|
5
|
-
run: async (sql, ...params) => {
|
|
6
|
-
if (sql.includes('INSERT INTO lessons'))
|
|
7
|
-
db.rows.push({ id: params[0], visibility: 'team', state: 'candidate', kind: 'memory', subject: params[4] });
|
|
8
|
-
if (sql.includes('INSERT INTO sync_outbox'))
|
|
9
|
-
db.outbox++;
|
|
10
|
-
return { changes: 1, lastID: 0 };
|
|
11
|
-
},
|
|
12
|
-
get: async (sql, ...params) => (sql.includes("kind = 'memory'") ? db.rows.find(r => r.subject === params[1]) : undefined),
|
|
13
|
-
all: async () => [],
|
|
14
|
-
exec: async () => undefined,
|
|
15
|
-
close: async () => undefined,
|
|
16
|
-
};
|
|
17
|
-
return { openDatabase: async () => store };
|
|
18
|
-
});
|
|
19
|
-
vi.mock('./local-encryption.js', () => ({ encryptLocalPayload: async (v) => JSON.stringify(v), decryptLocalPayload: async (v) => JSON.parse(v) }));
|
|
20
|
-
vi.mock('./team-store.js', () => ({ loadTeamConfiguration: async () => null }));
|
|
21
|
-
const { shareMemoryLesson } = await import('./lessons.js');
|
|
22
|
-
beforeEach(() => db.reset());
|
|
23
|
-
describe('shareMemoryLesson', () => {
|
|
24
|
-
it('stores a team candidate of kind memory, once per memory', async () => {
|
|
25
|
-
const first = await shareMemoryLesson(process.cwd(), 'retries', 'Use withRetry from lib/net.');
|
|
26
|
-
const again = await shareMemoryLesson(process.cwd(), 'retries', 'Use withRetry from lib/net.');
|
|
27
|
-
expect(first).toBe(again);
|
|
28
|
-
expect(db.rows).toEqual([{ id: first, visibility: 'team', state: 'candidate', kind: 'memory', subject: 'retries: Use withRetry from lib/net.' }]);
|
|
29
|
-
});
|
|
30
|
-
it('queues nothing for sync until team mode is configured', async () => {
|
|
31
|
-
await shareMemoryLesson(process.cwd(), 'naming', 'Hooks are named useX.');
|
|
32
|
-
expect(db.outbox).toBe(0);
|
|
33
|
-
});
|
|
34
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { diagnoseMissingMembership, explainTeamConnectionError } from './team-diagnostics.js';
|
|
3
|
-
import { TEAM_SCHEMA } from './team-schema.js';
|
|
4
|
-
function pool(rows) {
|
|
5
|
-
return {
|
|
6
|
-
query: async () => {
|
|
7
|
-
if (rows instanceof Error)
|
|
8
|
-
throw rows;
|
|
9
|
-
return { rows, rowCount: rows.length };
|
|
10
|
-
},
|
|
11
|
-
};
|
|
12
|
-
}
|
|
13
|
-
describe('explainTeamConnectionError', () => {
|
|
14
|
-
it('explains an untrusted certificate chain with the CA fix', () => {
|
|
15
|
-
const message = explainTeamConnectionError(Object.assign(new Error('self-signed certificate in certificate chain'), { code: 'SELF_SIGNED_CERT_IN_CHAIN' }));
|
|
16
|
-
expect(message).toContain('NODE_EXTRA_CA_CERTS');
|
|
17
|
-
expect(message).toContain('sslmode=verify-full');
|
|
18
|
-
});
|
|
19
|
-
it('points a missing schema at init-schema', () => {
|
|
20
|
-
expect(explainTeamConnectionError({ code: '42P01', message: 'relation "rigour.memberships" does not exist' })).toContain('rigour team init-schema');
|
|
21
|
-
});
|
|
22
|
-
it('explains a rejected login and an unreachable host', () => {
|
|
23
|
-
expect(explainTeamConnectionError({ code: '28P01', message: 'password authentication failed' })).toContain('rejected the login');
|
|
24
|
-
expect(explainTeamConnectionError({ code: 'ECONNREFUSED', message: 'connect ECONNREFUSED' })).toContain('Cannot reach');
|
|
25
|
-
});
|
|
26
|
-
it('passes other errors through', () => {
|
|
27
|
-
expect(explainTeamConnectionError(new Error('something else'))).toBe('something else');
|
|
28
|
-
});
|
|
29
|
-
});
|
|
30
|
-
describe('diagnoseMissingMembership', () => {
|
|
31
|
-
it('names row-level security without a policy as the cause', async () => {
|
|
32
|
-
const message = await diagnoseMissingMembership(pool([{ table_name: 'memberships' }, { table_name: 'meta' }]));
|
|
33
|
-
expect(message).toContain('rigour.memberships and rigour.meta');
|
|
34
|
-
expect(message).toContain('init-schema');
|
|
35
|
-
});
|
|
36
|
-
it('reports a missing membership when nothing hides it', async () => {
|
|
37
|
-
expect(await diagnoseMissingMembership(pool([]))).toContain('no membership');
|
|
38
|
-
});
|
|
39
|
-
it('falls back to the membership message when the catalog cannot be read', async () => {
|
|
40
|
-
expect(await diagnoseMissingMembership(pool(new Error('permission denied')))).toContain('no membership');
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
describe('team schema', () => {
|
|
44
|
-
it('protects every team table with row-level security and a read policy', () => {
|
|
45
|
-
for (const table of ['lessons', 'memberships', 'meta']) {
|
|
46
|
-
expect(TEAM_SCHEMA).toContain(`ALTER TABLE rigour.${table} ENABLE ROW LEVEL SECURITY;`);
|
|
47
|
-
}
|
|
48
|
-
expect(TEAM_SCHEMA).toContain('CREATE POLICY memberships_read_own ON rigour.memberships FOR SELECT USING (db_role = current_user);');
|
|
49
|
-
expect(TEAM_SCHEMA).toContain('CREATE POLICY meta_read ON rigour.meta FOR SELECT USING (true);');
|
|
50
|
-
});
|
|
51
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { repositoryAllowed, withheldReason } from './team-scope.js';
|
|
3
|
-
const scope = { organizationId: 'acme', repositories: ['github.com/acme/*', 'https://GitLab.com/Acme/api'] };
|
|
4
|
-
describe('team scope', () => {
|
|
5
|
-
it('sends lessons only from the repositories the team lists', () => {
|
|
6
|
-
expect(repositoryAllowed('https://github.com/acme/web', scope)).toBe(true);
|
|
7
|
-
expect(repositoryAllowed('https://gitlab.com/acme/api', scope)).toBe(true);
|
|
8
|
-
expect(repositoryAllowed('https://gitlab.com/acme/api-internal', scope)).toBe(false);
|
|
9
|
-
expect(repositoryAllowed('https://github.com/other-org/web', scope)).toBe(false);
|
|
10
|
-
expect(repositoryAllowed('https://github.com/acme-evil/web', scope)).toBe(false);
|
|
11
|
-
});
|
|
12
|
-
it('sends nothing when the team lists no repositories', () => {
|
|
13
|
-
expect(repositoryAllowed('https://github.com/acme/web', { organizationId: 'acme' })).toBe(false);
|
|
14
|
-
});
|
|
15
|
-
it('keeps personal lessons and unknown repositories on the machine, and says why', () => {
|
|
16
|
-
expect(withheldReason('personal', 'https://github.com/acme/web', scope)).toBe('personal lesson');
|
|
17
|
-
expect(withheldReason('personal', 'https://github.com/acme/web', { ...scope, syncPersonal: true })).toBeNull();
|
|
18
|
-
expect(withheldReason('team', undefined, scope)).toBe('repository unknown on this machine');
|
|
19
|
-
expect(withheldReason('team', 'https://github.com/other-org/web', scope)).toContain('github.com/other-org/web');
|
|
20
|
-
expect(withheldReason('team', 'https://github.com/acme/web', scope)).toBeNull();
|
|
21
|
-
});
|
|
22
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { validateTeamDatabaseUrl } from './team-store.js';
|
|
3
|
-
describe('team database configuration', () => {
|
|
4
|
-
it('requires TLS for remote PostgreSQL URLs', () => {
|
|
5
|
-
expect(() => validateTeamDatabaseUrl('postgresql://user:pass@db.example.com/rigour')).toThrow(/sslmode/);
|
|
6
|
-
expect(() => validateTeamDatabaseUrl('postgresql://user:pass@db.example.com/rigour?sslmode=verify-full')).not.toThrow();
|
|
7
|
-
});
|
|
8
|
-
it('rejects non-PostgreSQL URLs', () => {
|
|
9
|
-
expect(() => validateTeamDatabaseUrl('mysql://localhost/rigour')).toThrow(/postgres/);
|
|
10
|
-
});
|
|
11
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
/** The outbox and repositories a machine holds, and what reached the team database. */
|
|
3
|
-
const state = vi.hoisted(() => ({ outbox: [], repos: new Map(), sent: [], refuse: new Set(), meta: new Map(), remote: [], since: [] }));
|
|
4
|
-
vi.mock('./db.js', () => ({
|
|
5
|
-
openDatabase: async () => ({
|
|
6
|
-
all: async () => state.outbox.filter(o => o.synced_at == null),
|
|
7
|
-
get: async (sql, id) => sql.includes('FROM meta')
|
|
8
|
-
? (state.meta.has(id) ? { value: state.meta.get(id) } : undefined)
|
|
9
|
-
: (state.repos.has(id) ? { canonical_uri: state.repos.get(id) } : undefined),
|
|
10
|
-
run: async (sql, ...p) => {
|
|
11
|
-
if (sql.startsWith('INSERT INTO meta'))
|
|
12
|
-
state.meta.set(p[0], p[1]);
|
|
13
|
-
if (sql.startsWith('UPDATE sync_outbox SET synced_at')) {
|
|
14
|
-
const id = p.at(-1);
|
|
15
|
-
Object.assign(state.outbox.find(o => o.id === id) ?? {}, { synced_at: p[0], last_error: p.length === 3 ? p[1] : null });
|
|
16
|
-
}
|
|
17
|
-
return { changes: 1 };
|
|
18
|
-
},
|
|
19
|
-
close: async () => undefined,
|
|
20
|
-
}),
|
|
21
|
-
}));
|
|
22
|
-
vi.mock('./local-encryption.js', () => ({ encryptLocalPayload: async (v) => JSON.stringify(v), decryptLocalPayload: async (v) => JSON.parse(v) }));
|
|
23
|
-
vi.mock('pg', () => ({
|
|
24
|
-
Pool: class {
|
|
25
|
-
async query(sql, params) {
|
|
26
|
-
if (sql.includes('INSERT INTO rigour.lessons')) {
|
|
27
|
-
if (state.refuse.has(params[0]))
|
|
28
|
-
throw Object.assign(new Error('new row violates row-level security policy for table "lessons"'), { code: '42501' });
|
|
29
|
-
state.sent.push(params[0]);
|
|
30
|
-
return { rowCount: 1, rows: [] };
|
|
31
|
-
}
|
|
32
|
-
if (sql.includes('FROM rigour.lessons')) {
|
|
33
|
-
state.since.push(params[3]);
|
|
34
|
-
return { rowCount: 0, rows: state.remote.filter(row => row.updated_at > params[3]) };
|
|
35
|
-
}
|
|
36
|
-
return { rowCount: 1, rows: [] };
|
|
37
|
-
}
|
|
38
|
-
async end() { }
|
|
39
|
-
},
|
|
40
|
-
}));
|
|
41
|
-
const { syncTeamOutbox } = await import('./team-store.js');
|
|
42
|
-
const queue = (id, repositoryId, visibility) => state.outbox.push({ id: `outbox-${id}`, payload_json: JSON.stringify({ id, repositoryId, visibility, evidence: {} }), synced_at: null });
|
|
43
|
-
const TEAM_ENV = {
|
|
44
|
-
RIGOUR_ORGANIZATION_ID: 'acme', RIGOUR_TEAM_ID: 'web', RIGOUR_ACTOR_ID: 'dev-1',
|
|
45
|
-
RIGOUR_TEAM_DATABASE_URL: 'postgresql://localhost/rigour', RIGOUR_TEAM_REPOSITORIES: 'github.com/acme/*',
|
|
46
|
-
};
|
|
47
|
-
const saved = { ...process.env };
|
|
48
|
-
beforeEach(() => {
|
|
49
|
-
Object.assign(process.env, TEAM_ENV);
|
|
50
|
-
delete process.env.RIGOUR_TEAM_SYNC_PERSONAL;
|
|
51
|
-
state.outbox = [];
|
|
52
|
-
state.sent = [];
|
|
53
|
-
state.refuse = new Set();
|
|
54
|
-
state.meta = new Map();
|
|
55
|
-
state.remote = [];
|
|
56
|
-
state.since = [];
|
|
57
|
-
state.repos = new Map([['r-team', 'https://github.com/acme/web'], ['r-other', 'https://github.com/other-org/app']]);
|
|
58
|
-
queue('l-team', 'r-team', 'team');
|
|
59
|
-
queue('l-other', 'r-other', 'team');
|
|
60
|
-
queue('l-personal', 'r-team', 'personal');
|
|
61
|
-
queue('l-unknown', 'r-gone', 'team');
|
|
62
|
-
});
|
|
63
|
-
afterEach(() => { process.env = { ...saved }; });
|
|
64
|
-
describe('team sync scope', () => {
|
|
65
|
-
it("sends only the team's own shared lessons and says what it kept back", async () => {
|
|
66
|
-
expect(await syncTeamOutbox({ dryRun: true })).toMatchObject({ pending: 1, withheld: 3 });
|
|
67
|
-
expect(state.sent).toEqual([]);
|
|
68
|
-
const result = await syncTeamOutbox();
|
|
69
|
-
expect(result).toMatchObject({ synced: 1, withheld: 3 });
|
|
70
|
-
expect(state.sent).toEqual(['l-team']);
|
|
71
|
-
const reasons = Object.fromEntries(state.outbox.map(o => [o.id, o.last_error]));
|
|
72
|
-
expect(reasons['outbox-l-other']).toContain('github.com/other-org/app');
|
|
73
|
-
expect(reasons['outbox-l-personal']).toContain('personal lesson');
|
|
74
|
-
expect(reasons['outbox-l-unknown']).toContain('repository unknown');
|
|
75
|
-
expect(await syncTeamOutbox({ dryRun: true })).toMatchObject({ pending: 0, withheld: 0 }); // never retried
|
|
76
|
-
});
|
|
77
|
-
it('sets aside a lesson the database refuses and sends the rest of the queue', async () => {
|
|
78
|
-
state.outbox = [];
|
|
79
|
-
queue('l-refused', 'r-team', 'team'); // queued first: it must not hold back what comes after
|
|
80
|
-
queue('l-team', 'r-team', 'team');
|
|
81
|
-
state.refuse.add('l-refused');
|
|
82
|
-
expect(await syncTeamOutbox()).toMatchObject({ synced: 1 });
|
|
83
|
-
expect(state.sent).toEqual(['l-team']);
|
|
84
|
-
expect(state.outbox.find(o => o.id === 'outbox-l-refused').last_error).toContain('refused by the team database: new row violates row-level security');
|
|
85
|
-
expect(await syncTeamOutbox({ dryRun: true })).toMatchObject({ pending: 0 }); // not tried again
|
|
86
|
-
});
|
|
87
|
-
it('pulls only what changed since the last pull, re-reading a margin for slow clocks', async () => {
|
|
88
|
-
const lesson = (id, updated) => ({ id, repository_id: 'r-team', actor_id: 'dev-2', team_id: 'web', visibility: 'team', state: 'promoted', kind: 'k', subject: 's', evidence_json: {}, confidence: 1, source: 'x', supersedes_id: null, created_at: updated, updated_at: updated });
|
|
89
|
-
const HOUR = 3_600_000;
|
|
90
|
-
state.remote = [lesson('a', 10 * HOUR), lesson('b', 20 * HOUR)];
|
|
91
|
-
expect(await syncTeamOutbox()).toMatchObject({ pulled: 2 });
|
|
92
|
-
expect(state.since).toEqual([0]); // the first pull reads everything
|
|
93
|
-
expect(await syncTeamOutbox()).toMatchObject({ pulled: 1 }); // only b, inside the 15-minute margin
|
|
94
|
-
expect(state.since[1]).toBe(20 * HOUR - 15 * 60_000);
|
|
95
|
-
});
|
|
96
|
-
it('sends nothing when the team lists no repositories', async () => {
|
|
97
|
-
delete process.env.RIGOUR_TEAM_REPOSITORIES;
|
|
98
|
-
expect(await syncTeamOutbox({ dryRun: true })).toMatchObject({ pending: 0, withheld: 4 });
|
|
99
|
-
});
|
|
100
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { getTeamSemanticHealth, upsertTeamLessonEmbedding } from './team-vector-store.js';
|
|
3
|
-
const config = {
|
|
4
|
-
organizationId: 'acme',
|
|
5
|
-
teamId: 'platform',
|
|
6
|
-
actorId: 'alice',
|
|
7
|
-
databaseUrl: 'postgresql://localhost/rigour',
|
|
8
|
-
semantic: {
|
|
9
|
-
provider: 'pgvector',
|
|
10
|
-
model: 'Xenova/all-MiniLM-L6-v2',
|
|
11
|
-
dimensions: 384,
|
|
12
|
-
},
|
|
13
|
-
};
|
|
14
|
-
describe('pgvector team health', () => {
|
|
15
|
-
it('reports extension, model, and embedding coverage', async () => {
|
|
16
|
-
const pool = {
|
|
17
|
-
query: vi.fn(async () => ({ rows: [{
|
|
18
|
-
extversion: '0.8.1',
|
|
19
|
-
table_ready: true,
|
|
20
|
-
indexed_lessons: 12,
|
|
21
|
-
missing_lessons: 3,
|
|
22
|
-
}] })),
|
|
23
|
-
end: async () => undefined,
|
|
24
|
-
};
|
|
25
|
-
await expect(getTeamSemanticHealth(pool, config)).resolves.toEqual({
|
|
26
|
-
provider: 'pgvector',
|
|
27
|
-
status: 'ready',
|
|
28
|
-
extensionVersion: '0.8.1',
|
|
29
|
-
model: 'Xenova/all-MiniLM-L6-v2',
|
|
30
|
-
dimensions: 384,
|
|
31
|
-
indexedLessons: 12,
|
|
32
|
-
missingLessons: 3,
|
|
33
|
-
});
|
|
34
|
-
expect(pool.query).toHaveBeenCalledWith(expect.stringContaining("lesson.state IN ('validated', 'promoted')"), ['acme', 'platform', 'Xenova/all-MiniLM-L6-v2', 'alice']);
|
|
35
|
-
});
|
|
36
|
-
it('fails health checks when vector storage is incomplete', async () => {
|
|
37
|
-
const pool = {
|
|
38
|
-
query: async () => ({ rows: [{ extversion: null, table_ready: false }] }),
|
|
39
|
-
end: async () => undefined,
|
|
40
|
-
};
|
|
41
|
-
await expect(getTeamSemanticHealth(pool, config)).rejects.toThrow(/vector extension/);
|
|
42
|
-
});
|
|
43
|
-
it('does not embed unvalidated candidate lessons', async () => {
|
|
44
|
-
const pool = { query: vi.fn(), end: async () => undefined };
|
|
45
|
-
await expect(upsertTeamLessonEmbedding(pool, config, {
|
|
46
|
-
id: 'candidate-1',
|
|
47
|
-
repositoryId: 'repo-1',
|
|
48
|
-
actorId: 'alice',
|
|
49
|
-
kind: 'interaction',
|
|
50
|
-
subject: 'Unverified suggestion',
|
|
51
|
-
source: 'mcp',
|
|
52
|
-
state: 'candidate',
|
|
53
|
-
})).resolves.toBe(false);
|
|
54
|
-
expect(pool.query).not.toHaveBeenCalled();
|
|
55
|
-
});
|
|
56
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import os from 'os';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import { afterAll, describe, expect, it, vi } from 'vitest';
|
|
5
|
-
/** Every open goes to a scratch database, never ~/.rigour/rigour.db. */
|
|
6
|
-
const dbFile = vi.hoisted(() => require('path').join(require('os').tmpdir(), `rigour-telemetry-${process.pid}.db`));
|
|
7
|
-
vi.mock('./db.js', async (importOriginal) => {
|
|
8
|
-
const real = await importOriginal();
|
|
9
|
-
return { ...real, openDatabase: (p) => real.openDatabase(p ?? dbFile) };
|
|
10
|
-
});
|
|
11
|
-
const { openDatabase, isSQLiteAvailable } = await import('./db.js');
|
|
12
|
-
const { getCheckpointMetrics, getContextEvents, recordCheckpointMetric, recordContextEvent } = await import('./context-telemetry.js');
|
|
13
|
-
const repoA = fs.mkdtempSync(path.join(os.tmpdir(), 'telemetry-a-'));
|
|
14
|
-
const repoB = fs.mkdtempSync(path.join(os.tmpdir(), 'telemetry-b-'));
|
|
15
|
-
afterAll(() => {
|
|
16
|
-
for (const p of [dbFile, `${dbFile}-wal`, `${dbFile}-shm`])
|
|
17
|
-
fs.rmSync(p, { force: true });
|
|
18
|
-
for (const dir of [repoA, repoB])
|
|
19
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
20
|
-
});
|
|
21
|
-
describe.runIf(isSQLiteAvailable())('telemetry scoped to its repository', () => {
|
|
22
|
-
it('migrates idempotently when two processes open the database at once', async () => {
|
|
23
|
-
const [a, b] = await Promise.all([openDatabase(), openDatabase()]);
|
|
24
|
-
const columns = (await a.all('PRAGMA table_info(checkpoint_metrics)')).map((c) => c.name);
|
|
25
|
-
expect(columns).toContain('repository_id');
|
|
26
|
-
await a.close();
|
|
27
|
-
await b.close();
|
|
28
|
-
});
|
|
29
|
-
it("shows a repository its own checkpoints and context events, never another's", async () => {
|
|
30
|
-
await recordCheckpointMetric({ checkpointId: 'cp-a', taskId: 't', agentId: 'agent', checkpointTokens: 10, rawStateTokens: 0, replayTokensAvoided: 0 }, repoA);
|
|
31
|
-
await recordCheckpointMetric({ checkpointId: 'cp-b', taskId: 't', agentId: 'agent', checkpointTokens: 10, rawStateTokens: 0, replayTokensAvoided: 0 }, repoB);
|
|
32
|
-
await recordContextEvent({ toolName: 'rigour_recall', cacheStatus: 'miss', candidateTokens: 0, returnedTokens: 0 }, repoA);
|
|
33
|
-
expect((await getCheckpointMetrics(undefined, repoA)).map(m => m.checkpointId)).toEqual(['cp-a']);
|
|
34
|
-
expect((await getCheckpointMetrics(undefined, repoB)).map(m => m.checkpointId)).toEqual(['cp-b']);
|
|
35
|
-
expect(await getContextEvents(undefined, repoB)).toEqual([]);
|
|
36
|
-
expect((await getContextEvents(undefined, repoA)).map(e => e.toolName)).toEqual(['rigour_recall']);
|
|
37
|
-
});
|
|
38
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|