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.
Files changed (66) hide show
  1. package/LICENSE +207 -0
  2. package/README.md +9 -2
  3. package/package.json +44 -2
  4. package/src/api.js +16 -0
  5. package/src/badgr.js +2 -2
  6. package/src/commands/batch.js +11 -0
  7. package/src/commands/comfyui.js +31 -15
  8. package/src/commands/embed.js +13 -10
  9. package/src/commands/launch.js +8 -1
  10. package/src/commands/login.js +75 -20
  11. package/src/commands/run.js +45 -13
  12. package/src/commands/sbatch.js +6 -1
  13. package/src/commands/serve.js +44 -30
  14. package/src/commands/train.js +8 -12
  15. package/src/commands/transcribe.js +13 -10
  16. package/src/envFlag.js +10 -0
  17. package/src/onboarding.js +8 -1
  18. package/src/progress.js +48 -0
  19. package/tests/agent-images.test.js +0 -17
  20. package/tests/api.test.js +0 -168
  21. package/tests/artifactDownload.test.js +0 -113
  22. package/tests/artifacts.test.js +0 -168
  23. package/tests/batch.test.js +0 -641
  24. package/tests/browser.test.js +0 -51
  25. package/tests/capacity.test.js +0 -68
  26. package/tests/commands.test.js +0 -417
  27. package/tests/config.test.js +0 -96
  28. package/tests/connect.test.js +0 -83
  29. package/tests/detect.test.js +0 -191
  30. package/tests/down.test.js +0 -150
  31. package/tests/errors.test.js +0 -130
  32. package/tests/fallback-timeout.test.js +0 -41
  33. package/tests/fanout.test.js +0 -124
  34. package/tests/gpu-doctor-classifiers.test.js +0 -402
  35. package/tests/gpu-doctor-doctor.test.js +0 -304
  36. package/tests/gpu-doctor-probe-cache.test.js +0 -110
  37. package/tests/gpu-doctor-probes.test.js +0 -257
  38. package/tests/heartbeat.test.js +0 -70
  39. package/tests/job-progress-poll.test.js +0 -136
  40. package/tests/launch-command-argv.test.js +0 -93
  41. package/tests/launch-readiness.test.js +0 -403
  42. package/tests/launch.test.js +0 -440
  43. package/tests/onboarding.test.js +0 -134
  44. package/tests/productized-dry-run.test.js +0 -141
  45. package/tests/productized-runners.test.js +0 -237
  46. package/tests/pull.test.js +0 -266
  47. package/tests/rerun.test.js +0 -94
  48. package/tests/restart.test.js +0 -88
  49. package/tests/router.test.js +0 -98
  50. package/tests/run-lifecycle.test.js +0 -1054
  51. package/tests/sbatch.test.js +0 -190
  52. package/tests/secrets.test.js +0 -16
  53. package/tests/serve-apps.test.js +0 -189
  54. package/tests/serve-lifecycle.test.js +0 -931
  55. package/tests/slurm.test.js +0 -77
  56. package/tests/spec.test.js +0 -201
  57. package/tests/status.test.js +0 -73
  58. package/tests/store.test.js +0 -187
  59. package/tests/task.test.js +0 -109
  60. package/tests/template.test.js +0 -556
  61. package/tests/train-lora-dataset.test.js +0 -176
  62. package/tests/upload.test.js +0 -79
  63. package/tests/workload-rerun.test.js +0 -56
  64. package/tests/workload-spec.test.js +0 -180
  65. package/tests/workload-templates.test.js +0 -865
  66. package/tests/workload-workspace-paths.test.js +0 -46
