@rigour-labs/core 6.7.10 → 6.8.1-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/brief/briefing.d.ts +59 -0
- package/dist/brief/briefing.js +126 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/inference/cloud-provider.js +12 -2
- package/dist/review/backtest-init.d.ts +8 -5
- package/dist/review/backtest-init.js +27 -9
- package/dist/review/backtest-last.js +3 -1
- package/dist/review/reviewer/adapters.d.ts +4 -0
- package/dist/review/reviewer/adapters.js +23 -0
- package/dist/review/reviewer/inputs.d.ts +4 -1
- package/dist/review/reviewer/inputs.js +36 -2
- package/dist/review/reviewer/record.d.ts +5 -0
- package/dist/review/reviewer/record.js +3 -3
- package/dist/review/reviewer/verdict.d.ts +14 -0
- package/dist/review/reviewer/verdict.js +46 -6
- package/dist/review/reviewer.d.ts +2 -0
- package/dist/review/reviewer.js +24 -5
- package/dist/review-learning/repo-rules.d.ts +14 -2
- package/dist/review-learning/repo-rules.js +59 -9
- package/dist/task/thread.d.ts +47 -0
- package/dist/task/thread.js +234 -0
- package/dist/templates/universal-config.js +4 -0
- package/dist/types/index.d.ts +21 -0
- package/dist/types/index.js +7 -0
- package/package.json +11 -8
- package/dist/context/automatic-index-cache.test.d.ts +0 -1
- package/dist/context/automatic-index-cache.test.js +0 -30
- package/dist/context/automatic-index.test.d.ts +0 -1
- package/dist/context/automatic-index.test.js +0 -45
- package/dist/context/cache-engine.test.d.ts +0 -1
- package/dist/context/cache-engine.test.js +0 -99
- package/dist/context/dependency-graph.test.d.ts +0 -1
- package/dist/context/dependency-graph.test.js +0 -13
- package/dist/context/index-status.test.d.ts +0 -1
- package/dist/context/index-status.test.js +0 -26
- package/dist/context.test.d.ts +0 -1
- package/dist/context.test.js +0 -228
- package/dist/deep/agent-review.test.d.ts +0 -1
- package/dist/deep/agent-review.test.js +0 -53
- package/dist/deep/code-context.test.d.ts +0 -1
- package/dist/deep/code-context.test.js +0 -48
- package/dist/deep/code-pass.test.d.ts +0 -1
- package/dist/deep/code-pass.test.js +0 -112
- package/dist/deep/code-review-prompt.test.d.ts +0 -1
- package/dist/deep/code-review-prompt.test.js +0 -20
- package/dist/deep/code-verifier.test.d.ts +0 -1
- package/dist/deep/code-verifier.test.js +0 -54
- package/dist/deep/diff-tests/calls.test.d.ts +0 -1
- package/dist/deep/diff-tests/calls.test.js +0 -33
- package/dist/deep/diff-tests/run.test.d.ts +0 -1
- package/dist/deep/diff-tests/run.test.js +0 -58
- package/dist/deep/fact-extractor.test.d.ts +0 -1
- package/dist/deep/fact-extractor.test.js +0 -581
- package/dist/deep/parse-findings.test.d.ts +0 -1
- package/dist/deep/parse-findings.test.js +0 -25
- package/dist/deep/pr-review.test.d.ts +0 -1
- package/dist/deep/pr-review.test.js +0 -80
- package/dist/deep/prompts.test.d.ts +0 -1
- package/dist/deep/prompts.test.js +0 -235
- package/dist/deep/reference-pack.test.d.ts +0 -1
- package/dist/deep/reference-pack.test.js +0 -46
- package/dist/deep/related-changes.test.d.ts +0 -1
- package/dist/deep/related-changes.test.js +0 -33
- package/dist/deep/review-context-export.test.d.ts +0 -1
- package/dist/deep/review-context-export.test.js +0 -32
- package/dist/deep/review-tools.test.d.ts +0 -1
- package/dist/deep/review-tools.test.js +0 -39
- package/dist/deep/risk.test.d.ts +0 -1
- package/dist/deep/risk.test.js +0 -101
- package/dist/deep/verifier.test.d.ts +0 -1
- package/dist/deep/verifier.test.js +0 -635
- package/dist/discovery.test.d.ts +0 -1
- package/dist/discovery.test.js +0 -93
- package/dist/environment.test.d.ts +0 -1
- package/dist/environment.test.js +0 -94
- package/dist/firewall/firewall.test.d.ts +0 -1
- package/dist/firewall/firewall.test.js +0 -117
- package/dist/firewall/trust-boundaries.test.d.ts +0 -1
- package/dist/firewall/trust-boundaries.test.js +0 -64
- package/dist/firewall/trusted-control.test.d.ts +0 -1
- package/dist/firewall/trusted-control.test.js +0 -188
- package/dist/gates/agent-team.test.d.ts +0 -1
- package/dist/gates/agent-team.test.js +0 -113
- package/dist/gates/ast.test.d.ts +0 -1
- package/dist/gates/ast.test.js +0 -112
- package/dist/gates/checkpoint.test.d.ts +0 -1
- package/dist/gates/checkpoint.test.js +0 -105
- package/dist/gates/content.test.d.ts +0 -1
- package/dist/gates/content.test.js +0 -73
- package/dist/gates/coverage.test.d.ts +0 -1
- package/dist/gates/coverage.test.js +0 -53
- package/dist/gates/dedupe-failures.test.d.ts +0 -1
- package/dist/gates/dedupe-failures.test.js +0 -12
- package/dist/gates/deep-analysis.test.d.ts +0 -1
- package/dist/gates/deep-analysis.test.js +0 -86
- package/dist/gates/deep-intent.test.d.ts +0 -1
- package/dist/gates/deep-intent.test.js +0 -51
- package/dist/gates/deep-timeout.test.d.ts +0 -1
- package/dist/gates/deep-timeout.test.js +0 -10
- package/dist/gates/deprecated-apis.test.d.ts +0 -1
- package/dist/gates/deprecated-apis.test.js +0 -318
- package/dist/gates/deprecated-dependencies.test.d.ts +0 -1
- package/dist/gates/deprecated-dependencies.test.js +0 -55
- package/dist/gates/frontend-secret-exposure.test.d.ts +0 -1
- package/dist/gates/frontend-secret-exposure.test.js +0 -148
- package/dist/gates/hallucinated-imports/framework-modules-nuxt.test.d.ts +0 -1
- package/dist/gates/hallucinated-imports/framework-modules-nuxt.test.js +0 -27
- package/dist/gates/hallucinated-imports/js-resolver-types.test.d.ts +0 -1
- package/dist/gates/hallucinated-imports/js-resolver-types.test.js +0 -14
- package/dist/gates/hallucinated-imports-sveltekit.test.d.ts +0 -1
- package/dist/gates/hallucinated-imports-sveltekit.test.js +0 -132
- package/dist/gates/hallucinated-imports.test.d.ts +0 -1
- package/dist/gates/hallucinated-imports.test.js +0 -1206
- package/dist/gates/js-style-context.test.d.ts +0 -1
- package/dist/gates/js-style-context.test.js +0 -35
- package/dist/gates/logic-drift.test.d.ts +0 -1
- package/dist/gates/logic-drift.test.js +0 -52
- package/dist/gates/phantom-apis.test.d.ts +0 -1
- package/dist/gates/phantom-apis.test.js +0 -396
- package/dist/gates/promise-safety.test.d.ts +0 -1
- package/dist/gates/promise-safety.test.js +0 -34
- package/dist/gates/runner.test.d.ts +0 -1
- package/dist/gates/runner.test.js +0 -77
- package/dist/gates/scoped-gates.test.d.ts +0 -1
- package/dist/gates/scoped-gates.test.js +0 -52
- package/dist/gates/security-patterns-owasp.test.d.ts +0 -1
- package/dist/gates/security-patterns-owasp.test.js +0 -186
- package/dist/gates/security-patterns.test.d.ts +0 -1
- package/dist/gates/security-patterns.test.js +0 -194
- package/dist/gates/semantic-bugs.test.d.ts +0 -1
- package/dist/gates/semantic-bugs.test.js +0 -76
- package/dist/gates/side-effect-analysis.test.d.ts +0 -1
- package/dist/gates/side-effect-analysis.test.js +0 -162
- package/dist/gates/style-drift.test.d.ts +0 -1
- package/dist/gates/style-drift.test.js +0 -26
- package/dist/gates/test-quality.test.d.ts +0 -1
- package/dist/gates/test-quality.test.js +0 -325
- package/dist/gates/trusted-reviews.test.d.ts +0 -1
- package/dist/gates/trusted-reviews.test.js +0 -16
- package/dist/gates/unindexed-reads/queries.test.d.ts +0 -1
- package/dist/gates/unindexed-reads/queries.test.js +0 -54
- package/dist/gates/unindexed-reads/schema.test.d.ts +0 -1
- package/dist/gates/unindexed-reads/schema.test.js +0 -81
- package/dist/gates/unindexed-reads/unindexed-reads.test.d.ts +0 -1
- package/dist/gates/unindexed-reads/unindexed-reads.test.js +0 -90
- package/dist/hooks/checker.test.d.ts +0 -1
- package/dist/hooks/checker.test.js +0 -159
- package/dist/hooks/dlp-confidence.test.d.ts +0 -1
- package/dist/hooks/dlp-confidence.test.js +0 -51
- package/dist/hooks/dlp-feedback.test.d.ts +0 -1
- package/dist/hooks/dlp-feedback.test.js +0 -131
- package/dist/hooks/input-validator.test.d.ts +0 -1
- package/dist/hooks/input-validator.test.js +0 -329
- package/dist/hooks/templates.test.d.ts +0 -1
- package/dist/hooks/templates.test.js +0 -27
- package/dist/inference/brain-placeholder.test.d.ts +0 -1
- package/dist/inference/brain-placeholder.test.js +0 -28
- package/dist/inference/cloud-provider.test.d.ts +0 -1
- package/dist/inference/cloud-provider.test.js +0 -139
- package/dist/inference/executable.test.d.ts +0 -1
- package/dist/inference/executable.test.js +0 -41
- package/dist/inference/http-download.test.d.ts +0 -1
- package/dist/inference/http-download.test.js +0 -109
- package/dist/inference/llama-engine-checksum.test.d.ts +0 -1
- package/dist/inference/llama-engine-checksum.test.js +0 -27
- package/dist/inference/llama-engine.test.d.ts +0 -1
- package/dist/inference/llama-engine.test.js +0 -51
- package/dist/inference/llama-process.test.d.ts +0 -1
- package/dist/inference/llama-process.test.js +0 -61
- package/dist/inference/local-model.test.d.ts +0 -1
- package/dist/inference/local-model.test.js +0 -23
- package/dist/inference/model-download.test.d.ts +0 -1
- package/dist/inference/model-download.test.js +0 -125
- package/dist/inference/model-manager.test.d.ts +0 -1
- package/dist/inference/model-manager.test.js +0 -24
- package/dist/inference/types.test.d.ts +0 -1
- package/dist/inference/types.test.js +0 -19
- package/dist/memory/recall.test.d.ts +0 -1
- package/dist/memory/recall.test.js +0 -36
- package/dist/pattern-index/indexer.test.d.ts +0 -6
- package/dist/pattern-index/indexer.test.js +0 -197
- package/dist/pattern-index/matcher.test.d.ts +0 -6
- package/dist/pattern-index/matcher.test.js +0 -238
- package/dist/pattern-index/pattern-reuse.test.d.ts +0 -1
- package/dist/pattern-index/pattern-reuse.test.js +0 -76
- package/dist/pattern-index/semantic-runtime.test.d.ts +0 -1
- package/dist/pattern-index/semantic-runtime.test.js +0 -31
- package/dist/pattern-index/staleness.test.d.ts +0 -6
- package/dist/pattern-index/staleness.test.js +0 -211
- package/dist/review/agent-fixes.test.d.ts +0 -1
- package/dist/review/agent-fixes.test.js +0 -32
- package/dist/review/backtest-init.test.d.ts +0 -1
- package/dist/review/backtest-init.test.js +0 -98
- package/dist/review/backtest-judges.test.d.ts +0 -1
- package/dist/review/backtest-judges.test.js +0 -32
- package/dist/review/backtest-last.test.d.ts +0 -1
- package/dist/review/backtest-last.test.js +0 -109
- package/dist/review/backtest.test.d.ts +0 -1
- package/dist/review/backtest.test.js +0 -183
- package/dist/review/baseline.test.d.ts +0 -1
- package/dist/review/baseline.test.js +0 -22
- package/dist/review/branch-checks.test.d.ts +0 -1
- package/dist/review/branch-checks.test.js +0 -48
- package/dist/review/check-outcomes.test.d.ts +0 -1
- package/dist/review/check-outcomes.test.js +0 -56
- package/dist/review/code-patterns.test.d.ts +0 -1
- package/dist/review/code-patterns.test.js +0 -142
- package/dist/review/dead-code.test.d.ts +0 -1
- package/dist/review/dead-code.test.js +0 -154
- package/dist/review/deep-runs.test.d.ts +0 -1
- package/dist/review/deep-runs.test.js +0 -23
- package/dist/review/effectiveness.test.d.ts +0 -1
- package/dist/review/effectiveness.test.js +0 -42
- package/dist/review/fix-scope.test.d.ts +0 -1
- package/dist/review/fix-scope.test.js +0 -104
- package/dist/review/generated-files.test.d.ts +0 -1
- package/dist/review/generated-files.test.js +0 -25
- package/dist/review/migration-order.test.d.ts +0 -1
- package/dist/review/migration-order.test.js +0 -62
- package/dist/review/quiet.test.d.ts +0 -1
- package/dist/review/quiet.test.js +0 -38
- package/dist/review/receipt.test.d.ts +0 -1
- package/dist/review/receipt.test.js +0 -60
- package/dist/review/review-task.test.d.ts +0 -1
- package/dist/review/review-task.test.js +0 -89
- package/dist/review/review.test.d.ts +0 -1
- package/dist/review/review.test.js +0 -203
- package/dist/review/reviewer/adapters.test.d.ts +0 -1
- package/dist/review/reviewer/adapters.test.js +0 -60
- package/dist/review/reviewer/api-judge.test.d.ts +0 -1
- package/dist/review/reviewer/api-judge.test.js +0 -108
- package/dist/review/reviewer/background.test.d.ts +0 -1
- package/dist/review/reviewer/background.test.js +0 -118
- package/dist/review/reviewer/context.test.d.ts +0 -1
- package/dist/review/reviewer/context.test.js +0 -45
- package/dist/review/reviewer/exec.test.d.ts +0 -1
- package/dist/review/reviewer/exec.test.js +0 -59
- package/dist/review/reviewer/inputs.test.d.ts +0 -1
- package/dist/review/reviewer/inputs.test.js +0 -26
- package/dist/review/reviewer/panel.test.d.ts +0 -1
- package/dist/review/reviewer/panel.test.js +0 -110
- package/dist/review/reviewer/record.test.d.ts +0 -1
- package/dist/review/reviewer/record.test.js +0 -32
- package/dist/review/reviewer/rule-writer.test.d.ts +0 -1
- package/dist/review/reviewer/rule-writer.test.js +0 -52
- package/dist/review/reviewer/settings.test.d.ts +0 -1
- package/dist/review/reviewer/settings.test.js +0 -57
- package/dist/review/reviewer/usage.test.d.ts +0 -1
- package/dist/review/reviewer/usage.test.js +0 -14
- package/dist/review/reviewer.test.d.ts +0 -1
- package/dist/review/reviewer.test.js +0 -705
- package/dist/review/stories.test.d.ts +0 -1
- package/dist/review/stories.test.js +0 -58
- package/dist/review/toolchain.test.d.ts +0 -1
- package/dist/review/toolchain.test.js +0 -108
- package/dist/review/typed/redundancy.test.d.ts +0 -1
- package/dist/review/typed/redundancy.test.js +0 -315
- package/dist/review/typed/schema-nullability.test.d.ts +0 -1
- package/dist/review/typed/schema-nullability.test.js +0 -63
- package/dist/review-learning/human-edits.test.d.ts +0 -1
- package/dist/review-learning/human-edits.test.js +0 -47
- package/dist/review-learning/repo-rules.test.d.ts +0 -1
- package/dist/review-learning/repo-rules.test.js +0 -55
- package/dist/review-learning/review-learning.test.d.ts +0 -1
- package/dist/review-learning/review-learning.test.js +0 -249
- package/dist/safety.test.d.ts +0 -1
- package/dist/safety.test.js +0 -42
- package/dist/semantic/benchmark.test.d.ts +0 -1
- package/dist/semantic/benchmark.test.js +0 -22
- package/dist/semantic/intent/intent.test.d.ts +0 -1
- package/dist/semantic/intent/intent.test.js +0 -46
- package/dist/semantic/learn/learn.test.d.ts +0 -1
- package/dist/semantic/learn/learn.test.js +0 -67
- package/dist/semantic/origins.test.d.ts +0 -1
- package/dist/semantic/origins.test.js +0 -77
- package/dist/semantic/project-facts.test.d.ts +0 -1
- package/dist/semantic/project-facts.test.js +0 -45
- package/dist/semantic/sites/call-sites.test.d.ts +0 -1
- package/dist/semantic/sites/call-sites.test.js +0 -46
- package/dist/services/adaptive-thresholds.test.d.ts +0 -1
- package/dist/services/adaptive-thresholds.test.js +0 -53
- package/dist/services/agent-history.test.d.ts +0 -1
- package/dist/services/agent-history.test.js +0 -69
- package/dist/services/context-scope-summary.test.d.ts +0 -1
- package/dist/services/context-scope-summary.test.js +0 -17
- package/dist/services/context-telemetry-service.test.d.ts +0 -1
- package/dist/services/context-telemetry-service.test.js +0 -182
- package/dist/services/cursor-usage-sync.test.d.ts +0 -1
- package/dist/services/cursor-usage-sync.test.js +0 -173
- package/dist/services/engineering-knowledge-graph.test.d.ts +0 -1
- package/dist/services/engineering-knowledge-graph.test.js +0 -76
- package/dist/services/model-pricing.test.d.ts +0 -1
- package/dist/services/model-pricing.test.js +0 -44
- package/dist/services/observed-savings.test.d.ts +0 -1
- package/dist/services/observed-savings.test.js +0 -37
- package/dist/services/score-history.test.d.ts +0 -1
- package/dist/services/score-history.test.js +0 -61
- package/dist/smoke.test.d.ts +0 -1
- package/dist/smoke.test.js +0 -17
- package/dist/storage/cache-cleanup.test.d.ts +0 -1
- package/dist/storage/cache-cleanup.test.js +0 -54
- package/dist/storage/context-telemetry.test.d.ts +0 -1
- package/dist/storage/context-telemetry.test.js +0 -80
- package/dist/storage/db.test.d.ts +0 -1
- package/dist/storage/db.test.js +0 -46
- package/dist/storage/fix-lessons.test.d.ts +0 -1
- package/dist/storage/fix-lessons.test.js +0 -61
- package/dist/storage/lessons.test.d.ts +0 -1
- package/dist/storage/lessons.test.js +0 -81
- package/dist/storage/local-encryption.test.d.ts +0 -1
- package/dist/storage/local-encryption.test.js +0 -34
- package/dist/storage/local-memory.test.d.ts +0 -1
- package/dist/storage/local-memory.test.js +0 -55
- package/dist/storage/share-memory.test.d.ts +0 -1
- package/dist/storage/share-memory.test.js +0 -34
- package/dist/storage/team-diagnostics.test.d.ts +0 -1
- package/dist/storage/team-diagnostics.test.js +0 -51
- package/dist/storage/team-scope.test.d.ts +0 -1
- package/dist/storage/team-scope.test.js +0 -22
- package/dist/storage/team-store.test.d.ts +0 -1
- package/dist/storage/team-store.test.js +0 -11
- package/dist/storage/team-sync-scope.test.d.ts +0 -1
- package/dist/storage/team-sync-scope.test.js +0 -100
- package/dist/storage/team-vector-store.test.d.ts +0 -1
- package/dist/storage/team-vector-store.test.js +0 -56
- package/dist/storage/telemetry-scope.test.d.ts +0 -1
- package/dist/storage/telemetry-scope.test.js +0 -38
- package/dist/telemetry/telemetry.test.d.ts +0 -1
- package/dist/telemetry/telemetry.test.js +0 -64
- package/dist/types/index.test.d.ts +0 -1
- package/dist/types/index.test.js +0 -33
- package/dist/utils/command-line.test.d.ts +0 -1
- package/dist/utils/command-line.test.js +0 -8
- package/dist/utils/diff-removed.test.d.ts +0 -1
- package/dist/utils/diff-removed.test.js +0 -29
- package/dist/utils/diff.test.d.ts +0 -1
- package/dist/utils/diff.test.js +0 -38
- package/dist/utils/glob-paths.test.d.ts +0 -1
- package/dist/utils/glob-paths.test.js +0 -40
- package/dist/utils/profile.test.d.ts +0 -1
- package/dist/utils/profile.test.js +0 -68
- package/dist/utils/scanner.test.d.ts +0 -1
- package/dist/utils/scanner.test.js +0 -48
- package/dist/utils/scope.test.d.ts +0 -1
- package/dist/utils/scope.test.js +0 -38
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import fs from 'fs-extra';
|
|
2
|
-
import os from 'os';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import { createHash } from 'crypto';
|
|
5
|
-
import { Readable, PassThrough } from 'stream';
|
|
6
|
-
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
7
|
-
import { downloadToFile } from './http-download.js';
|
|
8
|
-
const PAYLOAD = Buffer.from('0123456789abcdefghijklmnopqrstuvwxyz');
|
|
9
|
-
const sha = (b) => createHash('sha256').update(b).digest('hex');
|
|
10
|
-
function response(statusCode, body = Buffer.alloc(0), headers = {}) {
|
|
11
|
-
return { statusCode, headers, body: Buffer.isBuffer(body) ? Readable.from([body]) : body };
|
|
12
|
-
}
|
|
13
|
-
/** Serves PAYLOAD, honouring Range unless told not to. */
|
|
14
|
-
function server(opts = {}) {
|
|
15
|
-
const calls = [];
|
|
16
|
-
const get = async (url, headers) => {
|
|
17
|
-
calls.push({ url, headers });
|
|
18
|
-
const range = headers.Range?.match(/bytes=(\d+)-/);
|
|
19
|
-
if (range && opts.honourRange !== false) {
|
|
20
|
-
const start = Number(range[1]);
|
|
21
|
-
if (start >= PAYLOAD.length)
|
|
22
|
-
return response(416);
|
|
23
|
-
return response(206, PAYLOAD.subarray(start), { 'content-length': String(PAYLOAD.length - start) });
|
|
24
|
-
}
|
|
25
|
-
return response(200, PAYLOAD, { 'content-length': String(PAYLOAD.length), etag: opts.etag });
|
|
26
|
-
};
|
|
27
|
-
return { get, calls };
|
|
28
|
-
}
|
|
29
|
-
describe('downloadToFile', () => {
|
|
30
|
-
let dir;
|
|
31
|
-
let dest;
|
|
32
|
-
beforeEach(async () => {
|
|
33
|
-
dir = await fs.mkdtemp(path.join(os.tmpdir(), 'rigour-dl-'));
|
|
34
|
-
dest = path.join(dir, 'model.gguf.download');
|
|
35
|
-
});
|
|
36
|
-
afterEach(async () => { await fs.remove(dir); });
|
|
37
|
-
it('downloads the full body and hashes it', async () => {
|
|
38
|
-
const { get } = server({ etag: '"abc"' });
|
|
39
|
-
const result = await downloadToFile('https://host/model', dest, { get });
|
|
40
|
-
expect(await fs.readFile(dest)).toEqual(PAYLOAD);
|
|
41
|
-
expect(result).toEqual({ sha256: sha(PAYLOAD), bytes: PAYLOAD.length, etag: '"abc"' });
|
|
42
|
-
});
|
|
43
|
-
it('follows redirects and keeps the x-linked-etag from the first hop', async () => {
|
|
44
|
-
const { get: origin, calls } = server();
|
|
45
|
-
const get = async (url, headers, timeout) => {
|
|
46
|
-
if (url === 'https://hub/model') {
|
|
47
|
-
return response(302, Buffer.alloc(0), { location: 'https://cdn/model', 'x-linked-etag': '"lfs-sha"' });
|
|
48
|
-
}
|
|
49
|
-
return origin(url, headers, timeout);
|
|
50
|
-
};
|
|
51
|
-
const result = await downloadToFile('https://hub/model', dest, { get });
|
|
52
|
-
expect(calls.map(c => c.url)).toEqual(['https://cdn/model']);
|
|
53
|
-
expect(result.etag).toBe('"lfs-sha"');
|
|
54
|
-
});
|
|
55
|
-
it('resumes a partial file with a Range request and hashes the whole file', async () => {
|
|
56
|
-
await fs.writeFile(dest, PAYLOAD.subarray(0, 10));
|
|
57
|
-
const { get, calls } = server();
|
|
58
|
-
const result = await downloadToFile('https://host/model', dest, { get });
|
|
59
|
-
expect(calls[0].headers.Range).toBe('bytes=10-');
|
|
60
|
-
expect(await fs.readFile(dest)).toEqual(PAYLOAD);
|
|
61
|
-
expect(result.sha256).toBe(sha(PAYLOAD));
|
|
62
|
-
expect(result.bytes).toBe(PAYLOAD.length);
|
|
63
|
-
});
|
|
64
|
-
it('restarts from zero when the server ignores Range', async () => {
|
|
65
|
-
await fs.writeFile(dest, Buffer.from('stale-bytes'));
|
|
66
|
-
const { get } = server({ honourRange: false });
|
|
67
|
-
const result = await downloadToFile('https://host/model', dest, { get });
|
|
68
|
-
expect(await fs.readFile(dest)).toEqual(PAYLOAD);
|
|
69
|
-
expect(result.sha256).toBe(sha(PAYLOAD));
|
|
70
|
-
});
|
|
71
|
-
it('restarts when the partial is already past the end (416)', async () => {
|
|
72
|
-
await fs.writeFile(dest, Buffer.concat([PAYLOAD, Buffer.from('extra')]));
|
|
73
|
-
const { get, calls } = server();
|
|
74
|
-
await downloadToFile('https://host/model', dest, { get });
|
|
75
|
-
expect(calls).toHaveLength(2);
|
|
76
|
-
expect(await fs.readFile(dest)).toEqual(PAYLOAD);
|
|
77
|
-
});
|
|
78
|
-
it('rejects non-success responses without creating the file', async () => {
|
|
79
|
-
const get = async () => response(401);
|
|
80
|
-
await expect(downloadToFile('https://host/model', dest, { get })).rejects.toThrow('HTTP 401');
|
|
81
|
-
expect(await fs.pathExists(dest)).toBe(false);
|
|
82
|
-
});
|
|
83
|
-
it('refuses non-https URLs, including redirects to them', async () => {
|
|
84
|
-
const { get } = server();
|
|
85
|
-
await expect(downloadToFile('http://host/model', dest, { get })).rejects.toThrow('non-https');
|
|
86
|
-
const redirecting = async () => response(302, Buffer.alloc(0), { location: 'http://evil/model' });
|
|
87
|
-
await expect(downloadToFile('https://host/model', dest, { get: redirecting })).rejects.toThrow('non-https');
|
|
88
|
-
});
|
|
89
|
-
it('stops after too many redirects', async () => {
|
|
90
|
-
const loop = async (url) => response(302, Buffer.alloc(0), { location: `${url}x` });
|
|
91
|
-
await expect(downloadToFile('https://host/m', dest, { get: loop })).rejects.toThrow('Too many redirects');
|
|
92
|
-
});
|
|
93
|
-
it('aborts a stalled body and keeps the partial for the next resume', async () => {
|
|
94
|
-
const body = new PassThrough();
|
|
95
|
-
body.write(PAYLOAD.subarray(0, 5));
|
|
96
|
-
const get = async () => response(200, body, { 'content-length': String(PAYLOAD.length) });
|
|
97
|
-
await expect(downloadToFile('https://host/model', dest, { get, stallTimeoutMs: 50 })).rejects.toThrow('stalled');
|
|
98
|
-
// The partial file is kept for the next resume (how many bytes reached disk is up to the OS).
|
|
99
|
-
expect(await fs.pathExists(dest)).toBe(true);
|
|
100
|
-
expect((await fs.stat(dest)).size).toBeLessThanOrEqual(5);
|
|
101
|
-
});
|
|
102
|
-
it('reports progress against the full size when resuming', async () => {
|
|
103
|
-
await fs.writeFile(dest, PAYLOAD.subarray(0, 10));
|
|
104
|
-
const { get } = server();
|
|
105
|
-
const seen = [];
|
|
106
|
-
await downloadToFile('https://host/model', dest, { get, onProgress: (d, t) => seen.push([d, t]) });
|
|
107
|
-
expect(seen.at(-1)).toEqual([PAYLOAD.length, PAYLOAD.length]);
|
|
108
|
-
});
|
|
109
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import os from 'os';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import { Readable } from 'stream';
|
|
5
|
-
import { afterAll, describe, expect, it, vi } from 'vitest';
|
|
6
|
-
/** Install into a scratch folder: the real one is ~/.rigour. */
|
|
7
|
-
const home = vi.hoisted(() => {
|
|
8
|
-
const p = require('path');
|
|
9
|
-
return p.join(require('os').tmpdir(), `rigour-engine-${process.pid}`);
|
|
10
|
-
});
|
|
11
|
-
vi.mock('../storage/db.js', async (importOriginal) => ({ ...(await importOriginal()), RIGOUR_DIR: home }));
|
|
12
|
-
const { installLlamaEngine, managedEngineDir, releaseAssetFor } = await import('./llama-engine.js');
|
|
13
|
-
afterAll(() => fs.rmSync(home, { recursive: true, force: true }));
|
|
14
|
-
describe('llama.cpp engine install', () => {
|
|
15
|
-
it('pins a checksum for every published platform', () => {
|
|
16
|
-
for (const key of ['darwin-arm64', 'darwin-x64', 'linux-x64', 'win32-x64']) {
|
|
17
|
-
expect(releaseAssetFor(key)?.sha256).toMatch(/^[0-9a-f]{64}$/);
|
|
18
|
-
}
|
|
19
|
-
expect(releaseAssetFor('linux-arm64')).toBeUndefined();
|
|
20
|
-
});
|
|
21
|
-
it.runIf(releaseAssetFor(`${os.platform()}-${os.arch()}`))('refuses an archive that does not match its pinned checksum, and installs nothing', async () => {
|
|
22
|
-
const tampered = async () => ({ statusCode: 200, headers: {}, body: Readable.from([Buffer.from('not the real llama.cpp archive')]) });
|
|
23
|
-
await expect(installLlamaEngine(undefined, tampered)).rejects.toThrow(/does not match its pinned checksum/);
|
|
24
|
-
expect(fs.existsSync(managedEngineDir())).toBe(false);
|
|
25
|
-
expect(fs.readdirSync(path.dirname(managedEngineDir())).filter(f => f.endsWith('.zip'))).toEqual([]);
|
|
26
|
-
});
|
|
27
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import fs from 'fs-extra';
|
|
2
|
-
import os from 'os';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
5
|
-
import { installBinFolder, llamaBinaryName, probeBinary, resetProbeCache } from './llama-engine.js';
|
|
6
|
-
const isWindows = process.platform === 'win32';
|
|
7
|
-
async function writeScript(file, body) {
|
|
8
|
-
await fs.outputFile(file, `#!/bin/sh\n${body}\n`);
|
|
9
|
-
await fs.chmod(file, 0o755);
|
|
10
|
-
}
|
|
11
|
-
describe.skipIf(isWindows)('probeBinary', () => {
|
|
12
|
-
let dir;
|
|
13
|
-
beforeEach(async () => {
|
|
14
|
-
dir = await fs.mkdtemp(path.join(os.tmpdir(), 'rigour-engine-'));
|
|
15
|
-
resetProbeCache();
|
|
16
|
-
});
|
|
17
|
-
afterEach(async () => { await fs.remove(dir); });
|
|
18
|
-
it('accepts a binary that reports its version', async () => {
|
|
19
|
-
const bin = path.join(dir, 'llama-cli');
|
|
20
|
-
await writeScript(bin, 'echo "version: 5604 (228f34c9)" >&2; exit 0');
|
|
21
|
-
expect(await probeBinary(bin)).toEqual({ ok: true, version: '5604' });
|
|
22
|
-
});
|
|
23
|
-
it('rejects the placeholder brain script', async () => {
|
|
24
|
-
const bin = path.join(dir, 'rigour-brain');
|
|
25
|
-
await writeScript(bin, 'echo "rigour-brain is a placeholder." >&2; exit 1');
|
|
26
|
-
const result = await probeBinary(bin);
|
|
27
|
-
expect(result.ok).toBe(false);
|
|
28
|
-
expect(result.error).toContain('placeholder');
|
|
29
|
-
});
|
|
30
|
-
it('rejects a missing binary', async () => {
|
|
31
|
-
expect((await probeBinary(path.join(dir, 'nope'))).ok).toBe(false);
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
describe('installBinFolder', () => {
|
|
35
|
-
let dir;
|
|
36
|
-
beforeEach(async () => { dir = await fs.mkdtemp(path.join(os.tmpdir(), 'rigour-engine-')); });
|
|
37
|
-
afterEach(async () => { await fs.remove(dir); });
|
|
38
|
-
it('installs the binary together with the shared libraries next to it', async () => {
|
|
39
|
-
const extracted = path.join(dir, 'extract', 'build', 'bin');
|
|
40
|
-
await fs.outputFile(path.join(extracted, llamaBinaryName()), 'bin');
|
|
41
|
-
await fs.outputFile(path.join(extracted, 'libllama.dylib'), 'lib');
|
|
42
|
-
await fs.outputFile(path.join(extracted, 'ggml-metal.metal'), 'metal');
|
|
43
|
-
const installDir = path.join(dir, 'llama-b5604');
|
|
44
|
-
await installBinFolder(path.join(dir, 'extract'), installDir);
|
|
45
|
-
expect((await fs.readdir(installDir)).sort()).toEqual(['ggml-metal.metal', 'libllama.dylib', llamaBinaryName()].sort());
|
|
46
|
-
});
|
|
47
|
-
it('fails clearly when the archive has no llama-cli', async () => {
|
|
48
|
-
await fs.outputFile(path.join(dir, 'extract', 'README.md'), 'x');
|
|
49
|
-
await expect(installBinFolder(path.join(dir, 'extract'), path.join(dir, 'out'))).rejects.toThrow('not found');
|
|
50
|
-
});
|
|
51
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { buildLlamaArgs, cleanLlamaOutput, contextSizeFor, runProcess } from './llama-process.js';
|
|
3
|
-
const base = { modelPath: '/m.gguf', prompt: 'hello', maxTokens: 1024, threads: 4, temperature: 0.1 };
|
|
4
|
-
describe('buildLlamaArgs', () => {
|
|
5
|
-
it('uses flags llama.cpp b5604 accepts and that keep generated text on stdout', () => {
|
|
6
|
-
const args = buildLlamaArgs(base);
|
|
7
|
-
expect(args).toContain('-st');
|
|
8
|
-
expect(args).toContain('--simple-io');
|
|
9
|
-
expect(args).toContain('--no-display-prompt');
|
|
10
|
-
expect(args).not.toContain('--json');
|
|
11
|
-
expect(args).not.toContain('--log-disable');
|
|
12
|
-
});
|
|
13
|
-
it('fixes the seed so a run is reproducible', () => {
|
|
14
|
-
const args = buildLlamaArgs(base);
|
|
15
|
-
expect(args[args.indexOf('--seed') + 1]).toBe('42');
|
|
16
|
-
});
|
|
17
|
-
it('constrains output with the findings schema file when given', () => {
|
|
18
|
-
const args = buildLlamaArgs({ ...base, schemaPath: '/schema.json' });
|
|
19
|
-
expect(args.slice(-2)).toEqual(['--json-schema-file', '/schema.json']);
|
|
20
|
-
expect(buildLlamaArgs(base)).not.toContain('--json-schema-file');
|
|
21
|
-
});
|
|
22
|
-
it('passes a context size that fits the prompt', () => {
|
|
23
|
-
const args = buildLlamaArgs({ ...base, prompt: 'x'.repeat(30_000) });
|
|
24
|
-
expect(Number(args[args.indexOf('--ctx-size') + 1])).toBe(contextSizeFor('x'.repeat(30_000), 1024));
|
|
25
|
-
});
|
|
26
|
-
});
|
|
27
|
-
describe('contextSizeFor', () => {
|
|
28
|
-
it('never goes below the 4096 default', () => {
|
|
29
|
-
expect(contextSizeFor('short', 512)).toBe(4096);
|
|
30
|
-
});
|
|
31
|
-
it('grows with the prompt, rounded up to 1024', () => {
|
|
32
|
-
const size = contextSizeFor('x'.repeat(30_000), 1024);
|
|
33
|
-
expect(size).toBeGreaterThanOrEqual(10_000 + 1024);
|
|
34
|
-
expect(size % 1024).toBe(0);
|
|
35
|
-
});
|
|
36
|
-
it('is capped at the model training context', () => {
|
|
37
|
-
expect(contextSizeFor('x'.repeat(1_000_000), 1024)).toBe(32768);
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
describe('cleanLlamaOutput', () => {
|
|
41
|
-
it('strips the end-of-generation marker', () => {
|
|
42
|
-
expect(cleanLlamaOutput('{"findings": []} [end of text]\n\n')).toBe('{"findings": []}');
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
describe('runProcess', () => {
|
|
46
|
-
it('collects stdout and stderr and returns the exit code', async () => {
|
|
47
|
-
const result = await runProcess(process.execPath, ['-e', 'process.stdout.write("out");process.stderr.write("err");process.exit(3)'], { timeoutMs: 10_000 });
|
|
48
|
-
expect(result).toEqual({ code: 3, stdout: 'out', stderr: 'err' });
|
|
49
|
-
});
|
|
50
|
-
it('runs with stdin closed so an interactive binary cannot wait on it', async () => {
|
|
51
|
-
const result = await runProcess(process.execPath, ['-e', 'process.stdin.on("end",()=>process.stdout.write("eof")).resume()'], { timeoutMs: 10_000 });
|
|
52
|
-
expect(result.stdout).toBe('eof');
|
|
53
|
-
});
|
|
54
|
-
it('kills and rejects on timeout', async () => {
|
|
55
|
-
await expect(runProcess(process.execPath, ['-e', 'setTimeout(()=>{}, 10_000)'], { timeoutMs: 100 }))
|
|
56
|
-
.rejects.toThrow('timed out');
|
|
57
|
-
});
|
|
58
|
-
it('rejects when the binary cannot be spawned', async () => {
|
|
59
|
-
await expect(runProcess('/nonexistent/llama-cli', [], { timeoutMs: 1_000 })).rejects.toThrow();
|
|
60
|
-
});
|
|
61
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,23 +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 { localModel } from './sidecar-provider.js';
|
|
6
|
-
let dir;
|
|
7
|
-
afterEach(() => { if (dir)
|
|
8
|
-
fs.rmSync(dir, { recursive: true, force: true }); dir = undefined; });
|
|
9
|
-
describe('a local GGUF for --model-path', () => {
|
|
10
|
-
it('is used as is, never reported as a fallback', async () => {
|
|
11
|
-
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'local-model-'));
|
|
12
|
-
const file = path.join(dir, 'rigour-max-candidate-q4_k_m.gguf');
|
|
13
|
-
fs.writeFileSync(file, Buffer.alloc(2_000_000));
|
|
14
|
-
expect(await localModel('max', file)).toEqual({
|
|
15
|
-
path: file, fallback: false,
|
|
16
|
-
info: { tier: 'max', name: 'rigour-max-candidate-q4_k_m.gguf (local)', filename: 'rigour-max-candidate-q4_k_m.gguf', url: '', sizeBytes: 2_000_000, sizeHuman: '2MB' },
|
|
17
|
-
});
|
|
18
|
-
});
|
|
19
|
-
it('fails clearly when the file is missing, rather than downloading something else', async () => {
|
|
20
|
-
const missing = path.resolve('/nonexistent/model.gguf'); // D:\\nonexistent\\model.gguf on Windows
|
|
21
|
-
await expect(localModel('max', missing)).rejects.toThrow(`Model file not found: ${missing}`);
|
|
22
|
-
});
|
|
23
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
import fs from 'fs-extra';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import { createHash } from 'crypto';
|
|
4
|
-
import { Readable } from 'stream';
|
|
5
|
-
import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
-
const home = vi.hoisted(() => {
|
|
7
|
-
const p = require('path');
|
|
8
|
-
const o = require('os');
|
|
9
|
-
return p.join(o.tmpdir(), `rigour-models-${process.pid}-${Date.now()}`);
|
|
10
|
-
});
|
|
11
|
-
vi.mock('../storage/db.js', () => ({ RIGOUR_DIR: home }));
|
|
12
|
-
const { checkForUpdates, downloadModel, getCachedModel, normalizeModelVersion } = await import('./model-manager.js');
|
|
13
|
-
const { FALLBACK_MODELS, MODELS } = await import('./types.js');
|
|
14
|
-
const modelsDir = path.join(home, 'models');
|
|
15
|
-
const PAYLOAD = Buffer.from('gguf-model-bytes');
|
|
16
|
-
const PAYLOAD_SHA = createHash('sha256').update(PAYLOAD).digest('hex');
|
|
17
|
-
function ok(body, sha) {
|
|
18
|
-
return Promise.resolve({ statusCode: 200, headers: { 'x-linked-etag': `"${sha}"` }, body: Readable.from([body]) });
|
|
19
|
-
}
|
|
20
|
-
const unauthorized = Promise.resolve({ statusCode: 401, headers: {}, body: Readable.from([]) });
|
|
21
|
-
describe('downloadModel', () => {
|
|
22
|
-
beforeEach(async () => {
|
|
23
|
-
await fs.remove(home);
|
|
24
|
-
// A fresh version check: no network call to Hugging Face.
|
|
25
|
-
await fs.outputJson(path.join(modelsDir, '.latest_version.json'), { format: 2, version: '5', checkedAt: new Date().toISOString() });
|
|
26
|
-
});
|
|
27
|
-
afterAll(async () => { await fs.remove(home); });
|
|
28
|
-
it('falls back to the stock model when the fine-tuned model is unavailable, and says so', async () => {
|
|
29
|
-
const requested = [];
|
|
30
|
-
const get = (url) => {
|
|
31
|
-
requested.push(url);
|
|
32
|
-
return url.includes('rigour-labs/') ? unauthorized : ok(PAYLOAD, PAYLOAD_SHA);
|
|
33
|
-
};
|
|
34
|
-
const messages = [];
|
|
35
|
-
const modelPath = await downloadModel('lite', m => messages.push(m), get);
|
|
36
|
-
expect(requested).toEqual([MODELS.lite.url, FALLBACK_MODELS.lite.url]);
|
|
37
|
-
expect(path.basename(modelPath)).toBe(FALLBACK_MODELS.lite.filename);
|
|
38
|
-
expect(messages.some(m => m.includes('Fine-tuned model unavailable (HTTP 401'))).toBe(true);
|
|
39
|
-
const meta = await fs.readJson(`${modelPath}.meta.json`);
|
|
40
|
-
expect(meta).toMatchObject({ sha256: PAYLOAD_SHA, sizeBytes: PAYLOAD.length, sourceUrl: FALLBACK_MODELS.lite.url });
|
|
41
|
-
});
|
|
42
|
-
it('retries the fine-tuned model once a day when only the stock fallback is cached', async () => {
|
|
43
|
-
const failing = (url) => (url.includes('rigour-labs/') ? unauthorized : ok(PAYLOAD, PAYLOAD_SHA));
|
|
44
|
-
const stockPath = await downloadModel('lite', undefined, failing);
|
|
45
|
-
const requested = [];
|
|
46
|
-
const again = await downloadModel('lite', undefined, (url) => { requested.push(url); return unauthorized; });
|
|
47
|
-
expect(requested).toEqual([]); // failed less than a day ago: keep the fallback, no request
|
|
48
|
-
expect(again).toBe(stockPath);
|
|
49
|
-
await fs.writeJson(path.join(modelsDir, '.fine-tuned-unavailable-lite.json'), { failedAt: '2000-01-01T00:00:00Z', url: MODELS.lite.url });
|
|
50
|
-
const fineTuned = await downloadModel('lite', undefined, (url) => { requested.push(url); return ok(PAYLOAD, PAYLOAD_SHA); });
|
|
51
|
-
expect(requested).toEqual([MODELS.lite.url]);
|
|
52
|
-
expect(path.basename(fineTuned)).toBe(MODELS.lite.filename);
|
|
53
|
-
expect(await fs.pathExists(path.join(modelsDir, '.fine-tuned-unavailable-lite.json'))).toBe(false);
|
|
54
|
-
});
|
|
55
|
-
it('rejects a checksum mismatch and deletes the partial download', async () => {
|
|
56
|
-
const get = () => ok(PAYLOAD, 'f'.repeat(64));
|
|
57
|
-
await expect(downloadModel('lite', undefined, get)).rejects.toThrow('Checksum mismatch');
|
|
58
|
-
expect(await fs.pathExists(path.join(modelsDir, `${FALLBACK_MODELS.lite.filename}.download`))).toBe(false);
|
|
59
|
-
expect(await fs.pathExists(path.join(modelsDir, FALLBACK_MODELS.lite.filename))).toBe(false);
|
|
60
|
-
});
|
|
61
|
-
});
|
|
62
|
-
describe('getCachedModel', () => {
|
|
63
|
-
beforeEach(async () => { await fs.remove(home); });
|
|
64
|
-
async function cacheFile(filename, sizeBytes) {
|
|
65
|
-
const file = path.join(modelsDir, filename);
|
|
66
|
-
await fs.ensureDir(modelsDir);
|
|
67
|
-
await fs.writeFile(file, '');
|
|
68
|
-
await fs.truncate(file, sizeBytes); // sparse: no real disk use
|
|
69
|
-
await fs.writeJson(`${file}.meta.json`, {
|
|
70
|
-
sha256: 'a'.repeat(64), sizeBytes, verifiedAt: new Date(Date.now() + 1000).toISOString(), sourceUrl: 'https://x',
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
it('returns null when nothing is cached', async () => {
|
|
74
|
-
expect(await getCachedModel('deep')).toBeNull();
|
|
75
|
-
});
|
|
76
|
-
it('reports the stock fallback when only it is cached', async () => {
|
|
77
|
-
await cacheFile(FALLBACK_MODELS.deep.filename, FALLBACK_MODELS.deep.sizeBytes);
|
|
78
|
-
const cached = await getCachedModel('deep');
|
|
79
|
-
expect(cached).toMatchObject({ fallback: true, info: { filename: FALLBACK_MODELS.deep.filename } });
|
|
80
|
-
});
|
|
81
|
-
it('accepts a verified fine-tuned file smaller than the size estimate', async () => {
|
|
82
|
-
await cacheFile(MODELS.lite.filename, 397_807_360); // the published v5 lite model
|
|
83
|
-
expect(await getCachedModel('lite')).toMatchObject({ fallback: false, info: { filename: MODELS.lite.filename } });
|
|
84
|
-
});
|
|
85
|
-
it('accepts a file verified within the same millisecond it was written', async () => {
|
|
86
|
-
const file = path.join(modelsDir, FALLBACK_MODELS.deep.filename);
|
|
87
|
-
await cacheFile(FALLBACK_MODELS.deep.filename, 10);
|
|
88
|
-
const written = new Date('2026-01-01T00:00:00.000Z');
|
|
89
|
-
await fs.utimes(file, written, written.getTime() / 1000 + 0.0007); // mtime 0.7 ms into the same millisecond
|
|
90
|
-
await fs.writeJson(`${file}.meta.json`, { sha256: 'a'.repeat(64), sizeBytes: 10, verifiedAt: written.toISOString(), sourceUrl: 'https://x' });
|
|
91
|
-
expect(await getCachedModel('deep')).toMatchObject({ fallback: true });
|
|
92
|
-
});
|
|
93
|
-
it('rejects a file modified after it was verified', async () => {
|
|
94
|
-
const file = path.join(modelsDir, FALLBACK_MODELS.deep.filename);
|
|
95
|
-
await cacheFile(FALLBACK_MODELS.deep.filename, 10);
|
|
96
|
-
await fs.writeJson(`${file}.meta.json`, { sha256: 'a'.repeat(64), sizeBytes: 10, verifiedAt: '2000-01-01T00:00:00.000Z', sourceUrl: 'https://x' });
|
|
97
|
-
expect(await getCachedModel('deep')).toBeNull();
|
|
98
|
-
});
|
|
99
|
-
it('ignores a file without verified metadata', async () => {
|
|
100
|
-
await fs.ensureDir(modelsDir);
|
|
101
|
-
await fs.writeFile(path.join(modelsDir, FALLBACK_MODELS.deep.filename), 'partial');
|
|
102
|
-
expect(await getCachedModel('deep')).toBeNull();
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
describe('model versions', () => {
|
|
106
|
-
beforeEach(async () => { await fs.remove(home); vi.unstubAllGlobals(); });
|
|
107
|
-
it('keeps published version strings as they are', () => {
|
|
108
|
-
expect(normalizeModelVersion(5)).toBe('5');
|
|
109
|
-
expect(normalizeModelVersion('5')).toBe('5');
|
|
110
|
-
expect(normalizeModelVersion('v2.1.0')).toBe('2.1.0');
|
|
111
|
-
expect(normalizeModelVersion('5.0')).toBeNull();
|
|
112
|
-
expect(normalizeModelVersion({})).toBeNull();
|
|
113
|
-
});
|
|
114
|
-
it('names the published v5 repositories by default', () => {
|
|
115
|
-
expect(MODELS.lite.url).toBe('https://huggingface.co/rigour-labs/rigour-lite-v5-gguf/resolve/main/rigour-lite-v5-q4_k_m.gguf');
|
|
116
|
-
expect(MODELS.deep.url).toBe('https://huggingface.co/rigour-labs/rigour-deep-v5-gguf/resolve/main/rigour-deep-v5-q4_k_m.gguf');
|
|
117
|
-
});
|
|
118
|
-
it('ignores a version cache from the old SemVer conversion and re-reads the published version', async () => {
|
|
119
|
-
await fs.outputJson(path.join(modelsDir, '.latest_version.json'), { version: '5.0.0', checkedAt: new Date().toISOString() });
|
|
120
|
-
vi.stubGlobal('fetch', vi.fn(async () => ({ ok: true, json: async () => ({ version: 5 }) })));
|
|
121
|
-
expect(await checkForUpdates()).toBe('5');
|
|
122
|
-
expect(await fs.readJson(path.join(modelsDir, '.latest_version.json'))).toMatchObject({ format: 2, version: '5' });
|
|
123
|
-
expect(MODELS.deep.filename).toBe('rigour-deep-v5-q4_k_m.gguf');
|
|
124
|
-
});
|
|
125
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import fs from 'fs-extra';
|
|
3
|
-
import os from 'os';
|
|
4
|
-
import path from 'path';
|
|
5
|
-
import { extractSha256FromEtag, hashFileSha256 } from './model-manager.js';
|
|
6
|
-
describe('model manager integrity helpers', () => {
|
|
7
|
-
it('extracts sha256 digest from a strong ETag', () => {
|
|
8
|
-
const digest = 'a'.repeat(64);
|
|
9
|
-
expect(extractSha256FromEtag(`"${digest}"`)).toBe(digest);
|
|
10
|
-
expect(extractSha256FromEtag(`W/"${digest}"`)).toBe(digest);
|
|
11
|
-
});
|
|
12
|
-
it('returns null for non-sha ETags', () => {
|
|
13
|
-
expect(extractSha256FromEtag('"not-a-digest"')).toBeNull();
|
|
14
|
-
expect(extractSha256FromEtag(null)).toBeNull();
|
|
15
|
-
});
|
|
16
|
-
it('hashes file contents with sha256', async () => {
|
|
17
|
-
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'rigour-model-hash-'));
|
|
18
|
-
const filePath = path.join(dir, 'sample.gguf');
|
|
19
|
-
await fs.writeFile(filePath, 'rigour-model-check');
|
|
20
|
-
const digest = await hashFileSha256(filePath);
|
|
21
|
-
expect(digest).toBe('e123266ea4b37a81948a0a844dd58eddfc81737aa6fdf9dafc818fd23bae75f0');
|
|
22
|
-
await fs.remove(dir);
|
|
23
|
-
});
|
|
24
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { buildModelInfo, FALLBACK_MODELS, localTier, MODELS } from './types.js';
|
|
3
|
-
describe('local model tiers', () => {
|
|
4
|
-
it('picks max over pro over the lite default', () => {
|
|
5
|
-
expect(localTier({})).toBe('lite');
|
|
6
|
-
expect(localTier({ pro: true })).toBe('deep');
|
|
7
|
-
expect(localTier({ max: true })).toBe('max');
|
|
8
|
-
expect(localTier({ max: true, pro: true })).toBe('max');
|
|
9
|
-
});
|
|
10
|
-
it('asks for a published max fine-tune and falls back to the stock 7B coder model', () => {
|
|
11
|
-
expect(buildModelInfo('max', '6.0.0').url)
|
|
12
|
-
.toBe('https://huggingface.co/rigour-labs/rigour-max-v6.0.0-gguf/resolve/main/rigour-max-v6.0.0-q4_k_m.gguf');
|
|
13
|
-
expect(MODELS.max.tier).toBe('max');
|
|
14
|
-
expect(FALLBACK_MODELS.max).toMatchObject({
|
|
15
|
-
filename: 'qwen2.5-coder-7b-instruct-q4_k_m.gguf',
|
|
16
|
-
url: 'https://huggingface.co/Qwen/Qwen2.5-Coder-7B-Instruct-GGUF/resolve/main/qwen2.5-coder-7b-instruct-q4_k_m.gguf',
|
|
17
|
-
});
|
|
18
|
-
});
|
|
19
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { RECALL_LIMIT, rankMemories } from './recall.js';
|
|
3
|
-
const memories = [
|
|
4
|
-
{ scope: 'repo', key: 'retry_policy', value: 'Wrap network calls in withRetry from lib/net; never hand-roll backoff.' },
|
|
5
|
-
{ scope: 'repo', key: 'db_access', value: 'Reports read from the replica, never the primary.' },
|
|
6
|
-
{ scope: 'user', key: 'pr_size', value: 'Keep pull requests under 400 lines.' },
|
|
7
|
-
];
|
|
8
|
-
/** A stand-in embedding: one dimension per topic word, so similarity is predictable. */
|
|
9
|
-
const TOPICS = ['retry', 'backoff', 'network', 'replica', 'report', 'pull', 'request'];
|
|
10
|
-
const embed = async (text) => TOPICS.map(topic => (text.toLowerCase().includes(topic) ? 1 : 0));
|
|
11
|
-
describe('rankMemories', () => {
|
|
12
|
-
it('returns the memories that match by meaning, best first, above the floor', async () => {
|
|
13
|
-
const ranked = await rankMemories('add exponential backoff to a flaky network call', memories, embed);
|
|
14
|
-
expect(ranked.map(m => [m.key, m.match])).toEqual([['retry_policy', 'semantic']]);
|
|
15
|
-
});
|
|
16
|
-
it('returns nothing rather than the closest weak match', async () => {
|
|
17
|
-
expect(await rankMemories('rename a css class', memories, embed)).toEqual([]);
|
|
18
|
-
});
|
|
19
|
-
it('falls back to keyword overlap when the model is unavailable, and says so', async () => {
|
|
20
|
-
const ranked = await rankMemories('replica reports', memories, async () => []);
|
|
21
|
-
expect(ranked.map(m => [m.key, m.match])).toEqual([['db_access', 'keyword']]);
|
|
22
|
-
});
|
|
23
|
-
it(`returns at most ${RECALL_LIMIT}`, async () => {
|
|
24
|
-
const many = Array.from({ length: 10 }, (_, i) => ({ scope: 'repo', key: `retry_${i}`, value: 'network retry backoff' }));
|
|
25
|
-
expect(await rankMemories('network retry backoff', many, embed)).toHaveLength(RECALL_LIMIT);
|
|
26
|
-
});
|
|
27
|
-
it('embeds each text once, not on every recall', async () => {
|
|
28
|
-
let calls = 0;
|
|
29
|
-
const counting = async (text) => { calls++; return embed(text); };
|
|
30
|
-
const fresh = memories.map(m => ({ ...m, value: `${m.value} (cache test)` }));
|
|
31
|
-
await rankMemories('network retry', fresh, counting);
|
|
32
|
-
const first = calls;
|
|
33
|
-
await rankMemories('replica reports', fresh, counting);
|
|
34
|
-
expect(calls - first).toBe(1); // only the new query
|
|
35
|
-
});
|
|
36
|
-
});
|