@memnexus-ai/mx-agent-cli 0.1.157 → 0.1.158

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,12 @@
1
+ /**
2
+ * Tests for the leader-session start guard (Platform v2.30, #3946).
3
+ *
4
+ * Covers: lock write/format, refusal on a live lock (real child process whose
5
+ * cwd is the worktree), stale-lock replacement (dead pid), the fail-open path
6
+ * (indeterminate probe), and the unlock command. The "teammates never hit this
7
+ * lock" property is process-design (Task-tool sub-agents are not spawned via
8
+ * `mx-agent start`), so it is asserted as a documented invariant, not a runtime
9
+ * check — see the final describe block.
10
+ */
11
+ export {};
12
+ //# sourceMappingURL=leader-lock.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"leader-lock.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/leader-lock.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG"}
@@ -0,0 +1,293 @@
1
+ /**
2
+ * Tests for the leader-session start guard (Platform v2.30, #3946).
3
+ *
4
+ * Covers: lock write/format, refusal on a live lock (real child process whose
5
+ * cwd is the worktree), stale-lock replacement (dead pid), the fail-open path
6
+ * (indeterminate probe), and the unlock command. The "teammates never hit this
7
+ * lock" property is process-design (Task-tool sub-agents are not spawned via
8
+ * `mx-agent start`), so it is asserted as a documented invariant, not a runtime
9
+ * check — see the final describe block.
10
+ */
11
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
12
+ import { spawn, spawnSync } from 'child_process';
13
+ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs';
14
+ // Fail-open simulation: a hoisted flag lets a single test hide `/proc` from
15
+ // existsSync (simulating a platform with no readable /proc) while every other
16
+ // fs call — and every other test — uses the real implementation.
17
+ const procHider = vi.hoisted(() => ({ hide: false }));
18
+ vi.mock('fs', async (importActual) => {
19
+ const actual = await importActual();
20
+ return {
21
+ ...actual,
22
+ default: actual,
23
+ existsSync: (p) => procHider.hide && p === '/proc' ? false : actual.existsSync(p),
24
+ };
25
+ });
26
+ import { tmpdir } from 'os';
27
+ import { join } from 'path';
28
+ import { LOCK_FILENAME, MAX_LOCK_BYTES, checkLiveness, claimLock, formatLockEvidence, lockPath, probePid, readLock, removeLock, sanitizeField, updateChildPid, writeLock, } from '../lib/leader-lock.js';
29
+ const hasProc = existsSync('/proc');
30
+ let wt;
31
+ function makeLock(over = {}) {
32
+ return {
33
+ team: 'platform',
34
+ worktreePath: wt,
35
+ wrapperPid: process.pid,
36
+ childPid: null,
37
+ startedAt: new Date().toISOString(),
38
+ version: '2.30.0',
39
+ ...over,
40
+ };
41
+ }
42
+ /** A pid guaranteed dead: spawn `true`, wait for it to exit, reuse its pid. */
43
+ function deadPid() {
44
+ const r = spawnSync('true', []);
45
+ // r.pid is the pid of the now-exited process. It is overwhelmingly unlikely
46
+ // to be immediately recycled to a process whose cwd is our fixture worktree.
47
+ return r.pid ?? 999999;
48
+ }
49
+ beforeEach(() => {
50
+ wt = mkdtempSync(join(tmpdir(), 'mx-leaderlock-'));
51
+ });
52
+ afterEach(() => {
53
+ rmSync(wt, { recursive: true, force: true });
54
+ });
55
+ // ── Lock write / read / format ───────────────────────────────────────────────
56
+ describe('lock write/read/format', () => {
57
+ it('writes a JSON lock with all required fields at <worktree>/.mx-leader.lock', () => {
58
+ const lock = makeLock({ childPid: 1234 });
59
+ writeLock(wt, lock);
60
+ expect(lockPath(wt)).toBe(join(wt, LOCK_FILENAME));
61
+ const parsed = JSON.parse(readFileSync(lockPath(wt), 'utf-8'));
62
+ expect(parsed.team).toBe('platform');
63
+ expect(parsed.worktreePath).toBe(wt);
64
+ expect(parsed.wrapperPid).toBe(process.pid);
65
+ expect(parsed.childPid).toBe(1234);
66
+ expect(parsed.version).toBe('2.30.0');
67
+ expect(typeof parsed.startedAt).toBe('string');
68
+ });
69
+ it('readLock reports absent when no lock exists', () => {
70
+ const r = readLock(wt);
71
+ expect(r.present).toBe(false);
72
+ expect(r.lock).toBeNull();
73
+ });
74
+ it('readLock reports malformed (present, null lock) on invalid JSON', () => {
75
+ writeFileSync(lockPath(wt), '{ not json', 'utf-8');
76
+ const r = readLock(wt);
77
+ expect(r.present).toBe(true);
78
+ expect(r.lock).toBeNull();
79
+ expect(r.parseError).toBeTruthy();
80
+ });
81
+ it('readLock rejects a JSON object missing required fields', () => {
82
+ writeFileSync(lockPath(wt), JSON.stringify({ team: 'x' }), 'utf-8');
83
+ const r = readLock(wt);
84
+ expect(r.present).toBe(true);
85
+ expect(r.lock).toBeNull();
86
+ });
87
+ it('updateChildPid patches the child pid in place', () => {
88
+ writeLock(wt, makeLock());
89
+ updateChildPid(wt, 5555);
90
+ expect(readLock(wt).lock?.childPid).toBe(5555);
91
+ });
92
+ it('formatLockEvidence includes pids, cwd/started-at, and version', () => {
93
+ const lock = makeLock({ childPid: 4242 });
94
+ const evidence = formatLockEvidence(lock);
95
+ expect(evidence).toContain(String(process.pid));
96
+ expect(evidence).toContain('4242');
97
+ expect(evidence).toContain(lock.startedAt);
98
+ expect(evidence).toContain(wt);
99
+ expect(evidence).toContain('2.30.0');
100
+ });
101
+ });
102
+ // ── Atomic claim (Should-Fix 1) ──────────────────────────────────────────────
103
+ describe('claimLock — atomic mutual exclusion', () => {
104
+ it('a single exclusive create succeeds and writes the lock', () => {
105
+ const r = claimLock(wt, makeLock());
106
+ expect(r.ok).toBe(true);
107
+ expect(readLock(wt).present).toBe(true);
108
+ });
109
+ it('two concurrent claims for the same worktree → exactly one wins (EEXIST for the loser)', () => {
110
+ // O_CREAT|O_EXCL is atomic on a local FS: back-to-back claims model the race
111
+ // (the second observes the first's file). Exactly one ok, one reason=exists.
112
+ const a = claimLock(wt, makeLock({ team: 'A' }));
113
+ const b = claimLock(wt, makeLock({ team: 'B' }));
114
+ const wins = [a, b].filter((r) => r.ok).length;
115
+ const losers = [a, b].filter((r) => !r.ok && r.reason === 'exists').length;
116
+ expect(wins).toBe(1);
117
+ expect(losers).toBe(1);
118
+ // The winner's content is intact — the loser did not overwrite it.
119
+ expect(readLock(wt).lock?.team).toBe('A');
120
+ });
121
+ it('claim after removal succeeds (stale-replacement path)', () => {
122
+ expect(claimLock(wt, makeLock()).ok).toBe(true);
123
+ expect(claimLock(wt, makeLock()).ok).toBe(false); // EEXIST
124
+ removeLock(wt);
125
+ expect(claimLock(wt, makeLock()).ok).toBe(true); // room freed
126
+ });
127
+ });
128
+ // ── Evidence sanitization (Should-Fix 2) ─────────────────────────────────────
129
+ describe('sanitizeField / formatLockEvidence — terminal-escape hardening', () => {
130
+ it('strips C0/C1/DEL control chars (OSC/CSI injection) from a crafted field', () => {
131
+ // Crafted team: OSC tab-title rewrite + CSI erase-line + a DEL char.
132
+ const evil = '\x1b]0;PWNED\x07\x1b[2Kplatform\x7f\x9b31m';
133
+ const clean = sanitizeField(evil);
134
+ // No ESC (0x1B), BEL (0x07), DEL (0x7F), or C1 (0x9B) survive.
135
+ expect(clean).not.toMatch(new RegExp("[\u0000-\u001F\u007F-\u009F]"));
136
+ expect(clean).toContain('platform');
137
+ });
138
+ it('caps field length', () => {
139
+ const long = 'x'.repeat(5000);
140
+ const out = sanitizeField(long, 256);
141
+ expect(out.length).toBeLessThanOrEqual(256 + '…(truncated)'.length);
142
+ expect(out).toContain('truncated');
143
+ });
144
+ it('formatLockEvidence output for an escape-bearing lock carries no control bytes', () => {
145
+ const lock = makeLock({
146
+ team: '\x1b]0;evil\x07',
147
+ version: 'v\x1b[31m2.30',
148
+ startedAt: '2026-01-01\x08T00:00',
149
+ worktreePath: wt + '\x1b[2K',
150
+ });
151
+ const evidence = formatLockEvidence(lock);
152
+ // Evidence is intentionally multi-line (\n is our own separator); assert no
153
+ // control chars OTHER than newline survive — all attacker escapes are stripped.
154
+ expect(evidence).not.toMatch(new RegExp("[\u0000-\u0009\u000B-\u001F\u007F-\u009F]"));
155
+ });
156
+ });
157
+ // ── Oversize lock file (INFO: resource-exhaustion guard) ──────────────────────
158
+ describe('readLock — size guard', () => {
159
+ it('treats an oversize lock file as malformed (fail open), never reads it', () => {
160
+ // Write a lock file larger than the cap.
161
+ writeFileSync(lockPath(wt), 'x'.repeat(MAX_LOCK_BYTES + 1), 'utf-8');
162
+ const r = readLock(wt);
163
+ expect(r.present).toBe(true);
164
+ expect(r.lock).toBeNull();
165
+ expect(r.parseError).toMatch(/too large/);
166
+ });
167
+ });
168
+ // ── Refusal on a live lock (real child process) ──────────────────────────────
169
+ describe('checkLiveness — live lock', () => {
170
+ let child;
171
+ afterEach(() => {
172
+ if (child && child.pid) {
173
+ try {
174
+ process.kill(child.pid, 'SIGKILL');
175
+ }
176
+ catch { /* already gone */ }
177
+ }
178
+ child = undefined;
179
+ });
180
+ it.skipIf(!hasProc)('detects a live child whose cwd is the worktree → state=live', async () => {
181
+ // Spawn a real, long-lived process with cwd set to the fixture worktree so
182
+ // the /proc/<pid>/cwd discriminator resolves to this worktree.
183
+ child = spawn(process.execPath, ['-e', 'setInterval(() => {}, 1000)'], {
184
+ cwd: wt,
185
+ stdio: 'ignore',
186
+ });
187
+ // Give the OS a moment to establish /proc/<pid>.
188
+ await new Promise((r) => setTimeout(r, 100));
189
+ const pid = child.pid;
190
+ // Probe directly.
191
+ const probe = probePid(pid, wt);
192
+ expect(probe.state).toBe('live');
193
+ expect(probe.matchedBy).toBe('cwd');
194
+ // And via the aggregate check (as the child pid on the lock).
195
+ const lock = makeLock({ wrapperPid: deadPid(), childPid: pid });
196
+ writeLock(wt, lock);
197
+ const result = checkLiveness(lock, wt);
198
+ expect(result.state).toBe('live');
199
+ expect(result.livePids).toContain(pid);
200
+ });
201
+ });
202
+ // ── Stale-lock replacement (dead pids) ───────────────────────────────────────
203
+ describe('checkLiveness — stale lock', () => {
204
+ it.skipIf(!hasProc)('reports stale when all recorded pids are dead', () => {
205
+ const lock = makeLock({ wrapperPid: deadPid(), childPid: deadPid() });
206
+ writeLock(wt, lock);
207
+ const result = checkLiveness(lock, wt);
208
+ expect(result.state).toBe('stale');
209
+ expect(result.livePids).toHaveLength(0);
210
+ });
211
+ it.skipIf(!hasProc)('a live process in a DIFFERENT worktree is not a live match (mismatch → dead)', async () => {
212
+ const other = mkdtempSync(join(tmpdir(), 'mx-other-'));
213
+ const child = spawn(process.execPath, ['-e', 'setInterval(() => {}, 1000)'], {
214
+ cwd: other,
215
+ stdio: 'ignore',
216
+ });
217
+ try {
218
+ await new Promise((r) => setTimeout(r, 100));
219
+ const probe = probePid(child.pid, wt);
220
+ expect(probe.state).toBe('dead');
221
+ expect(probe.reason).toMatch(/different worktree/);
222
+ }
223
+ finally {
224
+ if (child.pid) {
225
+ try {
226
+ process.kill(child.pid, 'SIGKILL');
227
+ }
228
+ catch { /* gone */ }
229
+ }
230
+ rmSync(other, { recursive: true, force: true });
231
+ }
232
+ });
233
+ });
234
+ // ── Fail-open path (indeterminate) ───────────────────────────────────────────
235
+ describe('checkLiveness — fail open', () => {
236
+ afterEach(() => {
237
+ vi.restoreAllMocks();
238
+ });
239
+ it('probePid returns indeterminate when /proc is unavailable (fail open)', () => {
240
+ procHider.hide = true;
241
+ try {
242
+ // Aggregate must fail open (indeterminate), not declare stale/absent.
243
+ const lock = makeLock({ wrapperPid: process.pid, childPid: null });
244
+ expect(probePid(process.pid, wt).state).toBe('indeterminate');
245
+ expect(checkLiveness(lock, wt).state).toBe('indeterminate');
246
+ }
247
+ finally {
248
+ procHider.hide = false;
249
+ }
250
+ });
251
+ it('an invalid pid is treated as dead, not indeterminate', () => {
252
+ expect(probePid(-1, wt).state).toBe('dead');
253
+ expect(probePid(0, wt).state).toBe('dead');
254
+ });
255
+ it('a lock referencing only dead/invalid pids never reports live', () => {
256
+ const lock = makeLock({ wrapperPid: 0, childPid: null });
257
+ const result = checkLiveness(lock, wt);
258
+ expect(result.state).not.toBe('live');
259
+ });
260
+ });
261
+ // ── removeLock ───────────────────────────────────────────────────────────────
262
+ describe('removeLock', () => {
263
+ it('removes an existing lock and is idempotent', () => {
264
+ writeLock(wt, makeLock());
265
+ expect(readLock(wt).present).toBe(true);
266
+ removeLock(wt);
267
+ expect(readLock(wt).present).toBe(false);
268
+ // Second call must not throw.
269
+ expect(() => removeLock(wt)).not.toThrow();
270
+ });
271
+ });
272
+ // ── Process-design invariant: teammates never hit this lock ───────────────────
273
+ describe('invariant — sub-agent teammates never touch the leader lock', () => {
274
+ // Sub-agent teammates are spawned via the Task tool INSIDE a running leader
275
+ // session, never via `mx-agent start`. Only `mx-agent start` claims/checks the
276
+ // lock (see start.ts guardAndClaimLeaderLock + claude.ts launchClaudeSession).
277
+ // There is therefore no runtime code path from a teammate to the lock — this is
278
+ // a process-design property, asserted here as documentation + a guard against a
279
+ // future refactor wiring lock code into a shared launch path.
280
+ it('lock claim/read is only reachable from the start command surface', () => {
281
+ const startSrc = readFileSync(join(__dirname, '..', 'commands', 'start.ts'), 'utf-8');
282
+ const claudeSrc = readFileSync(join(__dirname, '..', 'lib', 'claude.ts'), 'utf-8');
283
+ // The lock is claimed atomically from the default launch path.
284
+ expect(startSrc).toContain('guardAndClaimLeaderLock');
285
+ expect(startSrc).toContain('claimLock');
286
+ // The launcher patches childPid and removes the lock, but does not claim it.
287
+ expect(claudeSrc).toContain('updateChildPid');
288
+ // agent-session (the SDK/teammate-adjacent path) must not import the lock.
289
+ const agentSrc = readFileSync(join(__dirname, '..', 'lib', 'agent-session.ts'), 'utf-8');
290
+ expect(agentSrc).not.toContain('leader-lock');
291
+ });
292
+ });
293
+ //# sourceMappingURL=leader-lock.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"leader-lock.test.js","sourceRoot":"","sources":["../../src/__tests__/leader-lock.test.ts"],"names":[],"mappings":"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"}
@@ -5,6 +5,9 @@
5
5
  * child process env under CLAUDE_TEAM_NAME so that v2.10/v2.11 hook telemetry
