voyageai-cli 1.12.1 → 1.15.0
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/README.md +3 -3
- package/demo-readme.gif +0 -0
- package/package.json +1 -1
- package/src/cli.js +2 -0
- package/src/commands/benchmark.js +164 -0
- package/src/commands/completions.js +18 -1
- package/src/commands/estimate.js +209 -0
- package/src/commands/models.js +32 -4
- package/src/lib/catalog.js +42 -18
- package/src/lib/explanations.js +183 -0
- package/.github/workflows/ci.yml +0 -22
- package/CONTRIBUTING.md +0 -81
- package/demo.gif +0 -0
- package/demo.tape +0 -39
- package/scripts/record-demo.sh +0 -63
- package/test/commands/about.test.js +0 -23
- package/test/commands/benchmark.test.js +0 -319
- package/test/commands/completions.test.js +0 -166
- package/test/commands/config.test.js +0 -35
- package/test/commands/demo.test.js +0 -46
- package/test/commands/embed.test.js +0 -42
- package/test/commands/explain.test.js +0 -207
- package/test/commands/ingest.test.js +0 -261
- package/test/commands/models.test.js +0 -132
- package/test/commands/ping.test.js +0 -172
- package/test/commands/playground.test.js +0 -137
- package/test/commands/rerank.test.js +0 -32
- package/test/commands/similarity.test.js +0 -79
- package/test/commands/store.test.js +0 -26
- package/test/fixtures/sample.csv +0 -6
- package/test/fixtures/sample.json +0 -7
- package/test/fixtures/sample.jsonl +0 -5
- package/test/fixtures/sample.txt +0 -5
- package/test/lib/api.test.js +0 -133
- package/test/lib/banner.test.js +0 -44
- package/test/lib/catalog.test.js +0 -99
- package/test/lib/config.test.js +0 -124
- package/test/lib/explanations.test.js +0 -141
- package/test/lib/format.test.js +0 -75
- package/test/lib/input.test.js +0 -48
- package/test/lib/math.test.js +0 -43
- package/test/lib/ui.test.js +0 -79
- package/voyageai-cli-playground.png +0 -0
- package/voyageai-cli.png +0 -0
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const { describe, it, beforeEach, afterEach } = require('node:test');
|
|
4
|
-
const assert = require('node:assert/strict');
|
|
5
|
-
const { Command } = require('commander');
|
|
6
|
-
const { registerExplain } = require('../../src/commands/explain');
|
|
7
|
-
|
|
8
|
-
describe('explain command', () => {
|
|
9
|
-
let originalLog;
|
|
10
|
-
let originalError;
|
|
11
|
-
let originalExit;
|
|
12
|
-
let output;
|
|
13
|
-
let errorOutput;
|
|
14
|
-
|
|
15
|
-
beforeEach(() => {
|
|
16
|
-
originalLog = console.log;
|
|
17
|
-
originalError = console.error;
|
|
18
|
-
originalExit = process.exit;
|
|
19
|
-
output = [];
|
|
20
|
-
errorOutput = [];
|
|
21
|
-
console.log = (...args) => output.push(args.join(' '));
|
|
22
|
-
console.error = (...args) => errorOutput.push(args.join(' '));
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
afterEach(() => {
|
|
26
|
-
console.log = originalLog;
|
|
27
|
-
console.error = originalError;
|
|
28
|
-
process.exit = originalExit;
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('registers correctly on a program', () => {
|
|
32
|
-
const program = new Command();
|
|
33
|
-
registerExplain(program);
|
|
34
|
-
const cmd = program.commands.find(c => c.name() === 'explain');
|
|
35
|
-
assert.ok(cmd, 'explain command should be registered');
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('lists topics when no argument given', async () => {
|
|
39
|
-
const program = new Command();
|
|
40
|
-
program.exitOverride();
|
|
41
|
-
registerExplain(program);
|
|
42
|
-
|
|
43
|
-
await program.parseAsync(['node', 'test', 'explain']);
|
|
44
|
-
|
|
45
|
-
const combined = output.join('\n');
|
|
46
|
-
assert.ok(combined.includes('Available topics'), 'Should show available topics header');
|
|
47
|
-
assert.ok(combined.includes('embeddings'), 'Should list embeddings');
|
|
48
|
-
assert.ok(combined.includes('reranking'), 'Should list reranking');
|
|
49
|
-
assert.ok(combined.includes('rag'), 'Should list rag');
|
|
50
|
-
assert.ok(combined.includes('vector-search'), 'Should list vector-search');
|
|
51
|
-
assert.ok(combined.includes('vai explain <topic>'), 'Should show usage hint');
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it('shows content for a known concept', async () => {
|
|
55
|
-
const program = new Command();
|
|
56
|
-
program.exitOverride();
|
|
57
|
-
registerExplain(program);
|
|
58
|
-
|
|
59
|
-
await program.parseAsync(['node', 'test', 'explain', 'embeddings']);
|
|
60
|
-
|
|
61
|
-
const combined = output.join('\n');
|
|
62
|
-
assert.ok(combined.includes('Embeddings'), 'Should show title');
|
|
63
|
-
assert.ok(combined.includes('vector'), 'Should contain explanation about vectors');
|
|
64
|
-
assert.ok(combined.includes('Try it'), 'Should show Try it section');
|
|
65
|
-
assert.ok(combined.includes('Learn more'), 'Should show Learn more section');
|
|
66
|
-
assert.ok(combined.includes('not affiliated'), 'Should show disclaimer');
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('resolves alias "embed" to embeddings', async () => {
|
|
70
|
-
const program = new Command();
|
|
71
|
-
program.exitOverride();
|
|
72
|
-
registerExplain(program);
|
|
73
|
-
|
|
74
|
-
await program.parseAsync(['node', 'test', 'explain', 'embed']);
|
|
75
|
-
|
|
76
|
-
const combined = output.join('\n');
|
|
77
|
-
assert.ok(combined.includes('Embeddings'), 'Should resolve alias and show Embeddings');
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('resolves alias "rerank" to reranking', async () => {
|
|
81
|
-
const program = new Command();
|
|
82
|
-
program.exitOverride();
|
|
83
|
-
registerExplain(program);
|
|
84
|
-
|
|
85
|
-
await program.parseAsync(['node', 'test', 'explain', 'rerank']);
|
|
86
|
-
|
|
87
|
-
const combined = output.join('\n');
|
|
88
|
-
assert.ok(combined.includes('Reranking'), 'Should resolve alias and show Reranking');
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('resolves alias "vectors" to vector-search', async () => {
|
|
92
|
-
const program = new Command();
|
|
93
|
-
program.exitOverride();
|
|
94
|
-
registerExplain(program);
|
|
95
|
-
|
|
96
|
-
await program.parseAsync(['node', 'test', 'explain', 'vectors']);
|
|
97
|
-
|
|
98
|
-
const combined = output.join('\n');
|
|
99
|
-
assert.ok(combined.includes('Vector Search'), 'Should resolve alias and show Vector Search');
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it('resolves alias "similarity" to cosine-similarity', async () => {
|
|
103
|
-
const program = new Command();
|
|
104
|
-
program.exitOverride();
|
|
105
|
-
registerExplain(program);
|
|
106
|
-
|
|
107
|
-
await program.parseAsync(['node', 'test', 'explain', 'similarity']);
|
|
108
|
-
|
|
109
|
-
const combined = output.join('\n');
|
|
110
|
-
assert.ok(combined.includes('Cosine Similarity'), 'Should resolve alias');
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('resolves alias "two-stage" to two-stage-retrieval', async () => {
|
|
114
|
-
const program = new Command();
|
|
115
|
-
program.exitOverride();
|
|
116
|
-
registerExplain(program);
|
|
117
|
-
|
|
118
|
-
await program.parseAsync(['node', 'test', 'explain', 'two-stage']);
|
|
119
|
-
|
|
120
|
-
const combined = output.join('\n');
|
|
121
|
-
assert.ok(combined.includes('Two-Stage Retrieval'), 'Should resolve alias');
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it('resolves alias "keys" to api-keys', async () => {
|
|
125
|
-
const program = new Command();
|
|
126
|
-
program.exitOverride();
|
|
127
|
-
registerExplain(program);
|
|
128
|
-
|
|
129
|
-
await program.parseAsync(['node', 'test', 'explain', 'keys']);
|
|
130
|
-
|
|
131
|
-
const combined = output.join('\n');
|
|
132
|
-
assert.ok(combined.includes('API Keys'), 'Should resolve alias');
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it('resolves alias "batch" to batch-processing', async () => {
|
|
136
|
-
const program = new Command();
|
|
137
|
-
program.exitOverride();
|
|
138
|
-
registerExplain(program);
|
|
139
|
-
|
|
140
|
-
await program.parseAsync(['node', 'test', 'explain', 'batch']);
|
|
141
|
-
|
|
142
|
-
const combined = output.join('\n');
|
|
143
|
-
assert.ok(combined.includes('Batch Processing'), 'Should resolve alias');
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it('shows error and suggestions for unknown concept', async () => {
|
|
147
|
-
let exitCode = null;
|
|
148
|
-
process.exit = (code) => {
|
|
149
|
-
exitCode = code;
|
|
150
|
-
throw new Error('process.exit called');
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
const program = new Command();
|
|
154
|
-
program.exitOverride();
|
|
155
|
-
registerExplain(program);
|
|
156
|
-
|
|
157
|
-
await assert.rejects(
|
|
158
|
-
() => program.parseAsync(['node', 'test', 'explain', 'nonexistent']),
|
|
159
|
-
/process\.exit called/
|
|
160
|
-
);
|
|
161
|
-
|
|
162
|
-
assert.equal(exitCode, 1);
|
|
163
|
-
const combined = errorOutput.join('\n');
|
|
164
|
-
assert.ok(combined.includes('Unknown topic'), 'Should show unknown topic error');
|
|
165
|
-
assert.ok(combined.includes('vai explain'), 'Should suggest running vai explain');
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
it('outputs JSON for topic list with --json flag', async () => {
|
|
169
|
-
const program = new Command();
|
|
170
|
-
program.exitOverride();
|
|
171
|
-
registerExplain(program);
|
|
172
|
-
|
|
173
|
-
await program.parseAsync(['node', 'test', 'explain', '--json']);
|
|
174
|
-
|
|
175
|
-
const combined = output.join('\n');
|
|
176
|
-
const parsed = JSON.parse(combined);
|
|
177
|
-
assert.ok(Array.isArray(parsed.topics), 'Should have topics array');
|
|
178
|
-
assert.ok(parsed.topics.length >= 10, 'Should have at least 10 topics');
|
|
179
|
-
assert.ok(parsed.topics[0].key, 'Each topic should have a key');
|
|
180
|
-
assert.ok(parsed.topics[0].title, 'Each topic should have a title');
|
|
181
|
-
assert.ok(parsed.topics[0].summary, 'Each topic should have a summary');
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it('outputs JSON for a specific concept with --json flag', async () => {
|
|
185
|
-
const program = new Command();
|
|
186
|
-
program.exitOverride();
|
|
187
|
-
registerExplain(program);
|
|
188
|
-
|
|
189
|
-
await program.parseAsync(['node', 'test', 'explain', 'rag', '--json']);
|
|
190
|
-
|
|
191
|
-
const combined = output.join('\n');
|
|
192
|
-
const parsed = JSON.parse(combined);
|
|
193
|
-
assert.equal(parsed.concept, 'rag');
|
|
194
|
-
assert.equal(parsed.title, 'RAG (Retrieval-Augmented Generation)');
|
|
195
|
-
assert.ok(parsed.content, 'Should have content');
|
|
196
|
-
assert.ok(Array.isArray(parsed.links), 'Should have links');
|
|
197
|
-
assert.ok(Array.isArray(parsed.tryIt), 'Should have tryIt');
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
it('has --json option', () => {
|
|
201
|
-
const program = new Command();
|
|
202
|
-
registerExplain(program);
|
|
203
|
-
const cmd = program.commands.find(c => c.name() === 'explain');
|
|
204
|
-
const opts = cmd.options.map(o => o.long);
|
|
205
|
-
assert.ok(opts.includes('--json'), 'Should have --json option');
|
|
206
|
-
});
|
|
207
|
-
});
|
|
@@ -1,261 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const { describe, it, beforeEach, afterEach, mock } = require('node:test');
|
|
4
|
-
const assert = require('node:assert/strict');
|
|
5
|
-
const fs = require('fs');
|
|
6
|
-
const path = require('path');
|
|
7
|
-
const os = require('os');
|
|
8
|
-
|
|
9
|
-
// Helpers to create temp files
|
|
10
|
-
function tmpFile(name, content) {
|
|
11
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'vai-ingest-'));
|
|
12
|
-
const fp = path.join(dir, name);
|
|
13
|
-
fs.writeFileSync(fp, content, 'utf-8');
|
|
14
|
-
return fp;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
describe('ingest', () => {
|
|
18
|
-
describe('detectFormat', () => {
|
|
19
|
-
let detectFormat;
|
|
20
|
-
beforeEach(() => {
|
|
21
|
-
delete require.cache[require.resolve('../../src/commands/ingest')];
|
|
22
|
-
({ detectFormat } = require('../../src/commands/ingest'));
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it('detects .csv extension', () => {
|
|
26
|
-
const fp = tmpFile('data.csv', 'a,b\n1,2\n');
|
|
27
|
-
assert.equal(detectFormat(fp), 'csv');
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it('detects .json extension', () => {
|
|
31
|
-
const fp = tmpFile('data.json', '[{"text":"hi"}]');
|
|
32
|
-
assert.equal(detectFormat(fp), 'json');
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('detects .jsonl extension', () => {
|
|
36
|
-
const fp = tmpFile('data.jsonl', '{"text":"hi"}\n{"text":"bye"}\n');
|
|
37
|
-
assert.equal(detectFormat(fp), 'jsonl');
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('detects .ndjson extension', () => {
|
|
41
|
-
const fp = tmpFile('data.ndjson', '{"text":"hi"}\n');
|
|
42
|
-
assert.equal(detectFormat(fp), 'jsonl');
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('detects JSONL from content when no recognized extension', () => {
|
|
46
|
-
const fp = tmpFile('data.dat', '{"text":"hello"}\n{"text":"world"}\n');
|
|
47
|
-
assert.equal(detectFormat(fp), 'jsonl');
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('detects JSON array from content when no recognized extension', () => {
|
|
51
|
-
const fp = tmpFile('data.dat', '[{"text":"hello"}]');
|
|
52
|
-
assert.equal(detectFormat(fp), 'json');
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('defaults to text for plain content', () => {
|
|
56
|
-
const fp = tmpFile('data.dat', 'just plain text\nanother line\n');
|
|
57
|
-
assert.equal(detectFormat(fp), 'text');
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
describe('parseFile — JSONL', () => {
|
|
62
|
-
let parseFile;
|
|
63
|
-
beforeEach(() => {
|
|
64
|
-
delete require.cache[require.resolve('../../src/commands/ingest')];
|
|
65
|
-
({ parseFile } = require('../../src/commands/ingest'));
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it('parses JSONL documents with default text field', () => {
|
|
69
|
-
const fp = path.join(__dirname, '..', 'fixtures', 'sample.jsonl');
|
|
70
|
-
const { documents, textKey } = parseFile(fp, 'jsonl');
|
|
71
|
-
assert.equal(documents.length, 5);
|
|
72
|
-
assert.equal(textKey, 'text');
|
|
73
|
-
assert.ok(documents[0].text.includes('MongoDB'));
|
|
74
|
-
assert.equal(documents[0].source, 'docs');
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('throws on invalid JSON line', () => {
|
|
78
|
-
const fp = tmpFile('bad.jsonl', '{"text":"ok"}\nnot json\n');
|
|
79
|
-
assert.throws(() => parseFile(fp, 'jsonl'), /Invalid JSON on line 2/);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it('throws when text field is missing', () => {
|
|
83
|
-
const fp = tmpFile('notext.jsonl', '{"content":"hello"}\n');
|
|
84
|
-
assert.throws(() => parseFile(fp, 'jsonl'), /missing "text" field/);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('uses custom text field via textField option', () => {
|
|
88
|
-
const fp = tmpFile('custom.jsonl', '{"body":"hello","id":1}\n');
|
|
89
|
-
const { documents, textKey } = parseFile(fp, 'jsonl', { textField: 'body' });
|
|
90
|
-
assert.equal(documents.length, 1);
|
|
91
|
-
assert.equal(textKey, 'body');
|
|
92
|
-
assert.equal(documents[0].body, 'hello');
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
describe('parseFile — JSON', () => {
|
|
97
|
-
let parseFile;
|
|
98
|
-
beforeEach(() => {
|
|
99
|
-
delete require.cache[require.resolve('../../src/commands/ingest')];
|
|
100
|
-
({ parseFile } = require('../../src/commands/ingest'));
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('parses JSON array from fixture', () => {
|
|
104
|
-
const fp = path.join(__dirname, '..', 'fixtures', 'sample.json');
|
|
105
|
-
const { documents, textKey } = parseFile(fp, 'json');
|
|
106
|
-
assert.equal(documents.length, 5);
|
|
107
|
-
assert.equal(textKey, 'text');
|
|
108
|
-
assert.ok(documents[2].text.includes('Voyage AI'));
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it('throws on non-array JSON', () => {
|
|
112
|
-
const fp = tmpFile('obj.json', '{"text":"hello"}');
|
|
113
|
-
assert.throws(() => parseFile(fp, 'json'), /must contain an array/);
|
|
114
|
-
});
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
describe('parseFile — CSV', () => {
|
|
118
|
-
let parseFile;
|
|
119
|
-
beforeEach(() => {
|
|
120
|
-
delete require.cache[require.resolve('../../src/commands/ingest')];
|
|
121
|
-
({ parseFile } = require('../../src/commands/ingest'));
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it('parses CSV with header row', () => {
|
|
125
|
-
const fp = path.join(__dirname, '..', 'fixtures', 'sample.csv');
|
|
126
|
-
const { documents, textKey } = parseFile(fp, 'csv', { textColumn: 'content' });
|
|
127
|
-
assert.equal(documents.length, 5);
|
|
128
|
-
assert.equal(textKey, 'content');
|
|
129
|
-
assert.ok(documents[0].content.includes('MongoDB'));
|
|
130
|
-
assert.equal(documents[0].title, 'MongoDB Overview');
|
|
131
|
-
assert.equal(documents[0].category, 'databases');
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it('throws when --text-column is not provided', () => {
|
|
135
|
-
const fp = tmpFile('no-col.csv', 'a,b\n1,2\n');
|
|
136
|
-
assert.throws(() => parseFile(fp, 'csv'), /--text-column/);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('throws when column not found in headers', () => {
|
|
140
|
-
const fp = tmpFile('bad-col.csv', 'a,b\n1,2\n');
|
|
141
|
-
assert.throws(() => parseFile(fp, 'csv', { textColumn: 'missing' }), /not found in CSV headers/);
|
|
142
|
-
});
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
describe('parseFile — text', () => {
|
|
146
|
-
let parseFile;
|
|
147
|
-
beforeEach(() => {
|
|
148
|
-
delete require.cache[require.resolve('../../src/commands/ingest')];
|
|
149
|
-
({ parseFile } = require('../../src/commands/ingest'));
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it('parses plain text — one doc per line', () => {
|
|
153
|
-
const fp = path.join(__dirname, '..', 'fixtures', 'sample.txt');
|
|
154
|
-
const { documents, textKey } = parseFile(fp, 'text');
|
|
155
|
-
assert.equal(documents.length, 5);
|
|
156
|
-
assert.equal(textKey, 'text');
|
|
157
|
-
assert.ok(documents[0].text.includes('MongoDB'));
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it('skips empty lines', () => {
|
|
161
|
-
const fp = tmpFile('gaps.txt', 'line one\n\nline two\n \nline three\n');
|
|
162
|
-
const { documents } = parseFile(fp, 'text');
|
|
163
|
-
assert.equal(documents.length, 3);
|
|
164
|
-
});
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
describe('parseCSVLine', () => {
|
|
168
|
-
let parseCSVLine;
|
|
169
|
-
beforeEach(() => {
|
|
170
|
-
delete require.cache[require.resolve('../../src/commands/ingest')];
|
|
171
|
-
({ parseCSVLine } = require('../../src/commands/ingest'));
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it('handles simple fields', () => {
|
|
175
|
-
assert.deepEqual(parseCSVLine('a,b,c'), ['a', 'b', 'c']);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it('handles quoted fields with commas', () => {
|
|
179
|
-
assert.deepEqual(parseCSVLine('"hello, world",b,c'), ['hello, world', 'b', 'c']);
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
it('handles escaped quotes', () => {
|
|
183
|
-
assert.deepEqual(parseCSVLine('"say ""hi""",b'), ['say "hi"', 'b']);
|
|
184
|
-
});
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
describe('estimateTokens', () => {
|
|
188
|
-
let estimateTokens;
|
|
189
|
-
beforeEach(() => {
|
|
190
|
-
delete require.cache[require.resolve('../../src/commands/ingest')];
|
|
191
|
-
({ estimateTokens } = require('../../src/commands/ingest'));
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
it('estimates ~4 chars per token', () => {
|
|
195
|
-
const result = estimateTokens(['hello world']); // 11 chars → ceil(11/4) = 3
|
|
196
|
-
assert.equal(result, 3);
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
it('sums multiple texts', () => {
|
|
200
|
-
const result = estimateTokens(['abcd', 'efgh']); // 8 chars → 2
|
|
201
|
-
assert.equal(result, 2);
|
|
202
|
-
});
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
describe('batch splitting', () => {
|
|
206
|
-
let parseFile;
|
|
207
|
-
beforeEach(() => {
|
|
208
|
-
delete require.cache[require.resolve('../../src/commands/ingest')];
|
|
209
|
-
({ parseFile } = require('../../src/commands/ingest'));
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
it('batch-size controls number of batches', () => {
|
|
213
|
-
// 5 documents with batch size 2 → 3 batches
|
|
214
|
-
const fp = path.join(__dirname, '..', 'fixtures', 'sample.jsonl');
|
|
215
|
-
const { documents } = parseFile(fp, 'jsonl');
|
|
216
|
-
const batchSize = 2;
|
|
217
|
-
const totalBatches = Math.ceil(documents.length / batchSize);
|
|
218
|
-
assert.equal(totalBatches, 3);
|
|
219
|
-
});
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
describe('command registration', () => {
|
|
223
|
-
it('registers ingest command with required options', () => {
|
|
224
|
-
delete require.cache[require.resolve('../../src/commands/ingest')];
|
|
225
|
-
const { registerIngest } = require('../../src/commands/ingest');
|
|
226
|
-
const { Command } = require('commander');
|
|
227
|
-
const program = new Command();
|
|
228
|
-
registerIngest(program);
|
|
229
|
-
|
|
230
|
-
const ingestCmd = program.commands.find(c => c.name() === 'ingest');
|
|
231
|
-
assert.ok(ingestCmd, 'ingest command should be registered');
|
|
232
|
-
|
|
233
|
-
// Check required options exist
|
|
234
|
-
const optionNames = ingestCmd.options.map(o => o.long);
|
|
235
|
-
assert.ok(optionNames.includes('--file'), 'should have --file option');
|
|
236
|
-
assert.ok(optionNames.includes('--db'), 'should have --db option');
|
|
237
|
-
assert.ok(optionNames.includes('--collection'), 'should have --collection option');
|
|
238
|
-
assert.ok(optionNames.includes('--field'), 'should have --field option');
|
|
239
|
-
assert.ok(optionNames.includes('--dry-run'), 'should have --dry-run option');
|
|
240
|
-
assert.ok(optionNames.includes('--batch-size'), 'should have --batch-size option');
|
|
241
|
-
assert.ok(optionNames.includes('--text-column'), 'should have --text-column option');
|
|
242
|
-
assert.ok(optionNames.includes('--text-field'), 'should have --text-field option');
|
|
243
|
-
assert.ok(optionNames.includes('--json'), 'should have --json option');
|
|
244
|
-
assert.ok(optionNames.includes('--quiet'), 'should have --quiet option');
|
|
245
|
-
assert.ok(optionNames.includes('--strict'), 'should have --strict option');
|
|
246
|
-
assert.ok(optionNames.includes('--input-type'), 'should have --input-type option');
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
it('--input-type defaults to document', () => {
|
|
250
|
-
delete require.cache[require.resolve('../../src/commands/ingest')];
|
|
251
|
-
const { registerIngest } = require('../../src/commands/ingest');
|
|
252
|
-
const { Command } = require('commander');
|
|
253
|
-
const program = new Command();
|
|
254
|
-
registerIngest(program);
|
|
255
|
-
|
|
256
|
-
const ingestCmd = program.commands.find(c => c.name() === 'ingest');
|
|
257
|
-
const inputTypeOpt = ingestCmd.options.find(o => o.long === '--input-type');
|
|
258
|
-
assert.equal(inputTypeOpt.defaultValue, 'document', '--input-type should default to document');
|
|
259
|
-
});
|
|
260
|
-
});
|
|
261
|
-
});
|
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const { describe, it, beforeEach, afterEach, mock } = require('node:test');
|
|
4
|
-
const assert = require('node:assert/strict');
|
|
5
|
-
const { Command } = require('commander');
|
|
6
|
-
const { registerModels } = require('../../src/commands/models');
|
|
7
|
-
|
|
8
|
-
describe('models command', () => {
|
|
9
|
-
let originalLog;
|
|
10
|
-
let output;
|
|
11
|
-
|
|
12
|
-
beforeEach(() => {
|
|
13
|
-
originalLog = console.log;
|
|
14
|
-
output = [];
|
|
15
|
-
console.log = (...args) => output.push(args.join(' '));
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
afterEach(() => {
|
|
19
|
-
console.log = originalLog;
|
|
20
|
-
mock.restoreAll();
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it('registers correctly on a program', () => {
|
|
24
|
-
const program = new Command();
|
|
25
|
-
registerModels(program);
|
|
26
|
-
const modelsCmd = program.commands.find(c => c.name() === 'models');
|
|
27
|
-
assert.ok(modelsCmd, 'models command should be registered');
|
|
28
|
-
assert.ok(modelsCmd.description().includes('model'), 'should have a description about models');
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('lists all models by default', async () => {
|
|
32
|
-
const program = new Command();
|
|
33
|
-
program.exitOverride();
|
|
34
|
-
registerModels(program);
|
|
35
|
-
|
|
36
|
-
await program.parseAsync(['node', 'test', 'models', '--quiet']);
|
|
37
|
-
|
|
38
|
-
const combined = output.join('\n');
|
|
39
|
-
assert.ok(combined.includes('voyage-4-large'), 'Should include voyage-4-large');
|
|
40
|
-
assert.ok(combined.includes('rerank-2.5'), 'Should include rerank-2.5');
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it('filters by embedding type', async () => {
|
|
44
|
-
const program = new Command();
|
|
45
|
-
program.exitOverride();
|
|
46
|
-
registerModels(program);
|
|
47
|
-
|
|
48
|
-
await program.parseAsync(['node', 'test', 'models', '--type', 'embedding', '--quiet']);
|
|
49
|
-
|
|
50
|
-
const combined = output.join('\n');
|
|
51
|
-
assert.ok(combined.includes('voyage-4-large'), 'Should include embedding models');
|
|
52
|
-
assert.ok(!combined.includes('rerank-2.5\n'), 'Should not include reranking in data rows');
|
|
53
|
-
// More precise: check that rerank-2.5 doesn't appear as a data row start
|
|
54
|
-
const lines = combined.split('\n');
|
|
55
|
-
const dataLines = lines.filter(l => !l.includes('─') && l.trim().length > 0);
|
|
56
|
-
const hasRerankRow = dataLines.some(l => l.trim().startsWith('rerank-2.5'));
|
|
57
|
-
assert.ok(!hasRerankRow, 'Should not have reranking model rows');
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('filters by reranking type', async () => {
|
|
61
|
-
const program = new Command();
|
|
62
|
-
program.exitOverride();
|
|
63
|
-
registerModels(program);
|
|
64
|
-
|
|
65
|
-
await program.parseAsync(['node', 'test', 'models', '--type', 'reranking', '--quiet']);
|
|
66
|
-
|
|
67
|
-
const combined = output.join('\n');
|
|
68
|
-
assert.ok(combined.includes('rerank'), 'Should include reranking models');
|
|
69
|
-
const lines = combined.split('\n');
|
|
70
|
-
const dataLines = lines.filter(l => !l.includes('─') && !l.includes('Model') && l.trim().length > 0);
|
|
71
|
-
const hasEmbedRow = dataLines.some(l => l.includes('voyage-4-large'));
|
|
72
|
-
assert.ok(!hasEmbedRow, 'Should not have embedding model rows');
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('outputs JSON when --json flag is used', async () => {
|
|
76
|
-
const program = new Command();
|
|
77
|
-
program.exitOverride();
|
|
78
|
-
registerModels(program);
|
|
79
|
-
|
|
80
|
-
await program.parseAsync(['node', 'test', 'models', '--json']);
|
|
81
|
-
|
|
82
|
-
const combined = output.join('\n');
|
|
83
|
-
const parsed = JSON.parse(combined);
|
|
84
|
-
assert.ok(Array.isArray(parsed));
|
|
85
|
-
assert.ok(parsed.length > 0);
|
|
86
|
-
assert.ok(parsed[0].name);
|
|
87
|
-
assert.ok(parsed[0].type);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it('hides legacy models by default', async () => {
|
|
91
|
-
const program = new Command();
|
|
92
|
-
program.exitOverride();
|
|
93
|
-
registerModels(program);
|
|
94
|
-
|
|
95
|
-
await program.parseAsync(['node', 'test', 'models', '--quiet']);
|
|
96
|
-
|
|
97
|
-
const combined = output.join('\n');
|
|
98
|
-
assert.ok(combined.includes('voyage-4-large'), 'Should include current models');
|
|
99
|
-
assert.ok(!combined.includes('voyage-3-large'), 'Should hide legacy voyage-3-large');
|
|
100
|
-
assert.ok(!combined.includes('rerank-2-lite'), 'Should hide legacy rerank-2-lite');
|
|
101
|
-
assert.ok(!combined.includes('voyage-code-2'), 'Should hide legacy voyage-code-2');
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it('shows legacy models when --all is used', async () => {
|
|
105
|
-
const program = new Command();
|
|
106
|
-
program.exitOverride();
|
|
107
|
-
registerModels(program);
|
|
108
|
-
|
|
109
|
-
await program.parseAsync(['node', 'test', 'models', '--all', '--quiet']);
|
|
110
|
-
|
|
111
|
-
const combined = output.join('\n');
|
|
112
|
-
assert.ok(combined.includes('voyage-4-large'), 'Should include current models');
|
|
113
|
-
assert.ok(combined.includes('voyage-3-large'), 'Should include legacy voyage-3-large');
|
|
114
|
-
assert.ok(combined.includes('rerank-2'), 'Should include legacy rerank-2');
|
|
115
|
-
assert.ok(combined.includes('Legacy Models'), 'Should show legacy header');
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it('--all with --json shows legacy models in JSON', async () => {
|
|
119
|
-
const program = new Command();
|
|
120
|
-
program.exitOverride();
|
|
121
|
-
registerModels(program);
|
|
122
|
-
|
|
123
|
-
await program.parseAsync(['node', 'test', 'models', '--all', '--json']);
|
|
124
|
-
|
|
125
|
-
const combined = output.join('\n');
|
|
126
|
-
const parsed = JSON.parse(combined);
|
|
127
|
-
assert.ok(Array.isArray(parsed));
|
|
128
|
-
const legacyNames = parsed.filter(m => m.legacy).map(m => m.name);
|
|
129
|
-
assert.ok(legacyNames.includes('voyage-3-large'), 'JSON should include legacy models');
|
|
130
|
-
assert.ok(legacyNames.includes('rerank-2'), 'JSON should include legacy rerankers');
|
|
131
|
-
});
|
|
132
|
-
});
|