mirror of
https://github.com/signerless/llm-checker.git
synced 2026-10-20 11:37:29 +00:00
`search` and `list-models` were Ollama-catalog only, so the ~32k HF/Ollama/GPT4All registry was invisible unless you knew the separate registry-* commands. - `search <query> --registry` (or `--source <ollama|huggingface|gpt4all>`) now searches the multi-source registry with --max-params/--min-params/--runtime/ --format/--quant/--max-size filters and exact install commands. Default (no --registry/--source) keeps the Ollama-catalog behavior. - `list-models --registry`/`--source` lists the registry. - Extracted the registry search+render into one shared executeRegistrySearch() used by registry-search and the new search/list-models paths (enum validation included). Bumps to 3.7.5. New cases in tests/registry-cli-validation.test.js; full suite 48/48. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
114 lines
5.3 KiB
JavaScript
114 lines
5.3 KiB
JavaScript
/**
|
|
* Registry CLI validation + empty-pool guard test
|
|
* ===============================================
|
|
* - registry-search / registry-recommend reject invalid enum options with a
|
|
* clear error (exit 1 / JSON {error}) instead of silently returning "no
|
|
* results" or falling back to the built-in catalog.
|
|
* - When no registry artifacts match the filters, selectCategory returns an
|
|
* empty result rather than letting the deterministic selector substitute its
|
|
* internal catalog (which would mislabel non-registry rows as "registry").
|
|
*/
|
|
|
|
const assert = require('assert');
|
|
const path = require('path');
|
|
const { spawnSync } = require('child_process');
|
|
|
|
const { RegistryRecommender } = require('../src/data/registry-recommender');
|
|
|
|
const CLI = path.join(__dirname, '..', 'bin', 'enhanced_cli.js');
|
|
|
|
function runCli(args) {
|
|
const res = spawnSync(process.execPath, [CLI, ...args], { encoding: 'utf8' });
|
|
return { status: res.status, stdout: res.stdout || '', stderr: res.stderr || '' };
|
|
}
|
|
|
|
function testInvalidSourceRejected() {
|
|
const { status, stdout } = runCli(['registry-search', 'llama', '--source', 'bogus', '--json']);
|
|
assert.strictEqual(status, 1, 'invalid --source must exit non-zero');
|
|
const payload = JSON.parse(stdout);
|
|
assert.ok(/Invalid --source/.test(payload.error), 'must report an invalid-source error');
|
|
}
|
|
|
|
function testInvalidRuntimeRejected() {
|
|
const { status, stdout } = runCli(['registry-recommend', '--runtime', 'garbage', '--json']);
|
|
assert.strictEqual(status, 1, 'invalid --runtime must exit non-zero');
|
|
const payload = JSON.parse(stdout);
|
|
assert.ok(/Invalid --runtime/.test(payload.error), 'must report an invalid-runtime error');
|
|
}
|
|
|
|
function testValidSourceAccepted() {
|
|
const { status, stdout } = runCli(['registry-search', 'bert', '--source', 'huggingface', '--json', '--limit', '1']);
|
|
assert.strictEqual(status, 0, 'valid --source must exit 0');
|
|
const payload = JSON.parse(stdout);
|
|
assert.ok(!('error' in payload), 'valid filters must not produce an error');
|
|
}
|
|
|
|
async function testEmptyPoolDoesNotFallBackToInternalCatalog() {
|
|
// Fake DB that matches zero artifacts; fake selector whose selectModels MUST
|
|
// NOT be called (calling it would load the built-in catalog).
|
|
const fakeDatabase = {
|
|
async initialize() {},
|
|
searchModelArtifacts() { return []; },
|
|
getRegistryStats() { return { sources: 0, repos: 0, artifacts: 0 }; },
|
|
close() {}
|
|
};
|
|
const fakeSelector = {
|
|
normalizeOptimizationObjective(v) { return v || 'balanced'; },
|
|
async selectModels() { throw new Error('selectModels must not run when the registry pool is empty'); }
|
|
};
|
|
const rec = new RegistryRecommender({ database: fakeDatabase, selector: fakeSelector });
|
|
await rec.initialize();
|
|
const out = await rec.recommend({ category: 'coding', runtime: 'mlx' });
|
|
assert.strictEqual(out.total_artifacts, 0, 'no artifacts matched');
|
|
assert.deepStrictEqual(out.recommendations, [], 'empty pool yields no recommendations (not internal-catalog rows)');
|
|
}
|
|
|
|
function testSearchRegistryModeUsesRegistry() {
|
|
// `search --registry` searches the multi-source registry (registry JSON shape
|
|
// has `stats`/`count`), and can surface non-Ollama sources.
|
|
const { status, stdout } = runCli(['search', 'qwen', '--registry', '--source', 'huggingface', '--json', '--limit', '2']);
|
|
assert.strictEqual(status, 0, 'search --registry exits 0');
|
|
const payload = JSON.parse(stdout);
|
|
assert.ok('stats' in payload && 'count' in payload, 'registry mode returns the registry JSON shape');
|
|
const sources = new Set((payload.results || []).map((r) => r.source_id));
|
|
assert.ok(sources.size === 0 || sources.has('huggingface'), 'source filter restricts to huggingface');
|
|
}
|
|
|
|
function testPlainSearchStaysOllamaCatalog() {
|
|
// Without --registry/--source, search keeps its original Ollama-catalog shape
|
|
// (no registry `stats` field) — backward compatible.
|
|
const { status, stdout } = runCli(['search', 'llama', '--json', '--limit', '2']);
|
|
assert.strictEqual(status, 0, 'plain search exits 0');
|
|
const payload = JSON.parse(stdout);
|
|
assert.ok(!(payload && typeof payload === 'object' && 'stats' in payload), 'plain search must not use registry shape');
|
|
}
|
|
|
|
function testListModelsRegistrySource() {
|
|
const { status, stdout } = runCli(['list-models', '--source', 'gpt4all', '--json', '--limit', '2']);
|
|
assert.strictEqual(status, 0, 'list-models --source exits 0');
|
|
const payload = JSON.parse(stdout);
|
|
assert.ok('stats' in payload, 'list-models --source returns registry shape');
|
|
const sources = new Set((payload.results || []).map((r) => r.source_id));
|
|
assert.ok(sources.size === 0 || sources.has('gpt4all'), 'restricted to gpt4all');
|
|
}
|
|
|
|
async function run() {
|
|
testInvalidSourceRejected();
|
|
testSearchRegistryModeUsesRegistry();
|
|
testPlainSearchStaysOllamaCatalog();
|
|
testListModelsRegistrySource();
|
|
testInvalidRuntimeRejected();
|
|
testValidSourceAccepted();
|
|
await testEmptyPoolDoesNotFallBackToInternalCatalog();
|
|
console.log('registry-cli-validation.test.js: OK');
|
|
}
|
|
|
|
if (require.main === module) {
|
|
run().catch((error) => {
|
|
console.error('registry-cli-validation.test.js: FAILED');
|
|
console.error(error);
|
|
process.exit(1);
|
|
});
|
|
}
|
|
|
|
module.exports = { run };
|