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.
Files changed (44) hide show
  1. package/README.md +3 -3
  2. package/demo-readme.gif +0 -0
  3. package/package.json +1 -1
  4. package/src/cli.js +2 -0
  5. package/src/commands/benchmark.js +164 -0
  6. package/src/commands/completions.js +18 -1
  7. package/src/commands/estimate.js +209 -0
  8. package/src/commands/models.js +32 -4
  9. package/src/lib/catalog.js +42 -18
  10. package/src/lib/explanations.js +183 -0
  11. package/.github/workflows/ci.yml +0 -22
  12. package/CONTRIBUTING.md +0 -81
  13. package/demo.gif +0 -0
  14. package/demo.tape +0 -39
  15. package/scripts/record-demo.sh +0 -63
  16. package/test/commands/about.test.js +0 -23
  17. package/test/commands/benchmark.test.js +0 -319
  18. package/test/commands/completions.test.js +0 -166
  19. package/test/commands/config.test.js +0 -35
  20. package/test/commands/demo.test.js +0 -46
  21. package/test/commands/embed.test.js +0 -42
  22. package/test/commands/explain.test.js +0 -207
  23. package/test/commands/ingest.test.js +0 -261
  24. package/test/commands/models.test.js +0 -132
  25. package/test/commands/ping.test.js +0 -172
  26. package/test/commands/playground.test.js +0 -137
  27. package/test/commands/rerank.test.js +0 -32
  28. package/test/commands/similarity.test.js +0 -79
  29. package/test/commands/store.test.js +0 -26
  30. package/test/fixtures/sample.csv +0 -6
  31. package/test/fixtures/sample.json +0 -7
  32. package/test/fixtures/sample.jsonl +0 -5
  33. package/test/fixtures/sample.txt +0 -5
  34. package/test/lib/api.test.js +0 -133
  35. package/test/lib/banner.test.js +0 -44
  36. package/test/lib/catalog.test.js +0 -99
  37. package/test/lib/config.test.js +0 -124
  38. package/test/lib/explanations.test.js +0 -141
  39. package/test/lib/format.test.js +0 -75
  40. package/test/lib/input.test.js +0 -48
  41. package/test/lib/math.test.js +0 -43
  42. package/test/lib/ui.test.js +0 -79
  43. package/voyageai-cli-playground.png +0 -0
  44. package/voyageai-cli.png +0 -0
