Skip to content

Commit cc7c3e1

Browse files
test: add unit tests for interactive install prompt
Cover the four promptInstall/loadTransformers paths: - non-TTY skips prompt and exits with error - TTY + user declines → exits with error - TTY + user accepts but npm install fails → exits with error - TTY + install succeeds → retries import and loads module
1 parent 8e717b2 commit cc7c3e1

1 file changed

Lines changed: 138 additions & 0 deletions

File tree

tests/unit/prompt-install.test.js

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/**
2+
* Unit tests for the interactive install prompt in src/embedder.js.
3+
*
4+
* Tests the promptInstall() + loadTransformers() flow when
5+
* @huggingface/transformers is missing.
6+
*
7+
* Each test uses vi.resetModules() + vi.doMock() + dynamic import()
8+
* so every test gets a fresh embedder module with its own mocks.
9+
*/
10+
11+
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
12+
13+
describe('loadTransformers install prompt', () => {
14+
let exitSpy;
15+
let errorSpy;
16+
let logSpy;
17+
let origTTY;
18+
19+
beforeEach(() => {
20+
vi.resetModules();
21+
origTTY = process.stdin.isTTY;
22+
exitSpy = vi.spyOn(process, 'exit').mockImplementation((code) => {
23+
throw new Error(`process.exit(${code})`);
24+
});
25+
errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
26+
logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
27+
});
28+
29+
afterEach(() => {
30+
process.stdin.isTTY = origTTY;
31+
exitSpy.mockRestore();
32+
errorSpy.mockRestore();
33+
logSpy.mockRestore();
34+
vi.restoreAllMocks();
35+
});
36+
37+
test('non-TTY: prints error and exits without prompting', async () => {
38+
process.stdin.isTTY = undefined;
39+
40+
const rlFactory = vi.fn();
41+
vi.doMock('node:readline', () => ({ createInterface: rlFactory }));
42+
vi.doMock('node:child_process', () => ({ execFileSync: vi.fn() }));
43+
vi.doMock('@huggingface/transformers', () => {
44+
throw new Error('Cannot find package');
45+
});
46+
47+
const { embed } = await import('../../src/embedder.js');
48+
49+
await expect(embed(['test'], 'minilm')).rejects.toThrow('process.exit(1)');
50+
expect(errorSpy).toHaveBeenCalledWith(
51+
expect.stringContaining('Semantic search requires @huggingface/transformers'),
52+
);
53+
// readline should NOT have been called — no prompt in non-TTY
54+
expect(rlFactory).not.toHaveBeenCalled();
55+
});
56+
57+
test('TTY + user declines: prints error and exits', async () => {
58+
process.stdin.isTTY = true;
59+
60+
vi.doMock('node:readline', () => ({
61+
createInterface: () => ({
62+
question: (_prompt, cb) => cb('n'),
63+
close: vi.fn(),
64+
}),
65+
}));
66+
vi.doMock('node:child_process', () => ({ execFileSync: vi.fn() }));
67+
vi.doMock('@huggingface/transformers', () => {
68+
throw new Error('Cannot find package');
69+
});
70+
71+
const { embed } = await import('../../src/embedder.js');
72+
73+
await expect(embed(['test'], 'minilm')).rejects.toThrow('process.exit(1)');
74+
expect(errorSpy).toHaveBeenCalledWith(
75+
expect.stringContaining('Semantic search requires @huggingface/transformers'),
76+
);
77+
});
78+
79+
test('TTY + user accepts but npm install fails: prints error and exits', async () => {
80+
process.stdin.isTTY = true;
81+
82+
const execMock = vi.fn(() => {
83+
throw new Error('npm ERR!');
84+
});
85+
vi.doMock('node:readline', () => ({
86+
createInterface: () => ({
87+
question: (_prompt, cb) => cb('y'),
88+
close: vi.fn(),
89+
}),
90+
}));
91+
vi.doMock('node:child_process', () => ({ execFileSync: execMock }));
92+
vi.doMock('@huggingface/transformers', () => {
93+
throw new Error('Cannot find package');
94+
});
95+
96+
const { embed } = await import('../../src/embedder.js');
97+
98+
await expect(embed(['test'], 'minilm')).rejects.toThrow('process.exit(1)');
99+
expect(execMock).toHaveBeenCalledWith(
100+
'npm',
101+
['install', '@huggingface/transformers'],
102+
expect.objectContaining({ stdio: 'inherit', timeout: 300_000 }),
103+
);
104+
expect(errorSpy).toHaveBeenCalledWith(
105+
expect.stringContaining('Semantic search requires @huggingface/transformers'),
106+
);
107+
});
108+
109+
test('TTY + install succeeds: retries import and loads module', async () => {
110+
process.stdin.isTTY = true;
111+
112+
let importCount = 0;
113+
vi.doMock('node:readline', () => ({
114+
createInterface: () => ({
115+
question: (_prompt, cb) => cb('y'),
116+
close: vi.fn(),
117+
}),
118+
}));
119+
vi.doMock('node:child_process', () => ({ execFileSync: vi.fn() }));
120+
vi.doMock('@huggingface/transformers', () => {
121+
importCount++;
122+
if (importCount <= 1) throw new Error('Cannot find package');
123+
return {
124+
pipeline: async () => async (batch) => ({
125+
data: new Float32Array(384 * batch.length),
126+
}),
127+
cos_sim: () => 0,
128+
};
129+
});
130+
131+
const { embed } = await import('../../src/embedder.js');
132+
133+
const result = await embed(['test text'], 'minilm');
134+
expect(result.vectors).toHaveLength(1);
135+
expect(result.dim).toBe(384);
136+
expect(exitSpy).not.toHaveBeenCalled();
137+
});
138+
});

0 commit comments

Comments
 (0)