voyageai-cli 1.12.1 → 1.13.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/.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,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
|
-
});
|
|
@@ -1,172 +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 { registerPing } = require('../../src/commands/ping');
|
|
7
|
-
|
|
8
|
-
describe('ping command', () => {
|
|
9
|
-
let originalLog;
|
|
10
|
-
let originalError;
|
|
11
|
-
let originalExit;
|
|
12
|
-
let originalKey;
|
|
13
|
-
let originalMongoUri;
|
|
14
|
-
let output;
|
|
15
|
-
let errorOutput;
|
|
16
|
-
|
|
17
|
-
// Strip ANSI escape codes for reliable string assertions in CI
|
|
18
|
-
// (GitHub Actions sets FORCE_COLOR which adds ANSI codes via picocolors)
|
|
19
|
-
const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
20
|
-
|
|
21
|
-
beforeEach(() => {
|
|
22
|
-
originalLog = console.log;
|
|
23
|
-
originalError = console.error;
|
|
24
|
-
originalExit = process.exit;
|
|
25
|
-
originalKey = process.env.VOYAGE_API_KEY;
|
|
26
|
-
originalMongoUri = process.env.MONGODB_URI;
|
|
27
|
-
output = [];
|
|
28
|
-
errorOutput = [];
|
|
29
|
-
console.log = (...args) => output.push(args.join(' '));
|
|
30
|
-
console.error = (...args) => errorOutput.push(args.join(' '));
|
|
31
|
-
// Remove MONGODB_URI by default so we don't accidentally test mongo
|
|
32
|
-
delete process.env.MONGODB_URI;
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
afterEach(() => {
|
|
36
|
-
console.log = originalLog;
|
|
37
|
-
console.error = originalError;
|
|
38
|
-
process.exit = originalExit;
|
|
39
|
-
if (originalKey !== undefined) {
|
|
40
|
-
process.env.VOYAGE_API_KEY = originalKey;
|
|
41
|
-
} else {
|
|
42
|
-
delete process.env.VOYAGE_API_KEY;
|
|
43
|
-
}
|
|
44
|
-
if (originalMongoUri !== undefined) {
|
|
45
|
-
process.env.MONGODB_URI = originalMongoUri;
|
|
46
|
-
} else {
|
|
47
|
-
delete process.env.MONGODB_URI;
|
|
48
|
-
}
|
|
49
|
-
mock.restoreAll();
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('registers correctly on a program', () => {
|
|
53
|
-
const program = new Command();
|
|
54
|
-
registerPing(program);
|
|
55
|
-
const pingCmd = program.commands.find(c => c.name() === 'ping');
|
|
56
|
-
assert.ok(pingCmd, 'ping command should be registered');
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('prints success on valid API response', async () => {
|
|
60
|
-
process.env.VOYAGE_API_KEY = 'test-key';
|
|
61
|
-
|
|
62
|
-
mock.method(global, 'fetch', async () => ({
|
|
63
|
-
ok: true,
|
|
64
|
-
status: 200,
|
|
65
|
-
json: async () => ({
|
|
66
|
-
data: [{ embedding: new Array(1024).fill(0) }],
|
|
67
|
-
usage: { total_tokens: 1 },
|
|
68
|
-
}),
|
|
69
|
-
}));
|
|
70
|
-
|
|
71
|
-
const program = new Command();
|
|
72
|
-
program.exitOverride();
|
|
73
|
-
registerPing(program);
|
|
74
|
-
|
|
75
|
-
await program.parseAsync(['node', 'test', 'ping']);
|
|
76
|
-
|
|
77
|
-
const combined = stripAnsi(output.join('\n'));
|
|
78
|
-
assert.ok(combined.includes('✓ Connected to Voyage AI API'), 'Should show success message');
|
|
79
|
-
assert.ok(combined.includes('voyage-4-lite'), 'Should show model name');
|
|
80
|
-
assert.ok(combined.includes('1024'), 'Should show dimensions');
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('exits with error on auth failure', async () => {
|
|
84
|
-
process.env.VOYAGE_API_KEY = 'bad-key';
|
|
85
|
-
|
|
86
|
-
mock.method(global, 'fetch', async () => ({
|
|
87
|
-
ok: false,
|
|
88
|
-
status: 401,
|
|
89
|
-
text: async () => 'Unauthorized',
|
|
90
|
-
}));
|
|
91
|
-
|
|
92
|
-
let exitCode = null;
|
|
93
|
-
process.exit = (code) => {
|
|
94
|
-
exitCode = code;
|
|
95
|
-
throw new Error('process.exit called');
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
const program = new Command();
|
|
99
|
-
program.exitOverride();
|
|
100
|
-
registerPing(program);
|
|
101
|
-
|
|
102
|
-
await assert.rejects(
|
|
103
|
-
() => program.parseAsync(['node', 'test', 'ping']),
|
|
104
|
-
/process\.exit called/
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
assert.equal(exitCode, 1);
|
|
108
|
-
const combined = stripAnsi(errorOutput.join('\n'));
|
|
109
|
-
assert.ok(combined.includes('Authentication failed'), 'Should show auth error');
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it('exits when VOYAGE_API_KEY is not set and no config', async () => {
|
|
113
|
-
delete process.env.VOYAGE_API_KEY;
|
|
114
|
-
// Mock config to return nothing so the key isn't found in ~/.vai/config.json
|
|
115
|
-
delete require.cache[require.resolve('../../src/lib/config')];
|
|
116
|
-
delete require.cache[require.resolve('../../src/lib/api')];
|
|
117
|
-
delete require.cache[require.resolve('../../src/commands/ping')];
|
|
118
|
-
const config = require('../../src/lib/config');
|
|
119
|
-
const origGetConfigValue = config.getConfigValue;
|
|
120
|
-
config.getConfigValue = () => undefined;
|
|
121
|
-
|
|
122
|
-
const { registerPing: registerPingFresh } = require('../../src/commands/ping');
|
|
123
|
-
|
|
124
|
-
let exitCode = null;
|
|
125
|
-
process.exit = (code) => {
|
|
126
|
-
exitCode = code;
|
|
127
|
-
throw new Error('process.exit called');
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
const program = new Command();
|
|
131
|
-
program.exitOverride();
|
|
132
|
-
registerPingFresh(program);
|
|
133
|
-
|
|
134
|
-
try {
|
|
135
|
-
await assert.rejects(
|
|
136
|
-
() => program.parseAsync(['node', 'test', 'ping']),
|
|
137
|
-
/process\.exit called/
|
|
138
|
-
);
|
|
139
|
-
|
|
140
|
-
assert.equal(exitCode, 1);
|
|
141
|
-
const combined = stripAnsi(errorOutput.join('\n'));
|
|
142
|
-
assert.ok(combined.includes('VOYAGE_API_KEY'), 'Should mention missing key');
|
|
143
|
-
} finally {
|
|
144
|
-
config.getConfigValue = origGetConfigValue;
|
|
145
|
-
}
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it('outputs JSON when --json flag is used', async () => {
|
|
149
|
-
process.env.VOYAGE_API_KEY = 'test-key';
|
|
150
|
-
|
|
151
|
-
mock.method(global, 'fetch', async () => ({
|
|
152
|
-
ok: true,
|
|
153
|
-
status: 200,
|
|
154
|
-
json: async () => ({
|
|
155
|
-
data: [{ embedding: new Array(1024).fill(0) }],
|
|
156
|
-
usage: { total_tokens: 1 },
|
|
157
|
-
}),
|
|
158
|
-
}));
|
|
159
|
-
|
|
160
|
-
const program = new Command();
|
|
161
|
-
program.exitOverride();
|
|
162
|
-
registerPing(program);
|
|
163
|
-
|
|
164
|
-
await program.parseAsync(['node', 'test', 'ping', '--json']);
|
|
165
|
-
|
|
166
|
-
const combined = output.join('\n');
|
|
167
|
-
const parsed = JSON.parse(combined);
|
|
168
|
-
assert.equal(parsed.ok, true);
|
|
169
|
-
assert.ok(parsed.voyage);
|
|
170
|
-
assert.equal(parsed.voyage.ok, true);
|
|
171
|
-
});
|
|
172
|
-
});
|