@@ -1,99 +0,0 @@
1
- 'use strict';
2
-
3
- const { describe, it } = require('node:test');
4
- const assert = require('node:assert/strict');
5
- const {
6
- MODEL_CATALOG,
7
- DEFAULT_EMBED_MODEL,
8
- DEFAULT_RERANK_MODEL,
9
- DEFAULT_DIMENSIONS,
10
- } = require('../../src/lib/catalog');
11
-
12
- describe('catalog', () => {
13
- it('MODEL_CATALOG is a non-empty array', () => {
14
- assert.ok(Array.isArray(MODEL_CATALOG));
15
- assert.ok(MODEL_CATALOG.length > 0);
16
- });
17
-
18
- it('contains expected models', () => {
19
- const names = MODEL_CATALOG.map(m => m.name);
20
- assert.ok(names.includes('voyage-4-large'));
21
- assert.ok(names.includes('voyage-4'));
22
- assert.ok(names.includes('voyage-4-lite'));
23
- assert.ok(names.includes('rerank-2.5'));
24
- });
25
-
26
- it('DEFAULT_EMBED_MODEL is a valid embedding model', () => {
27
- assert.equal(DEFAULT_EMBED_MODEL, 'voyage-4-large');
28
- const model = MODEL_CATALOG.find(m => m.name === DEFAULT_EMBED_MODEL);
29
- assert.ok(model);
30
- assert.equal(model.type, 'embedding');
31
- });
32
-
33
- it('DEFAULT_RERANK_MODEL is a valid reranking model', () => {
34
- assert.equal(DEFAULT_RERANK_MODEL, 'rerank-2.5');
35
- const model = MODEL_CATALOG.find(m => m.name === DEFAULT_RERANK_MODEL);
36
- assert.ok(model);
37
- assert.equal(model.type, 'reranking');
38
- });
39
-
40
- it('DEFAULT_DIMENSIONS is 1024', () => {
41
- assert.equal(DEFAULT_DIMENSIONS, 1024);
42
- });
43
-
44
- it('all models have required fields', () => {
45
- const requiredFields = ['name', 'type', 'context', 'dimensions', 'price', 'bestFor'];
46
- for (const model of MODEL_CATALOG) {
47
- for (const field of requiredFields) {
48
- assert.ok(
49
- model[field] !== undefined && model[field] !== null && model[field] !== '',
50
- `Model "${model.name}" is missing field "${field}"`
51
- );
52
- }
53
- }
54
- });
55
-
56
- it('has both embedding and reranking types', () => {
57
- const types = new Set(MODEL_CATALOG.map(m => m.type));
58
- assert.ok(types.has('embedding'), 'Should have embedding models');
59
- assert.ok(types.has('reranking'), 'Should have reranking models');
60
- });
61
-
62
- it('embedding models outnumber reranking models', () => {
63
- const embedCount = MODEL_CATALOG.filter(m => m.type === 'embedding').length;
64
- const rerankCount = MODEL_CATALOG.filter(m => m.type === 'reranking').length;
65
- assert.ok(embedCount > rerankCount);
66
- });
67
-
68
- it('contains voyage-4-nano as current model', () => {
69
- const nano = MODEL_CATALOG.find(m => m.name === 'voyage-4-nano');
70
- assert.ok(nano, 'Should have voyage-4-nano');
71
- assert.equal(nano.type, 'embedding');
72
- assert.ok(!nano.legacy, 'voyage-4-nano should not be legacy');
73
- });
74
-
75
- it('contains legacy models with legacy flag', () => {
76
- const legacyModels = MODEL_CATALOG.filter(m => m.legacy);
77
- assert.ok(legacyModels.length > 0, 'Should have legacy models');
78
-
79
- const legacyNames = legacyModels.map(m => m.name);
80
- assert.ok(legacyNames.includes('voyage-3-large'), 'Should have voyage-3-large');
81
- assert.ok(legacyNames.includes('voyage-3.5'), 'Should have voyage-3.5');
82
- assert.ok(legacyNames.includes('voyage-3.5-lite'), 'Should have voyage-3.5-lite');
83
- assert.ok(legacyNames.includes('voyage-code-2'), 'Should have voyage-code-2');
84
- assert.ok(legacyNames.includes('voyage-multimodal-3'), 'Should have voyage-multimodal-3');
85
- assert.ok(legacyNames.includes('rerank-2'), 'Should have rerank-2');
86
- assert.ok(legacyNames.includes('rerank-2-lite'), 'Should have rerank-2-lite');
87
- });
88
-
89
- it('legacy models have required fields', () => {
90
- const legacyModels = MODEL_CATALOG.filter(m => m.legacy);
91
- for (const model of legacyModels) {
92
- assert.ok(model.name, `Legacy model missing name`);
93
- assert.ok(model.type, `Legacy model ${model.name} missing type`);
94
- assert.ok(model.context, `Legacy model ${model.name} missing context`);
95
- assert.ok(model.price, `Legacy model ${model.name} missing price`);
96
- assert.ok(model.bestFor, `Legacy model ${model.name} missing bestFor`);
97
- }
98
- });
99
- });
@@ -1,124 +0,0 @@
1
- 'use strict';
2
-
3
- const { describe, it, beforeEach, afterEach } = 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
- const {
10
- loadConfig,
11
- saveConfig,
12
- getConfigValue,
13
- setConfigValue,
14
- deleteConfigValue,
15
- maskSecret,
16
- } = require('../../src/lib/config');
17
-
18
- describe('config lib', () => {
19
- let tmpDir;
20
- let tmpConfigPath;
21
-
22
- beforeEach(() => {
23
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vai-test-'));
24
- tmpConfigPath = path.join(tmpDir, 'config.json');
25
- });
26
-
27
- afterEach(() => {
28
- fs.rmSync(tmpDir, { recursive: true, force: true });
29
- });
30
-
31
- describe('loadConfig', () => {
32
- it('returns {} when config file does not exist', () => {
33
- const config = loadConfig(tmpConfigPath);
34
- assert.deepEqual(config, {});
35
- });
36
-
37
- it('returns parsed config when file exists', () => {
38
- fs.writeFileSync(tmpConfigPath, JSON.stringify({ apiKey: 'test' }));
39
- const config = loadConfig(tmpConfigPath);
40
- assert.deepEqual(config, { apiKey: 'test' });
41
- });
42
- });
43
-
44
- describe('saveConfig', () => {
45
- it('creates directory and file', () => {
46
- const nestedPath = path.join(tmpDir, 'nested', 'config.json');
47
- saveConfig({ foo: 'bar' }, nestedPath);
48
- assert.ok(fs.existsSync(nestedPath));
49
- const data = JSON.parse(fs.readFileSync(nestedPath, 'utf-8'));
50
- assert.deepEqual(data, { foo: 'bar' });
51
- });
52
-
53
- it('sets file permissions to 600', () => {
54
- saveConfig({ secret: 'value' }, tmpConfigPath);
55
- const stats = fs.statSync(tmpConfigPath);
56
- const mode = (stats.mode & 0o777).toString(8);
57
- assert.equal(mode, '600');
58
- });
59
- });
60
-
61
- describe('setConfigValue / getConfigValue', () => {
62
- it('round-trips a value', () => {
63
- setConfigValue('apiKey', 'my-secret-key', tmpConfigPath);
64
- const value = getConfigValue('apiKey', tmpConfigPath);
65
- assert.equal(value, 'my-secret-key');
66
- });
67
-
68
- it('returns undefined for missing key', () => {
69
- const value = getConfigValue('nonExistent', tmpConfigPath);
70
- assert.equal(value, undefined);
71
- });
72
-
73
- it('handles multiple values', () => {
74
- setConfigValue('apiKey', 'key1', tmpConfigPath);
75
- setConfigValue('mongodbUri', 'mongodb://localhost', tmpConfigPath);
76
- assert.equal(getConfigValue('apiKey', tmpConfigPath), 'key1');
77
- assert.equal(getConfigValue('mongodbUri', tmpConfigPath), 'mongodb://localhost');
78
- });
79
-
80
- it('overwrites existing value', () => {
81
- setConfigValue('apiKey', 'old', tmpConfigPath);
82
- setConfigValue('apiKey', 'new', tmpConfigPath);
83
- assert.equal(getConfigValue('apiKey', tmpConfigPath), 'new');
84
- });
85
- });
86
-
87
- describe('deleteConfigValue', () => {
88
- it('removes a key', () => {
89
- setConfigValue('apiKey', 'val', tmpConfigPath);
90
- deleteConfigValue('apiKey', tmpConfigPath);
91
- assert.equal(getConfigValue('apiKey', tmpConfigPath), undefined);
92
- });
93
-
94
- it('does not affect other keys', () => {
95
- setConfigValue('apiKey', 'key', tmpConfigPath);
96
- setConfigValue('mongodbUri', 'uri', tmpConfigPath);
97
- deleteConfigValue('apiKey', tmpConfigPath);
98
- assert.equal(getConfigValue('mongodbUri', tmpConfigPath), 'uri');
99
- });
100
- });
101
-
102
- describe('maskSecret', () => {
103
- it('masks long strings: first 4 + ... + last 4', () => {
104
- assert.equal(maskSecret('al-EdFh1FwUCPTZw7ofd93ulmRNxEmt-JOCRmmWc96wWJ8'), 'al-E...wWJ8');
105
- });
106
-
107
- it('masks strings of exactly 10 chars', () => {
108
- assert.equal(maskSecret('1234567890'), '1234...7890');
109
- });
110
-
111
- it('returns **** for strings shorter than 10 chars', () => {
112
- assert.equal(maskSecret('short'), '****');
113
- assert.equal(maskSecret('123456789'), '****');
114
- });
115
-
116
- it('returns **** for empty string', () => {
117
- assert.equal(maskSecret(''), '****');
118
- });
119
-
120
- it('converts non-string values to string', () => {
121
- assert.equal(maskSecret(12345), '12345');
122
- });
123
- });
124
- });
@@ -1,141 +0,0 @@
1
- 'use strict';
2
-
3
- const { describe, it } = require('node:test');
4
- const assert = require('node:assert/strict');
5
- const { concepts, aliases, resolveConcept, listConcepts, getConcept } = require('../../src/lib/explanations');
6
-
7
- describe('explanations', () => {
8
- const expectedConcepts = [
9
- 'embeddings',
10
- 'reranking',
11
- 'vector-search',
12
- 'rag',
13
- 'cosine-similarity',
14
- 'two-stage-retrieval',
15
- 'input-type',
16
- 'models',
17
- 'api-keys',
18
- 'api-access',
19
- 'batch-processing',
20
- 'quantization',
21
- 'benchmarking',
22
- ];
23
-
24
- it('has all expected concepts', () => {
25
- for (const key of expectedConcepts) {
26
- assert.ok(concepts[key], `Missing concept: ${key}`);
27
- }
28
- });
29
-
30
- it('listConcepts returns all concept keys', () => {
31
- const keys = listConcepts();
32
- assert.equal(keys.length, expectedConcepts.length);
33
- for (const key of expectedConcepts) {
34
- assert.ok(keys.includes(key), `listConcepts should include ${key}`);
35
- }
36
- });
37
-
38
- it('all concepts have required fields: title, summary, content, links, tryIt', () => {
39
- for (const key of expectedConcepts) {
40
- const concept = concepts[key];
41
- assert.ok(concept.title, `${key} should have title`);
42
- assert.ok(typeof concept.title === 'string', `${key}.title should be a string`);
43
- assert.ok(concept.summary, `${key} should have summary`);
44
- assert.ok(typeof concept.summary === 'string', `${key}.summary should be a string`);
45
- assert.ok(concept.content, `${key} should have content`);
46
- assert.ok(typeof concept.content === 'string', `${key}.content should be a string`);
47
- assert.ok(Array.isArray(concept.links), `${key}.links should be an array`);
48
- assert.ok(concept.links.length > 0, `${key}.links should not be empty`);
49
- assert.ok(Array.isArray(concept.tryIt), `${key}.tryIt should be an array`);
50
- assert.ok(concept.tryIt.length > 0, `${key}.tryIt should not be empty`);
51
- }
52
- });
53
-
54
- it('all concepts have substantial content (at least 200 chars)', () => {
55
- const stripAnsi = (str) => str.replace(/\x1b\[[0-9;]*m/g, '');
56
- for (const key of expectedConcepts) {
57
- const plainContent = stripAnsi(concepts[key].content);
58
- assert.ok(
59
- plainContent.length >= 200,
60
- `${key} content should be substantial (got ${plainContent.length} chars)`
61
- );
62
- }
63
- });
64
-
65
- it('getConcept returns concept for valid key', () => {
66
- const concept = getConcept('embeddings');
67
- assert.ok(concept);
68
- assert.equal(concept.title, 'Embeddings');
69
- });
70
-
71
- it('getConcept returns null for invalid key', () => {
72
- const concept = getConcept('nonexistent');
73
- assert.equal(concept, null);
74
- });
75
-
76
- describe('alias resolution', () => {
77
- const expectedAliases = {
78
- embed: 'embeddings',
79
- embedding: 'embeddings',
80
- rerank: 'reranking',
81
- vectors: 'vector-search',
82
- search: 'vector-search',
83
- cosine: 'cosine-similarity',
84
- similarity: 'cosine-similarity',
85
- 'two-stage': 'two-stage-retrieval',
86
- keys: 'api-keys',
87
- access: 'api-access',
88
- auth: 'api-access',
89
- 'atlas-vs-voyage': 'api-access',
90
- endpoint: 'api-access',
91
- batch: 'batch-processing',
92
- model: 'models',
93
- batching: 'batch-processing',
94
- quantize: 'quantization',
95
- int8: 'quantization',
96
- binary: 'quantization',
97
- matryoshka: 'quantization',
98
- dtype: 'quantization',
99
- };
100
-
101
- it('alias map covers expected aliases', () => {
102
- for (const [alias, expected] of Object.entries(expectedAliases)) {
103
- assert.ok(aliases[alias], `Alias map should include "${alias}"`);
104
- assert.equal(aliases[alias], expected, `Alias "${alias}" should resolve to "${expected}"`);
105
- }
106
- });
107
-
108
- it('resolveConcept resolves direct keys', () => {
109
- for (const key of expectedConcepts) {
110
- assert.equal(resolveConcept(key), key, `Direct key "${key}" should resolve to itself`);
111
- }
112
- });
113
-
114
- it('resolveConcept resolves aliases', () => {
115
- for (const [alias, expected] of Object.entries(expectedAliases)) {
116
- assert.equal(
117
- resolveConcept(alias),
118
- expected,
119
- `Alias "${alias}" should resolve to "${expected}"`
120
- );
121
- }
122
- });
123
-
124
- it('resolveConcept is case-insensitive', () => {
125
- assert.equal(resolveConcept('EMBEDDINGS'), 'embeddings');
126
- assert.equal(resolveConcept('Rerank'), 'reranking');
127
- assert.equal(resolveConcept('RAG'), 'rag');
128
- });
129
-
130
- it('resolveConcept returns null for unknown input', () => {
131
- assert.equal(resolveConcept('nonexistent'), null);
132
- assert.equal(resolveConcept('foobar'), null);
133
- });
134
-
135
- it('resolveConcept returns null for empty/null input', () => {
136
- assert.equal(resolveConcept(''), null);
137
- assert.equal(resolveConcept(null), null);
138
- assert.equal(resolveConcept(undefined), null);
139
- });
140
- });
141
- });
@@ -1,75 +0,0 @@
1
- 'use strict';
2
-
3
- const { describe, it } = require('node:test');
4
- const assert = require('node:assert/strict');
5
- const { formatTable } = require('../../src/lib/format');
6
-
7
- describe('formatTable', () => {
8
- it('formats a simple table', () => {
9
- const headers = ['Name', 'Age'];
10
- const rows = [
11
- ['Alice', '30'],
12
- ['Bob', '25'],
13
- ];
14
- const result = formatTable(headers, rows);
15
- assert.ok(result.includes('Alice'));
16
- assert.ok(result.includes('Bob'));
17
- assert.ok(result.includes('Name'));
18
- assert.ok(result.includes('Age'));
19
- });
20
-
21
- it('handles empty rows', () => {
22
- const headers = ['Col1', 'Col2'];
23
- const rows = [];
24
- const result = formatTable(headers, rows);
25
- // Should still have the header and separator
26
- const lines = result.split('\n');
27
- assert.equal(lines.length, 2); // header + separator, no data lines
28
- assert.ok(lines[0].includes('Col1'));
29
- assert.ok(lines[0].includes('Col2'));
30
- });
31
-
32
- it('handles varying column widths', () => {
33
- const headers = ['X', 'Long Header'];
34
- const rows = [
35
- ['Short', 'Y'],
36
- ['A very long cell value', 'Z'],
37
- ];
38
- const result = formatTable(headers, rows);
39
- // The widest cell should determine column width
40
- assert.ok(result.includes('A very long cell value'));
41
- // All rows should have consistent separators
42
- const lines = result.split('\n');
43
- assert.equal(lines.length, 4); // header + separator + 2 data rows
44
- });
45
-
46
- it('uses correct separator character', () => {
47
- const headers = ['A', 'B'];
48
- const rows = [['1', '2']];
49
- const result = formatTable(headers, rows);
50
- const lines = result.split('\n');
51
- // Separator line should contain ─ and ┼
52
- assert.ok(lines[1].includes('─'));
53
- assert.ok(lines[1].includes('┼'));
54
- });
55
-
56
- it('uses │ as column separator in data lines', () => {
57
- const headers = ['A', 'B'];
58
- const rows = [['1', '2']];
59
- const result = formatTable(headers, rows);
60
- const lines = result.split('\n');
61
- // Header and data lines should use │
62
- assert.ok(lines[0].includes('│'));
63
- assert.ok(lines[2].includes('│'));
64
- });
65
-
66
- it('pads cells correctly', () => {
67
- const headers = ['Name', 'Value'];
68
- const rows = [['A', 'B']];
69
- const result = formatTable(headers, rows);
70
- const lines = result.split('\n');
71
- // Each cell should be padded to column width
72
- // "Name" is 4 chars, "A" should be padded to 4
73
- assert.ok(lines[2].includes(' A ') || lines[2].includes(' A '));
74
- });
75
- });
@@ -1,48 +0,0 @@
1
- 'use strict';
2
-
3
- const { describe, it, after } = require('node:test');
4
- const assert = require('node:assert/strict');
5
- const fs = require('fs');
6
- const os = require('os');
7
- const path = require('path');
8
- const { resolveTextInput } = require('../../src/lib/input');
9
-
10
- describe('resolveTextInput', () => {
11
- const tmpFiles = [];
12
-
13
- function createTempFile(content) {
14
- const tmpPath = path.join(os.tmpdir(), `vai-test-${Date.now()}-${Math.random().toString(36).slice(2)}.txt`);
15
- fs.writeFileSync(tmpPath, content, 'utf-8');
16
- tmpFiles.push(tmpPath);
17
- return tmpPath;
18
- }
19
-
20
- after(() => {
21
- for (const f of tmpFiles) {
22
- try { fs.unlinkSync(f); } catch { /* ignore */ }
23
- }
24
- });
25
-
26
- it('returns direct text argument as array', async () => {
27
- const result = await resolveTextInput('hello world', undefined);
28
- assert.deepEqual(result, ['hello world']);
29
- });
30
-
31
- it('reads from file path', async () => {
32
- const tmpPath = createTempFile('file content here');
33
- const result = await resolveTextInput(undefined, tmpPath);
34
- assert.deepEqual(result, ['file content here']);
35
- });
36
-
37
- it('trims file content', async () => {
38
- const tmpPath = createTempFile(' trimmed \n');
39
- const result = await resolveTextInput(undefined, tmpPath);
40
- assert.deepEqual(result, ['trimmed']);
41
- });
42
-
43
- it('prefers file over text argument', async () => {
44
- const tmpPath = createTempFile('from file');
45
- const result = await resolveTextInput('from arg', tmpPath);
46
- assert.deepEqual(result, ['from file']);
47
- });
48
- });
@@ -1,43 +0,0 @@
1
- 'use strict';
2
-
3
- const { describe, it } = require('node:test');
4
- const assert = require('node:assert/strict');
5
- const { cosineSimilarity } = require('../../src/lib/math');
6
-
7
- describe('cosineSimilarity', () => {
8
- it('returns 1.0 for identical vectors', () => {
9
- const v = [1, 2, 3, 4, 5];
10
- const result = cosineSimilarity(v, v);
11
- assert.ok(Math.abs(result - 1.0) < 1e-10, `Expected ~1.0, got ${result}`);
12
- });
13
-
14
- it('returns 0.0 for orthogonal vectors', () => {
15
- const a = [1, 0, 0];
16
- const b = [0, 1, 0];
17
- const result = cosineSimilarity(a, b);
18
- assert.ok(Math.abs(result) < 1e-10, `Expected ~0.0, got ${result}`);
19
- });
20
-
21
- it('returns -1.0 for opposite vectors', () => {
22
- const a = [1, 2, 3];
23
- const b = [-1, -2, -3];
24
- const result = cosineSimilarity(a, b);
25
- assert.ok(Math.abs(result - (-1.0)) < 1e-10, `Expected ~-1.0, got ${result}`);
26
- });
27
-
28
- it('computes correct value for known vectors', () => {
29
- // cos([1,0], [1,1]) = 1 / (1 * sqrt(2)) ≈ 0.7071
30
- const a = [1, 0];
31
- const b = [1, 1];
32
- const result = cosineSimilarity(a, b);
33
- const expected = 1 / Math.sqrt(2);
34
- assert.ok(Math.abs(result - expected) < 1e-10, `Expected ~${expected}, got ${result}`);
35
- });
36
-
37
- it('returns ~1.0 for vectors of different magnitudes but same direction', () => {
38
- const a = [1, 2, 3];
39
- const b = [10, 20, 30];
40
- const result = cosineSimilarity(a, b);
41
- assert.ok(Math.abs(result - 1.0) < 1e-10, `Expected ~1.0, got ${result}`);
42
- });
43
- });
@@ -1,79 +0,0 @@
1
- 'use strict';
2
-
3
- const { describe, it } = require('node:test');
4
- const assert = require('node:assert/strict');
5
- const ui = require('../../src/lib/ui');
6
-
7
- describe('ui helpers', () => {
8
- it('ui.success() contains ✓', () => {
9
- const result = ui.success('done');
10
- assert.ok(result.includes('✓'), `Expected "✓" in: ${result}`);
11
- assert.ok(result.includes('done'), `Expected "done" in: ${result}`);
12
- });
13
-
14
- it('ui.error() contains ✗', () => {
15
- const result = ui.error('fail');
16
- assert.ok(result.includes('✗'), `Expected "✗" in: ${result}`);
17
- assert.ok(result.includes('fail'), `Expected "fail" in: ${result}`);
18
- });
19
-
20
- it('ui.warn() contains ⚠', () => {
21
- const result = ui.warn('careful');
22
- assert.ok(result.includes('⚠'), `Expected "⚠" in: ${result}`);
23
- });
24
-
25
- it('ui.info() contains ℹ', () => {
26
- const result = ui.info('note');
27
- assert.ok(result.includes('ℹ'), `Expected "ℹ" in: ${result}`);
28
- });
29
-
30
- it('ui.score() returns a string for various values', () => {
31
- const high = ui.score(0.85);
32
- assert.equal(typeof high, 'string');
33
- assert.ok(high.includes('0.850000'), `Expected "0.850000" in: ${high}`);
34
-
35
- const mid = ui.score(0.55);
36
- assert.equal(typeof mid, 'string');
37
- assert.ok(mid.includes('0.550000'), `Expected "0.550000" in: ${mid}`);
38
-
39
- const low = ui.score(0.1);
40
- assert.equal(typeof low, 'string');
41
- assert.ok(low.includes('0.100000'), `Expected "0.100000" in: ${low}`);
42
- });
43
-
44
- it('ui.label() formats key: value with indentation', () => {
45
- const result = ui.label('Model', 'voyage-4');
46
- assert.ok(result.includes('Model'), `Expected "Model" in: ${result}`);
47
- assert.ok(result.includes(':'), `Expected ":" in: ${result}`);
48
- assert.ok(result.includes('voyage-4'), `Expected "voyage-4" in: ${result}`);
49
- // Should start with spaces (indentation)
50
- assert.ok(result.startsWith(' '), `Expected leading spaces in: ${result}`);
51
- });
52
-
53
- it('ui.status() colors READY green, BUILDING yellow, FAILED red', () => {
54
- const ready = ui.status('READY');
55
- assert.equal(typeof ready, 'string');
56
- assert.ok(ready.includes('READY'), `Expected "READY" in: ${ready}`);
57
-
58
- const building = ui.status('BUILDING');
59
- assert.ok(building.includes('BUILDING'), `Expected "BUILDING" in: ${building}`);
60
-
61
- const failed = ui.status('FAILED');
62
- assert.ok(failed.includes('FAILED'), `Expected "FAILED" in: ${failed}`);
63
- });
64
-
65
- it('ui.spinner() returns an object with start and stop methods', () => {
66
- const spin = ui.spinner('loading...');
67
- assert.equal(typeof spin.start, 'function', 'spinner should have start()');
68
- assert.equal(typeof spin.stop, 'function', 'spinner should have stop()');
69
- });
70
-
71
- it('ui style functions exist and return strings', () => {
72
- assert.equal(typeof ui.bold('x'), 'string');
73
- assert.equal(typeof ui.dim('x'), 'string');
74
- assert.equal(typeof ui.green('x'), 'string');
75
- assert.equal(typeof ui.red('x'), 'string');
76
- assert.equal(typeof ui.cyan('x'), 'string');
77
- assert.equal(typeof ui.yellow('x'), 'string');
78
- });
79
- });
Binary file
package/voyageai-cli.png DELETED
Binary file