@onebrain-ai/cli 2.0.0 → 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 +24 -2
- 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,419 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* vault-sync integration tests (TDD)
|
|
3
|
-
*
|
|
4
|
-
* Uses a mock GitHub tarball — does NOT hit real GitHub.
|
|
5
|
-
* Verifies all 7 steps with a temp vault dir.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
|
9
|
-
import { spawnSync } from 'node:child_process';
|
|
10
|
-
import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
11
|
-
import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
|
12
|
-
import { tmpdir } from 'node:os';
|
|
13
|
-
import { join } from 'node:path';
|
|
14
|
-
|
|
15
|
-
import { type VaultSyncOptions, runVaultSync } from './vault-sync.js';
|
|
16
|
-
|
|
17
|
-
// ---------------------------------------------------------------------------
|
|
18
|
-
// Tarball builder helpers (uses tar CLI — available on macOS/Linux)
|
|
19
|
-
// ---------------------------------------------------------------------------
|
|
20
|
-
|
|
21
|
-
interface TarballOpts {
|
|
22
|
-
prefix?: string;
|
|
23
|
-
pluginVersion?: string;
|
|
24
|
-
extraPluginFiles?: Record<string, string>;
|
|
25
|
-
claudeMdContent?: string;
|
|
26
|
-
geminiMdContent?: string;
|
|
27
|
-
agentsMdContent?: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Build a minimal fake tarball (.tar.gz) using the tar CLI.
|
|
32
|
-
* Layout mirrors a real GitHub archive:
|
|
33
|
-
* kengio-onebrain-<sha>/
|
|
34
|
-
* .claude/plugins/onebrain/.claude-plugin/plugin.json
|
|
35
|
-
* .claude/plugins/onebrain/INSTRUCTIONS.md
|
|
36
|
-
* .claude/plugins/onebrain/skills/example/SKILL.md
|
|
37
|
-
* README.md CONTRIBUTING.md CHANGELOG.md
|
|
38
|
-
* CLAUDE.md GEMINI.md AGENTS.md
|
|
39
|
-
*/
|
|
40
|
-
function buildMockTarball(opts: TarballOpts = {}): Buffer {
|
|
41
|
-
const prefix = opts.prefix ?? 'kengio-onebrain-abc1234';
|
|
42
|
-
const version = opts.pluginVersion ?? '1.11.0';
|
|
43
|
-
|
|
44
|
-
const files: Record<string, string> = {
|
|
45
|
-
[`${prefix}/.claude/plugins/onebrain/.claude-plugin/plugin.json`]: JSON.stringify({
|
|
46
|
-
id: 'onebrain',
|
|
47
|
-
version,
|
|
48
|
-
name: 'OneBrain',
|
|
49
|
-
}),
|
|
50
|
-
[`${prefix}/.claude/plugins/onebrain/INSTRUCTIONS.md`]: '# OneBrain Instructions\n',
|
|
51
|
-
[`${prefix}/.claude/plugins/onebrain/skills/example/SKILL.md`]: '# Example Skill\n',
|
|
52
|
-
[`${prefix}/README.md`]: '# OneBrain\n',
|
|
53
|
-
[`${prefix}/CONTRIBUTING.md`]: '# Contributing\n',
|
|
54
|
-
[`${prefix}/CHANGELOG.md`]: '# Changelog\n',
|
|
55
|
-
[`${prefix}/CLAUDE.md`]: opts.claudeMdContent ?? '@.claude/plugins/onebrain/INSTRUCTIONS.md\n',
|
|
56
|
-
[`${prefix}/GEMINI.md`]: opts.geminiMdContent ?? '@.claude/plugins/onebrain/INSTRUCTIONS.md\n',
|
|
57
|
-
[`${prefix}/AGENTS.md`]: opts.agentsMdContent ?? '@.claude/plugins/onebrain/INSTRUCTIONS.md\n',
|
|
58
|
-
...Object.fromEntries(
|
|
59
|
-
Object.entries(opts.extraPluginFiles ?? {}).map(([k, v]) => [
|
|
60
|
-
`${prefix}/.claude/plugins/onebrain/${k}`,
|
|
61
|
-
v,
|
|
62
|
-
]),
|
|
63
|
-
),
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
// Write files to a temp staging directory
|
|
67
|
-
const stageDir = `${tmpdir()}/onebrain-stage-${process.pid}`;
|
|
68
|
-
mkdirSync(stageDir, { recursive: true });
|
|
69
|
-
|
|
70
|
-
for (const [relPath, content] of Object.entries(files)) {
|
|
71
|
-
const fullPath = join(stageDir, relPath);
|
|
72
|
-
mkdirSync(join(fullPath, '..'), { recursive: true });
|
|
73
|
-
writeFileSync(fullPath, content, 'utf8');
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const tarPath = join(stageDir, 'bundle.tar.gz');
|
|
77
|
-
const r = spawnSync('tar', ['-czf', tarPath, '-C', stageDir, prefix], { encoding: 'utf8' });
|
|
78
|
-
if (r.status !== 0) {
|
|
79
|
-
throw new Error(`tar failed: ${r.stderr}`);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const buf = readFileSync(tarPath);
|
|
83
|
-
rmSync(stageDir, { recursive: true, force: true });
|
|
84
|
-
return buf as Buffer;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Create a mock fetch that returns the tarball buffer as a Response.
|
|
89
|
-
*/
|
|
90
|
-
function mockFetchWithTarball(tarball: Buffer): VaultSyncOptions['fetchFn'] {
|
|
91
|
-
return async (_url: string | URL | Request) => {
|
|
92
|
-
return new Response(tarball, {
|
|
93
|
-
status: 200,
|
|
94
|
-
headers: { 'content-type': 'application/x-gzip' },
|
|
95
|
-
});
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// ---------------------------------------------------------------------------
|
|
100
|
-
// Test vault setup
|
|
101
|
-
// ---------------------------------------------------------------------------
|
|
102
|
-
|
|
103
|
-
async function makeVaultDir(): Promise<string> {
|
|
104
|
-
return mkdtemp(join(tmpdir(), 'onebrain-vs-test-'));
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const VALID_VAULT_YML =
|
|
108
|
-
'method: onebrain\nupdate_channel: stable\nfolders:\n inbox: 00-inbox\n logs: 07-logs\n';
|
|
109
|
-
|
|
110
|
-
// ---------------------------------------------------------------------------
|
|
111
|
-
// Tests
|
|
112
|
-
// ---------------------------------------------------------------------------
|
|
113
|
-
|
|
114
|
-
describe('runVaultSync', () => {
|
|
115
|
-
let vaultDir: string;
|
|
116
|
-
let tarball: Buffer;
|
|
117
|
-
|
|
118
|
-
beforeEach(async () => {
|
|
119
|
-
vaultDir = await makeVaultDir();
|
|
120
|
-
await writeFile(join(vaultDir, 'vault.yml'), VALID_VAULT_YML, 'utf8');
|
|
121
|
-
tarball = buildMockTarball({});
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
afterEach(async () => {
|
|
125
|
-
await rm(vaultDir, { recursive: true, force: true });
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
// ── Test 1: fresh sync ─────────────────────────────────────────────────
|
|
129
|
-
|
|
130
|
-
it('fresh sync: syncs all plugin files and root docs', async () => {
|
|
131
|
-
const result = await runVaultSync(vaultDir, {
|
|
132
|
-
fetchFn: mockFetchWithTarball(tarball),
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
expect(result.ok).toBe(true);
|
|
136
|
-
expect(result.filesAdded).toBeGreaterThan(0);
|
|
137
|
-
expect(result.filesRemoved).toBe(0);
|
|
138
|
-
|
|
139
|
-
// plugin.json should be present (lives under .claude-plugin/ within the plugin dir)
|
|
140
|
-
const pluginJson = join(vaultDir, '.claude/plugins/onebrain/.claude-plugin/plugin.json');
|
|
141
|
-
const pj = JSON.parse(await readFile(pluginJson, 'utf8'));
|
|
142
|
-
expect(pj.version).toBe('1.11.0');
|
|
143
|
-
|
|
144
|
-
// Root docs should be present
|
|
145
|
-
const readme = await readFile(join(vaultDir, 'README.md'), 'utf8');
|
|
146
|
-
expect(readme).toContain('# OneBrain');
|
|
147
|
-
|
|
148
|
-
// vault.yml should have onebrain_version
|
|
149
|
-
const vaultYml = await readFile(join(vaultDir, 'vault.yml'), 'utf8');
|
|
150
|
-
expect(vaultYml).toContain('onebrain_version: 1.11.0');
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
// ── Test 2: stale file removal ──────────────────────────────────────────
|
|
154
|
-
|
|
155
|
-
it('sync with stale files: removes files not in tarball', async () => {
|
|
156
|
-
// Pre-populate vault plugin dir with a stale file not in tarball
|
|
157
|
-
const pluginDir = join(vaultDir, '.claude/plugins/onebrain');
|
|
158
|
-
await mkdir(pluginDir, { recursive: true });
|
|
159
|
-
await writeFile(join(pluginDir, 'stale-file.md'), '# Stale\n', 'utf8');
|
|
160
|
-
|
|
161
|
-
const result = await runVaultSync(vaultDir, {
|
|
162
|
-
fetchFn: mockFetchWithTarball(tarball),
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
expect(result.ok).toBe(true);
|
|
166
|
-
expect(result.filesRemoved).toBeGreaterThanOrEqual(1);
|
|
167
|
-
|
|
168
|
-
// stale file should be gone
|
|
169
|
-
let staleExists = false;
|
|
170
|
-
try {
|
|
171
|
-
await stat(join(pluginDir, 'stale-file.md'));
|
|
172
|
-
staleExists = true;
|
|
173
|
-
} catch {
|
|
174
|
-
staleExists = false;
|
|
175
|
-
}
|
|
176
|
-
expect(staleExists).toBe(false);
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
// ── Test 3: marketplace pin skip ────────────────────────────────────────
|
|
180
|
-
|
|
181
|
-
it('source: marketplace → pin step is skipped', async () => {
|
|
182
|
-
const pluginsDir = join(vaultDir, '.fake-claude-plugins');
|
|
183
|
-
await mkdir(pluginsDir, { recursive: true });
|
|
184
|
-
const installedJson = {
|
|
185
|
-
plugins: {
|
|
186
|
-
'onebrain@marketplace': [
|
|
187
|
-
{
|
|
188
|
-
id: 'onebrain',
|
|
189
|
-
source: 'marketplace',
|
|
190
|
-
installPath: join(pluginsDir, 'cache/marketplace/onebrain/1.10.0'),
|
|
191
|
-
},
|
|
192
|
-
],
|
|
193
|
-
},
|
|
194
|
-
};
|
|
195
|
-
const installedPath = join(pluginsDir, 'installed_plugins.json');
|
|
196
|
-
await writeFile(installedPath, JSON.stringify(installedJson, null, 2), 'utf8');
|
|
197
|
-
|
|
198
|
-
const result = await runVaultSync(vaultDir, {
|
|
199
|
-
fetchFn: mockFetchWithTarball(tarball),
|
|
200
|
-
installedPluginsPath: installedPath,
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
expect(result.ok).toBe(true);
|
|
204
|
-
expect(result.pinSkipped).toBe(true);
|
|
205
|
-
|
|
206
|
-
// installed_plugins.json installPath must NOT be rewritten
|
|
207
|
-
const afterJson = JSON.parse(await readFile(installedPath, 'utf8'));
|
|
208
|
-
const entry = afterJson.plugins['onebrain@marketplace'][0];
|
|
209
|
-
expect(entry.installPath).toContain('cache/marketplace/onebrain/1.10.0');
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
// ── Test 4: harness file import injection ───────────────────────────────
|
|
213
|
-
|
|
214
|
-
it('injects new @import lines not already in vault harness file', async () => {
|
|
215
|
-
// Vault CLAUDE.md has existing import + custom content
|
|
216
|
-
await writeFile(
|
|
217
|
-
join(vaultDir, 'CLAUDE.md'),
|
|
218
|
-
'# My Config\n\n@.claude/plugins/onebrain/INSTRUCTIONS.md\n',
|
|
219
|
-
'utf8',
|
|
220
|
-
);
|
|
221
|
-
|
|
222
|
-
const tarballWithNewImport = buildMockTarball({
|
|
223
|
-
claudeMdContent:
|
|
224
|
-
'@.claude/plugins/onebrain/INSTRUCTIONS.md\n@.claude/plugins/onebrain/NEW.md\n',
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
const result = await runVaultSync(vaultDir, {
|
|
228
|
-
fetchFn: mockFetchWithTarball(tarballWithNewImport),
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
expect(result.ok).toBe(true);
|
|
232
|
-
|
|
233
|
-
const claude = await readFile(join(vaultDir, 'CLAUDE.md'), 'utf8');
|
|
234
|
-
// New import added
|
|
235
|
-
expect(claude).toContain('@.claude/plugins/onebrain/NEW.md');
|
|
236
|
-
// Original vault content preserved
|
|
237
|
-
expect(claude).toContain('# My Config');
|
|
238
|
-
// No duplicate of the existing import
|
|
239
|
-
const matches = claude.match(/@\.claude\/plugins\/onebrain\/INSTRUCTIONS\.md/g);
|
|
240
|
-
expect(matches?.length).toBe(1);
|
|
241
|
-
|
|
242
|
-
expect(result.importsAdded).toBeGreaterThan(0);
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
// ── Test 5: no duplicate imports injected ──────────────────────────────
|
|
246
|
-
|
|
247
|
-
it('does not inject @import lines already in vault', async () => {
|
|
248
|
-
// Pre-create all three harness files so no file triggers the "created fresh" path
|
|
249
|
-
const existingImport = '@.claude/plugins/onebrain/INSTRUCTIONS.md\n';
|
|
250
|
-
await writeFile(join(vaultDir, 'CLAUDE.md'), existingImport, 'utf8');
|
|
251
|
-
await writeFile(join(vaultDir, 'GEMINI.md'), existingImport, 'utf8');
|
|
252
|
-
await writeFile(join(vaultDir, 'AGENTS.md'), existingImport, 'utf8');
|
|
253
|
-
|
|
254
|
-
const tarballSameImport = buildMockTarball({
|
|
255
|
-
claudeMdContent: existingImport,
|
|
256
|
-
geminiMdContent: existingImport,
|
|
257
|
-
agentsMdContent: existingImport,
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
const result = await runVaultSync(vaultDir, {
|
|
261
|
-
fetchFn: mockFetchWithTarball(tarballSameImport),
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
expect(result.ok).toBe(true);
|
|
265
|
-
|
|
266
|
-
const claude = await readFile(join(vaultDir, 'CLAUDE.md'), 'utf8');
|
|
267
|
-
const matches = claude.match(/@\.claude\/plugins\/onebrain\/INSTRUCTIONS\.md/g);
|
|
268
|
-
expect(matches?.length).toBe(1);
|
|
269
|
-
// All three harness files had identical imports — nothing new to inject
|
|
270
|
-
expect(result.importsAdded).toBe(0);
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
// ── Test 6: vault.yml version + channel written ─────────────────────────
|
|
274
|
-
|
|
275
|
-
it('writes onebrain_version and preserves update_channel in vault.yml', async () => {
|
|
276
|
-
const result = await runVaultSync(vaultDir, {
|
|
277
|
-
fetchFn: mockFetchWithTarball(tarball),
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
expect(result.ok).toBe(true);
|
|
281
|
-
|
|
282
|
-
const vaultYml = await readFile(join(vaultDir, 'vault.yml'), 'utf8');
|
|
283
|
-
expect(vaultYml).toContain('onebrain_version: 1.11.0');
|
|
284
|
-
expect(vaultYml).toContain('update_channel: stable');
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
// ── Test 7: missing installed_plugins.json → pin no-op ─────────────────
|
|
288
|
-
|
|
289
|
-
it('pin step is no-op when installed_plugins.json does not exist', async () => {
|
|
290
|
-
const result = await runVaultSync(vaultDir, {
|
|
291
|
-
fetchFn: mockFetchWithTarball(tarball),
|
|
292
|
-
installedPluginsPath: join(vaultDir, 'nonexistent-installed_plugins.json'),
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
expect(result.ok).toBe(true);
|
|
296
|
-
expect(result.pinSkipped).toBe(true);
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
// ── Test 8: pin updates installPath when not marketplace ───────────────
|
|
300
|
-
|
|
301
|
-
it('pin updates installPath for non-marketplace entry inside cache dir', async () => {
|
|
302
|
-
const pluginsDir = join(vaultDir, '.fake-claude-plugins');
|
|
303
|
-
const cacheDir = join(pluginsDir, 'cache');
|
|
304
|
-
const entryPath = join(cacheDir, 'onebrain-local', 'onebrain', '1.10.0');
|
|
305
|
-
await mkdir(entryPath, { recursive: true });
|
|
306
|
-
|
|
307
|
-
const installedJson = {
|
|
308
|
-
plugins: {
|
|
309
|
-
'onebrain@onebrain-local': [
|
|
310
|
-
{
|
|
311
|
-
id: 'onebrain',
|
|
312
|
-
source: 'local',
|
|
313
|
-
installPath: entryPath,
|
|
314
|
-
},
|
|
315
|
-
],
|
|
316
|
-
},
|
|
317
|
-
};
|
|
318
|
-
const installedPath = join(pluginsDir, 'installed_plugins.json');
|
|
319
|
-
await writeFile(installedPath, JSON.stringify(installedJson, null, 2), 'utf8');
|
|
320
|
-
|
|
321
|
-
const result = await runVaultSync(vaultDir, {
|
|
322
|
-
fetchFn: mockFetchWithTarball(tarball),
|
|
323
|
-
installedPluginsPath: installedPath,
|
|
324
|
-
installedPluginsCacheDir: cacheDir,
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
expect(result.ok).toBe(true);
|
|
328
|
-
expect(result.pinSkipped).toBe(false);
|
|
329
|
-
|
|
330
|
-
const afterJson = JSON.parse(await readFile(installedPath, 'utf8'));
|
|
331
|
-
const entry = afterJson.plugins['onebrain@onebrain-local'][0];
|
|
332
|
-
expect(entry.installPath).toBe(join(vaultDir, '.claude/plugins/onebrain'));
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
// ── Test 9: steps 6–7 errors are non-fatal ─────────────────────────────
|
|
336
|
-
|
|
337
|
-
it('pin/cache errors are non-fatal — result.ok remains true', async () => {
|
|
338
|
-
// vault.yml is valid YAML but not valid JSON → will cause JSON parse error in pin step
|
|
339
|
-
const result = await runVaultSync(vaultDir, {
|
|
340
|
-
fetchFn: mockFetchWithTarball(tarball),
|
|
341
|
-
installedPluginsPath: join(vaultDir, 'vault.yml'),
|
|
342
|
-
});
|
|
343
|
-
|
|
344
|
-
expect(result.ok).toBe(true);
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
// ── Test 10: fetch failure → result.ok is false ────────────────────────
|
|
348
|
-
|
|
349
|
-
it('download failure → result.ok is false', async () => {
|
|
350
|
-
const vaultDir2 = await mkdtemp(join(tmpdir(), 'vs-fail-'));
|
|
351
|
-
await writeFile(join(vaultDir2, 'vault.yml'), 'update_channel: stable\n');
|
|
352
|
-
|
|
353
|
-
const result = await runVaultSync(vaultDir2, {
|
|
354
|
-
fetchFn: async () => new Response('Not Found', { status: 404 }),
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
expect(result.ok).toBe(false);
|
|
358
|
-
|
|
359
|
-
await rm(vaultDir2, { recursive: true });
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
// ── Test 11: corrupted tarball → result.ok false, error defined ────────
|
|
363
|
-
|
|
364
|
-
it('corrupted tarball → result.ok is false, result.error defined', async () => {
|
|
365
|
-
const result = await runVaultSync(vaultDir, {
|
|
366
|
-
fetchFn: async () =>
|
|
367
|
-
new Response(new Uint8Array([0x00, 0x01, 0x02, 0x03, 0xff, 0xfe]).buffer, {
|
|
368
|
-
status: 200,
|
|
369
|
-
headers: { 'content-type': 'application/x-gzip' },
|
|
370
|
-
}),
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
expect(result.ok).toBe(false);
|
|
374
|
-
expect(result.error).toBeDefined();
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
// ── Test 12: HTTP 403 → result.ok false, error contains '403' ──────────
|
|
378
|
-
|
|
379
|
-
it('HTTP 403 response → result.ok is false, error contains 403', async () => {
|
|
380
|
-
const result = await runVaultSync(vaultDir, {
|
|
381
|
-
fetchFn: async () => new Response('Forbidden', { status: 403 }),
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
expect(result.ok).toBe(false);
|
|
385
|
-
expect(result.error).toBeDefined();
|
|
386
|
-
expect(result.error).toContain('403');
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
// ── Test 13: filesRemoved counts actual deletions only ─────────────────
|
|
390
|
-
|
|
391
|
-
it('filesRemoved counts actual deletions: unlinkFn throws for one of 2 stale files → filesRemoved === 1', async () => {
|
|
392
|
-
// Pre-populate vault plugin dir with 2 stale files not in tarball
|
|
393
|
-
const pluginDir = join(vaultDir, '.claude/plugins/onebrain');
|
|
394
|
-
await mkdir(pluginDir, { recursive: true });
|
|
395
|
-
await writeFile(join(pluginDir, 'stale-a.md'), '# Stale A\n', 'utf8');
|
|
396
|
-
await writeFile(join(pluginDir, 'stale-b.md'), '# Stale B\n', 'utf8');
|
|
397
|
-
|
|
398
|
-
let _callCount = 0;
|
|
399
|
-
const partialUnlink: typeof import('node:fs/promises').unlink = async (path) => {
|
|
400
|
-
_callCount++;
|
|
401
|
-
if (String(path).endsWith('stale-a.md')) {
|
|
402
|
-
const err = new Error('Permission denied') as NodeJS.ErrnoException;
|
|
403
|
-
err.code = 'EACCES';
|
|
404
|
-
throw err;
|
|
405
|
-
}
|
|
406
|
-
const { unlink: realUnlink } = await import('node:fs/promises');
|
|
407
|
-
return realUnlink(path as string);
|
|
408
|
-
};
|
|
409
|
-
|
|
410
|
-
const result = await runVaultSync(vaultDir, {
|
|
411
|
-
fetchFn: mockFetchWithTarball(tarball),
|
|
412
|
-
unlinkFn: partialUnlink,
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
expect(result.ok).toBe(true);
|
|
416
|
-
// Only 1 of the 2 stale files was actually deleted
|
|
417
|
-
expect(result.filesRemoved).toBe(1);
|
|
418
|
-
});
|
|
419
|
-
});
|