@dalzoubi/dev-agents-sync 1.0.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.
@@ -0,0 +1,163 @@
1
+ /**
2
+ * tests/range.test.mjs
3
+ *
4
+ * Tests for semver range resolution.
5
+ *
6
+ * The range resolver takes:
7
+ * - a list of available version tags (strings like "v1.0.0", "v1.1.0")
8
+ * - a semver range string (like "^1", "~1.0", ">=1.2.0")
9
+ *
10
+ * It returns the latest version satisfying the range, or throws/returns null
11
+ * with an actionable error message when nothing matches.
12
+ *
13
+ * Note: tags in the content repo include the "v" prefix but semver ranges do
14
+ * not. The resolver normalizes tag strings by stripping the leading "v".
15
+ */
16
+
17
+ import { describe, it } from 'node:test';
18
+ import assert from 'node:assert/strict';
19
+
20
+ import { resolveRange } from '../src/range.mjs';
21
+
22
+ // Available tags as they would come from the GitHub API
23
+ const AVAILABLE_TAGS = ['v1.0.0', 'v1.1.0', 'v1.2.0', 'v2.0.0'];
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Caret range (^)
27
+ // ---------------------------------------------------------------------------
28
+
29
+ describe('resolveRange — caret range', () => {
30
+ it('picks the latest v1.x.x for range "^1"', () => {
31
+ const result = resolveRange(AVAILABLE_TAGS, '^1');
32
+ assert.equal(result, '1.2.0');
33
+ });
34
+
35
+ it('picks the latest v1.x.x for range "^1.0"', () => {
36
+ const result = resolveRange(AVAILABLE_TAGS, '^1.0');
37
+ assert.equal(result, '1.2.0');
38
+ });
39
+
40
+ it('picks the latest v2.x.x for range "^2"', () => {
41
+ const result = resolveRange(AVAILABLE_TAGS, '^2');
42
+ assert.equal(result, '2.0.0');
43
+ });
44
+
45
+ it('does NOT pick v2.0.0 for range "^1"', () => {
46
+ const result = resolveRange(AVAILABLE_TAGS, '^1');
47
+ assert.notEqual(result, '2.0.0');
48
+ });
49
+ });
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // Tilde range (~)
53
+ // ---------------------------------------------------------------------------
54
+
55
+ describe('resolveRange — tilde range', () => {
56
+ it('picks v1.0.0 for range "~1.0" (no v1.0.x above v1.0.0)', () => {
57
+ const result = resolveRange(AVAILABLE_TAGS, '~1.0');
58
+ assert.equal(result, '1.0.0');
59
+ });
60
+
61
+ it('picks v1.1.0 for range "~1.1"', () => {
62
+ const result = resolveRange(AVAILABLE_TAGS, '~1.1');
63
+ assert.equal(result, '1.1.0');
64
+ });
65
+
66
+ it('picks v1.2.0 for range "~1.2"', () => {
67
+ const result = resolveRange(AVAILABLE_TAGS, '~1.2');
68
+ assert.equal(result, '1.2.0');
69
+ });
70
+ });
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // Exact version
74
+ // ---------------------------------------------------------------------------
75
+
76
+ describe('resolveRange — exact version', () => {
77
+ it('returns exactly v1.1.0 when range is "1.1.0"', () => {
78
+ const result = resolveRange(AVAILABLE_TAGS, '1.1.0');
79
+ assert.equal(result, '1.1.0');
80
+ });
81
+
82
+ it('returns exactly v2.0.0 when range is "2.0.0"', () => {
83
+ const result = resolveRange(AVAILABLE_TAGS, '2.0.0');
84
+ assert.equal(result, '2.0.0');
85
+ });
86
+ });
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // Range that matches nothing
90
+ // ---------------------------------------------------------------------------
91
+
92
+ describe('resolveRange — no match', () => {
93
+ it('throws (or returns null with error) when no tag satisfies the range', () => {
94
+ // v3 does not exist in the tag list
95
+ let threw = false;
96
+ let result = null;
97
+ try {
98
+ result = resolveRange(AVAILABLE_TAGS, '^3');
99
+ } catch (err) {
100
+ threw = true;
101
+ // Error message must be actionable
102
+ assert.ok(
103
+ err.message && (err.message.includes('range') || err.message.includes('^3')),
104
+ `expected actionable error, got: ${err.message}`,
105
+ );
106
+ }
107
+ if (!threw) {
108
+ // Alternative: returns null
109
+ assert.equal(result, null);
110
+ }
111
+ });
112
+
113
+ it('provides an actionable error message naming the unsatisfied range', () => {
114
+ let message = '';
115
+ try {
116
+ resolveRange(AVAILABLE_TAGS, '^99');
117
+ } catch (err) {
118
+ message = err.message;
119
+ }
120
+ // Must mention the range or provide guidance
121
+ assert.ok(
122
+ message.length > 0,
123
+ 'error message must be non-empty when range is unsatisfied',
124
+ );
125
+ });
126
+
127
+ it('throws for an empty tag list', () => {
128
+ assert.throws(() => resolveRange([], '^1'));
129
+ });
130
+ });
131
+
132
+ // ---------------------------------------------------------------------------
133
+ // Tag normalization (v prefix)
134
+ // ---------------------------------------------------------------------------
135
+
136
+ describe('resolveRange — tag normalization', () => {
137
+ it('handles tags with "v" prefix correctly', () => {
138
+ // GitHub tags come as "v1.0.0" — resolver must strip the v
139
+ const result = resolveRange(['v1.0.0', 'v1.1.0'], '^1');
140
+ assert.equal(result, '1.1.0');
141
+ });
142
+
143
+ it('handles tags without "v" prefix correctly', () => {
144
+ const result = resolveRange(['1.0.0', '1.1.0'], '^1');
145
+ assert.equal(result, '1.1.0');
146
+ });
147
+
148
+ it('handles a mixed list of prefixed and unprefixed tags', () => {
149
+ const result = resolveRange(['1.0.0', 'v1.1.0'], '^1');
150
+ assert.equal(result, '1.1.0');
151
+ });
152
+ });
153
+
154
+ // ---------------------------------------------------------------------------
155
+ // Returned value is always a bare version (no "v" prefix)
156
+ // ---------------------------------------------------------------------------
157
+
158
+ describe('resolveRange — return value format', () => {
159
+ it('returned version string does NOT include a "v" prefix', () => {
160
+ const result = resolveRange(AVAILABLE_TAGS, '^1');
161
+ assert.ok(!result.startsWith('v'), `expected no v prefix, got: ${result}`);
162
+ });
163
+ });
@@ -0,0 +1,489 @@
1
+ /**
2
+ * tests/status-check-diff.test.mjs
3
+ *
4
+ * Tests for the `status`, `check`, and `diff` subcommands.
5
+ *
6
+ * status — informational, always exit 0
7
+ * check — CI-friendly; exit 0 (in sync), 1 (drift), 2 (tooling error)
8
+ * diff — prints unified diff, no filesystem writes, always exit 0
9
+ */
10
+
11
+ import { describe, it, beforeEach, afterEach } from 'node:test';
12
+ import assert from 'node:assert/strict';
13
+ import {
14
+ mkdtempSync,
15
+ mkdirSync,
16
+ writeFileSync,
17
+ readFileSync,
18
+ existsSync,
19
+ rmSync,
20
+ readdirSync,
21
+ } from 'node:fs';
22
+ import { tmpdir } from 'node:os';
23
+ import path from 'node:path';
24
+ import { fileURLToPath } from 'node:url';
25
+
26
+ import { runStatus } from '../src/commands/status.mjs';
27
+ import { runCheck } from '../src/commands/check.mjs';
28
+ import { runDiff } from '../src/commands/diff.mjs';
29
+ import { writeLockfile } from '../src/lockfile.mjs';
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Fixture helpers
33
+ // ---------------------------------------------------------------------------
34
+
35
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
36
+ const FIXTURE_DIR = path.join(__dirname, 'fixtures', 'release-v1.0.0');
37
+
38
+ function buildFixtureFileMap() {
39
+ const map = {};
40
+ for (const target of ['claude', 'cursor']) {
41
+ const targetDir = path.join(FIXTURE_DIR, target);
42
+ if (!existsSync(targetDir)) continue;
43
+ collectFiles(targetDir, targetDir, map, target);
44
+ }
45
+ return map;
46
+ }
47
+
48
+ function collectFiles(baseDir, currentDir, map, prefix) {
49
+ const entries = readdirSync(currentDir, { withFileTypes: true });
50
+ for (const entry of entries) {
51
+ const full = path.join(currentDir, entry.name);
52
+ if (entry.isDirectory()) {
53
+ collectFiles(baseDir, full, map, prefix);
54
+ } else {
55
+ const rel = path.relative(baseDir, full).replace(/\\/g, '/');
56
+ map[`${prefix}/${rel}`] = readFileSync(full, 'utf8');
57
+ }
58
+ }
59
+ }
60
+
61
+ function makeFixtureFetcher() {
62
+ return async (_repo, _tag, _token) => buildFixtureFileMap();
63
+ }
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // Helpers
67
+ // ---------------------------------------------------------------------------
68
+
69
+ function makeTmpDir() {
70
+ return mkdtempSync(path.join(tmpdir(), 'das-scd-test-'));
71
+ }
72
+
73
+ function setupConsumerRepo(consumerDir, opts = {}) {
74
+ const {
75
+ targets = ['claude'],
76
+ resolvedVersion = '1.2.0',
77
+ range = '^1',
78
+ writeFiles = true,
79
+ driftFile = null, // { path, content } to simulate drift
80
+ } = opts;
81
+
82
+ writeLockfile(consumerDir, {
83
+ source: 'github:dalzoubi/dev-agents',
84
+ range,
85
+ resolvedVersion,
86
+ targets,
87
+ lastUpdated: '2026-04-01T00:00:00Z',
88
+ });
89
+
90
+ if (writeFiles && targets.includes('claude')) {
91
+ const agentsDir = path.join(consumerDir, '.claude', 'agents');
92
+ mkdirSync(agentsDir, { recursive: true });
93
+
94
+ // Write fixture files as if init already ran
95
+ const fixtureContent = readFileSync(
96
+ path.join(FIXTURE_DIR, 'claude', 'agents', 'define.md'),
97
+ 'utf8',
98
+ );
99
+ const content = driftFile?.path?.includes('define')
100
+ ? driftFile.content
101
+ : fixtureContent;
102
+ writeFileSync(path.join(agentsDir, 'define.md'), content, 'utf8');
103
+
104
+ const testContent = readFileSync(
105
+ path.join(FIXTURE_DIR, 'claude', 'agents', 'test.md'),
106
+ 'utf8',
107
+ );
108
+ writeFileSync(path.join(agentsDir, 'test.md'), testContent, 'utf8');
109
+
110
+ // Materialize remaining claude fixture files so check() — which now treats
111
+ // missing managed files as drift (M1) — sees a complete in-sync state.
112
+ const commandsDir = path.join(consumerDir, '.claude', 'commands');
113
+ mkdirSync(commandsDir, { recursive: true });
114
+ const preflightContent = readFileSync(
115
+ path.join(FIXTURE_DIR, 'claude', 'commands', 'preflight.md'),
116
+ 'utf8',
117
+ );
118
+ writeFileSync(path.join(commandsDir, 'preflight.md'), preflightContent, 'utf8');
119
+ }
120
+ }
121
+
122
+ // ---------------------------------------------------------------------------
123
+ // status subcommand
124
+ // ---------------------------------------------------------------------------
125
+
126
+ describe('status', () => {
127
+ let consumerDir;
128
+
129
+ beforeEach(() => {
130
+ consumerDir = makeTmpDir();
131
+ setupConsumerRepo(consumerDir);
132
+ });
133
+
134
+ afterEach(() => {
135
+ rmSync(consumerDir, { recursive: true, force: true });
136
+ });
137
+
138
+ it('exits 0 and returns output object', async () => {
139
+ let exitCode = 0;
140
+ let result;
141
+
142
+ try {
143
+ result = await runStatus(consumerDir, {
144
+ fetcher: makeFixtureFetcher(),
145
+ availableTags: ['v1.0.0', 'v1.1.0', 'v1.2.0'],
146
+ });
147
+ } catch (err) {
148
+ exitCode = err.exitCode ?? 1;
149
+ }
150
+
151
+ assert.equal(exitCode, 0);
152
+ assert.ok(result, 'status must return a result object');
153
+ });
154
+
155
+ it('result includes range', async () => {
156
+ const result = await runStatus(consumerDir, {
157
+ fetcher: makeFixtureFetcher(),
158
+ availableTags: ['v1.0.0', 'v1.2.0'],
159
+ });
160
+
161
+ assert.ok('range' in result, 'result must include range');
162
+ assert.equal(result.range, '^1');
163
+ });
164
+
165
+ it('result includes resolvedVersion', async () => {
166
+ const result = await runStatus(consumerDir, {
167
+ fetcher: makeFixtureFetcher(),
168
+ availableTags: ['v1.0.0', 'v1.2.0'],
169
+ });
170
+
171
+ assert.ok('resolvedVersion' in result, 'result must include resolvedVersion');
172
+ assert.equal(result.resolvedVersion, '1.2.0');
173
+ });
174
+
175
+ it('result includes latest available version', async () => {
176
+ const result = await runStatus(consumerDir, {
177
+ fetcher: makeFixtureFetcher(),
178
+ availableTags: ['v1.0.0', 'v1.1.0', 'v1.2.0'],
179
+ });
180
+
181
+ assert.ok(
182
+ 'latestAvailable' in result || 'latest' in result,
183
+ 'result must include latest available version',
184
+ );
185
+ });
186
+
187
+ it('result includes targets list', async () => {
188
+ const result = await runStatus(consumerDir, {
189
+ fetcher: makeFixtureFetcher(),
190
+ availableTags: ['v1.0.0', 'v1.2.0'],
191
+ });
192
+
193
+ assert.ok('targets' in result, 'result must include targets');
194
+ assert.deepEqual(result.targets, ['claude']);
195
+ });
196
+
197
+ it('result includes managed file counts per target', async () => {
198
+ const result = await runStatus(consumerDir, {
199
+ fetcher: makeFixtureFetcher(),
200
+ availableTags: ['v1.0.0', 'v1.2.0'],
201
+ });
202
+
203
+ assert.ok(
204
+ 'fileCounts' in result || 'managedFiles' in result || 'counts' in result,
205
+ 'result must include file counts',
206
+ );
207
+ });
208
+ });
209
+
210
+ // ---------------------------------------------------------------------------
211
+ // check subcommand — exit 0 (in sync)
212
+ // ---------------------------------------------------------------------------
213
+
214
+ describe('check — in sync (exit 0)', () => {
215
+ let consumerDir;
216
+
217
+ beforeEach(() => {
218
+ consumerDir = makeTmpDir();
219
+ setupConsumerRepo(consumerDir, { resolvedVersion: '1.2.0' });
220
+ });
221
+
222
+ afterEach(() => {
223
+ rmSync(consumerDir, { recursive: true, force: true });
224
+ });
225
+
226
+ it('exits 0 when local files match resolved version', async () => {
227
+ let exitCode = 0;
228
+ try {
229
+ await runCheck(consumerDir, {
230
+ fetcher: makeFixtureFetcher(),
231
+ availableTags: ['v1.0.0', 'v1.2.0'],
232
+ });
233
+ } catch (err) {
234
+ exitCode = err.exitCode ?? 2;
235
+ }
236
+
237
+ assert.equal(exitCode, 0);
238
+ });
239
+ });
240
+
241
+ // ---------------------------------------------------------------------------
242
+ // check subcommand — exit 1 (drift)
243
+ // ---------------------------------------------------------------------------
244
+
245
+ describe('check — drift detected (exit 1)', () => {
246
+ let consumerDir;
247
+
248
+ beforeEach(() => {
249
+ consumerDir = makeTmpDir();
250
+ });
251
+
252
+ afterEach(() => {
253
+ rmSync(consumerDir, { recursive: true, force: true });
254
+ });
255
+
256
+ it('exits 1 when managed file content has been locally modified', async () => {
257
+ // Set up repo but manually modify a managed file after init
258
+ setupConsumerRepo(consumerDir, { resolvedVersion: '1.2.0' });
259
+
260
+ // Tamper with a managed file (still has marker but content differs)
261
+ const defineFile = path.join(consumerDir, '.claude', 'agents', 'define.md');
262
+ const original = readFileSync(defineFile, 'utf8');
263
+ writeFileSync(defineFile, original + '\n\n# Someone added this locally.', 'utf8');
264
+
265
+ let exitCode;
266
+ try {
267
+ await runCheck(consumerDir, {
268
+ fetcher: makeFixtureFetcher(),
269
+ availableTags: ['v1.2.0'],
270
+ });
271
+ exitCode = 0;
272
+ } catch (err) {
273
+ exitCode = err.exitCode;
274
+ }
275
+
276
+ assert.equal(exitCode, 1, 'expected exit code 1 for drifted managed file');
277
+ });
278
+
279
+ it('exits 1 when an expected managed file has been deleted locally', async () => {
280
+ // Init writes define.md and test.md. Delete one to simulate the canonical
281
+ // CI deletion-drift case: managed file removed between init and check.
282
+ setupConsumerRepo(consumerDir, { resolvedVersion: '1.2.0' });
283
+
284
+ const defineFile = path.join(consumerDir, '.claude', 'agents', 'define.md');
285
+ rmSync(defineFile);
286
+
287
+ let exitCode;
288
+ let errorMessage = '';
289
+ try {
290
+ await runCheck(consumerDir, {
291
+ fetcher: makeFixtureFetcher(),
292
+ availableTags: ['v1.2.0'],
293
+ });
294
+ exitCode = 0;
295
+ } catch (err) {
296
+ exitCode = err.exitCode;
297
+ errorMessage = err.message;
298
+ }
299
+
300
+ assert.equal(exitCode, 1, 'expected exit code 1 for a missing managed file');
301
+ assert.ok(
302
+ errorMessage.includes('<missing>'),
303
+ `deletion-drift message must mark the file as missing, got: ${errorMessage}`,
304
+ );
305
+ assert.ok(
306
+ errorMessage.includes('define.md'),
307
+ `deletion-drift message must name the offending path, got: ${errorMessage}`,
308
+ );
309
+ });
310
+
311
+ it('drift error message names the offending file path', async () => {
312
+ setupConsumerRepo(consumerDir, { resolvedVersion: '1.2.0' });
313
+
314
+ const defineFile = path.join(consumerDir, '.claude', 'agents', 'define.md');
315
+ writeFileSync(
316
+ defineFile,
317
+ readFileSync(defineFile, 'utf8') + '\nDrifted content.',
318
+ 'utf8',
319
+ );
320
+
321
+ let errorMessage = '';
322
+ try {
323
+ await runCheck(consumerDir, {
324
+ fetcher: makeFixtureFetcher(),
325
+ availableTags: ['v1.2.0'],
326
+ });
327
+ } catch (err) {
328
+ errorMessage = err.message;
329
+ }
330
+
331
+ assert.ok(
332
+ errorMessage.includes('define.md') || errorMessage.includes('.claude'),
333
+ `drift error must name the offending path, got: ${errorMessage}`,
334
+ );
335
+ });
336
+ });
337
+
338
+ // ---------------------------------------------------------------------------
339
+ // check subcommand — exit 2 (tooling error)
340
+ // ---------------------------------------------------------------------------
341
+
342
+ describe('check — tooling error (exit 2)', () => {
343
+ it('exits 2 when fetcher throws an auth error', async () => {
344
+ const consumerDir = makeTmpDir();
345
+ try {
346
+ setupConsumerRepo(consumerDir);
347
+
348
+ const authFailFetcher = async () => {
349
+ const err = new Error('Authentication failed: bad credentials');
350
+ err.type = 'auth';
351
+ throw err;
352
+ };
353
+
354
+ let exitCode;
355
+ try {
356
+ await runCheck(consumerDir, {
357
+ fetcher: authFailFetcher,
358
+ availableTags: ['v1.0.0', 'v1.2.0'],
359
+ });
360
+ exitCode = 0;
361
+ } catch (err) {
362
+ exitCode = err.exitCode;
363
+ }
364
+
365
+ assert.equal(exitCode, 2, 'auth failure must produce exit code 2');
366
+ } finally {
367
+ rmSync(consumerDir, { recursive: true, force: true });
368
+ }
369
+ });
370
+
371
+ it('exits 2 when fetcher throws a network error', async () => {
372
+ const consumerDir = makeTmpDir();
373
+ try {
374
+ setupConsumerRepo(consumerDir);
375
+
376
+ const networkFailFetcher = async () => {
377
+ const err = new Error('ECONNREFUSED: network error');
378
+ err.type = 'network';
379
+ throw err;
380
+ };
381
+
382
+ let exitCode;
383
+ try {
384
+ await runCheck(consumerDir, {
385
+ fetcher: networkFailFetcher,
386
+ availableTags: ['v1.0.0', 'v1.2.0'],
387
+ });
388
+ exitCode = 0;
389
+ } catch (err) {
390
+ exitCode = err.exitCode;
391
+ }
392
+
393
+ assert.equal(exitCode, 2, 'network failure must produce exit code 2');
394
+ } finally {
395
+ rmSync(consumerDir, { recursive: true, force: true });
396
+ }
397
+ });
398
+ });
399
+
400
+ // ---------------------------------------------------------------------------
401
+ // diff subcommand
402
+ // ---------------------------------------------------------------------------
403
+
404
+ describe('diff', () => {
405
+ let consumerDir;
406
+
407
+ beforeEach(() => {
408
+ consumerDir = makeTmpDir();
409
+ setupConsumerRepo(consumerDir, { resolvedVersion: '1.2.0' });
410
+ });
411
+
412
+ afterEach(() => {
413
+ rmSync(consumerDir, { recursive: true, force: true });
414
+ });
415
+
416
+ it('exits 0 even when diffs are present (informational only)', async () => {
417
+ // Tamper with a managed file to create a diff
418
+ const defineFile = path.join(consumerDir, '.claude', 'agents', 'define.md');
419
+ writeFileSync(
420
+ defineFile,
421
+ readFileSync(defineFile, 'utf8') + '\nExtra line added locally.',
422
+ 'utf8',
423
+ );
424
+
425
+ let exitCode = 0;
426
+ try {
427
+ await runDiff(consumerDir, {
428
+ fetcher: makeFixtureFetcher(),
429
+ availableTags: ['v1.2.0'],
430
+ });
431
+ } catch (err) {
432
+ exitCode = err.exitCode ?? 1;
433
+ }
434
+
435
+ assert.equal(exitCode, 0, 'diff must exit 0 even when diffs are present');
436
+ });
437
+
438
+ it('exits 0 when files are in sync (no diff)', async () => {
439
+ let exitCode = 0;
440
+ try {
441
+ await runDiff(consumerDir, {
442
+ fetcher: makeFixtureFetcher(),
443
+ availableTags: ['v1.2.0'],
444
+ });
445
+ } catch (err) {
446
+ exitCode = err.exitCode ?? 1;
447
+ }
448
+
449
+ assert.equal(exitCode, 0);
450
+ });
451
+
452
+ it('does not write any files to disk', async () => {
453
+ // Tamper with a file so there is actually a diff
454
+ const defineFile = path.join(consumerDir, '.claude', 'agents', 'define.md');
455
+ const originalContent = readFileSync(defineFile, 'utf8');
456
+ writeFileSync(defineFile, originalContent + '\nLocal addition.', 'utf8');
457
+
458
+ await runDiff(consumerDir, {
459
+ fetcher: makeFixtureFetcher(),
460
+ availableTags: ['v1.2.0'],
461
+ });
462
+
463
+ // File must remain tampered — diff does not restore it
464
+ const afterDiff = readFileSync(defineFile, 'utf8');
465
+ assert.ok(
466
+ afterDiff.includes('Local addition.'),
467
+ 'diff must not modify local files',
468
+ );
469
+ });
470
+
471
+ it('result contains diff output string', async () => {
472
+ const defineFile = path.join(consumerDir, '.claude', 'agents', 'define.md');
473
+ writeFileSync(
474
+ defineFile,
475
+ readFileSync(defineFile, 'utf8') + '\nAdded line.',
476
+ 'utf8',
477
+ );
478
+
479
+ const result = await runDiff(consumerDir, {
480
+ fetcher: makeFixtureFetcher(),
481
+ availableTags: ['v1.2.0'],
482
+ });
483
+
484
+ assert.ok(
485
+ typeof result === 'string' || typeof result?.diff === 'string',
486
+ 'diff must return a string or an object with a diff string',
487
+ );
488
+ });
489
+ });