@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,202 @@
1
+ /**
2
+ * tests/lockfile.test.mjs
3
+ *
4
+ * Tests for lockfile read/write/validation.
5
+ *
6
+ * The lockfile is `.dev-agents-sync.json` at the consumer repo root.
7
+ * Schema:
8
+ * {
9
+ * "$schema": "...",
10
+ * "source": "github:dalzoubi/dev-agents",
11
+ * "range": "^1",
12
+ * "resolvedVersion": "1.0.0",
13
+ * "targets": ["claude", "cursor"],
14
+ * "lastUpdated": "2026-04-24T00:00:00Z"
15
+ * }
16
+ *
17
+ * Privacy invariant: the lockfile MUST NOT contain auth tokens.
18
+ */
19
+
20
+ import { describe, it, before, after } from 'node:test';
21
+ import assert from 'node:assert/strict';
22
+ import { mkdtempSync, writeFileSync, readFileSync, rmSync } from 'node:fs';
23
+ import { tmpdir } from 'node:os';
24
+ import path from 'node:path';
25
+
26
+ // The module under test — will not exist until implement runs.
27
+ // Tests must fail on missing module, not on setup errors.
28
+ import { readLockfile, writeLockfile, validateLockfile } from '../src/lockfile.mjs';
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Helpers
32
+ // ---------------------------------------------------------------------------
33
+
34
+ function makeTmpDir() {
35
+ return mkdtempSync(path.join(tmpdir(), 'das-lockfile-test-'));
36
+ }
37
+
38
+ const VALID_LOCKFILE = {
39
+ $schema: 'https://raw.githubusercontent.com/dalzoubi/dev-agents/v1/schema/lockfile.schema.json',
40
+ source: 'github:dalzoubi/dev-agents',
41
+ range: '^1',
42
+ resolvedVersion: '1.0.0',
43
+ targets: ['claude', 'cursor'],
44
+ lastUpdated: '2026-04-24T00:00:00Z',
45
+ };
46
+
47
+ // ---------------------------------------------------------------------------
48
+ // validateLockfile — pure validation, no I/O
49
+ // ---------------------------------------------------------------------------
50
+
51
+ describe('validateLockfile', () => {
52
+ it('accepts a fully valid lockfile object', () => {
53
+ assert.doesNotThrow(() => validateLockfile(VALID_LOCKFILE));
54
+ });
55
+
56
+ it('rejects when range is missing', () => {
57
+ const bad = { ...VALID_LOCKFILE };
58
+ delete bad.range;
59
+ assert.throws(() => validateLockfile(bad), /range/i);
60
+ });
61
+
62
+ it('rejects when range is not a string', () => {
63
+ assert.throws(() => validateLockfile({ ...VALID_LOCKFILE, range: 1 }), /range/i);
64
+ });
65
+
66
+ it('rejects when targets is not an array', () => {
67
+ assert.throws(() => validateLockfile({ ...VALID_LOCKFILE, targets: 'claude' }), /targets/i);
68
+ });
69
+
70
+ it('rejects when targets contains an unknown value', () => {
71
+ assert.throws(
72
+ () => validateLockfile({ ...VALID_LOCKFILE, targets: ['claude', 'vscode'] }),
73
+ /targets/i,
74
+ );
75
+ });
76
+
77
+ it('rejects when targets is an empty array', () => {
78
+ assert.throws(() => validateLockfile({ ...VALID_LOCKFILE, targets: [] }), /targets/i);
79
+ });
80
+
81
+ it('accepts targets: ["claude"]', () => {
82
+ assert.doesNotThrow(() => validateLockfile({ ...VALID_LOCKFILE, targets: ['claude'] }));
83
+ });
84
+
85
+ it('accepts targets: ["cursor"]', () => {
86
+ assert.doesNotThrow(() => validateLockfile({ ...VALID_LOCKFILE, targets: ['cursor'] }));
87
+ });
88
+
89
+ it('rejects when source is missing', () => {
90
+ const bad = { ...VALID_LOCKFILE };
91
+ delete bad.source;
92
+ assert.throws(() => validateLockfile(bad), /source/i);
93
+ });
94
+
95
+ it('rejects when resolvedVersion is missing', () => {
96
+ const bad = { ...VALID_LOCKFILE };
97
+ delete bad.resolvedVersion;
98
+ assert.throws(() => validateLockfile(bad), /resolvedVersion/i);
99
+ });
100
+
101
+ it('rejects a completely wrong shape (empty object)', () => {
102
+ assert.throws(() => validateLockfile({}), /range|source|targets/i);
103
+ });
104
+
105
+ it('rejects null', () => {
106
+ assert.throws(() => validateLockfile(null));
107
+ });
108
+ });
109
+
110
+ // ---------------------------------------------------------------------------
111
+ // writeLockfile / readLockfile — I/O round-trip
112
+ // ---------------------------------------------------------------------------
113
+
114
+ describe('writeLockfile / readLockfile round-trip', () => {
115
+ let tmpDir;
116
+
117
+ before(() => {
118
+ tmpDir = makeTmpDir();
119
+ });
120
+
121
+ after(() => {
122
+ rmSync(tmpDir, { recursive: true, force: true });
123
+ });
124
+
125
+ it('writes a valid lockfile and reads it back with identical shape', () => {
126
+ writeLockfile(tmpDir, VALID_LOCKFILE);
127
+ const lockfilePath = path.join(tmpDir, '.dev-agents-sync.json');
128
+ const raw = readFileSync(lockfilePath, 'utf8');
129
+ const parsed = JSON.parse(raw);
130
+
131
+ assert.equal(parsed.range, VALID_LOCKFILE.range);
132
+ assert.equal(parsed.resolvedVersion, VALID_LOCKFILE.resolvedVersion);
133
+ assert.deepEqual(parsed.targets, VALID_LOCKFILE.targets);
134
+ assert.equal(parsed.source, VALID_LOCKFILE.source);
135
+ assert.equal(parsed.lastUpdated, VALID_LOCKFILE.lastUpdated);
136
+ });
137
+
138
+ it('readLockfile returns an object equal to what was written', () => {
139
+ writeLockfile(tmpDir, VALID_LOCKFILE);
140
+ const back = readLockfile(tmpDir);
141
+ assert.equal(back.range, VALID_LOCKFILE.range);
142
+ assert.equal(back.resolvedVersion, VALID_LOCKFILE.resolvedVersion);
143
+ assert.deepEqual(back.targets, VALID_LOCKFILE.targets);
144
+ });
145
+
146
+ it('lockfile JSON is deterministic — same inputs produce byte-identical output', () => {
147
+ const dir1 = makeTmpDir();
148
+ const dir2 = makeTmpDir();
149
+ try {
150
+ writeLockfile(dir1, VALID_LOCKFILE);
151
+ writeLockfile(dir2, VALID_LOCKFILE);
152
+ const raw1 = readFileSync(path.join(dir1, '.dev-agents-sync.json'), 'utf8');
153
+ const raw2 = readFileSync(path.join(dir2, '.dev-agents-sync.json'), 'utf8');
154
+ assert.equal(raw1, raw2);
155
+ } finally {
156
+ rmSync(dir1, { recursive: true, force: true });
157
+ rmSync(dir2, { recursive: true, force: true });
158
+ }
159
+ });
160
+
161
+ it('lockfile does NOT contain the auth token', () => {
162
+ const secretToken = 'ghp_SUPERSECRETTOKEN12345';
163
+ // Even if a token was passed into context, writeLockfile should only persist the schema fields
164
+ writeLockfile(tmpDir, VALID_LOCKFILE, { token: secretToken });
165
+ const raw = readFileSync(path.join(tmpDir, '.dev-agents-sync.json'), 'utf8');
166
+ assert.ok(!raw.includes(secretToken), 'lockfile must not contain the auth token');
167
+ });
168
+
169
+ it('readLockfile throws a structured error when the file is missing', () => {
170
+ const emptyDir = makeTmpDir();
171
+ try {
172
+ assert.throws(() => readLockfile(emptyDir), /no lockfile|not found|ENOENT/i);
173
+ } finally {
174
+ rmSync(emptyDir, { recursive: true, force: true });
175
+ }
176
+ });
177
+
178
+ it('readLockfile throws a structured error when the file is invalid JSON', () => {
179
+ const corruptDir = makeTmpDir();
180
+ try {
181
+ writeFileSync(path.join(corruptDir, '.dev-agents-sync.json'), '{not: json}', 'utf8');
182
+ assert.throws(() => readLockfile(corruptDir), /invalid|parse|JSON/i);
183
+ } finally {
184
+ rmSync(corruptDir, { recursive: true, force: true });
185
+ }
186
+ });
187
+
188
+ it('readLockfile throws a structured validation error when schema is wrong', () => {
189
+ const badDir = makeTmpDir();
190
+ try {
191
+ writeFileSync(
192
+ path.join(badDir, '.dev-agents-sync.json'),
193
+ JSON.stringify({ source: 'github:dalzoubi/dev-agents' }),
194
+ 'utf8',
195
+ );
196
+ // Missing required fields — should fail validation
197
+ assert.throws(() => readLockfile(badDir), /range|targets|invalid/i);
198
+ } finally {
199
+ rmSync(badDir, { recursive: true, force: true });
200
+ }
201
+ });
202
+ });
@@ -0,0 +1,190 @@
1
+ /**
2
+ * tests/marker.test.mjs
3
+ *
4
+ * Tests for managed-file marker detection.
5
+ *
6
+ * Marker format (from spec):
7
+ * <!-- managed-by: dev-agents-sync vX.Y.Z -->
8
+ *
9
+ * Rules:
10
+ * - The marker is the first non-empty body line AFTER any YAML frontmatter.
11
+ * - Files with the marker are "managed" — safe to overwrite.
12
+ * - Files without the marker at the same path are "unmanaged" — refused without --force.
13
+ * - Detection works whether the file has frontmatter or not.
14
+ */
15
+
16
+ import { describe, it } from 'node:test';
17
+ import assert from 'node:assert/strict';
18
+
19
+ import { hasMarker, buildMarker, extractMarkerVersion } from '../src/marker.mjs';
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // buildMarker
23
+ // ---------------------------------------------------------------------------
24
+
25
+ describe('buildMarker', () => {
26
+ it('returns the correct marker string for a given version', () => {
27
+ assert.equal(buildMarker('1.0.0'), '<!-- managed-by: dev-agents-sync v1.0.0 -->');
28
+ });
29
+
30
+ it('includes the v prefix', () => {
31
+ const marker = buildMarker('2.3.4');
32
+ assert.ok(marker.includes('v2.3.4'), 'marker must include version with v prefix');
33
+ });
34
+
35
+ it('starts with <!-- and ends with -->', () => {
36
+ const marker = buildMarker('1.0.0');
37
+ assert.ok(marker.startsWith('<!--'));
38
+ assert.ok(marker.endsWith('-->'));
39
+ });
40
+ });
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // hasMarker — file content with frontmatter
44
+ // ---------------------------------------------------------------------------
45
+
46
+ describe('hasMarker — files WITH YAML frontmatter', () => {
47
+ const version = '1.0.0';
48
+
49
+ it('detects marker as first body line after closing ---', () => {
50
+ const content = [
51
+ '---',
52
+ 'name: "define"',
53
+ 'description: "A test agent."',
54
+ '---',
55
+ '<!-- managed-by: dev-agents-sync v1.0.0 -->',
56
+ 'Body text here.',
57
+ ].join('\n');
58
+ assert.ok(hasMarker(content));
59
+ });
60
+
61
+ it('detects marker after frontmatter with blank line between --- and marker', () => {
62
+ // The spec says "first non-empty body line" — blank lines between frontmatter and marker are OK
63
+ const content = [
64
+ '---',
65
+ 'name: "define"',
66
+ '---',
67
+ '',
68
+ '<!-- managed-by: dev-agents-sync v1.0.0 -->',
69
+ 'Body text here.',
70
+ ].join('\n');
71
+ assert.ok(hasMarker(content));
72
+ });
73
+
74
+ it('returns false when marker is absent (file has frontmatter but no marker)', () => {
75
+ const content = [
76
+ '---',
77
+ 'name: "define"',
78
+ 'description: "A test agent."',
79
+ '---',
80
+ 'Body text without marker.',
81
+ ].join('\n');
82
+ assert.ok(!hasMarker(content));
83
+ });
84
+
85
+ it('returns false when a different comment appears first in the body', () => {
86
+ const content = [
87
+ '---',
88
+ 'name: "define"',
89
+ '---',
90
+ '<!-- some other comment -->',
91
+ 'Body text here.',
92
+ ].join('\n');
93
+ assert.ok(!hasMarker(content));
94
+ });
95
+
96
+ it('returns false when the marker appears AFTER other body content (not first body line)', () => {
97
+ const content = [
98
+ '---',
99
+ 'name: "define"',
100
+ '---',
101
+ 'Some text first.',
102
+ '<!-- managed-by: dev-agents-sync v1.0.0 -->',
103
+ ].join('\n');
104
+ assert.ok(!hasMarker(content));
105
+ });
106
+ });
107
+
108
+ // ---------------------------------------------------------------------------
109
+ // hasMarker — file content WITHOUT frontmatter
110
+ // ---------------------------------------------------------------------------
111
+
112
+ describe('hasMarker — files WITHOUT YAML frontmatter', () => {
113
+ it('detects marker as first non-empty line when there is no frontmatter', () => {
114
+ const content = [
115
+ '<!-- managed-by: dev-agents-sync v1.0.0 -->',
116
+ 'Body text here.',
117
+ ].join('\n');
118
+ assert.ok(hasMarker(content));
119
+ });
120
+
121
+ it('returns false when marker is absent and there is no frontmatter', () => {
122
+ const content = [
123
+ '# My Heading',
124
+ 'Some body text.',
125
+ ].join('\n');
126
+ assert.ok(!hasMarker(content));
127
+ });
128
+
129
+ it('detects marker when there are leading blank lines (no frontmatter)', () => {
130
+ const content = [
131
+ '',
132
+ '<!-- managed-by: dev-agents-sync v1.0.0 -->',
133
+ 'Body text here.',
134
+ ].join('\n');
135
+ assert.ok(hasMarker(content));
136
+ });
137
+
138
+ it('returns false for an empty file', () => {
139
+ assert.ok(!hasMarker(''));
140
+ });
141
+ });
142
+
143
+ // ---------------------------------------------------------------------------
144
+ // hasMarker — different marker versions
145
+ // ---------------------------------------------------------------------------
146
+
147
+ describe('hasMarker — version in marker is informational only', () => {
148
+ it('returns true for marker with v0.9.0 (older version is still a valid marker)', () => {
149
+ const content = [
150
+ '---',
151
+ 'name: "define"',
152
+ '---',
153
+ '<!-- managed-by: dev-agents-sync v0.9.0 -->',
154
+ 'Body.',
155
+ ].join('\n');
156
+ assert.ok(hasMarker(content));
157
+ });
158
+
159
+ it('returns true for marker with v9.99.0', () => {
160
+ const content = '<!-- managed-by: dev-agents-sync v9.99.0 -->\nBody.';
161
+ assert.ok(hasMarker(content));
162
+ });
163
+ });
164
+
165
+ // ---------------------------------------------------------------------------
166
+ // extractMarkerVersion
167
+ // ---------------------------------------------------------------------------
168
+
169
+ describe('extractMarkerVersion', () => {
170
+ it('extracts version from a managed file with frontmatter', () => {
171
+ const content = [
172
+ '---',
173
+ 'name: "define"',
174
+ '---',
175
+ '<!-- managed-by: dev-agents-sync v1.3.0 -->',
176
+ 'Body.',
177
+ ].join('\n');
178
+ assert.equal(extractMarkerVersion(content), '1.3.0');
179
+ });
180
+
181
+ it('extracts version from a managed file without frontmatter', () => {
182
+ const content = '<!-- managed-by: dev-agents-sync v2.0.1 -->\nBody.';
183
+ assert.equal(extractMarkerVersion(content), '2.0.1');
184
+ });
185
+
186
+ it('returns null when no marker is present', () => {
187
+ const content = '# No marker here\nBody.';
188
+ assert.equal(extractMarkerVersion(content), null);
189
+ });
190
+ });
@@ -0,0 +1,212 @@
1
+ /**
2
+ * tests/paths.test.mjs
3
+ *
4
+ * Tests for cross-platform path handling.
5
+ *
6
+ * These tests ensure that on Windows (this machine) the CLI uses path.join /
7
+ * path-agnostic logic rather than hardcoded POSIX slashes when writing to the
8
+ * consumer repo filesystem.
9
+ *
10
+ * The FileMap keys from the content repo always use forward slashes (they come
11
+ * from the GitHub release's dist/ tree). The CLI must translate those into
12
+ * platform-appropriate paths before writing.
13
+ */
14
+
15
+ import { describe, it, beforeEach, afterEach } from 'node:test';
16
+ import assert from 'node:assert/strict';
17
+ import {
18
+ mkdtempSync,
19
+ existsSync,
20
+ readFileSync,
21
+ rmSync,
22
+ mkdirSync,
23
+ readdirSync,
24
+ } from 'node:fs';
25
+ import { tmpdir } from 'node:os';
26
+ import path from 'node:path';
27
+ import { fileURLToPath } from 'node:url';
28
+
29
+ import { runInit } from '../src/commands/init.mjs';
30
+ import { hasMarker } from '../src/marker.mjs';
31
+ import { resolveConsumerPath } from '../src/writer.mjs';
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Fixture helpers
35
+ // ---------------------------------------------------------------------------
36
+
37
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
38
+ const FIXTURE_DIR = path.join(__dirname, 'fixtures', 'release-v1.0.0');
39
+
40
+ function buildFixtureFileMap() {
41
+ const map = {};
42
+ for (const target of ['claude', 'cursor']) {
43
+ const targetDir = path.join(FIXTURE_DIR, target);
44
+ if (!existsSync(targetDir)) continue;
45
+ collectFiles(targetDir, targetDir, map, target);
46
+ }
47
+ return map;
48
+ }
49
+
50
+ function collectFiles(baseDir, currentDir, map, prefix) {
51
+ const entries = readdirSync(currentDir, { withFileTypes: true });
52
+ for (const entry of entries) {
53
+ const full = path.join(currentDir, entry.name);
54
+ if (entry.isDirectory()) {
55
+ collectFiles(baseDir, full, map, prefix);
56
+ } else {
57
+ const rel = path.relative(baseDir, full).replace(/\\/g, '/');
58
+ map[`${prefix}/${rel}`] = readFileSync(full, 'utf8');
59
+ }
60
+ }
61
+ }
62
+
63
+ function makeFixtureFetcher() {
64
+ return async () => buildFixtureFileMap();
65
+ }
66
+
67
+ // ---------------------------------------------------------------------------
68
+ // Helpers
69
+ // ---------------------------------------------------------------------------
70
+
71
+ function makeTmpDir() {
72
+ return mkdtempSync(path.join(tmpdir(), 'das-paths-test-'));
73
+ }
74
+
75
+ // ---------------------------------------------------------------------------
76
+ // resolveConsumerPath — pure path translation unit
77
+ // ---------------------------------------------------------------------------
78
+
79
+ describe('resolveConsumerPath', () => {
80
+ it('translates "claude/agents/define.md" to .claude/agents/define.md', () => {
81
+ const consumerRoot = '/some/repo';
82
+ const result = resolveConsumerPath(consumerRoot, 'claude/agents/define.md');
83
+ // Must use path.join so it's platform-correct
84
+ const expected = path.join(consumerRoot, '.claude', 'agents', 'define.md');
85
+ assert.equal(result, expected);
86
+ });
87
+
88
+ it('translates "claude/commands/preflight.md" to .claude/commands/preflight.md', () => {
89
+ const consumerRoot = '/some/repo';
90
+ const result = resolveConsumerPath(consumerRoot, 'claude/commands/preflight.md');
91
+ const expected = path.join(consumerRoot, '.claude', 'commands', 'preflight.md');
92
+ assert.equal(result, expected);
93
+ });
94
+
95
+ it('translates "cursor/rules/define.mdc" to .cursor/rules/define.mdc', () => {
96
+ const consumerRoot = '/some/repo';
97
+ const result = resolveConsumerPath(consumerRoot, 'cursor/rules/define.mdc');
98
+ const expected = path.join(consumerRoot, '.cursor', 'rules', 'define.mdc');
99
+ assert.equal(result, expected);
100
+ });
101
+
102
+ it('works with a Windows-style absolute consumer root path', () => {
103
+ // Simulate a Windows-style path
104
+ const consumerRoot = 'C:\\Users\\developer\\projects\\my-app';
105
+ const result = resolveConsumerPath(consumerRoot, 'claude/agents/define.md');
106
+ // Must not throw and must produce a valid path
107
+ assert.ok(typeof result === 'string', 'must return a string');
108
+ assert.ok(result.length > 0);
109
+ // Must end with the correct filename
110
+ assert.ok(result.endsWith('define.md'), `expected path ending with define.md, got: ${result}`);
111
+ });
112
+
113
+ it('does not produce double-dot or traversal sequences', () => {
114
+ const consumerRoot = path.join(tmpdir(), 'consumer');
115
+ const result = resolveConsumerPath(consumerRoot, 'claude/agents/define.md');
116
+ assert.ok(!result.includes('..'), `path must not contain traversal, got: ${result}`);
117
+ });
118
+
119
+ it('does not allow path traversal from a malicious FileMap key', () => {
120
+ const consumerRoot = path.join(tmpdir(), 'consumer');
121
+ // Attempt path traversal via the FileMap key
122
+ assert.throws(
123
+ () => resolveConsumerPath(consumerRoot, '../../../etc/passwd'),
124
+ /traversal|invalid|outside/i,
125
+ );
126
+ });
127
+ });
128
+
129
+ // ---------------------------------------------------------------------------
130
+ // init writes files with platform-correct paths
131
+ // ---------------------------------------------------------------------------
132
+
133
+ describe('init — platform-correct file paths', () => {
134
+ let consumerDir;
135
+
136
+ beforeEach(() => {
137
+ consumerDir = makeTmpDir();
138
+ });
139
+
140
+ afterEach(() => {
141
+ rmSync(consumerDir, { recursive: true, force: true });
142
+ });
143
+
144
+ it('writes .claude/agents/define.md using the OS path separator', async () => {
145
+ await runInit(consumerDir, {
146
+ targets: ['claude'],
147
+ fetcher: makeFixtureFetcher(),
148
+ availableTags: ['v1.0.0', 'v1.2.0'],
149
+ });
150
+
151
+ // The file must exist using the OS path (path.join does the right thing)
152
+ const expectedPath = path.join(consumerDir, '.claude', 'agents', 'define.md');
153
+ assert.ok(
154
+ existsSync(expectedPath),
155
+ `file must exist at OS-correct path: ${expectedPath}`,
156
+ );
157
+ });
158
+
159
+ it('writes .cursor/rules/define.mdc using the OS path separator', async () => {
160
+ await runInit(consumerDir, {
161
+ targets: ['cursor'],
162
+ fetcher: makeFixtureFetcher(),
163
+ availableTags: ['v1.0.0', 'v1.2.0'],
164
+ });
165
+
166
+ const expectedPath = path.join(consumerDir, '.cursor', 'rules', 'define.mdc');
167
+ assert.ok(
168
+ existsSync(expectedPath),
169
+ `file must exist at OS-correct path: ${expectedPath}`,
170
+ );
171
+ });
172
+
173
+ it('written file content is readable via path.join on this platform', async () => {
174
+ await runInit(consumerDir, {
175
+ targets: ['claude'],
176
+ fetcher: makeFixtureFetcher(),
177
+ availableTags: ['v1.0.0', 'v1.2.0'],
178
+ });
179
+
180
+ const filePath = path.join(consumerDir, '.claude', 'agents', 'define.md');
181
+ let content;
182
+ assert.doesNotThrow(() => {
183
+ content = readFileSync(filePath, 'utf8');
184
+ });
185
+ assert.ok(hasMarker(content), 'written file must have the managed-by marker');
186
+ });
187
+ });
188
+
189
+ // ---------------------------------------------------------------------------
190
+ // Lockfile path
191
+ // ---------------------------------------------------------------------------
192
+
193
+ describe('lockfile path — platform correct', () => {
194
+ it('lockfile is written at <consumerRoot>/.dev-agents-sync.json using path.join', async () => {
195
+ const consumerDir = makeTmpDir();
196
+ try {
197
+ await runInit(consumerDir, {
198
+ targets: ['claude'],
199
+ fetcher: makeFixtureFetcher(),
200
+ availableTags: ['v1.0.0', 'v1.2.0'],
201
+ });
202
+
203
+ const expectedLockfilePath = path.join(consumerDir, '.dev-agents-sync.json');
204
+ assert.ok(
205
+ existsSync(expectedLockfilePath),
206
+ `lockfile must exist at OS-correct path: ${expectedLockfilePath}`,
207
+ );
208
+ } finally {
209
+ rmSync(consumerDir, { recursive: true, force: true });
210
+ }
211
+ });
212
+ });