badgr-cli 1.1.1 → 1.1.2
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/LICENSE +207 -0
- package/README.md +9 -2
- package/package.json +44 -2
- package/src/api.js +16 -0
- package/src/badgr.js +2 -2
- package/src/commands/batch.js +11 -0
- package/src/commands/comfyui.js +31 -15
- package/src/commands/embed.js +13 -10
- package/src/commands/launch.js +8 -1
- package/src/commands/login.js +75 -20
- package/src/commands/run.js +45 -13
- package/src/commands/sbatch.js +6 -1
- package/src/commands/serve.js +44 -30
- package/src/commands/train.js +8 -12
- package/src/commands/transcribe.js +13 -10
- package/src/envFlag.js +10 -0
- package/src/onboarding.js +8 -1
- package/src/progress.js +48 -0
- package/tests/agent-images.test.js +0 -17
- package/tests/api.test.js +0 -168
- package/tests/artifactDownload.test.js +0 -113
- package/tests/artifacts.test.js +0 -168
- package/tests/batch.test.js +0 -641
- package/tests/browser.test.js +0 -51
- package/tests/capacity.test.js +0 -68
- package/tests/commands.test.js +0 -417
- package/tests/config.test.js +0 -96
- package/tests/connect.test.js +0 -83
- package/tests/detect.test.js +0 -191
- package/tests/down.test.js +0 -150
- package/tests/errors.test.js +0 -130
- package/tests/fallback-timeout.test.js +0 -41
- package/tests/fanout.test.js +0 -124
- package/tests/gpu-doctor-classifiers.test.js +0 -402
- package/tests/gpu-doctor-doctor.test.js +0 -304
- package/tests/gpu-doctor-probe-cache.test.js +0 -110
- package/tests/gpu-doctor-probes.test.js +0 -257
- package/tests/heartbeat.test.js +0 -70
- package/tests/job-progress-poll.test.js +0 -136
- package/tests/launch-command-argv.test.js +0 -93
- package/tests/launch-readiness.test.js +0 -403
- package/tests/launch.test.js +0 -440
- package/tests/onboarding.test.js +0 -134
- package/tests/productized-dry-run.test.js +0 -141
- package/tests/productized-runners.test.js +0 -237
- package/tests/pull.test.js +0 -266
- package/tests/rerun.test.js +0 -94
- package/tests/restart.test.js +0 -88
- package/tests/router.test.js +0 -98
- package/tests/run-lifecycle.test.js +0 -1054
- package/tests/sbatch.test.js +0 -190
- package/tests/secrets.test.js +0 -16
- package/tests/serve-apps.test.js +0 -189
- package/tests/serve-lifecycle.test.js +0 -931
- package/tests/slurm.test.js +0 -77
- package/tests/spec.test.js +0 -201
- package/tests/status.test.js +0 -73
- package/tests/store.test.js +0 -187
- package/tests/task.test.js +0 -109
- package/tests/template.test.js +0 -556
- package/tests/train-lora-dataset.test.js +0 -176
- package/tests/upload.test.js +0 -79
- package/tests/workload-rerun.test.js +0 -56
- package/tests/workload-spec.test.js +0 -180
- package/tests/workload-templates.test.js +0 -865
- package/tests/workload-workspace-paths.test.js +0 -46
|
@@ -1,304 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import fs from 'fs';
|
|
3
|
-
import os from 'os';
|
|
4
|
-
import path from 'path';
|
|
5
|
-
import { runGpuDoctor } from '../src/gpuDoctor/doctor.js';
|
|
6
|
-
|
|
7
|
-
let tmpFiles;
|
|
8
|
-
|
|
9
|
-
beforeEach(() => { tmpFiles = []; });
|
|
10
|
-
afterEach(() => {
|
|
11
|
-
for (const f of tmpFiles) fs.rmSync(f, { recursive: true, force: true });
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
function tmpFile(name, content) {
|
|
15
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gpu-doctor-'));
|
|
16
|
-
const file = path.join(dir, name);
|
|
17
|
-
fs.writeFileSync(file, content);
|
|
18
|
-
tmpFiles.push(dir);
|
|
19
|
-
return file;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const noGpuEnv = {
|
|
23
|
-
detectGpus: () => ({ available: false, error: 'nvidia-smi not found', driverVersion: null, cudaVersion: null, gpus: [], processes: [] }),
|
|
24
|
-
detectTorch: () => ({ pythonAvailable: false, torchInstalled: false, cudaAvailable: false, error: 'python not found' }),
|
|
25
|
-
detectDisk: () => ({ cachePath: '/tmp/hf', cacheSource: 'default', cacheExists: false, writable: true, freeGb: 100, totalGb: 500 }),
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
const gpuNoCudaEnv = {
|
|
29
|
-
detectGpus: () => ({
|
|
30
|
-
available: true,
|
|
31
|
-
driverVersion: '535.104.05',
|
|
32
|
-
cudaVersion: '12.2',
|
|
33
|
-
gpus: [{ index: 0, name: 'NVIDIA GeForce RTX 4090', vramTotalGb: 24, vramFreeGb: 23.1, vramUsedGb: 0.9, temperatureC: 40, powerDrawW: 30 }],
|
|
34
|
-
processes: [],
|
|
35
|
-
}),
|
|
36
|
-
detectTorch: () => ({ pythonAvailable: true, pythonBinary: 'python3', torchInstalled: true, cudaAvailable: false, cudaVersion: null, deviceName: null, error: null }),
|
|
37
|
-
detectDisk: () => ({ cachePath: '/tmp/hf', cacheSource: 'default', cacheExists: true, writable: true, freeGb: 100, totalGb: 500 }),
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
const gpu24Env = {
|
|
41
|
-
detectGpus: () => ({
|
|
42
|
-
available: true,
|
|
43
|
-
driverVersion: '550.54.15',
|
|
44
|
-
cudaVersion: '12.4',
|
|
45
|
-
gpus: [{ index: 0, name: 'NVIDIA GeForce RTX 4090', vramTotalGb: 24, vramFreeGb: 18.6, vramUsedGb: 5.4, temperatureC: 45, powerDrawW: 120 }],
|
|
46
|
-
processes: [],
|
|
47
|
-
}),
|
|
48
|
-
detectTorch: () => ({ pythonAvailable: true, pythonBinary: 'python3', torchInstalled: true, cudaAvailable: true, cudaVersion: '12.4', deviceName: 'NVIDIA GeForce RTX 4090', error: null }),
|
|
49
|
-
detectDisk: () => ({ cachePath: '/tmp/hf', cacheSource: 'default', cacheExists: true, writable: true, freeGb: 100, totalGb: 500 }),
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
describe('Scenario 1 — no NVIDIA GPU', () => {
|
|
53
|
-
it('reports no GPU detected without crashing, exit-worthy verdict only (not an error)', async () => {
|
|
54
|
-
const report = await runGpuDoctor({}, noGpuEnv);
|
|
55
|
-
expect(report.verdict).toBe('no NVIDIA GPU detected');
|
|
56
|
-
expect(report.verdictSlug).toBe('no_gpu_detected');
|
|
57
|
-
expect(report.likelyCause).toMatch(/GPU unavailable|not passed into the container/);
|
|
58
|
-
expect(report.suggestedFixes.some((f) => /Docker|NVIDIA Container Toolkit|badgr serve/.test(f))).toBe(true);
|
|
59
|
-
});
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
describe('Scenario 2 — GPU exists but PyTorch cannot use CUDA', () => {
|
|
63
|
-
it('reports CUDA visible to system but not to PyTorch', async () => {
|
|
64
|
-
const report = await runGpuDoctor({}, gpuNoCudaEnv);
|
|
65
|
-
expect(report.verdict).toBe('CUDA visible to system but not to PyTorch');
|
|
66
|
-
expect(report.verdictSlug).toBe('cuda_not_visible_to_pytorch');
|
|
67
|
-
expect(report.likelyCause).toMatch(/CPU-only torch wheel|CUDA runtime|container/);
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
describe('Scenario 3 — vLLM OOM log', () => {
|
|
72
|
-
it('reports likely fail / vram_oom for a vLLM OOM log', async () => {
|
|
73
|
-
const logFile = tmpFile('vllm-oom.log', [
|
|
74
|
-
'torch.cuda.OutOfMemoryError: CUDA out of memory. Tried to allocate 2.00 GiB',
|
|
75
|
-
'ERROR: EngineDeadError: engine core process died',
|
|
76
|
-
'ERROR: worker died',
|
|
77
|
-
].join('\n'));
|
|
78
|
-
const report = await runGpuDoctor({ logsPath: logFile }, noGpuEnv);
|
|
79
|
-
expect(report.verdict).toBe('likely fail');
|
|
80
|
-
expect(report.category).toBe('vram_oom');
|
|
81
|
-
expect(report.suggestedFixes.length).toBeGreaterThan(0);
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
describe('Scenario 4 — model too large for GPU', () => {
|
|
86
|
-
it('reports likely fail and recommends an 80GB+ class GPU on a 24GB card', async () => {
|
|
87
|
-
const report = await runGpuDoctor({ model: 'meta-llama/Llama-3.1-70B-Instruct', serve: true }, gpu24Env);
|
|
88
|
-
expect(report.verdict).toBe('likely fail');
|
|
89
|
-
expect(report.category).toBe('model_too_large');
|
|
90
|
-
expect(report.suggestedFixes.some((f) => /80GB\+|multi-GPU/.test(f))).toBe(true);
|
|
91
|
-
});
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
describe('Scenario 5 — small model likely fits', () => {
|
|
95
|
-
it('reports likely okay with conservative settings on a 24GB card', async () => {
|
|
96
|
-
const report = await runGpuDoctor({ model: 'Qwen/Qwen2.5-7B-Instruct', serve: true }, gpu24Env);
|
|
97
|
-
expect(report.verdict).toBe('likely okay with conservative settings');
|
|
98
|
-
expect(report.suggestedFixes.some((f) => /max model length/i.test(f))).toBe(true);
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
describe('Scenario 6 — Comfy workflow with missing model names', () => {
|
|
103
|
-
it('detects SDXL, referenced models, and custom nodes', async () => {
|
|
104
|
-
const workflowFile = tmpFile('sdxl-workflow.json', JSON.stringify({
|
|
105
|
-
1: { class_type: 'CheckpointLoaderSimple', inputs: { ckpt_name: 'sd_xl_base_1.0.safetensors' } },
|
|
106
|
-
2: { class_type: 'KSampler', inputs: {} },
|
|
107
|
-
3: { class_type: 'ImpactWildcardEncode', inputs: {} },
|
|
108
|
-
}));
|
|
109
|
-
const report = await runGpuDoctor({ workflowPath: workflowFile }, noGpuEnv);
|
|
110
|
-
expect(report.category).toBe('comfy_workflow');
|
|
111
|
-
expect(report.evidence.some((e) => /likely SDXL/.test(e))).toBe(true);
|
|
112
|
-
expect(report.evidence.some((e) => /sd_xl_base_1\.0\.safetensors/.test(e))).toBe(true);
|
|
113
|
-
expect(report.evidence.some((e) => /ImpactWildcardEncode/.test(e))).toBe(true);
|
|
114
|
-
});
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
describe('Scenario 7 — endpoint health check fails', () => {
|
|
118
|
-
it('reports endpoint not reachable on connection refused', async () => {
|
|
119
|
-
const err = new Error('fetch failed');
|
|
120
|
-
err.cause = { code: 'ECONNREFUSED' };
|
|
121
|
-
const fetchImpl = async () => { throw err; };
|
|
122
|
-
const report = await runGpuDoctor({ url: 'http://localhost:8000/v1/models' }, { ...noGpuEnv, fetchImpl });
|
|
123
|
-
expect(report.verdict).toBe('endpoint not reachable');
|
|
124
|
-
expect(report.category).toBe('endpoint_health');
|
|
125
|
-
expect(report.likelyCause).toMatch(/not started|port|health check/);
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
describe('Scenario 8 — JSON-shape report fields', () => {
|
|
130
|
-
it('includes verdict, category, evidence, suggested fixes, and badgr command', async () => {
|
|
131
|
-
const logFile = tmpFile('vllm-oom.log', 'CUDA out of memory');
|
|
132
|
-
const report = await runGpuDoctor({ logsPath: logFile }, noGpuEnv);
|
|
133
|
-
expect(report).toMatchObject({
|
|
134
|
-
verdictSlug: 'likely_fail',
|
|
135
|
-
category: 'vram_oom',
|
|
136
|
-
});
|
|
137
|
-
expect(Array.isArray(report.evidence)).toBe(true);
|
|
138
|
-
expect(Array.isArray(report.suggestedFixes)).toBe(true);
|
|
139
|
-
expect('badgrCommand' in report).toBe(true);
|
|
140
|
-
});
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
describe('tool errors — missing files', () => {
|
|
144
|
-
it('throws (does not silently succeed) when --logs points at a missing file', async () => {
|
|
145
|
-
await expect(runGpuDoctor({ logsPath: '/nonexistent/path.log' }, noGpuEnv)).rejects.toThrow();
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it('throws when --workflow points at a missing file', async () => {
|
|
149
|
-
await expect(runGpuDoctor({ workflowPath: '/nonexistent/workflow.json' }, noGpuEnv)).rejects.toThrow();
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it('throws on invalid workflow JSON', async () => {
|
|
153
|
-
const badFile = tmpFile('bad.json', 'not valid json');
|
|
154
|
-
await expect(runGpuDoctor({ workflowPath: badFile }, noGpuEnv)).rejects.toThrow();
|
|
155
|
-
});
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
describe('no GPU exists — still gives useful output', () => {
|
|
159
|
-
it('never throws for the default no-GPU baseline invocation', async () => {
|
|
160
|
-
await expect(runGpuDoctor({}, noGpuEnv)).resolves.toBeTruthy();
|
|
161
|
-
});
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
describe('non-NVIDIA system — nvidia-smi errors that are not ENOENT', () => {
|
|
165
|
-
it('reports no GPU detected without crashing on a generic exec failure', async () => {
|
|
166
|
-
const env = {
|
|
167
|
-
detectGpus: () => ({ available: false, error: 'permission denied', driverVersion: null, cudaVersion: null, gpus: [], processes: [] }),
|
|
168
|
-
detectTorch: () => ({ pythonAvailable: true, pythonBinary: 'python3', torchInstalled: false, cudaAvailable: false, error: null }),
|
|
169
|
-
detectDisk: () => ({ cachePath: '/tmp/hf', cacheSource: 'default', cacheExists: true, writable: true, freeGb: 100, totalGb: 500 }),
|
|
170
|
-
};
|
|
171
|
-
const report = await runGpuDoctor({}, env);
|
|
172
|
-
expect(report.verdict).toBe('no NVIDIA GPU detected');
|
|
173
|
-
});
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
describe('missing Python — torch probe unavailable', () => {
|
|
177
|
-
it('still reports a GPU-visible baseline without crashing when python is not found', async () => {
|
|
178
|
-
const env = {
|
|
179
|
-
detectGpus: () => gpu24Env.detectGpus(),
|
|
180
|
-
detectTorch: () => ({ pythonAvailable: false, pythonBinary: null, torchInstalled: false, cudaAvailable: false, error: 'python not found' }),
|
|
181
|
-
detectDisk: () => ({ cachePath: '/tmp/hf', cacheSource: 'default', cacheExists: true, writable: true, freeGb: 100, totalGb: 500 }),
|
|
182
|
-
};
|
|
183
|
-
const report = await runGpuDoctor({}, env);
|
|
184
|
-
expect(report.verdict).toBe('GPU visible but PyTorch not installed');
|
|
185
|
-
expect(report.evidence.some((e) => /PyTorch: not installed/.test(e))).toBe(true);
|
|
186
|
-
});
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
describe('multiple GPUs', () => {
|
|
190
|
-
it('picks the GPU with the most free VRAM when sizing a model', async () => {
|
|
191
|
-
const env = {
|
|
192
|
-
detectGpus: () => ({
|
|
193
|
-
available: true,
|
|
194
|
-
driverVersion: '550.54.15',
|
|
195
|
-
cudaVersion: '12.4',
|
|
196
|
-
gpus: [
|
|
197
|
-
{ index: 0, name: 'NVIDIA A100', vramTotalGb: 40, vramFreeGb: 2.1, vramUsedGb: 37.9, temperatureC: 60, powerDrawW: 200 },
|
|
198
|
-
{ index: 1, name: 'NVIDIA A100', vramTotalGb: 40, vramFreeGb: 38.5, vramUsedGb: 1.5, temperatureC: 45, powerDrawW: 90 },
|
|
199
|
-
],
|
|
200
|
-
processes: [],
|
|
201
|
-
}),
|
|
202
|
-
detectTorch: () => ({ pythonAvailable: true, pythonBinary: 'python3', torchInstalled: true, cudaAvailable: true, cudaVersion: '12.4', deviceName: 'NVIDIA A100', error: null }),
|
|
203
|
-
detectDisk: () => ({ cachePath: '/tmp/hf', cacheSource: 'default', cacheExists: true, writable: true, freeGb: 100, totalGb: 500 }),
|
|
204
|
-
};
|
|
205
|
-
const report = await runGpuDoctor({ model: 'meta-llama/Llama-3.1-8B-Instruct', serve: true }, env);
|
|
206
|
-
expect(report.evidence.some((e) => /GPU 0:/.test(e))).toBe(true);
|
|
207
|
-
expect(report.evidence.some((e) => /GPU 1:/.test(e))).toBe(true);
|
|
208
|
-
expect(report.evidence.some((e) => /Available VRAM: 38.5GB/.test(e))).toBe(true);
|
|
209
|
-
});
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
describe('unrecognized --gpu name degrades to insufficient data', () => {
|
|
213
|
-
it('does not fabricate a confident verdict for an unknown GPU with no local GPU present', async () => {
|
|
214
|
-
const report = await runGpuDoctor({ model: 'Qwen/Qwen2.5-7B-Instruct', gpu: 'NotARealGpu9000' }, noGpuEnv);
|
|
215
|
-
expect(report.verdictSlug).toBe('insufficient_data');
|
|
216
|
-
expect(report.evidence.some((e) => /not recognized/.test(e))).toBe(true);
|
|
217
|
-
});
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
describe('source-priority note', () => {
|
|
221
|
-
it('records which input was selected and which were ignored when multiple are given', async () => {
|
|
222
|
-
const logFile = tmpFile('vllm-oom.log', 'CUDA out of memory');
|
|
223
|
-
const report = await runGpuDoctor({ logsPath: logFile, model: 'Qwen/Qwen2.5-7B-Instruct', url: 'http://localhost:8000' }, noGpuEnv);
|
|
224
|
-
expect(report.evidence[0]).toMatch(/^Diagnosing from: --logs/);
|
|
225
|
-
expect(report.evidence.some((e) => /^Ignored/.test(e) && /--url/.test(e) && /--model/.test(e))).toBe(true);
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
it('notes baseline diagnosis when no source flags are given', async () => {
|
|
229
|
-
const report = await runGpuDoctor({}, noGpuEnv);
|
|
230
|
-
expect(report.evidence[0]).toMatch(/^Diagnosing from: baseline/);
|
|
231
|
-
});
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
describe('multi-GPU model fit (--gpu-count)', () => {
|
|
235
|
-
const twoA100Env = {
|
|
236
|
-
detectGpus: () => ({
|
|
237
|
-
available: true,
|
|
238
|
-
driverVersion: '550.54.15',
|
|
239
|
-
cudaVersion: '12.4',
|
|
240
|
-
gpus: [
|
|
241
|
-
{ index: 0, name: 'NVIDIA A100', vramTotalGb: 80, vramFreeGb: 78, vramUsedGb: 2, temperatureC: 45, powerDrawW: 90 },
|
|
242
|
-
{ index: 1, name: 'NVIDIA A100', vramTotalGb: 80, vramFreeGb: 40, vramUsedGb: 40, temperatureC: 50, powerDrawW: 110 },
|
|
243
|
-
],
|
|
244
|
-
processes: [],
|
|
245
|
-
}),
|
|
246
|
-
detectTorch: () => ({ pythonAvailable: true, pythonBinary: 'python3', torchInstalled: true, cudaAvailable: true, cudaVersion: '12.4', deviceName: 'NVIDIA A100', error: null }),
|
|
247
|
-
detectDisk: () => ({ cachePath: '/tmp/hf', cacheSource: 'default', cacheExists: true, writable: true, freeGb: 100, totalGb: 500 }),
|
|
248
|
-
};
|
|
249
|
-
|
|
250
|
-
it('a 70B-AWQ model that fails on one 24GB GPU fits once split across 2 GPUs', async () => {
|
|
251
|
-
const single = await runGpuDoctor({ model: 'meta-llama/Llama-3.1-70B-Instruct-AWQ', serve: true }, gpu24Env);
|
|
252
|
-
expect(single.verdict).toBe('likely fail');
|
|
253
|
-
|
|
254
|
-
const dual = await runGpuDoctor({ model: 'meta-llama/Llama-3.1-70B-Instruct-AWQ', serve: true, gpuCount: 2 }, twoA100Env);
|
|
255
|
-
expect(dual.category).not.toBe('model_too_large');
|
|
256
|
-
expect(dual.evidence.some((e) => /GPU count: 2 \(tensor-parallel\)/.test(e))).toBe(true);
|
|
257
|
-
expect(dual.evidence.some((e) => /per GPU/.test(e))).toBe(true);
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
it('sizes against the weaker of the requested GPUs, not the strongest', async () => {
|
|
261
|
-
const dual = await runGpuDoctor({ model: 'meta-llama/Llama-3.1-70B-Instruct-AWQ', serve: true, gpuCount: 2 }, twoA100Env);
|
|
262
|
-
expect(dual.evidence.some((e) => /Available VRAM: 40GB per GPU/.test(e))).toBe(true);
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
it('flags when fewer physical GPUs are detected than requested', async () => {
|
|
266
|
-
const report = await runGpuDoctor({ model: 'meta-llama/Llama-3.1-8B-Instruct', serve: true, gpuCount: 4 }, gpu24Env);
|
|
267
|
-
expect(report.evidence.some((e) => /Only 1 local GPU\(s\) detected — fewer than the 4 requested/.test(e))).toBe(true);
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
it('defaults to a single-GPU estimate when --gpu-count is omitted', async () => {
|
|
271
|
-
const report = await runGpuDoctor({ model: 'Qwen/Qwen2.5-7B-Instruct', serve: true }, gpu24Env);
|
|
272
|
-
expect(report.evidence.some((e) => /per GPU/.test(e))).toBe(false);
|
|
273
|
-
expect(report.evidence.some((e) => /GPU count:/.test(e))).toBe(false);
|
|
274
|
-
});
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
describe('endpoint health — non-OpenAI formats', () => {
|
|
278
|
-
it('recognizes a ComfyUI /system_stats response', async () => {
|
|
279
|
-
const fetchImpl = async () => new Response(JSON.stringify({ system: {}, devices: [{ name: 'cuda:0' }] }), { status: 200 });
|
|
280
|
-
const report = await runGpuDoctor({ url: 'http://localhost:8188/system_stats' }, { ...noGpuEnv, fetchImpl });
|
|
281
|
-
expect(report.verdict).toBe('endpoint ready');
|
|
282
|
-
expect(report.evidence.some((e) => /ComfyUI system stats — 1 device\(s\) reported/.test(e))).toBe(true);
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
it('recognizes a llama.cpp-style {status: "..."} health response', async () => {
|
|
286
|
-
const fetchImpl = async () => new Response(JSON.stringify({ status: 'ok' }), { status: 200 });
|
|
287
|
-
const report = await runGpuDoctor({ url: 'http://localhost:8080/health' }, { ...noGpuEnv, fetchImpl });
|
|
288
|
-
expect(report.verdict).toBe('endpoint ready');
|
|
289
|
-
expect(report.evidence.some((e) => /status: ok/.test(e))).toBe(true);
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
it('recognizes a bare-text "OK" health response', async () => {
|
|
293
|
-
const fetchImpl = async () => new Response('OK', { status: 200 });
|
|
294
|
-
const report = await runGpuDoctor({ url: 'http://localhost:9000/healthz' }, { ...noGpuEnv, fetchImpl });
|
|
295
|
-
expect(report.verdict).toBe('endpoint ready');
|
|
296
|
-
expect(report.evidence.some((e) => /response: OK/.test(e))).toBe(true);
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
it('still reports the OpenAI-style model count for /v1/models', async () => {
|
|
300
|
-
const fetchImpl = async () => new Response(JSON.stringify({ data: [{ id: 'a' }, { id: 'b' }] }), { status: 200 });
|
|
301
|
-
const report = await runGpuDoctor({ url: 'http://localhost:8000/v1/models' }, { ...noGpuEnv, fetchImpl });
|
|
302
|
-
expect(report.evidence.some((e) => /\/v1\/models reports 2 model\(s\)/.test(e))).toBe(true);
|
|
303
|
-
});
|
|
304
|
-
});
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { readProbeCache, writeProbeCache } from '../src/gpuDoctor/probeCache.js';
|
|
3
|
-
import { collectEnvironment } from '../src/gpuDoctor/doctor.js';
|
|
4
|
-
|
|
5
|
-
function memoryFs() {
|
|
6
|
-
let contents;
|
|
7
|
-
return {
|
|
8
|
-
readFileSync: () => {
|
|
9
|
-
if (contents === undefined) {
|
|
10
|
-
const err = new Error('ENOENT');
|
|
11
|
-
err.code = 'ENOENT';
|
|
12
|
-
throw err;
|
|
13
|
-
}
|
|
14
|
-
return contents;
|
|
15
|
-
},
|
|
16
|
-
writeFileSync: (_path, data) => { contents = data; },
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
describe('probeCache', () => {
|
|
21
|
-
it('is a miss when nothing has been written yet', () => {
|
|
22
|
-
const fsImpl = memoryFs();
|
|
23
|
-
expect(readProbeCache('gpu', { fsImpl })).toBeUndefined();
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it('returns a written value within the TTL window', () => {
|
|
27
|
-
const fsImpl = memoryFs();
|
|
28
|
-
writeProbeCache('gpu', { available: true }, { fsImpl });
|
|
29
|
-
expect(readProbeCache('gpu', { fsImpl, ttlMs: 10_000 })).toEqual({ available: true });
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it('expires a value once the TTL has elapsed', () => {
|
|
33
|
-
const fsImpl = memoryFs();
|
|
34
|
-
writeProbeCache('gpu', { available: true }, { fsImpl });
|
|
35
|
-
expect(readProbeCache('gpu', { fsImpl, ttlMs: -1 })).toBeUndefined();
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('keeps separate entries per key in the same store', () => {
|
|
39
|
-
const fsImpl = memoryFs();
|
|
40
|
-
writeProbeCache('gpu', { a: 1 }, { fsImpl });
|
|
41
|
-
writeProbeCache('torch', { b: 2 }, { fsImpl });
|
|
42
|
-
expect(readProbeCache('gpu', { fsImpl })).toEqual({ a: 1 });
|
|
43
|
-
expect(readProbeCache('torch', { fsImpl })).toEqual({ b: 2 });
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it('degrades to a miss instead of throwing when the store is corrupt', () => {
|
|
47
|
-
const fsImpl = { readFileSync: () => 'not json', writeFileSync: () => {} };
|
|
48
|
-
expect(() => readProbeCache('gpu', { fsImpl })).not.toThrow();
|
|
49
|
-
expect(readProbeCache('gpu', { fsImpl })).toBeUndefined();
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('never throws even when the filesystem write fails', () => {
|
|
53
|
-
const fsImpl = {
|
|
54
|
-
readFileSync: () => { throw new Error('ENOENT'); },
|
|
55
|
-
writeFileSync: () => { throw new Error('EACCES'); },
|
|
56
|
-
};
|
|
57
|
-
expect(() => writeProbeCache('gpu', { available: true }, { fsImpl })).not.toThrow();
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
describe('collectEnvironment — probe caching', () => {
|
|
62
|
-
it('bypasses the cache entirely when deps.detectGpus/detectTorch are injected', () => {
|
|
63
|
-
const fsImpl = memoryFs();
|
|
64
|
-
// Pre-populate the cache with a decoy value the injected fakes must not return.
|
|
65
|
-
writeProbeCache('gpu', { available: false, decoy: true }, { fsImpl });
|
|
66
|
-
const realGpu = { available: true, gpus: [{ index: 0, name: 'stub', vramTotalGb: 24, vramFreeGb: 20 }] };
|
|
67
|
-
const env = collectEnvironment({
|
|
68
|
-
detectGpus: () => realGpu,
|
|
69
|
-
detectTorch: () => ({ torchInstalled: true }),
|
|
70
|
-
detectDisk: () => ({ cachePath: '/x', cacheSource: 'x', cacheExists: true, writable: true }),
|
|
71
|
-
cacheFsImpl: fsImpl,
|
|
72
|
-
});
|
|
73
|
-
expect(env.gpu).toBe(realGpu);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('reuses a cached result for the real (non-injected) probe path instead of running the probe again', () => {
|
|
77
|
-
const fsImpl = memoryFs();
|
|
78
|
-
const cachedGpu = { available: true, gpus: [{ index: 0, name: 'cached-gpu' }] };
|
|
79
|
-
const cachedTorch = { torchInstalled: true, cudaAvailable: true };
|
|
80
|
-
writeProbeCache('gpu', cachedGpu, { fsImpl });
|
|
81
|
-
writeProbeCache('torch', cachedTorch, { fsImpl });
|
|
82
|
-
|
|
83
|
-
// No deps.detectGpus/detectTorch here — this exercises the real-probe
|
|
84
|
-
// branch, which must consult the cache before shelling out.
|
|
85
|
-
const env = collectEnvironment({
|
|
86
|
-
cacheFsImpl: fsImpl,
|
|
87
|
-
cacheTtlMs: 60_000,
|
|
88
|
-
detectDisk: () => ({ cachePath: '/x', cacheSource: 'x', cacheExists: true, writable: true }),
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
expect(env.gpu).toEqual(cachedGpu);
|
|
92
|
-
expect(env.torch).toEqual(cachedTorch);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it('forces fresh probes when deps.noCache is set, even with a warm cache', () => {
|
|
96
|
-
const fsImpl = memoryFs();
|
|
97
|
-
writeProbeCache('gpu', { available: false, decoy: true }, { fsImpl });
|
|
98
|
-
let probeCalls = 0;
|
|
99
|
-
const freshGpu = { available: true, calls: 0 };
|
|
100
|
-
const env = collectEnvironment({
|
|
101
|
-
noCache: true,
|
|
102
|
-
cacheFsImpl: fsImpl,
|
|
103
|
-
detectGpus: () => { probeCalls += 1; return freshGpu; },
|
|
104
|
-
detectTorch: () => ({ torchInstalled: true }),
|
|
105
|
-
detectDisk: () => ({ cachePath: '/x', cacheSource: 'x', cacheExists: true, writable: true }),
|
|
106
|
-
});
|
|
107
|
-
expect(env.gpu).toBe(freshGpu);
|
|
108
|
-
expect(probeCalls).toBe(1);
|
|
109
|
-
});
|
|
110
|
-
});
|
|
@@ -1,257 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import { detectGpus } from '../src/gpuDoctor/gpuInfo.js';
|
|
3
|
-
import { detectTorch } from '../src/gpuDoctor/torchInfo.js';
|
|
4
|
-
import { detectDisk } from '../src/gpuDoctor/diskInfo.js';
|
|
5
|
-
import { collectEnvironment } from '../src/gpuDoctor/doctor.js';
|
|
6
|
-
|
|
7
|
-
function enoent() {
|
|
8
|
-
const err = new Error('spawn ENOENT');
|
|
9
|
-
err.code = 'ENOENT';
|
|
10
|
-
return err;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
describe('detectGpus', () => {
|
|
14
|
-
it('reports unavailable with "nvidia-smi not found" on ENOENT', () => {
|
|
15
|
-
const exec = () => { throw enoent(); };
|
|
16
|
-
const result = detectGpus(exec);
|
|
17
|
-
expect(result.available).toBe(false);
|
|
18
|
-
expect(result.error).toBe('nvidia-smi not found');
|
|
19
|
-
expect(result.gpus).toEqual([]);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it('reports unavailable with the raw error message on a non-ENOENT failure', () => {
|
|
23
|
-
const exec = () => { throw new Error('permission denied'); };
|
|
24
|
-
const result = detectGpus(exec);
|
|
25
|
-
expect(result.available).toBe(false);
|
|
26
|
-
expect(result.error).toBe('permission denied');
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('parses driver version, CUDA version, and a single GPU from real nvidia-smi output shapes', () => {
|
|
30
|
-
const plainText = `Wed Jul 16 08:00:00 2026
|
|
31
|
-
+-----------------------------------------------------------------------------+
|
|
32
|
-
| NVIDIA-SMI 550.54.15 Driver Version: 550.54.15 CUDA Version: 12.4 |
|
|
33
|
-
+-------------------------------+----------------------+----------------------+`;
|
|
34
|
-
const csv = '0, NVIDIA GeForce RTX 4090, 24564, 19022, 5542, 45, 120.5';
|
|
35
|
-
const exec = (cmd, args) => {
|
|
36
|
-
if (!args || args.length === 0) return plainText;
|
|
37
|
-
if (args[0].includes('query-gpu')) return csv;
|
|
38
|
-
if (args[0].includes('query-compute-apps')) return '';
|
|
39
|
-
throw new Error('unexpected args');
|
|
40
|
-
};
|
|
41
|
-
const result = detectGpus(exec);
|
|
42
|
-
expect(result.available).toBe(true);
|
|
43
|
-
expect(result.driverVersion).toBe('550.54.15');
|
|
44
|
-
expect(result.cudaVersion).toBe('12.4');
|
|
45
|
-
expect(result.gpus).toHaveLength(1);
|
|
46
|
-
expect(result.gpus[0]).toMatchObject({ index: 0, name: 'NVIDIA GeForce RTX 4090' });
|
|
47
|
-
expect(result.gpus[0].vramTotalGb).toBeCloseTo(24, 0);
|
|
48
|
-
expect(result.gpus[0].vramFreeGb).toBeCloseTo(18.6, 0);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('parses multiple GPUs from multi-line CSV output', () => {
|
|
52
|
-
const csv = [
|
|
53
|
-
'0, NVIDIA A100, 40960, 2150, 38810, 60, 200',
|
|
54
|
-
'1, NVIDIA A100, 40960, 39424, 1536, 45, 90',
|
|
55
|
-
].join('\n');
|
|
56
|
-
const exec = (cmd, args) => {
|
|
57
|
-
if (!args || args.length === 0) return 'Driver Version: 535.104.05 CUDA Version: 12.2';
|
|
58
|
-
if (args[0].includes('query-gpu')) return csv;
|
|
59
|
-
return '';
|
|
60
|
-
};
|
|
61
|
-
const result = detectGpus(exec);
|
|
62
|
-
expect(result.gpus).toHaveLength(2);
|
|
63
|
-
expect(result.gpus.map((g) => g.index)).toEqual([0, 1]);
|
|
64
|
-
expect(result.gpus[1].vramFreeGb).toBeGreaterThan(result.gpus[0].vramFreeGb);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('parses compute-apps processes and handles [N/A] temperature/power', () => {
|
|
68
|
-
const gpuCsv = '0, NVIDIA T4, 16384, 16000, 384, [N/A], [N/A]';
|
|
69
|
-
const procCsv = '1234, python, 2048\n5678, vllm-worker, 4096';
|
|
70
|
-
const exec = (cmd, args) => {
|
|
71
|
-
if (!args || args.length === 0) return 'Driver Version: 470.199.02 CUDA Version: 11.4';
|
|
72
|
-
if (args[0].includes('query-gpu')) return gpuCsv;
|
|
73
|
-
if (args[0].includes('query-compute-apps')) return procCsv;
|
|
74
|
-
return '';
|
|
75
|
-
};
|
|
76
|
-
const result = detectGpus(exec);
|
|
77
|
-
expect(result.gpus[0].temperatureC).toBeNull();
|
|
78
|
-
expect(result.gpus[0].powerDrawW).toBeNull();
|
|
79
|
-
expect(result.processes).toHaveLength(2);
|
|
80
|
-
expect(result.processes[0]).toEqual({ pid: 1234, name: 'python', memoryMb: 2048 });
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('still reports available=true when the CSV query is unsupported but the plain probe succeeded', () => {
|
|
84
|
-
const exec = (cmd, args) => {
|
|
85
|
-
if (!args || args.length === 0) return 'Driver Version: 550.54.15 CUDA Version: 12.4';
|
|
86
|
-
throw new Error('unsupported field');
|
|
87
|
-
};
|
|
88
|
-
const result = detectGpus(exec);
|
|
89
|
-
expect(result.available).toBe(true);
|
|
90
|
-
expect(result.driverVersion).toBe('550.54.15');
|
|
91
|
-
expect(result.gpus).toEqual([]);
|
|
92
|
-
expect(result.processes).toEqual([]);
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
describe('detectTorch', () => {
|
|
97
|
-
it('parses a successful python3 probe result', () => {
|
|
98
|
-
const payload = JSON.stringify({
|
|
99
|
-
torchInstalled: true, cudaAvailable: true, cudaVersion: '12.4',
|
|
100
|
-
deviceName: 'NVIDIA GeForce RTX 4090', deviceCount: 1,
|
|
101
|
-
freeVramGb: 18.6, totalVramGb: 24.0, error: null,
|
|
102
|
-
});
|
|
103
|
-
const exec = (bin) => {
|
|
104
|
-
if (bin === 'python3') return payload + '\n';
|
|
105
|
-
throw enoent();
|
|
106
|
-
};
|
|
107
|
-
const result = detectTorch(exec);
|
|
108
|
-
expect(result.pythonAvailable).toBe(true);
|
|
109
|
-
expect(result.pythonBinary).toBe('python3');
|
|
110
|
-
expect(result.cudaAvailable).toBe(true);
|
|
111
|
-
expect(result.deviceName).toBe('NVIDIA GeForce RTX 4090');
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it('falls back to "python" when "python3" is not found', () => {
|
|
115
|
-
const payload = JSON.stringify({ torchInstalled: false, cudaAvailable: false, error: null });
|
|
116
|
-
const exec = (bin) => {
|
|
117
|
-
if (bin === 'python3') throw enoent();
|
|
118
|
-
if (bin === 'python') return payload;
|
|
119
|
-
throw new Error('unexpected binary');
|
|
120
|
-
};
|
|
121
|
-
const result = detectTorch(exec);
|
|
122
|
-
expect(result.pythonAvailable).toBe(true);
|
|
123
|
-
expect(result.pythonBinary).toBe('python');
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it('reports "python not found" when neither binary exists', () => {
|
|
127
|
-
const exec = () => { throw enoent(); };
|
|
128
|
-
const result = detectTorch(exec);
|
|
129
|
-
expect(result.pythonAvailable).toBe(false);
|
|
130
|
-
expect(result.pythonBinary).toBeNull();
|
|
131
|
-
expect(result.error).toBe('python not found');
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it('reports pythonAvailable=true but torchInstalled=false when python exists but the probe output cannot be parsed', () => {
|
|
135
|
-
const exec = () => { throw new Error('SyntaxError: invalid syntax'); };
|
|
136
|
-
const result = detectTorch(exec);
|
|
137
|
-
expect(result.pythonAvailable).toBe(true);
|
|
138
|
-
expect(result.torchInstalled).toBe(false);
|
|
139
|
-
expect(result.error).toBe('could not read torch probe output');
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
it('reads the last line of output when extra warning lines precede the JSON', () => {
|
|
143
|
-
const payload = JSON.stringify({ torchInstalled: true, cudaAvailable: false, error: null });
|
|
144
|
-
const exec = () => `FutureWarning: something is deprecated\n${payload}`;
|
|
145
|
-
const result = detectTorch(exec);
|
|
146
|
-
expect(result.torchInstalled).toBe(true);
|
|
147
|
-
expect(result.cudaAvailable).toBe(false);
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it('falls through to the next binary when output is not valid JSON', () => {
|
|
151
|
-
const payload = JSON.stringify({ torchInstalled: true, cudaAvailable: true, error: null });
|
|
152
|
-
const exec = (bin) => {
|
|
153
|
-
if (bin === 'python3') return 'not json at all';
|
|
154
|
-
if (bin === 'python') return payload;
|
|
155
|
-
throw new Error('unexpected');
|
|
156
|
-
};
|
|
157
|
-
const result = detectTorch(exec);
|
|
158
|
-
expect(result.pythonBinary).toBe('python');
|
|
159
|
-
expect(result.cudaAvailable).toBe(true);
|
|
160
|
-
});
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
describe('detectDisk', () => {
|
|
164
|
-
const ORIGINAL_ENV = { ...process.env };
|
|
165
|
-
|
|
166
|
-
beforeEach(() => {
|
|
167
|
-
delete process.env.HF_HOME;
|
|
168
|
-
delete process.env.HUGGINGFACE_HUB_CACHE;
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
afterEach(() => {
|
|
172
|
-
process.env = { ...ORIGINAL_ENV };
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
it('uses HF_HOME when set, and reports it as the source', () => {
|
|
176
|
-
process.env.HF_HOME = '/custom/hf-cache';
|
|
177
|
-
const fsImpl = {
|
|
178
|
-
existsSync: (p) => p === '/custom/hf-cache',
|
|
179
|
-
statfsSync: () => ({ bavail: 1000, bsize: 4096, blocks: 5000 }),
|
|
180
|
-
accessSync: () => {},
|
|
181
|
-
};
|
|
182
|
-
const result = detectDisk(fsImpl);
|
|
183
|
-
expect(result.cachePath).toBe('/custom/hf-cache');
|
|
184
|
-
expect(result.cacheSource).toBe('HF_HOME');
|
|
185
|
-
expect(result.cacheExists).toBe(true);
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
it('falls back to HUGGINGFACE_HUB_CACHE when HF_HOME is unset', () => {
|
|
189
|
-
process.env.HUGGINGFACE_HUB_CACHE = '/other/cache';
|
|
190
|
-
const fsImpl = { existsSync: () => false, statfsSync: () => { throw new Error('no'); }, accessSync: () => {} };
|
|
191
|
-
const result = detectDisk(fsImpl);
|
|
192
|
-
expect(result.cachePath).toBe('/other/cache');
|
|
193
|
-
expect(result.cacheSource).toBe('HUGGINGFACE_HUB_CACHE');
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it('computes freeGb/totalGb from statfsSync fields', () => {
|
|
197
|
-
const fsImpl = {
|
|
198
|
-
existsSync: () => true,
|
|
199
|
-
statfsSync: () => ({ bavail: 2_000_000, bsize: 4096, blocks: 10_000_000 }),
|
|
200
|
-
accessSync: () => {},
|
|
201
|
-
};
|
|
202
|
-
const result = detectDisk(fsImpl);
|
|
203
|
-
expect(result.freeGb).toBeCloseTo((2_000_000 * 4096) / 1e9, 1);
|
|
204
|
-
expect(result.totalGb).toBeCloseTo((10_000_000 * 4096) / 1e9, 1);
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
it('leaves freeGb/totalGb null when statfsSync is unsupported, without throwing', () => {
|
|
208
|
-
const fsImpl = { existsSync: () => true, statfsSync: () => { throw new Error('ENOSYS'); }, accessSync: () => {} };
|
|
209
|
-
const result = detectDisk(fsImpl);
|
|
210
|
-
expect(result.freeGb).toBeNull();
|
|
211
|
-
expect(result.totalGb).toBeNull();
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
it('reports writable=false when accessSync throws (read-only path)', () => {
|
|
215
|
-
const fsImpl = {
|
|
216
|
-
existsSync: () => true,
|
|
217
|
-
statfsSync: () => ({ bavail: 1, bsize: 4096, blocks: 1 }),
|
|
218
|
-
accessSync: () => { throw new Error('EACCES'); },
|
|
219
|
-
};
|
|
220
|
-
const result = detectDisk(fsImpl);
|
|
221
|
-
expect(result.writable).toBe(false);
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
it('reports writable=true when accessSync succeeds', () => {
|
|
225
|
-
const fsImpl = {
|
|
226
|
-
existsSync: () => true,
|
|
227
|
-
statfsSync: () => ({ bavail: 1, bsize: 4096, blocks: 1 }),
|
|
228
|
-
accessSync: () => {},
|
|
229
|
-
};
|
|
230
|
-
const result = detectDisk(fsImpl);
|
|
231
|
-
expect(result.writable).toBe(true);
|
|
232
|
-
});
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
describe('collectEnvironment', () => {
|
|
236
|
-
it('wires deps.detectGpus/detectTorch/detectDisk overrides through to the returned env', () => {
|
|
237
|
-
const gpu = { available: true, gpus: [{ index: 0, name: 'stub-gpu', vramTotalGb: 24, vramFreeGb: 20 }], processes: [], driverVersion: '1.0', cudaVersion: '12.0', error: null };
|
|
238
|
-
const torch = { pythonAvailable: true, pythonBinary: 'python3', torchInstalled: true, cudaAvailable: true, error: null };
|
|
239
|
-
const disk = { cachePath: '/stub', cacheSource: 'stub', cacheExists: true, writable: true, freeGb: 10, totalGb: 20 };
|
|
240
|
-
const env = collectEnvironment({
|
|
241
|
-
detectGpus: () => gpu,
|
|
242
|
-
detectTorch: () => torch,
|
|
243
|
-
detectDisk: () => disk,
|
|
244
|
-
});
|
|
245
|
-
expect(env.gpu).toBe(gpu);
|
|
246
|
-
expect(env.torch).toBe(torch);
|
|
247
|
-
expect(env.disk).toBe(disk);
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
it('falls back to the real probe modules when no deps are given, without throwing', () => {
|
|
251
|
-
expect(() => collectEnvironment()).not.toThrow();
|
|
252
|
-
const env = collectEnvironment();
|
|
253
|
-
expect(env).toHaveProperty('gpu');
|
|
254
|
-
expect(env).toHaveProperty('torch');
|
|
255
|
-
expect(env).toHaveProperty('disk');
|
|
256
|
-
});
|
|
257
|
-
});
|