@onebrain-ai/cli 2.0.1 → 2.0.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/dist/onebrain +3 -3
- package/package.json +23 -1
- package/src/commands/doctor.test.ts +0 -416
- package/src/commands/doctor.ts +0 -203
- package/src/commands/init.test.ts +0 -318
- package/src/commands/init.ts +0 -477
- package/src/commands/update.test.ts +0 -413
- package/src/commands/update.ts +0 -353
- package/src/index.ts +0 -144
- package/src/internal/__snapshots__/checkpoint.test.ts.snap +0 -12
- package/src/internal/__snapshots__/orphan-scan.test.ts.snap +0 -13
- package/src/internal/__snapshots__/session-init.test.ts.snap +0 -15
- package/src/internal/checkpoint.test.ts +0 -741
- package/src/internal/checkpoint.ts +0 -427
- package/src/internal/migrate.test.ts +0 -301
- package/src/internal/migrate.ts +0 -186
- package/src/internal/orphan-scan.test.ts +0 -271
- package/src/internal/orphan-scan.ts +0 -213
- package/src/internal/qmd-reindex.test.ts +0 -117
- package/src/internal/qmd-reindex.ts +0 -44
- package/src/internal/register-hooks.test.ts +0 -343
- package/src/internal/register-hooks.ts +0 -418
- package/src/internal/session-init.test.ts +0 -318
- package/src/internal/session-init.ts +0 -264
- package/src/internal/vault-sync.test.ts +0 -419
- package/src/internal/vault-sync.ts +0 -764
- package/tests/integration/init.integration.test.ts +0 -304
- package/tests/integration/update.integration.test.ts +0 -306
- package/tsconfig.json +0 -12
|
@@ -1,413 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unit tests for `onebrain update`
|
|
3
|
-
*
|
|
4
|
-
* Uses injectable dependencies (mock fetch, vault-sync, binary install/validate,
|
|
5
|
-
* register-hooks) so tests run offline and fast.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
|
9
|
-
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
10
|
-
import { tmpdir } from 'node:os';
|
|
11
|
-
import { join } from 'node:path';
|
|
12
|
-
|
|
13
|
-
import { type UpdateOptions, runUpdate } from './update.js';
|
|
14
|
-
|
|
15
|
-
// ---------------------------------------------------------------------------
|
|
16
|
-
// Helpers
|
|
17
|
-
// ---------------------------------------------------------------------------
|
|
18
|
-
|
|
19
|
-
async function makeTempVault(): Promise<string> {
|
|
20
|
-
const dir = join(
|
|
21
|
-
tmpdir(),
|
|
22
|
-
`onebrain-update-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
23
|
-
);
|
|
24
|
-
await mkdir(dir, { recursive: true });
|
|
25
|
-
return dir;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
async function writeVaultYml(vaultDir: string, content: Record<string, unknown>): Promise<void> {
|
|
29
|
-
const { stringify } = await import('yaml');
|
|
30
|
-
await writeFile(join(vaultDir, 'vault.yml'), stringify(content), 'utf8');
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async function readVaultYml(vaultDir: string): Promise<Record<string, unknown>> {
|
|
34
|
-
const { parse } = await import('yaml');
|
|
35
|
-
const text = await readFile(join(vaultDir, 'vault.yml'), 'utf8');
|
|
36
|
-
return (parse(text) ?? {}) as Record<string, unknown>;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** Build a mock fetch that returns a fake GitHub releases/latest response. */
|
|
40
|
-
function makeMockFetch(tagName: string): typeof fetch {
|
|
41
|
-
return async (input: RequestInfo | URL, _init?: RequestInit): Promise<Response> => {
|
|
42
|
-
const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
|
|
43
|
-
if (url.includes('/releases/latest')) {
|
|
44
|
-
return new Response(JSON.stringify({ tag_name: tagName }), {
|
|
45
|
-
status: 200,
|
|
46
|
-
headers: { 'Content-Type': 'application/json' },
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
return new Response('Not Found', { status: 404 });
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** Noop mocks */
|
|
54
|
-
const noopVaultSync = async (
|
|
55
|
-
_vaultDir: string,
|
|
56
|
-
_opts: Record<string, unknown>,
|
|
57
|
-
): Promise<{ filesAdded: number; filesRemoved: number }> => ({
|
|
58
|
-
filesAdded: 47,
|
|
59
|
-
filesRemoved: 2,
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
const noopInstallBinary = async (_version: string): Promise<void> => {};
|
|
63
|
-
|
|
64
|
-
const noopValidateBinary = async (): Promise<boolean> => true;
|
|
65
|
-
|
|
66
|
-
const noopRegisterHooks = async (_vaultDir: string): Promise<void> => {};
|
|
67
|
-
|
|
68
|
-
let tempDir: string;
|
|
69
|
-
|
|
70
|
-
beforeEach(async () => {
|
|
71
|
-
tempDir = await makeTempVault();
|
|
72
|
-
await writeVaultYml(tempDir, {
|
|
73
|
-
method: 'onebrain',
|
|
74
|
-
update_channel: 'stable',
|
|
75
|
-
onebrain_version: 'v1.10.18',
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
afterEach(async () => {
|
|
80
|
-
await rm(tempDir, { recursive: true, force: true });
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
// ---------------------------------------------------------------------------
|
|
84
|
-
// Tests
|
|
85
|
-
// ---------------------------------------------------------------------------
|
|
86
|
-
|
|
87
|
-
describe('runUpdate', () => {
|
|
88
|
-
it('full update — all 6 steps complete and vault.yml updated with new version', async () => {
|
|
89
|
-
const calls: string[] = [];
|
|
90
|
-
|
|
91
|
-
const opts: UpdateOptions = {
|
|
92
|
-
vaultDir: tempDir,
|
|
93
|
-
isTTY: false,
|
|
94
|
-
fetchFn: makeMockFetch('v2.0.0'),
|
|
95
|
-
vaultSyncFn: async (vaultDir, syncOpts) => {
|
|
96
|
-
calls.push('vault-sync');
|
|
97
|
-
return noopVaultSync(vaultDir, syncOpts);
|
|
98
|
-
},
|
|
99
|
-
installBinaryFn: async (version) => {
|
|
100
|
-
calls.push(`install:${version}`);
|
|
101
|
-
return noopInstallBinary(version);
|
|
102
|
-
},
|
|
103
|
-
validateBinaryFn: async () => {
|
|
104
|
-
calls.push('validate');
|
|
105
|
-
return true;
|
|
106
|
-
},
|
|
107
|
-
registerHooksFn: async (vaultDir) => {
|
|
108
|
-
calls.push('register-hooks');
|
|
109
|
-
return noopRegisterHooks(vaultDir);
|
|
110
|
-
},
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
const result = await runUpdate(opts);
|
|
114
|
-
|
|
115
|
-
expect(result.ok).toBe(true);
|
|
116
|
-
expect(result.exitCode).toBe(0);
|
|
117
|
-
expect(result.latestVersion).toBe('v2.0.0');
|
|
118
|
-
|
|
119
|
-
// All steps called in exact order
|
|
120
|
-
expect(calls).toEqual(['vault-sync', 'install:v2.0.0', 'validate', 'register-hooks']);
|
|
121
|
-
|
|
122
|
-
// vault.yml updated with new version
|
|
123
|
-
const vaultYml = await readVaultYml(tempDir);
|
|
124
|
-
expect(vaultYml.onebrain_version).toBe('v2.0.0');
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it('--check flag — dry run exits 0, makes no changes', async () => {
|
|
128
|
-
const calls: string[] = [];
|
|
129
|
-
|
|
130
|
-
const opts: UpdateOptions = {
|
|
131
|
-
vaultDir: tempDir,
|
|
132
|
-
isTTY: false,
|
|
133
|
-
check: true,
|
|
134
|
-
fetchFn: makeMockFetch('v2.0.0'),
|
|
135
|
-
vaultSyncFn: async (vaultDir, syncOpts) => {
|
|
136
|
-
calls.push('vault-sync');
|
|
137
|
-
return noopVaultSync(vaultDir, syncOpts);
|
|
138
|
-
},
|
|
139
|
-
installBinaryFn: async (version) => {
|
|
140
|
-
calls.push(`install:${version}`);
|
|
141
|
-
},
|
|
142
|
-
validateBinaryFn: async () => {
|
|
143
|
-
calls.push('validate');
|
|
144
|
-
return true;
|
|
145
|
-
},
|
|
146
|
-
registerHooksFn: async (vaultDir) => {
|
|
147
|
-
calls.push('register-hooks');
|
|
148
|
-
return noopRegisterHooks(vaultDir);
|
|
149
|
-
},
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
const result = await runUpdate(opts);
|
|
153
|
-
|
|
154
|
-
expect(result.ok).toBe(true);
|
|
155
|
-
expect(result.exitCode).toBe(0);
|
|
156
|
-
expect(result.latestVersion).toBe('v2.0.0');
|
|
157
|
-
|
|
158
|
-
// No side-effecting steps called
|
|
159
|
-
expect(calls).not.toContain('vault-sync');
|
|
160
|
-
expect(calls).not.toContain('register-hooks');
|
|
161
|
-
|
|
162
|
-
// vault.yml not modified
|
|
163
|
-
const vaultYml = await readVaultYml(tempDir);
|
|
164
|
-
expect(vaultYml.onebrain_version).toBe('v1.10.18');
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it('atomic guarantee — validateBinaryFn fails → register-hooks NOT called, exit 1', async () => {
|
|
168
|
-
const calls: string[] = [];
|
|
169
|
-
|
|
170
|
-
const opts: UpdateOptions = {
|
|
171
|
-
vaultDir: tempDir,
|
|
172
|
-
isTTY: false,
|
|
173
|
-
fetchFn: makeMockFetch('v2.0.0'),
|
|
174
|
-
vaultSyncFn: async (vaultDir, syncOpts) => {
|
|
175
|
-
calls.push('vault-sync');
|
|
176
|
-
return noopVaultSync(vaultDir, syncOpts);
|
|
177
|
-
},
|
|
178
|
-
installBinaryFn: async (version) => {
|
|
179
|
-
calls.push(`install:${version}`);
|
|
180
|
-
},
|
|
181
|
-
validateBinaryFn: async () => {
|
|
182
|
-
calls.push('validate-fail');
|
|
183
|
-
return false; // binary validation fails
|
|
184
|
-
},
|
|
185
|
-
registerHooksFn: async (vaultDir) => {
|
|
186
|
-
calls.push('register-hooks');
|
|
187
|
-
return noopRegisterHooks(vaultDir);
|
|
188
|
-
},
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
const result = await runUpdate(opts);
|
|
192
|
-
|
|
193
|
-
expect(result.ok).toBe(false);
|
|
194
|
-
expect(result.exitCode).toBe(1);
|
|
195
|
-
expect(result.error).toMatch(/Binary validation failed/);
|
|
196
|
-
|
|
197
|
-
// validate was called
|
|
198
|
-
expect(calls).toContain('validate-fail');
|
|
199
|
-
|
|
200
|
-
// register-hooks was NOT called
|
|
201
|
-
expect(calls).not.toContain('register-hooks');
|
|
202
|
-
|
|
203
|
-
// vault.yml NOT updated
|
|
204
|
-
const vaultYml = await readVaultYml(tempDir);
|
|
205
|
-
expect(vaultYml.onebrain_version).toBe('v1.10.18');
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
it('GitHub fetch failure → exits 1 with error', async () => {
|
|
209
|
-
const failFetch: typeof fetch = async () =>
|
|
210
|
-
new Response('Service Unavailable', { status: 503 });
|
|
211
|
-
|
|
212
|
-
const opts: UpdateOptions = {
|
|
213
|
-
vaultDir: tempDir,
|
|
214
|
-
isTTY: false,
|
|
215
|
-
fetchFn: failFetch,
|
|
216
|
-
vaultSyncFn: noopVaultSync,
|
|
217
|
-
installBinaryFn: noopInstallBinary,
|
|
218
|
-
validateBinaryFn: noopValidateBinary,
|
|
219
|
-
registerHooksFn: noopRegisterHooks,
|
|
220
|
-
};
|
|
221
|
-
|
|
222
|
-
const result = await runUpdate(opts);
|
|
223
|
-
|
|
224
|
-
expect(result.ok).toBe(false);
|
|
225
|
-
expect(result.exitCode).toBe(1);
|
|
226
|
-
expect(result.error).toBeDefined();
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
it('--channel flag overrides vault.yml update_channel', async () => {
|
|
230
|
-
let syncOptsReceived: Record<string, unknown> = {};
|
|
231
|
-
|
|
232
|
-
const opts: UpdateOptions = {
|
|
233
|
-
vaultDir: tempDir,
|
|
234
|
-
isTTY: false,
|
|
235
|
-
channel: 'next',
|
|
236
|
-
fetchFn: makeMockFetch('v2.0.0'),
|
|
237
|
-
vaultSyncFn: async (vaultDir, syncOpts) => {
|
|
238
|
-
syncOptsReceived = syncOpts;
|
|
239
|
-
return noopVaultSync(vaultDir, syncOpts);
|
|
240
|
-
},
|
|
241
|
-
installBinaryFn: noopInstallBinary,
|
|
242
|
-
validateBinaryFn: noopValidateBinary,
|
|
243
|
-
registerHooksFn: noopRegisterHooks,
|
|
244
|
-
};
|
|
245
|
-
|
|
246
|
-
const result = await runUpdate(opts);
|
|
247
|
-
|
|
248
|
-
expect(result.ok).toBe(true);
|
|
249
|
-
expect(syncOptsReceived.branch).toBe('next');
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
it('non-TTY output format — includes key status lines', async () => {
|
|
253
|
-
const lines: string[] = [];
|
|
254
|
-
const originalWrite = process.stdout.write.bind(process.stdout);
|
|
255
|
-
process.stdout.write = (chunk: string | Uint8Array, ...args: unknown[]) => {
|
|
256
|
-
if (typeof chunk === 'string') lines.push(chunk);
|
|
257
|
-
return originalWrite(chunk, ...(args as Parameters<typeof originalWrite>).slice(1));
|
|
258
|
-
};
|
|
259
|
-
|
|
260
|
-
try {
|
|
261
|
-
const opts: UpdateOptions = {
|
|
262
|
-
vaultDir: tempDir,
|
|
263
|
-
isTTY: false,
|
|
264
|
-
fetchFn: makeMockFetch('v2.0.0'),
|
|
265
|
-
vaultSyncFn: noopVaultSync,
|
|
266
|
-
installBinaryFn: noopInstallBinary,
|
|
267
|
-
validateBinaryFn: noopValidateBinary,
|
|
268
|
-
registerHooksFn: noopRegisterHooks,
|
|
269
|
-
};
|
|
270
|
-
await runUpdate(opts);
|
|
271
|
-
} finally {
|
|
272
|
-
process.stdout.write = originalWrite;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const fullOutput = lines.join('');
|
|
276
|
-
expect(fullOutput).toMatch(/OneBrain Update/);
|
|
277
|
-
expect(fullOutput).toMatch(/v2\.0\.0 available/);
|
|
278
|
-
expect(fullOutput).toMatch(/done:/i);
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
it('vault.yml missing onebrain_version — shows "unknown" as current', async () => {
|
|
282
|
-
// Write vault.yml without onebrain_version
|
|
283
|
-
await writeVaultYml(tempDir, { method: 'onebrain', update_channel: 'stable' });
|
|
284
|
-
|
|
285
|
-
const opts: UpdateOptions = {
|
|
286
|
-
vaultDir: tempDir,
|
|
287
|
-
isTTY: false,
|
|
288
|
-
fetchFn: makeMockFetch('v2.0.0'),
|
|
289
|
-
vaultSyncFn: noopVaultSync,
|
|
290
|
-
installBinaryFn: noopInstallBinary,
|
|
291
|
-
validateBinaryFn: noopValidateBinary,
|
|
292
|
-
registerHooksFn: noopRegisterHooks,
|
|
293
|
-
};
|
|
294
|
-
|
|
295
|
-
const result = await runUpdate(opts);
|
|
296
|
-
|
|
297
|
-
expect(result.ok).toBe(true);
|
|
298
|
-
expect(result.currentVersion).toBe('unknown');
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
it('update_channel from vault.yml used when --channel not set', async () => {
|
|
302
|
-
await writeVaultYml(tempDir, {
|
|
303
|
-
method: 'onebrain',
|
|
304
|
-
update_channel: 'next',
|
|
305
|
-
onebrain_version: 'v1.10.18',
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
let syncBranchUsed = '';
|
|
309
|
-
|
|
310
|
-
const opts: UpdateOptions = {
|
|
311
|
-
vaultDir: tempDir,
|
|
312
|
-
isTTY: false,
|
|
313
|
-
fetchFn: makeMockFetch('v2.0.0'),
|
|
314
|
-
vaultSyncFn: async (vaultDir, syncOpts) => {
|
|
315
|
-
syncBranchUsed = syncOpts.branch as string;
|
|
316
|
-
return noopVaultSync(vaultDir, syncOpts);
|
|
317
|
-
},
|
|
318
|
-
installBinaryFn: noopInstallBinary,
|
|
319
|
-
validateBinaryFn: noopValidateBinary,
|
|
320
|
-
registerHooksFn: noopRegisterHooks,
|
|
321
|
-
};
|
|
322
|
-
|
|
323
|
-
const result = await runUpdate(opts);
|
|
324
|
-
|
|
325
|
-
expect(result.ok).toBe(true);
|
|
326
|
-
// update_channel 'next' → branch 'next'
|
|
327
|
-
expect(syncBranchUsed).toBe('next');
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
it('channel stable resolves to branch main passed to vault-sync', async () => {
|
|
331
|
-
let syncBranchUsed = '';
|
|
332
|
-
|
|
333
|
-
const opts: UpdateOptions = {
|
|
334
|
-
vaultDir: tempDir,
|
|
335
|
-
isTTY: false,
|
|
336
|
-
channel: 'stable',
|
|
337
|
-
fetchFn: makeMockFetch('v2.0.0'),
|
|
338
|
-
vaultSyncFn: async (vaultDir, syncOpts) => {
|
|
339
|
-
syncBranchUsed = syncOpts.branch as string;
|
|
340
|
-
return noopVaultSync(vaultDir, syncOpts);
|
|
341
|
-
},
|
|
342
|
-
installBinaryFn: noopInstallBinary,
|
|
343
|
-
validateBinaryFn: noopValidateBinary,
|
|
344
|
-
registerHooksFn: noopRegisterHooks,
|
|
345
|
-
};
|
|
346
|
-
|
|
347
|
-
const result = await runUpdate(opts);
|
|
348
|
-
|
|
349
|
-
expect(result.ok).toBe(true);
|
|
350
|
-
expect(syncBranchUsed).toBe('main');
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
it('register-hooks failure is non-fatal — vault.yml still updated', async () => {
|
|
354
|
-
const opts: UpdateOptions = {
|
|
355
|
-
vaultDir: tempDir,
|
|
356
|
-
isTTY: false,
|
|
357
|
-
fetchFn: makeMockFetch('v2.0.0'),
|
|
358
|
-
vaultSyncFn: noopVaultSync,
|
|
359
|
-
installBinaryFn: noopInstallBinary,
|
|
360
|
-
validateBinaryFn: noopValidateBinary,
|
|
361
|
-
registerHooksFn: async (_vaultDir) => {
|
|
362
|
-
throw new Error('hooks: permission denied');
|
|
363
|
-
},
|
|
364
|
-
};
|
|
365
|
-
|
|
366
|
-
const result = await runUpdate(opts);
|
|
367
|
-
|
|
368
|
-
// register-hooks failure is a warning — update still completes
|
|
369
|
-
expect(result.ok).toBe(true);
|
|
370
|
-
expect(result.exitCode).toBe(0);
|
|
371
|
-
|
|
372
|
-
// vault.yml updated with new version despite hooks failure
|
|
373
|
-
const vaultYml = await readVaultYml(tempDir);
|
|
374
|
-
expect(vaultYml.onebrain_version).toBe('v2.0.0');
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
it('binary validation failure → register-hooks NOT called AND vault.yml unchanged', async () => {
|
|
378
|
-
const calls: string[] = [];
|
|
379
|
-
|
|
380
|
-
const opts: UpdateOptions = {
|
|
381
|
-
vaultDir: tempDir,
|
|
382
|
-
isTTY: false,
|
|
383
|
-
fetchFn: makeMockFetch('v2.0.0'),
|
|
384
|
-
vaultSyncFn: async (vaultDir, syncOpts) => {
|
|
385
|
-
calls.push('vault-sync');
|
|
386
|
-
return noopVaultSync(vaultDir, syncOpts);
|
|
387
|
-
},
|
|
388
|
-
installBinaryFn: async (version) => {
|
|
389
|
-
calls.push(`install:${version}`);
|
|
390
|
-
},
|
|
391
|
-
validateBinaryFn: async () => {
|
|
392
|
-
calls.push('validate-fail');
|
|
393
|
-
return false;
|
|
394
|
-
},
|
|
395
|
-
registerHooksFn: async (vaultDir) => {
|
|
396
|
-
calls.push('register-hooks');
|
|
397
|
-
return noopRegisterHooks(vaultDir);
|
|
398
|
-
},
|
|
399
|
-
};
|
|
400
|
-
|
|
401
|
-
const result = await runUpdate(opts);
|
|
402
|
-
|
|
403
|
-
expect(result.ok).toBe(false);
|
|
404
|
-
expect(result.exitCode).toBe(1);
|
|
405
|
-
|
|
406
|
-
// register-hooks was NOT called
|
|
407
|
-
expect(calls).not.toContain('register-hooks');
|
|
408
|
-
|
|
409
|
-
// vault.yml unchanged
|
|
410
|
-
const vaultYml = await readVaultYml(tempDir);
|
|
411
|
-
expect(vaultYml.onebrain_version).toBe('v1.10.18');
|
|
412
|
-
});
|
|
413
|
-
});
|