@indigoai-us/hq-cli 5.1.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/dist/__tests__/credentials.test.d.ts +5 -0
- package/dist/__tests__/credentials.test.d.ts.map +1 -0
- package/dist/__tests__/credentials.test.js +169 -0
- package/dist/__tests__/credentials.test.js.map +1 -0
- package/dist/commands/add.d.ts +6 -0
- package/dist/commands/add.d.ts.map +1 -0
- package/dist/commands/add.js +60 -0
- package/dist/commands/add.js.map +1 -0
- package/dist/commands/auth.d.ts +17 -0
- package/dist/commands/auth.d.ts.map +1 -0
- package/dist/commands/auth.js +269 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/cloud-setup.d.ts +19 -0
- package/dist/commands/cloud-setup.d.ts.map +1 -0
- package/dist/commands/cloud-setup.js +206 -0
- package/dist/commands/cloud-setup.js.map +1 -0
- package/dist/commands/cloud.d.ts +16 -0
- package/dist/commands/cloud.d.ts.map +1 -0
- package/dist/commands/cloud.js +263 -0
- package/dist/commands/cloud.js.map +1 -0
- package/dist/commands/initial-upload.d.ts +67 -0
- package/dist/commands/initial-upload.d.ts.map +1 -0
- package/dist/commands/initial-upload.js +205 -0
- package/dist/commands/initial-upload.js.map +1 -0
- package/dist/commands/list.d.ts +6 -0
- package/dist/commands/list.d.ts.map +1 -0
- package/dist/commands/list.js +55 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/sync.d.ts +6 -0
- package/dist/commands/sync.d.ts.map +1 -0
- package/dist/commands/sync.js +104 -0
- package/dist/commands/sync.js.map +1 -0
- package/dist/commands/update.d.ts +7 -0
- package/dist/commands/update.d.ts.map +1 -0
- package/dist/commands/update.js +60 -0
- package/dist/commands/update.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -0
- package/dist/strategies/link.d.ts +7 -0
- package/dist/strategies/link.d.ts.map +1 -0
- package/dist/strategies/link.js +51 -0
- package/dist/strategies/link.js.map +1 -0
- package/dist/strategies/merge.d.ts +7 -0
- package/dist/strategies/merge.d.ts.map +1 -0
- package/dist/strategies/merge.js +110 -0
- package/dist/strategies/merge.js.map +1 -0
- package/dist/sync-worker.d.ts +11 -0
- package/dist/sync-worker.d.ts.map +1 -0
- package/dist/sync-worker.js +77 -0
- package/dist/sync-worker.js.map +1 -0
- package/dist/types.d.ts +41 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/api-client.d.ts +26 -0
- package/dist/utils/api-client.d.ts.map +1 -0
- package/dist/utils/api-client.js +87 -0
- package/dist/utils/api-client.js.map +1 -0
- package/dist/utils/credentials.d.ts +44 -0
- package/dist/utils/credentials.d.ts.map +1 -0
- package/dist/utils/credentials.js +101 -0
- package/dist/utils/credentials.js.map +1 -0
- package/dist/utils/git.d.ts +13 -0
- package/dist/utils/git.d.ts.map +1 -0
- package/dist/utils/git.js +70 -0
- package/dist/utils/git.js.map +1 -0
- package/dist/utils/manifest.d.ts +16 -0
- package/dist/utils/manifest.d.ts.map +1 -0
- package/dist/utils/manifest.js +95 -0
- package/dist/utils/manifest.js.map +1 -0
- package/dist/utils/sync.d.ts +125 -0
- package/dist/utils/sync.d.ts.map +1 -0
- package/dist/utils/sync.js +291 -0
- package/dist/utils/sync.js.map +1 -0
- package/package.json +36 -0
- package/src/__tests__/cloud-setup.test.ts +117 -0
- package/src/__tests__/credentials.test.ts +203 -0
- package/src/__tests__/initial-upload.test.ts +414 -0
- package/src/__tests__/sync.test.ts +627 -0
- package/src/commands/add.ts +74 -0
- package/src/commands/auth.ts +303 -0
- package/src/commands/cloud-setup.ts +251 -0
- package/src/commands/cloud.ts +300 -0
- package/src/commands/initial-upload.ts +263 -0
- package/src/commands/list.ts +66 -0
- package/src/commands/sync.ts +149 -0
- package/src/commands/update.ts +71 -0
- package/src/hq-cloud.d.ts +19 -0
- package/src/index.ts +46 -0
- package/src/strategies/link.ts +62 -0
- package/src/strategies/merge.ts +142 -0
- package/src/sync-worker.ts +82 -0
- package/src/types.ts +47 -0
- package/src/utils/api-client.ts +111 -0
- package/src/utils/credentials.ts +124 -0
- package/src/utils/git.ts +74 -0
- package/src/utils/manifest.ts +111 -0
- package/src/utils/sync.ts +381 -0
- package/tsconfig.json +9 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for initial-upload command (commands/initial-upload.ts)
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - runInitialUpload core logic with mocked API
|
|
6
|
+
* - Empty HQ directory handling
|
|
7
|
+
* - Progress tracking
|
|
8
|
+
* - Merge vs replace conflict handling
|
|
9
|
+
* - Error collection during upload
|
|
10
|
+
* - Sync state update after upload
|
|
11
|
+
* - Command registration in cloud-setup
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
15
|
+
import * as fs from 'fs';
|
|
16
|
+
import * as path from 'path';
|
|
17
|
+
import * as os from 'os';
|
|
18
|
+
|
|
19
|
+
// Mock the api-client before importing modules that use it
|
|
20
|
+
vi.mock('../utils/api-client.js', () => ({
|
|
21
|
+
apiRequest: vi.fn(),
|
|
22
|
+
getApiUrl: vi.fn(() => 'https://api.test.local'),
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
// Mock credentials for cloud-setup registration tests
|
|
26
|
+
vi.mock('../utils/credentials.js', () => ({
|
|
27
|
+
readCredentials: vi.fn(() => ({
|
|
28
|
+
token: 'test-token',
|
|
29
|
+
userId: 'user_test',
|
|
30
|
+
email: 'test@example.com',
|
|
31
|
+
storedAt: new Date().toISOString(),
|
|
32
|
+
})),
|
|
33
|
+
isExpired: vi.fn(() => false),
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
import { apiRequest } from '../utils/api-client.js';
|
|
37
|
+
import {
|
|
38
|
+
runInitialUpload,
|
|
39
|
+
writeProgress,
|
|
40
|
+
type InitialUploadResult,
|
|
41
|
+
} from '../commands/initial-upload.js';
|
|
42
|
+
import { readSyncState } from '../utils/sync.js';
|
|
43
|
+
import { Command } from 'commander';
|
|
44
|
+
import { registerCloudSetupCommand } from '../commands/cloud-setup.js';
|
|
45
|
+
|
|
46
|
+
const mockApiRequest = vi.mocked(apiRequest);
|
|
47
|
+
|
|
48
|
+
// ── Test helpers ─────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
let tmpDir: string;
|
|
51
|
+
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hq-upload-test-'));
|
|
54
|
+
vi.clearAllMocks();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
afterEach(() => {
|
|
58
|
+
try {
|
|
59
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
60
|
+
} catch {
|
|
61
|
+
// Ignore cleanup errors on Windows
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
/** Create a file in tmpDir with given relative path and content. */
|
|
66
|
+
function createFile(relativePath: string, content: string): string {
|
|
67
|
+
const absPath = path.join(tmpDir, relativePath);
|
|
68
|
+
const dir = path.dirname(absPath);
|
|
69
|
+
if (!fs.existsSync(dir)) {
|
|
70
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
71
|
+
}
|
|
72
|
+
fs.writeFileSync(absPath, content);
|
|
73
|
+
return absPath;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Set up mocks for a typical upload flow:
|
|
78
|
+
* 1. GET /api/files/list -> empty
|
|
79
|
+
* 2. POST /api/files/upload -> success (for each file)
|
|
80
|
+
*/
|
|
81
|
+
function mockEmptyRemoteAndSuccessfulUploads(fileCount: number): void {
|
|
82
|
+
// 1. list returns empty
|
|
83
|
+
mockApiRequest.mockResolvedValueOnce({
|
|
84
|
+
ok: true,
|
|
85
|
+
status: 200,
|
|
86
|
+
data: { files: [] },
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// 2. each upload succeeds
|
|
90
|
+
for (let i = 0; i < fileCount; i++) {
|
|
91
|
+
mockApiRequest.mockResolvedValueOnce({ ok: true, status: 200 });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── runInitialUpload ─────────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
describe('runInitialUpload', () => {
|
|
98
|
+
it('uploads all local files when remote is empty', async () => {
|
|
99
|
+
createFile('README.md', '# HQ');
|
|
100
|
+
createFile('workers/dev/worker.yaml', 'name: dev');
|
|
101
|
+
createFile('knowledge/index.md', '# Knowledge');
|
|
102
|
+
|
|
103
|
+
// list returns empty, 3 uploads succeed
|
|
104
|
+
mockEmptyRemoteAndSuccessfulUploads(3);
|
|
105
|
+
|
|
106
|
+
const result = await runInitialUpload(tmpDir, { quiet: true });
|
|
107
|
+
|
|
108
|
+
expect(result.totalFiles).toBe(3);
|
|
109
|
+
expect(result.uploaded).toBe(3);
|
|
110
|
+
expect(result.failed).toBe(0);
|
|
111
|
+
expect(result.errors).toEqual([]);
|
|
112
|
+
expect(result.skipped).toBe(false);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('returns zero counts for empty HQ directory', async () => {
|
|
116
|
+
// list returns empty
|
|
117
|
+
mockApiRequest.mockResolvedValueOnce({
|
|
118
|
+
ok: true,
|
|
119
|
+
status: 200,
|
|
120
|
+
data: { files: [] },
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const result = await runInitialUpload(tmpDir, { quiet: true });
|
|
124
|
+
|
|
125
|
+
expect(result.totalFiles).toBe(0);
|
|
126
|
+
expect(result.uploaded).toBe(0);
|
|
127
|
+
expect(result.failed).toBe(0);
|
|
128
|
+
expect(result.skipped).toBe(false);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('respects ignore rules — skips .git, node_modules, .claude', async () => {
|
|
132
|
+
createFile('.git/config', 'gitconfig');
|
|
133
|
+
createFile('node_modules/dep/index.js', 'code');
|
|
134
|
+
createFile('.claude/config.json', '{}');
|
|
135
|
+
createFile('src/index.ts', 'code'); // only this should be uploaded
|
|
136
|
+
|
|
137
|
+
// list returns empty, 1 upload for src/index.ts
|
|
138
|
+
mockEmptyRemoteAndSuccessfulUploads(1);
|
|
139
|
+
|
|
140
|
+
const result = await runInitialUpload(tmpDir, { quiet: true });
|
|
141
|
+
|
|
142
|
+
expect(result.totalFiles).toBe(1);
|
|
143
|
+
expect(result.uploaded).toBe(1);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('respects ignore rules — skips .env and .log files', async () => {
|
|
147
|
+
createFile('.env', 'SECRET=abc');
|
|
148
|
+
createFile('.env.local', 'LOCAL=abc');
|
|
149
|
+
createFile('debug.log', 'log data');
|
|
150
|
+
createFile('agents.md', 'agent config'); // only this should be uploaded
|
|
151
|
+
|
|
152
|
+
mockEmptyRemoteAndSuccessfulUploads(1);
|
|
153
|
+
|
|
154
|
+
const result = await runInitialUpload(tmpDir, { quiet: true });
|
|
155
|
+
|
|
156
|
+
expect(result.totalFiles).toBe(1);
|
|
157
|
+
expect(result.uploaded).toBe(1);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('collects upload errors without stopping', async () => {
|
|
161
|
+
createFile('good.txt', 'good');
|
|
162
|
+
createFile('bad.txt', 'bad');
|
|
163
|
+
|
|
164
|
+
// list returns empty
|
|
165
|
+
mockApiRequest.mockResolvedValueOnce({
|
|
166
|
+
ok: true,
|
|
167
|
+
status: 200,
|
|
168
|
+
data: { files: [] },
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// First upload succeeds
|
|
172
|
+
mockApiRequest.mockResolvedValueOnce({ ok: true, status: 200 });
|
|
173
|
+
|
|
174
|
+
// Second upload fails
|
|
175
|
+
mockApiRequest.mockResolvedValueOnce({
|
|
176
|
+
ok: false,
|
|
177
|
+
status: 413,
|
|
178
|
+
error: 'File too large',
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const result = await runInitialUpload(tmpDir, { quiet: true });
|
|
182
|
+
|
|
183
|
+
expect(result.totalFiles).toBe(2);
|
|
184
|
+
expect(result.uploaded).toBe(1);
|
|
185
|
+
expect(result.failed).toBe(1);
|
|
186
|
+
expect(result.errors.length).toBe(1);
|
|
187
|
+
expect(result.errors[0]).toContain('Upload failed');
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('skips upload when onConflict is "skip" and remote has files', async () => {
|
|
191
|
+
createFile('local.txt', 'local content');
|
|
192
|
+
|
|
193
|
+
// list returns files
|
|
194
|
+
mockApiRequest.mockResolvedValueOnce({
|
|
195
|
+
ok: true,
|
|
196
|
+
status: 200,
|
|
197
|
+
data: { files: ['existing.txt'] },
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const result = await runInitialUpload(tmpDir, {
|
|
201
|
+
quiet: true,
|
|
202
|
+
onConflict: 'skip',
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
expect(result.skipped).toBe(true);
|
|
206
|
+
expect(result.uploaded).toBe(0);
|
|
207
|
+
// No upload calls should have been made
|
|
208
|
+
expect(mockApiRequest).toHaveBeenCalledTimes(1); // only the list call
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('merges when onConflict is "merge" and remote has files', async () => {
|
|
212
|
+
createFile('local.txt', 'local content');
|
|
213
|
+
|
|
214
|
+
// list returns existing files
|
|
215
|
+
mockApiRequest.mockResolvedValueOnce({
|
|
216
|
+
ok: true,
|
|
217
|
+
status: 200,
|
|
218
|
+
data: { files: ['existing.txt'] },
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// upload succeeds
|
|
222
|
+
mockApiRequest.mockResolvedValueOnce({ ok: true, status: 200 });
|
|
223
|
+
|
|
224
|
+
const result = await runInitialUpload(tmpDir, {
|
|
225
|
+
quiet: true,
|
|
226
|
+
onConflict: 'merge',
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
expect(result.skipped).toBe(false);
|
|
230
|
+
expect(result.uploaded).toBe(1);
|
|
231
|
+
// Should have: 1 list + 1 upload = 2 calls (no delete)
|
|
232
|
+
expect(mockApiRequest).toHaveBeenCalledTimes(2);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('deletes remote files when onConflict is "replace"', async () => {
|
|
236
|
+
createFile('local.txt', 'local content');
|
|
237
|
+
|
|
238
|
+
// list returns existing files
|
|
239
|
+
mockApiRequest.mockResolvedValueOnce({
|
|
240
|
+
ok: true,
|
|
241
|
+
status: 200,
|
|
242
|
+
data: { files: ['existing.txt', 'old.txt'] },
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// DELETE /api/files/all succeeds
|
|
246
|
+
mockApiRequest.mockResolvedValueOnce({ ok: true, status: 200 });
|
|
247
|
+
|
|
248
|
+
// upload succeeds
|
|
249
|
+
mockApiRequest.mockResolvedValueOnce({ ok: true, status: 200 });
|
|
250
|
+
|
|
251
|
+
const result = await runInitialUpload(tmpDir, {
|
|
252
|
+
quiet: true,
|
|
253
|
+
onConflict: 'replace',
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
expect(result.skipped).toBe(false);
|
|
257
|
+
expect(result.uploaded).toBe(1);
|
|
258
|
+
|
|
259
|
+
// Verify DELETE was called
|
|
260
|
+
expect(mockApiRequest).toHaveBeenCalledWith('DELETE', '/api/files/all');
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('proceeds when remote list API fails (treats as empty)', async () => {
|
|
264
|
+
createFile('file.txt', 'content');
|
|
265
|
+
|
|
266
|
+
// list fails (e.g., endpoint not deployed yet)
|
|
267
|
+
mockApiRequest.mockRejectedValueOnce(new Error('Network error'));
|
|
268
|
+
|
|
269
|
+
// upload succeeds
|
|
270
|
+
mockApiRequest.mockResolvedValueOnce({ ok: true, status: 200 });
|
|
271
|
+
|
|
272
|
+
const result = await runInitialUpload(tmpDir, { quiet: true });
|
|
273
|
+
|
|
274
|
+
expect(result.uploaded).toBe(1);
|
|
275
|
+
expect(result.skipped).toBe(false);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('updates sync state after successful upload', async () => {
|
|
279
|
+
createFile('file.txt', 'content');
|
|
280
|
+
|
|
281
|
+
mockEmptyRemoteAndSuccessfulUploads(1);
|
|
282
|
+
|
|
283
|
+
await runInitialUpload(tmpDir, { quiet: true });
|
|
284
|
+
|
|
285
|
+
const state = readSyncState(tmpDir);
|
|
286
|
+
expect(state.lastSync).toBeTruthy();
|
|
287
|
+
expect(state.fileCount).toBe(1);
|
|
288
|
+
expect(state.errors).toEqual([]);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('updates sync state with errors after partial upload', async () => {
|
|
292
|
+
createFile('ok.txt', 'ok');
|
|
293
|
+
createFile('fail.txt', 'fail');
|
|
294
|
+
|
|
295
|
+
// list returns empty
|
|
296
|
+
mockApiRequest.mockResolvedValueOnce({
|
|
297
|
+
ok: true,
|
|
298
|
+
status: 200,
|
|
299
|
+
data: { files: [] },
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// First upload succeeds
|
|
303
|
+
mockApiRequest.mockResolvedValueOnce({ ok: true, status: 200 });
|
|
304
|
+
|
|
305
|
+
// Second upload fails
|
|
306
|
+
mockApiRequest.mockResolvedValueOnce({
|
|
307
|
+
ok: false,
|
|
308
|
+
status: 500,
|
|
309
|
+
error: 'Server error',
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
const result = await runInitialUpload(tmpDir, { quiet: true });
|
|
313
|
+
|
|
314
|
+
const state = readSyncState(tmpDir);
|
|
315
|
+
expect(state.lastSync).toBeTruthy();
|
|
316
|
+
expect(state.errors.length).toBe(1);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('does not call upload when no local files exist', async () => {
|
|
320
|
+
// list returns empty
|
|
321
|
+
mockApiRequest.mockResolvedValueOnce({
|
|
322
|
+
ok: true,
|
|
323
|
+
status: 200,
|
|
324
|
+
data: { files: [] },
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
const result = await runInitialUpload(tmpDir, { quiet: true });
|
|
328
|
+
|
|
329
|
+
expect(result.totalFiles).toBe(0);
|
|
330
|
+
// Only the list call
|
|
331
|
+
expect(mockApiRequest).toHaveBeenCalledTimes(1);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('does not prompt when onConflict is specified and remote is empty', async () => {
|
|
335
|
+
createFile('file.txt', 'content');
|
|
336
|
+
|
|
337
|
+
mockEmptyRemoteAndSuccessfulUploads(1);
|
|
338
|
+
|
|
339
|
+
// Even with onConflict set, should work fine when remote is empty
|
|
340
|
+
const result = await runInitialUpload(tmpDir, {
|
|
341
|
+
quiet: true,
|
|
342
|
+
onConflict: 'merge',
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
expect(result.uploaded).toBe(1);
|
|
346
|
+
expect(result.skipped).toBe(false);
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// ── writeProgress ────────────────────────────────────────────────────────────
|
|
351
|
+
|
|
352
|
+
describe('writeProgress', () => {
|
|
353
|
+
it('does not throw for valid inputs', () => {
|
|
354
|
+
// writeProgress writes to stdout; just ensure no errors
|
|
355
|
+
expect(() => writeProgress(0, 100)).not.toThrow();
|
|
356
|
+
expect(() => writeProgress(50, 100)).not.toThrow();
|
|
357
|
+
expect(() => writeProgress(100, 100)).not.toThrow();
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('handles zero total without error', () => {
|
|
361
|
+
expect(() => writeProgress(0, 0)).not.toThrow();
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
// ── Command registration ─────────────────────────────────────────────────────
|
|
366
|
+
|
|
367
|
+
describe('cloud upload command registration', () => {
|
|
368
|
+
it('registers "upload" subcommand under "cloud"', () => {
|
|
369
|
+
const program = new Command();
|
|
370
|
+
registerCloudSetupCommand(program);
|
|
371
|
+
|
|
372
|
+
const cloudCmd = program.commands.find((c) => c.name() === 'cloud');
|
|
373
|
+
expect(cloudCmd).toBeDefined();
|
|
374
|
+
|
|
375
|
+
const uploadCmd = cloudCmd!.commands.find((c) => c.name() === 'upload');
|
|
376
|
+
expect(uploadCmd).toBeDefined();
|
|
377
|
+
expect(uploadCmd!.description()).toContain('Upload');
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('"upload" subcommand accepts --hq-root option', () => {
|
|
381
|
+
const program = new Command();
|
|
382
|
+
registerCloudSetupCommand(program);
|
|
383
|
+
|
|
384
|
+
const cloudCmd = program.commands.find((c) => c.name() === 'cloud');
|
|
385
|
+
const uploadCmd = cloudCmd!.commands.find((c) => c.name() === 'upload');
|
|
386
|
+
expect(uploadCmd).toBeDefined();
|
|
387
|
+
|
|
388
|
+
// Check that the option is registered
|
|
389
|
+
const options = uploadCmd!.options.map((o) => o.long);
|
|
390
|
+
expect(options).toContain('--hq-root');
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it('"upload" subcommand accepts --on-conflict option', () => {
|
|
394
|
+
const program = new Command();
|
|
395
|
+
registerCloudSetupCommand(program);
|
|
396
|
+
|
|
397
|
+
const cloudCmd = program.commands.find((c) => c.name() === 'cloud');
|
|
398
|
+
const uploadCmd = cloudCmd!.commands.find((c) => c.name() === 'upload');
|
|
399
|
+
|
|
400
|
+
const options = uploadCmd!.options.map((o) => o.long);
|
|
401
|
+
expect(options).toContain('--on-conflict');
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it('cloud command group still has setup-token and status alongside upload', () => {
|
|
405
|
+
const program = new Command();
|
|
406
|
+
registerCloudSetupCommand(program);
|
|
407
|
+
|
|
408
|
+
const cloudCmd = program.commands.find((c) => c.name() === 'cloud');
|
|
409
|
+
const subcommandNames = cloudCmd!.commands.map((c) => c.name());
|
|
410
|
+
expect(subcommandNames).toContain('setup-token');
|
|
411
|
+
expect(subcommandNames).toContain('status');
|
|
412
|
+
expect(subcommandNames).toContain('upload');
|
|
413
|
+
});
|
|
414
|
+
});
|