package/tests/api.test.js DELETED
@@ -1,168 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { callApi, listModels, chatCompletion, submitJob, getJobStatus, listReceipts, getReceipt, runJob, serveModel, uploadBlob } from '../src/api.js';
3
-
4
- const mockConfig = { apiKey: 'sk-test', baseUrl: 'https://api.test/v1', defaultModel: 'llama-3' };
5
-
6
- function mockFetch(data, ok = true, status = 200) {
7
- global.fetch = vi.fn().mockResolvedValue({
8
- ok, status,
9
- json: async () => data,
10
- text: async () => JSON.stringify(data),
11
- });
12
- }
13
-
14
- describe('callApi', () => {
15
- it('includes Authorization header', async () => {
16
- mockFetch({ data: [] });
17
- await callApi('/models', { apiKey: 'sk-abc', baseUrl: 'https://api.test/v1' });
18
- const [, init] = global.fetch.mock.calls[0];
19
- expect(init.headers.Authorization).toBe('Bearer sk-abc');
20
- });
21
-
22
- it('throws on non-ok response with status code', async () => {
23
- mockFetch('Unauthorized', false, 401);
24
- await expect(callApi('/models', { apiKey: 'bad', baseUrl: 'https://api.test/v1' })).rejects.toThrow('401');
25
- });
26
-
27
- it('sends JSON body for POST', async () => {
28
- mockFetch({ id: 'j1' });
29
- await callApi('/jobs', { method: 'POST', apiKey: 'sk-x', baseUrl: 'https://api.test/v1', body: { script: 'a.py' } });
30
- expect(JSON.parse(global.fetch.mock.calls[0][1].body)).toEqual({ script: 'a.py' });
31
- });
32
-
33
- it('omits body for GET', async () => {
34
- mockFetch({});
35
- await callApi('/models', { apiKey: 'x', baseUrl: 'https://api.test/v1' });
36
- expect(global.fetch.mock.calls[0][1].body).toBeUndefined();
37
- });
38
- });
39
-
40
- describe('chatCompletion', () => {
41
- it('POST /chat/completions with messages', async () => {
42
- mockFetch({ choices: [{ message: { content: 'hi' } }] });
43
- const msgs = [{ role: 'user', content: 'hello' }];
44
- await chatCompletion(mockConfig, msgs);
45
- const [url, init] = global.fetch.mock.calls[0];
46
- expect(url).toContain('/chat/completions');
47
- expect(JSON.parse(init.body).messages).toEqual(msgs);
48
- });
49
-
50
- it('uses defaultModel when no model option', async () => {
51
- mockFetch({ choices: [] });
52
- await chatCompletion(mockConfig, []);
53
- expect(JSON.parse(global.fetch.mock.calls[0][1].body).model).toBe('llama-3');
54
- });
55
-
56
- it('uses provided model over default', async () => {
57
- mockFetch({ choices: [] });
58
- await chatCompletion(mockConfig, [], { model: 'gpt-4o' });
59
- expect(JSON.parse(global.fetch.mock.calls[0][1].body).model).toBe('gpt-4o');
60
- });
61
- });
62
-
63
- describe('listModels', () => {
64
- it('calls GET /models', async () => {
65
- mockFetch({ data: [{ id: 'llama-3' }] });
66
- const result = await listModels(mockConfig);
67
- expect(global.fetch).toHaveBeenCalledWith(expect.stringContaining('/models'), expect.any(Object));
68
- expect(result.data[0].id).toBe('llama-3');
69
- });
70
- });
71
-
72
- describe('submitJob / getJobStatus', () => {
73
- it('POST /jobs with job body', async () => {
74
- mockFetch({ jobId: 'abc-123' });
75
- const result = await submitJob(mockConfig, { script: 'train.py', gpuType: 'rtx-4090' });
76
- expect(result.jobId).toBe('abc-123');
77
- });
78
-
79
- it('GET /jobs/:id', async () => {
80
- mockFetch({ status: 'running' });
81
- await getJobStatus(mockConfig, 'job-xyz');
82
- expect(global.fetch).toHaveBeenCalledWith(expect.stringContaining('/jobs/job-xyz'), expect.any(Object));
83
- });
84
- });
85
-
86
- describe('listReceipts', () => {
87
- it('calls GET /receipts', async () => {
88
- mockFetch({ object: 'list', data: [] });
89
- await listReceipts(mockConfig);
90
- const [url] = global.fetch.mock.calls[0];
91
- expect(url).toContain('/receipts');
92
- });
93
-
94
- it('appends limit query param', async () => {
95
- mockFetch({ object: 'list', data: [] });
96
- await listReceipts(mockConfig, { limit: 50 });
97
- const [url] = global.fetch.mock.calls[0];
98
- expect(url).toContain('limit=50');
99
- });
100
-
101
- it('appends status filter when provided', async () => {
102
- mockFetch({ object: 'list', data: [] });
103
- await listReceipts(mockConfig, { status: 'success' });
104
- const [url] = global.fetch.mock.calls[0];
105
- expect(url).toContain('status=success');
106
- });
107
- });
108
-
109
- describe('getReceipt', () => {
110
- it('calls GET /receipts/:id', async () => {
111
- mockFetch({ request_id: 'req-123', status: 'success' });
112
- const result = await getReceipt(mockConfig, 'req-123');
113
- expect(global.fetch).toHaveBeenCalledWith(expect.stringContaining('/receipts/req-123'), expect.any(Object));
114
- expect(result.request_id).toBe('req-123');
115
- });
116
- });
117
-
118
- describe('runJob', () => {
119
- it('POST /run with job spec', async () => {
120
- mockFetch({ deployment_id: 'dep-abc', status: 'running', provider: 'runpod' });
121
- const result = await runJob(mockConfig, { command: ['python', 'train.py'], gpu: 'A100' });
122
- const [url, init] = global.fetch.mock.calls[0];
123
- expect(url).toContain('/run');
124
- expect(init.method).toBe('POST');
125
- expect(JSON.parse(init.body).gpu).toBe('A100');
126
- expect(result.deployment_id).toBe('dep-abc');
127
- });
128
- });
129
-
130
- describe('serveModel', () => {
131
- it('POST /serve with model spec', async () => {
132
- mockFetch({ deployment_id: 'dep-xyz', status: 'running', endpoint_url: 'https://api.badgr.ai/v1' });
133
- const result = await serveModel(mockConfig, { model: 'meta-llama/Llama-3.1-8B-Instruct', gpu: 'L40S' });
134
- const [url, init] = global.fetch.mock.calls[0];
135
- expect(url).toContain('/serve');
136
- expect(init.method).toBe('POST');
137
- expect(JSON.parse(init.body).model).toBe('meta-llama/Llama-3.1-8B-Instruct');
138
- expect(result.deployment_id).toBe('dep-xyz');
139
- });
140
- });
141
-
142
- describe('uploadBlob', () => {
143
- it('POSTs multipart form data to /v1/uploads with Authorization, no manual Content-Type', async () => {
144
- mockFetch({ upload_id: 'up_1', code_uri: 'https://api.test/v1/uploads/up_1/download?token=t' });
145
- const result = await uploadBlob(mockConfig, { data: Buffer.from('hello'), filename: 'a.txt' });
146
- const [url, init] = global.fetch.mock.calls[0];
147
- expect(url).toBe('https://api.test/v1/uploads');
148
- expect(init.method).toBe('POST');
149
- expect(init.body).toBeInstanceOf(FormData);
150
- expect(init.headers.Authorization).toBe('Bearer sk-test');
151
- expect(init.headers['Content-Type']).toBeUndefined();
152
- expect(result.upload_id).toBe('up_1');
153
- expect(result.code_uri).toContain('up_1');
154
- });
155
-
156
- it('sets the Blob content type when contentType is given', async () => {
157
- mockFetch({ upload_id: 'up_2', code_uri: 'https://x/up_2' });
158
- await uploadBlob(mockConfig, { data: Buffer.from('x'), filename: 'a.tar.gz', contentType: 'application/gzip' });
159
- const [, init] = global.fetch.mock.calls[0];
160
- expect(init.body.get('file').type).toBe('application/gzip');
161
- });
162
-
163
- it('throws a clear error on a non-ok response', async () => {
164
- mockFetch('boom', false, 500);
165
- await expect(uploadBlob(mockConfig, { data: Buffer.from('x'), filename: 'a.txt' }))
166
- .rejects.toThrow(/Upload failed: 500/);
167
- });
168
- });
@@ -1,113 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- import fs from 'fs';
3
- import os from 'os';
4
- import path from 'path';
5
- import { artifactDownloadUrl } from '../src/artifactDownload.js';
6
-
7
- describe('artifactDownloadUrl', () => {
8
- it('appends /v1/deployments/.../artifacts/download when baseUrl already ends in /v1', () => {
9
- expect(artifactDownloadUrl('https://aibadgr.com/v1', 'dep-abc')).toBe(
10
- 'https://aibadgr.com/v1/deployments/dep-abc/artifacts/download',
11
- );
12
- });
13
-
14
- it('appends /v1 when baseUrl has no /v1 suffix', () => {
15
- expect(artifactDownloadUrl('https://aibadgr.com', 'dep-abc')).toBe(
16
- 'https://aibadgr.com/v1/deployments/dep-abc/artifacts/download',
17
- );
18
- });
19
-
20
- it('normalizes a trailing slash on baseUrl', () => {
21
- expect(artifactDownloadUrl('https://aibadgr.com/v1/', 'dep-abc')).toBe(
22
- 'https://aibadgr.com/v1/deployments/dep-abc/artifacts/download',
23
- );
24
- });
25
-
26
- it('treats a missing/empty baseUrl as an empty string prefix', () => {
27
- expect(artifactDownloadUrl('', 'dep-abc')).toBe('/v1/deployments/dep-abc/artifacts/download');
28
- expect(artifactDownloadUrl(undefined, 'dep-abc')).toBe('/v1/deployments/dep-abc/artifacts/download');
29
- });
30
- });
31
-
32
- let tarExtractBehavior = () => {};
33
-
34
- // Matches the real node-tar v7 export shape: named exports only, no
35
- // default — the mock previously provided both shapes ({ default: { x }, x
36
- // }), which silently masked a real bug where the source destructured
37
- // `{ default: tar }` and crashed with "Cannot read properties of undefined
38
- // (reading 'x')" on every real extraction (found via a live artifact
39
- // download, never caught by this test).
40
- vi.mock('tar', () => {
41
- const x = vi.fn(async opts => tarExtractBehavior(opts));
42
- return { x };
43
- });
44
-
45
- const { downloadAndExtractArtifact } = await import('../src/artifactDownload.js');
46
-
47
- describe('downloadAndExtractArtifact', () => {
48
- const config = { apiKey: 'test-key', baseUrl: 'https://example.test/v1' };
49
- let destDir;
50
-
51
- beforeEach(() => {
52
- tarExtractBehavior = ({ cwd }) => {
53
- fs.writeFileSync(path.join(cwd, 'marker.txt'), 'ok');
54
- };
55
- destDir = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'badgr-artifact-dl-')), 'dest');
56
- });
57
-
58
- afterEach(() => {
59
- vi.restoreAllMocks();
60
- delete global.fetch;
61
- fs.rmSync(path.dirname(destDir), { recursive: true, force: true });
62
- });
63
-
64
- it('creates destDir, downloads, and extracts', async () => {
65
- global.fetch = vi.fn().mockResolvedValue({ ok: true, arrayBuffer: async () => new ArrayBuffer(8) });
66
- await downloadAndExtractArtifact(config, 'dep-abc', destDir);
67
-
68
- expect(fs.existsSync(destDir)).toBe(true);
69
- expect(fs.existsSync(path.join(destDir, 'marker.txt'))).toBe(true);
70
- expect(global.fetch).toHaveBeenCalledWith(
71
- 'https://example.test/v1/deployments/dep-abc/artifacts/download',
72
- { headers: { Authorization: 'Bearer test-key' } },
73
- );
74
- });
75
-
76
- it('throws an Error with httpStatus/statusText/bodyText set on a non-OK response', async () => {
77
- global.fetch = vi.fn().mockResolvedValue({ ok: false, status: 404, statusText: 'Not Found', text: async () => 'nope' });
78
- await expect(downloadAndExtractArtifact(config, 'dep-missing', destDir)).rejects.toMatchObject({
79
- httpStatus: 404,
80
- statusText: 'Not Found',
81
- bodyText: 'nope',
82
- });
83
- });
84
-
85
- it('includes the response body text in the default error message', async () => {
86
- global.fetch = vi.fn().mockResolvedValue({ ok: false, status: 500, statusText: 'Internal Server Error', text: async () => 'boom' });
87
- await expect(downloadAndExtractArtifact(config, 'dep-broken', destDir)).rejects.toThrow(/500 Internal Server Error — boom/);
88
- });
89
-
90
- it('propagates a network-level failure with no httpStatus set', async () => {
91
- global.fetch = vi.fn().mockRejectedValue(new Error('getaddrinfo ENOTFOUND'));
92
- let caught;
93
- try {
94
- await downloadAndExtractArtifact(config, 'dep-offline', destDir);
95
- } catch (err) {
96
- caught = err;
97
- }
98
- expect(caught).toBeDefined();
99
- expect(caught.message).toBe('getaddrinfo ENOTFOUND');
100
- expect(caught.httpStatus).toBeUndefined();
101
- });
102
-
103
- it('cleans up the temp tarball file after extraction', async () => {
104
- let capturedTmpFile;
105
- tarExtractBehavior = ({ file }) => { capturedTmpFile = file; };
106
- global.fetch = vi.fn().mockResolvedValue({ ok: true, arrayBuffer: async () => new ArrayBuffer(8) });
107
-
108
- await downloadAndExtractArtifact(config, 'dep-cleanup', destDir);
109
-
110
- expect(capturedTmpFile).toBeTruthy();
111
- expect(fs.existsSync(capturedTmpFile)).toBe(false);
112
- });
113
- });
@@ -1,168 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach, afterAll } from 'vitest';
2
- import fs from 'fs';
3
- import os from 'os';
4
- import path from 'path';
5
-
6
- const _fakeConfigDir = fs.mkdtempSync(path.join(os.tmpdir(), 'badgr-artifacts-configdir-'));
7
-
8
- vi.mock('../src/config.js', async (importOriginal) => {
9
- const actual = await importOriginal();
10
- return { ...actual, CONFIG_DIR: _fakeConfigDir };
11
- });
12
-
13
- const { parseArtifactsArgs, listFilesRecursive, artifactsCommand } = await import('../src/commands/artifacts.js');
14
-
15
- afterAll(() => {
16
- fs.rmSync(_fakeConfigDir, { recursive: true, force: true });
17
- });
18
-
19
- describe('parseArtifactsArgs', () => {
20
- it('parses the deployment id and --output', () => {
21
- expect(parseArtifactsArgs(['dep-123'])).toEqual({ deploymentId: 'dep-123', flags: {} });
22
- expect(parseArtifactsArgs(['dep-123', '--output', './out'])).toEqual({
23
- deploymentId: 'dep-123',
24
- flags: { output: './out' },
25
- });
26
- });
27
-
28
- it('returns an undefined deploymentId when no positional arg is given', () => {
29
- expect(parseArtifactsArgs(['--output', './out']).deploymentId).toBeUndefined();
30
- });
31
-
32
- it('parses the deployment id without any flags', () => {
33
- expect(parseArtifactsArgs(['dep-xyz'])).toEqual({ deploymentId: 'dep-xyz', flags: {} });
34
- });
35
- });
36
-
37
- describe('listFilesRecursive', () => {
38
- let dir;
39
-
40
- beforeEach(() => {
41
- dir = fs.mkdtempSync(path.join(os.tmpdir(), 'badgr-artifacts-test-'));
42
- });
43
-
44
- afterEach(() => {
45
- fs.rmSync(dir, { recursive: true, force: true });
46
- });
47
-
48
- it('lists nested files with relative paths', () => {
49
- fs.mkdirSync(path.join(dir, 'playwright-report'), { recursive: true });
50
- fs.writeFileSync(path.join(dir, 'playwright-report', 'index.html'), '<html></html>');
51
- fs.writeFileSync(path.join(dir, 'summary.json'), '{}');
52
-
53
- const files = listFilesRecursive(dir);
54
- expect(files).toEqual(['playwright-report/index.html', 'summary.json']);
55
- });
56
-
57
- it('returns an empty array for an empty directory', () => {
58
- expect(listFilesRecursive(dir)).toEqual([]);
59
- });
60
-
61
- it('lists deeply nested files and sorts the result', () => {
62
- fs.mkdirSync(path.join(dir, 'a', 'b', 'c'), { recursive: true });
63
- fs.writeFileSync(path.join(dir, 'a', 'b', 'c', 'deep.txt'), 'x');
64
- fs.writeFileSync(path.join(dir, 'zzz.txt'), 'x');
65
- fs.writeFileSync(path.join(dir, 'aaa.txt'), 'x');
66
-
67
- expect(listFilesRecursive(dir)).toEqual(['a/b/c/deep.txt', 'aaa.txt', 'zzz.txt']);
68
- });
69
- });
70
-
71
- let tarExtractBehavior = () => {};
72
-
73
- vi.mock('tar', () => {
74
- const x = vi.fn(async opts => tarExtractBehavior(opts));
75
- return { default: { x }, x };
76
- });
77
-
78
- function extractTestResults({ cwd }) {
79
- fs.mkdirSync(path.join(cwd, 'test-results'), { recursive: true });
80
- fs.writeFileSync(path.join(cwd, 'test-results', 'trace.zip'), 'fake-zip');
81
- }
82
-
83
- function extractNothing() {
84
- // empty artifact — no files written
85
- }
86
-
87
- describe('artifactsCommand', () => {
88
- const chalk = { red: s => s, dim: s => s, green: s => s };
89
- const config = { apiKey: 'test-key', baseUrl: 'https://example.test/v1' };
90
- let destDir;
91
-
92
- beforeEach(() => {
93
- process.exitCode = undefined;
94
- tarExtractBehavior = extractTestResults;
95
- vi.spyOn(console, 'log').mockImplementation(() => {});
96
- vi.spyOn(console, 'error').mockImplementation(() => {});
97
- destDir = fs.mkdtempSync(path.join(os.tmpdir(), 'badgr-artifacts-dest-'));
98
- });
99
-
100
- afterEach(() => {
101
- vi.restoreAllMocks();
102
- process.exitCode = undefined;
103
- delete global.fetch;
104
- fs.rmSync(destDir, { recursive: true, force: true });
105
- });
106
-
107
- it('requires a deployment id', async () => {
108
- await artifactsCommand(config, [], chalk);
109
- expect(process.exitCode).toBe(1);
110
- const logged = console.error.mock.calls.map(c => c.join(' ')).join('\n');
111
- expect(logged).toContain('Usage');
112
- });
113
-
114
- it('reports a clear error on 404 (no artifact uploaded)', async () => {
115
- global.fetch = vi.fn().mockResolvedValue({ ok: false, status: 404, statusText: 'Not Found', text: async () => '' });
116
- await artifactsCommand(config, ['dep-missing'], chalk);
117
- expect(process.exitCode).toBe(1);
118
- const logged = console.error.mock.calls.map(c => c.join(' ')).join('\n');
119
- expect(logged).toContain('No artifact found for dep-missing');
120
- });
121
-
122
- it('reports a generic error on a 500', async () => {
123
- global.fetch = vi.fn().mockResolvedValue({ ok: false, status: 500, statusText: 'Internal Server Error', text: async () => 'boom' });
124
- await artifactsCommand(config, ['dep-broken'], chalk);
125
- expect(process.exitCode).toBe(1);
126
- const logged = console.error.mock.calls.map(c => c.join(' ')).join('\n');
127
- expect(logged).toContain('500');
128
- expect(logged).toContain('boom');
129
- });
130
-
131
- it('reports a clear error when the network request itself fails', async () => {
132
- global.fetch = vi.fn().mockRejectedValue(new Error('getaddrinfo ENOTFOUND'));
133
- await artifactsCommand(config, ['dep-offline'], chalk);
134
- expect(process.exitCode).toBe(1);
135
- const logged = console.error.mock.calls.map(c => c.join(' ')).join('\n');
136
- expect(logged).toContain('Could not reach Badgr');
137
- });
138
-
139
- it('downloads and extracts the artifact tarball, listing files', async () => {
140
- global.fetch = vi.fn().mockResolvedValue({ ok: true, arrayBuffer: async () => new ArrayBuffer(8) });
141
- await artifactsCommand(config, ['dep-abc', '--output', destDir], chalk);
142
-
143
- expect(process.exitCode).toBeUndefined();
144
- expect(fs.existsSync(path.join(destDir, 'test-results', 'trace.zip'))).toBe(true);
145
- const logged = console.log.mock.calls.map(c => c.join(' ')).join('\n');
146
- expect(logged).toContain('test-results/trace.zip');
147
- expect(logged).toContain(destDir);
148
- });
149
-
150
- it('reports an empty artifact without erroring', async () => {
151
- tarExtractBehavior = extractNothing;
152
- global.fetch = vi.fn().mockResolvedValue({ ok: true, arrayBuffer: async () => new ArrayBuffer(8) });
153
- await artifactsCommand(config, ['dep-empty', '--output', destDir], chalk);
154
-
155
- expect(process.exitCode).toBeUndefined();
156
- const logged = console.log.mock.calls.map(c => c.join(' ')).join('\n');
157
- expect(logged).toContain('artifact was empty');
158
- });
159
-
160
- it('defaults to <CONFIG_DIR>/artifacts/<id> when --output is not given', async () => {
161
- global.fetch = vi.fn().mockResolvedValue({ ok: true, arrayBuffer: async () => new ArrayBuffer(8) });
162
- await artifactsCommand(config, ['dep-default-dir'], chalk);
163
-
164
- expect(process.exitCode).toBeUndefined();
165
- const logged = console.log.mock.calls.map(c => c.join(' ')).join('\n');
166
- expect(logged).toContain(path.join(_fakeConfigDir, 'artifacts', 'dep-default-dir'));
167
- });
168
- });