6
6
  * can attribute data to the consumer team. See issue #3083 and dev-log
7
7
  * product/dev-log/platform-team/v2.11-hook-telemetry-and-team-name-inspection.md.
8
+ *
9
+ * v2.30 (#3946): launchClaudeSession switched spawnSync → async spawn to expose
10
+ * the child pid for the leader-session lock. The mock now models the async child.
8
11
  */
9
12
  export {};
10
13
  //# sourceMappingURL=team-name-propagation.test.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"team-name-propagation.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/team-name-propagation.test.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG"}
1
+ {"version":3,"file":"team-name-propagation.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/team-name-propagation.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG"}
@@ -5,64 +5,122 @@
5
5
  * child process env under CLAUDE_TEAM_NAME so that v2.10/v2.11 hook telemetry
6
6
  * can attribute data to the consumer team. See issue #3083 and dev-log
7
7
  * product/dev-log/platform-team/v2.11-hook-telemetry-and-team-name-inspection.md.
8
+ *
9
+ * v2.30 (#3946): launchClaudeSession switched spawnSync → async spawn to expose
10
+ * the child pid for the leader-session lock. The mock now models the async child.
8
11
  */
9
12
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
10
- import { readFileSync } from 'fs';
13
+ import { mkdtempSync, readFileSync, rmSync } from 'fs';
14
+ import { tmpdir } from 'os';
11
15
  import { dirname, join } from 'path';
12
16
  import { fileURLToPath } from 'url';
17
+ import { readLock, writeLock } from '../lib/leader-lock.js';
13
18
  const __dirname = dirname(fileURLToPath(import.meta.url));
14
19
  // -- Mock child_process so launchClaudeSession can be tested in isolation. ---
15
- // The mock captures the env dict passed to spawnSync and returns a no-op result
16
- // so the function returns without exiting the test process.
17
- const spawnSyncMock = vi.fn();
20
+ // The mock captures the env dict passed to spawn and returns a fake child that
21
+ // exits so the function resolves without exiting the test process.
22
+ const spawnMock = vi.fn();
18
23
  vi.mock('child_process', () => ({
19
- spawnSync: (...args) => spawnSyncMock(...args),
24
+ spawn: (...args) => spawnMock(...args),
25
+ spawnSync: vi.fn(),
20
26
  execSync: vi.fn(),
21
27
  }));
28
+ /** Fake child process that reports a pid and fires the given exit asynchronously. */
29
+ function makeFakeChild(code = 0, signal = null) {
30
+ return {
31
+ pid: 4242,
32
+ once(event, cb) {
33
+ if (event === 'exit')
34
+ setImmediate(() => cb(code, signal));
35
+ // 'error' never fires in these fakes.
36
+ },
37
+ };
38
+ }
22
39
  describe('launchClaudeSession — CLAUDE_TEAM_NAME propagation', () => {
40
+ let wt;
23
41
  beforeEach(() => {
24
- spawnSyncMock.mockReset();
25
- spawnSyncMock.mockReturnValue({ status: 0, error: undefined });
42
+ wt = mkdtempSync(join(tmpdir(), 'mx-launch-'));
43
+ spawnMock.mockReset();
44
+ spawnMock.mockReturnValue(makeFakeChild());
26
45
  });
27
46
  afterEach(() => {
28
47
  vi.restoreAllMocks();
48
+ rmSync(wt, { recursive: true, force: true });
29
49
  });
30
50
  it('passes CLAUDE_TEAM_NAME into the child env when spawning claude', async () => {
31
51
  const { launchClaudeSession } = await import('../lib/claude.js');
32
- launchClaudeSession({
33
- worktreePath: '/tmp/wt',
52
+ await launchClaudeSession({
53
+ worktreePath: wt,
34
54
  worktreeName: 'wt-test',
35
55
  branch: 'main',
36
56
  slot: 0,
37
57
  initialPrompt: 'hello',
38
58
  teamName: 'platform-team',
39
59
  });
40
- expect(spawnSyncMock).toHaveBeenCalledTimes(1);
41
- const [, , opts] = spawnSyncMock.mock.calls[0];
60
+ expect(spawnMock).toHaveBeenCalledTimes(1);
61
+ const [, , opts] = spawnMock.mock.calls[0];
42
62
  expect(opts.env).toBeDefined();
43
63
  expect(opts.env.CLAUDE_TEAM_NAME).toBe('platform-team');
44
64
  // Sanity: existing worktree vars still present
45
- expect(opts.env.CLAUDE_WORKTREE_PATH).toBe('/tmp/wt');
65
+ expect(opts.env.CLAUDE_WORKTREE_PATH).toBe(wt);
46
66
  expect(opts.env.CLAUDE_WORKTREE_NAME).toBe('wt-test');
47
67
  expect(opts.env.CLAUDE_WORKTREE_SLOT).toBe('0');
48
68
  expect(opts.env.CLAUDE_WORKTREE_BRANCH).toBe('main');
49
69
  });
50
70
  it('passes an empty-string branch through but still sets CLAUDE_TEAM_NAME', async () => {
51
71
  const { launchClaudeSession } = await import('../lib/claude.js');
52
- launchClaudeSession({
53
- worktreePath: '/tmp/wt',
72
+ await launchClaudeSession({
73
+ worktreePath: wt,
54
74
  worktreeName: 'wt-test',
55
75
  branch: null,
56
76
  slot: 2,
57
77
  initialPrompt: 'hi',
58
78
  teamName: 'extraction-team',
59
79
  });
60
- const [, , opts] = spawnSyncMock.mock.calls[0];
80
+ const [, , opts] = spawnMock.mock.calls[0];
61
81
  expect(opts.env.CLAUDE_TEAM_NAME).toBe('extraction-team');
62
82
  expect(opts.env.CLAUDE_WORKTREE_BRANCH).toBe('');
63
83
  expect(opts.env.CLAUDE_WORKTREE_SLOT).toBe('2');
64
84
  });
65
85
  });
86
+ describe('launchClaudeSession — leader-lock cleanup on exit (#3946 Should-Fix 3)', () => {
87
+ let wt;
88
+ beforeEach(() => {
89
+ wt = mkdtempSync(join(tmpdir(), 'mx-launch-exit-'));
90
+ spawnMock.mockReset();
91
+ });
92
+ afterEach(() => {
93
+ vi.restoreAllMocks();
94
+ rmSync(wt, { recursive: true, force: true });
95
+ });
96
+ function seedLock() {
97
+ const lock = {
98
+ team: 'platform', worktreePath: wt, wrapperPid: process.pid,
99
+ childPid: null, startedAt: new Date().toISOString(), version: 't',
100
+ };
101
+ writeLock(wt, lock);
102
+ expect(readLock(wt).present).toBe(true);
103
+ }
104
+ it('removes the lock on a NON-ZERO child exit (process.exit path skips finally)', async () => {
105
+ seedLock();
106
+ spawnMock.mockReturnValue(makeFakeChild(3, null)); // child exits code 3
107
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => {
108
+ throw new Error('process.exit called');
109
+ }));
110
+ const { launchClaudeSession } = await import('../lib/claude.js');
111
+ await expect(launchClaudeSession({ worktreePath: wt, worktreeName: 'w', branch: 'main', slot: 0, initialPrompt: 'x', teamName: 'platform' })).rejects.toThrow('process.exit called');
112
+ expect(exitSpy).toHaveBeenCalledWith(3);
113
+ // The explicit removeLock before process.exit must have cleaned the lock.
114
+ expect(readLock(wt).present).toBe(false);
115
+ });
116
+ it('removes the lock on a clean (zero) exit via finally', async () => {
117
+ seedLock();
118
+ spawnMock.mockReturnValue(makeFakeChild(0, null));
119
+ const { launchClaudeSession } = await import('../lib/claude.js');
120
+ await launchClaudeSession({ worktreePath: wt, worktreeName: 'w', branch: 'main', slot: 0, initialPrompt: 'x', teamName: 'platform' });
121
+ expect(readLock(wt).present).toBe(false);
122
+ });
123
+ });
66
124
  describe('launchAgentSession — CLAUDE_TEAM_NAME propagation (source-level)', () => {
67
125
  // launchAgentSession drives an interactive Agent SDK loop with stdin raw
68
126
  // mode, a MessageBus, and an async iterator. Full behavioral testing would
@@ -1 +1 @@
1
- {"version":3,"file":"team-name-propagation.test.js","sourceRoot":"","sources":["../../src/__tests__/team-name-propagation.test.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"team-name-propagation.test.js","sourceRoot":"","sources":["../../src/__tests__/team-name-propagation.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Tests for `mx-agent unlock <team>` (Platform v2.30, #3946).
3
+ *
4
+ * The command resolves the team worktree, enforces a containment check (the
5
+ * resolved worktree must live under <projectRoot>/<worktreeDir>), prints lock
6
+ * evidence, and removes the lock after confirmation. We mock worktree/project
7
+ * resolution so the test targets a temp fixture, and assert removal, the
8
+ * confirmation gate, and the containment guard.
9
+ */
10
+ export {};
11
+ //# sourceMappingURL=unlock.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unlock.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/unlock.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG"}
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Tests for `mx-agent unlock <team>` (Platform v2.30, #3946).
3
+ *
4
+ * The command resolves the team worktree, enforces a containment check (the
5
+ * resolved worktree must live under <projectRoot>/<worktreeDir>), prints lock
6
+ * evidence, and removes the lock after confirmation. We mock worktree/project
7
+ * resolution so the test targets a temp fixture, and assert removal, the
8
+ * confirmation gate, and the containment guard.
9
+ */
10
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
11
+ import { mkdirSync, mkdtempSync, rmSync } from 'fs';
12
+ import { tmpdir } from 'os';
13
+ import { join } from 'path';
14
+ import { readLock, writeLock } from '../lib/leader-lock.js';
15
+ let projectRoot;
16
+ let wt;
17
+ // Overridable per-test: what findWorktreePath returns (defaults to the in-tree wt).
18
+ let resolvedWorktree;
19
+ vi.mock('../lib/worktree.js', () => ({
20
+ findProjectRoot: () => projectRoot,
21
+ findWorktreePath: () => resolvedWorktree,
22
+ }));
23
+ vi.mock('../lib/project-config.js', () => ({
24
+ loadProjectConfig: () => ({}),
25
+ resolveProjectConfig: () => ({ worktreeDir: '.worktrees' }),
26
+ }));
27
+ function makeLock() {
28
+ return {
29
+ team: 'platform',
30
+ worktreePath: wt,
31
+ wrapperPid: 0, // invalid → never "live", so no scary output paths break the test
32
+ childPid: null,
33
+ startedAt: new Date().toISOString(),
34
+ version: '2.30.0',
35
+ };
36
+ }
37
+ beforeEach(() => {
38
+ projectRoot = mkdtempSync(join(tmpdir(), 'mx-unlock-root-'));
39
+ wt = join(projectRoot, '.worktrees', 'platform');
40
+ mkdirSync(wt, { recursive: true });
41
+ resolvedWorktree = wt;
42
+ vi.spyOn(console, 'log').mockImplementation(() => { });
43
+ vi.spyOn(console, 'error').mockImplementation(() => { });
44
+ });
45
+ afterEach(() => {
46
+ vi.restoreAllMocks();
47
+ rmSync(projectRoot, { recursive: true, force: true });
48
+ });
49
+ describe('runUnlock', () => {
50
+ it('removes the lock when --yes is passed (non-interactive)', async () => {
51
+ writeLock(wt, makeLock());
52
+ expect(readLock(wt).present).toBe(true);
53
+ const { runUnlock } = await import('../commands/unlock.js');
54
+ await runUnlock({ name: 'platform', yes: true });
55
+ expect(readLock(wt).present).toBe(false);
56
+ });
57
+ it('no-ops cleanly when no lock is present', async () => {
58
+ const { runUnlock } = await import('../commands/unlock.js');
59
+ await expect(runUnlock({ name: 'platform', yes: true })).resolves.toBeUndefined();
60
+ expect(readLock(wt).present).toBe(false);
61
+ });
62
+ it('refuses without --yes when stdin is not a TTY (leaves lock in place)', async () => {
63
+ writeLock(wt, makeLock());
64
+ const isTTY = process.stdin.isTTY;
65
+ Object.defineProperty(process.stdin, 'isTTY', { value: false, configurable: true });
66
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => {
67
+ throw new Error('process.exit called');
68
+ }));
69
+ try {
70
+ const { runUnlock } = await import('../commands/unlock.js');
71
+ await expect(runUnlock({ name: 'platform', yes: false })).rejects.toThrow('process.exit called');
72
+ expect(exitSpy).toHaveBeenCalledWith(1);
73
+ // Lock must remain — refusal must not remove it.
74
+ expect(readLock(wt).present).toBe(true);
75
+ }
76
+ finally {
77
+ Object.defineProperty(process.stdin, 'isTTY', { value: isTTY, configurable: true });
78
+ }
79
+ });
80
+ it('refuses to operate on a worktree outside the worktrees directory (containment guard)', async () => {
81
+ // Point resolution at a path OUTSIDE <projectRoot>/.worktrees and seed a lock there.
82
+ const outside = mkdtempSync(join(tmpdir(), 'mx-unlock-outside-'));
83
+ resolvedWorktree = outside;
84
+ writeLock(outside, makeLock());
85
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => {
86
+ throw new Error('process.exit called');
87
+ }));
88
+ try {
89
+ const { runUnlock } = await import('../commands/unlock.js');
90
+ await expect(runUnlock({ name: 'platform', yes: true })).rejects.toThrow('process.exit called');
91
+ expect(exitSpy).toHaveBeenCalledWith(1);
92
+ // The out-of-tree lock must be untouched — containment refused before removal.
93
+ expect(readLock(outside).present).toBe(true);
94
+ }
95
+ finally {
96
+ rmSync(outside, { recursive: true, force: true });
97
+ }
98
+ });
99
+ });
100
+ //# sourceMappingURL=unlock.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unlock.test.js","sourceRoot":"","sources":["../../src/__tests__/unlock.test.ts"],"names":[],"mappings":"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"}