tmux-team 2.1.0 → 3.0.0-alpha.1

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.
@@ -1,1128 +0,0 @@
1
- // ─────────────────────────────────────────────────────────────
2
- // PM Commands Tests
3
- // ─────────────────────────────────────────────────────────────
4
-
5
- import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
6
- import fs from 'fs';
7
- import path from 'path';
8
- import os from 'os';
9
- import type { Context } from '../types.js';
10
- import type { UI } from '../types.js';
11
- // ExitCodes imported for reference but tested via ctx.exit mock
12
- import {
13
- cmdPm,
14
- cmdPmInit,
15
- cmdPmMilestone,
16
- cmdPmTask,
17
- cmdPmDoc,
18
- cmdPmLog,
19
- cmdPmList,
20
- } from './commands.js';
21
- import { findCurrentTeamId, linkTeam, getTeamsDir } from './manager.js';
22
-
23
- // ─────────────────────────────────────────────────────────────
24
- // Test helpers
25
- // ─────────────────────────────────────────────────────────────
26
-
27
- function createMockUI(): UI & { logs: string[]; errors: string[]; jsonData: unknown[] } {
28
- const logs: string[] = [];
29
- const errors: string[] = [];
30
- const jsonData: unknown[] = [];
31
-
32
- return {
33
- logs,
34
- errors,
35
- jsonData,
36
- info: vi.fn((msg: string) => logs.push(`[info] ${msg}`)),
37
- success: vi.fn((msg: string) => logs.push(`[success] ${msg}`)),
38
- warn: vi.fn((msg: string) => logs.push(`[warn] ${msg}`)),
39
- error: vi.fn((msg: string) => errors.push(msg)),
40
- table: vi.fn((_headers: string[], _rows: string[][]) => logs.push('[table]')),
41
- json: vi.fn((data: unknown) => jsonData.push(data)),
42
- };
43
- }
44
-
45
- function createMockContext(
46
- globalDir: string,
47
- options: { json?: boolean; cwd?: string; agents?: Record<string, { deny?: string[] }> } = {}
48
- ): Context & { ui: ReturnType<typeof createMockUI>; exitCode: number | null } {
49
- const ui = createMockUI();
50
- let exitCode: number | null = null;
51
-
52
- // Override cwd for tests
53
- const originalCwd = process.cwd;
54
- if (options.cwd) {
55
- vi.spyOn(process, 'cwd').mockReturnValue(options.cwd);
56
- }
57
-
58
- return {
59
- ui,
60
- exitCode,
61
- flags: { json: options.json ?? false },
62
- paths: { globalDir, configFile: path.join(globalDir, 'config.json') },
63
- config: {
64
- mode: 'polling',
65
- preambleMode: 'always',
66
- defaults: { timeout: 60, pollInterval: 1, captureLines: 100 },
67
- agents: options.agents ?? {},
68
- paneRegistry: {},
69
- },
70
- exit: vi.fn((code: number) => {
71
- exitCode = code;
72
- throw new Error(`Exit: ${code}`);
73
- }),
74
- restoreCwd: () => {
75
- if (options.cwd) {
76
- vi.spyOn(process, 'cwd').mockImplementation(originalCwd);
77
- }
78
- },
79
- } as unknown as Context & { ui: ReturnType<typeof createMockUI>; exitCode: number | null };
80
- }
81
-
82
- // ─────────────────────────────────────────────────────────────
83
- // requireTeam tests
84
- // ─────────────────────────────────────────────────────────────
85
-
86
- describe('requireTeam', () => {
87
- let testDir: string;
88
- let globalDir: string;
89
-
90
- beforeEach(() => {
91
- testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tmux-team-test-'));
92
- globalDir = path.join(testDir, 'global');
93
- fs.mkdirSync(globalDir, { recursive: true });
94
- });
95
-
96
- afterEach(() => {
97
- vi.restoreAllMocks();
98
- if (fs.existsSync(testDir)) {
99
- fs.rmSync(testDir, { recursive: true, force: true });
100
- }
101
- });
102
-
103
- it('finds team ID from .tmux-team-id file in cwd', () => {
104
- const projectDir = path.join(testDir, 'project');
105
- fs.mkdirSync(projectDir, { recursive: true });
106
- fs.writeFileSync(path.join(projectDir, '.tmux-team-id'), 'test-team-123\n');
107
-
108
- const teamId = findCurrentTeamId(projectDir, globalDir);
109
- expect(teamId).toBe('test-team-123');
110
- });
111
-
112
- it('finds team ID from TMUX_TEAM_ID environment variable', () => {
113
- const originalEnv = process.env.TMUX_TEAM_ID;
114
- process.env.TMUX_TEAM_ID = 'env-team-456';
115
-
116
- try {
117
- const teamId = findCurrentTeamId(testDir, globalDir);
118
- expect(teamId).toBe('env-team-456');
119
- } finally {
120
- if (originalEnv) {
121
- process.env.TMUX_TEAM_ID = originalEnv;
122
- } else {
123
- delete process.env.TMUX_TEAM_ID;
124
- }
125
- }
126
- });
127
-
128
- it('validates team.json exists for team ID', async () => {
129
- const projectDir = path.join(testDir, 'project');
130
- fs.mkdirSync(projectDir, { recursive: true });
131
-
132
- // Create team directory with team.json
133
- const teamId = 'valid-team-id';
134
- const teamDir = path.join(globalDir, 'teams', teamId);
135
- fs.mkdirSync(teamDir, { recursive: true });
136
- fs.writeFileSync(
137
- path.join(teamDir, 'team.json'),
138
- JSON.stringify({ id: teamId, name: 'Test', createdAt: new Date().toISOString() })
139
- );
140
-
141
- // Link project to team
142
- linkTeam(projectDir, teamId);
143
-
144
- const ctx = createMockContext(globalDir, { cwd: projectDir });
145
- vi.spyOn(process, 'cwd').mockReturnValue(projectDir);
146
-
147
- // Should not throw - team is valid
148
- await cmdPmTask(ctx, ['list']);
149
- expect(ctx.ui.logs.some((l) => l.includes('[info]') || l.includes('[table]'))).toBe(true);
150
- });
151
-
152
- it('exits with error when no .tmux-team-id found', async () => {
153
- const projectDir = path.join(testDir, 'empty-project');
154
- fs.mkdirSync(projectDir, { recursive: true });
155
-
156
- const ctx = createMockContext(globalDir, { cwd: projectDir });
157
- vi.spyOn(process, 'cwd').mockReturnValue(projectDir);
158
-
159
- await expect(cmdPmTask(ctx, ['list'])).rejects.toThrow('Exit');
160
- expect(ctx.ui.errors[0]).toContain('No team found');
161
- });
162
-
163
- it('exits with error when team.json does not exist (stale ID)', async () => {
164
- const projectDir = path.join(testDir, 'stale-project');
165
- fs.mkdirSync(projectDir, { recursive: true });
166
-
167
- // Create .tmux-team-id pointing to non-existent team
168
- fs.writeFileSync(path.join(projectDir, '.tmux-team-id'), 'stale-team-id\n');
169
-
170
- const ctx = createMockContext(globalDir, { cwd: projectDir });
171
- vi.spyOn(process, 'cwd').mockReturnValue(projectDir);
172
-
173
- await expect(cmdPmTask(ctx, ['list'])).rejects.toThrow('Exit');
174
- expect(ctx.ui.errors[0]).toContain('not found');
175
- });
176
- });
177
-
178
- // ─────────────────────────────────────────────────────────────
179
- // cmdPmInit tests
180
- // ─────────────────────────────────────────────────────────────
181
-
182
- describe('cmdPmInit', () => {
183
- let testDir: string;
184
- let globalDir: string;
185
- let projectDir: string;
186
-
187
- beforeEach(() => {
188
- testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tmux-team-test-'));
189
- globalDir = path.join(testDir, 'global');
190
- projectDir = path.join(testDir, 'project');
191
- fs.mkdirSync(globalDir, { recursive: true });
192
- fs.mkdirSync(projectDir, { recursive: true });
193
- });
194
-
195
- afterEach(() => {
196
- vi.restoreAllMocks();
197
- if (fs.existsSync(testDir)) {
198
- fs.rmSync(testDir, { recursive: true, force: true });
199
- }
200
- });
201
-
202
- it('creates team with generated UUID', async () => {
203
- const ctx = createMockContext(globalDir, { cwd: projectDir });
204
- vi.spyOn(process, 'cwd').mockReturnValue(projectDir);
205
-
206
- await cmdPmInit(ctx, []);
207
-
208
- // Check that a team directory was created
209
- const teamsDir = getTeamsDir(globalDir);
210
- const teamDirs = fs.readdirSync(teamsDir);
211
- expect(teamDirs.length).toBe(1);
212
-
213
- // UUID format validation
214
- expect(teamDirs[0]).toMatch(/^[0-9a-f-]{36}$/);
215
- });
216
-
217
- it('uses --name flag for team name', async () => {
218
- const ctx = createMockContext(globalDir, { cwd: projectDir });
219
- vi.spyOn(process, 'cwd').mockReturnValue(projectDir);
220
-
221
- await cmdPmInit(ctx, ['--name', 'My Custom Project']);
222
-
223
- expect(ctx.ui.logs.some((l) => l.includes('My Custom Project'))).toBe(true);
224
- });
225
-
226
- it('creates .tmux-team-id file in current directory', async () => {
227
- const ctx = createMockContext(globalDir, { cwd: projectDir });
228
- vi.spyOn(process, 'cwd').mockReturnValue(projectDir);
229
-
230
- await cmdPmInit(ctx, ['--name', 'Test']);
231
-
232
- const idFile = path.join(projectDir, '.tmux-team-id');
233
- expect(fs.existsSync(idFile)).toBe(true);
234
-
235
- const teamId = fs.readFileSync(idFile, 'utf-8').trim();
236
- expect(teamId).toMatch(/^[0-9a-f-]{36}$/);
237
- });
238
-
239
- it('logs team_created event to audit log', async () => {
240
- const ctx = createMockContext(globalDir, { cwd: projectDir });
241
- vi.spyOn(process, 'cwd').mockReturnValue(projectDir);
242
-
243
- await cmdPmInit(ctx, ['--name', 'Test']);
244
-
245
- // Read the events file
246
- const teamsDir = getTeamsDir(globalDir);
247
- const teamDirs = fs.readdirSync(teamsDir);
248
- const eventsFile = path.join(teamsDir, teamDirs[0], 'events.jsonl');
249
-
250
- expect(fs.existsSync(eventsFile)).toBe(true);
251
- const events = fs
252
- .readFileSync(eventsFile, 'utf-8')
253
- .trim()
254
- .split('\n')
255
- .map((l) => JSON.parse(l));
256
- expect(events[0].event).toBe('team_created');
257
- });
258
-
259
- it('outputs team info in JSON when --json flag set', async () => {
260
- const ctx = createMockContext(globalDir, { json: true, cwd: projectDir });
261
- vi.spyOn(process, 'cwd').mockReturnValue(projectDir);
262
-
263
- await cmdPmInit(ctx, ['--name', 'JSON Test']);
264
-
265
- expect(ctx.ui.jsonData.length).toBe(1);
266
- const data = ctx.ui.jsonData[0] as { team: { name: string } };
267
- expect(data.team.name).toBe('JSON Test');
268
- });
269
- });
270
-
271
- // ─────────────────────────────────────────────────────────────
272
- // cmdPmMilestone tests
273
- // ─────────────────────────────────────────────────────────────
274
-
275
- describe('cmdPmMilestone', () => {
276
- let testDir: string;
277
- let globalDir: string;
278
- let projectDir: string;
279
- let teamId: string;
280
-
281
- beforeEach(async () => {
282
- testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tmux-team-test-'));
283
- globalDir = path.join(testDir, 'global');
284
- projectDir = path.join(testDir, 'project');
285
- fs.mkdirSync(globalDir, { recursive: true });
286
- fs.mkdirSync(projectDir, { recursive: true });
287
-
288
- // Initialize a team
289
- const ctx = createMockContext(globalDir, { cwd: projectDir });
290
- vi.spyOn(process, 'cwd').mockReturnValue(projectDir);
291
- await cmdPmInit(ctx, ['--name', 'Test Project']);
292
- vi.restoreAllMocks();
293
-
294
- teamId = fs.readFileSync(path.join(projectDir, '.tmux-team-id'), 'utf-8').trim();
295
- });
296
-
297
- afterEach(() => {
298
- vi.restoreAllMocks();
299
- if (fs.existsSync(testDir)) {
300
- fs.rmSync(testDir, { recursive: true, force: true });
301
- }
302
- });
303
-
304
- it('creates milestone with given name', async () => {
305
- const ctx = createMockContext(globalDir, { cwd: projectDir });
306
- vi.spyOn(process, 'cwd').mockReturnValue(projectDir);
307
-
308
- await cmdPmMilestone(ctx, ['add', 'Sprint 1']);
309
-
310
- expect(ctx.ui.logs.some((l) => l.includes('Sprint 1'))).toBe(true);
311
-
312
- // Verify file was created
313
- const milestonePath = path.join(globalDir, 'teams', teamId, 'milestones', '1.json');
314
- expect(fs.existsSync(milestonePath)).toBe(true);
315
- });
316
-
317
- it('lists all milestones in table format', async () => {
318
- const ctx = createMockContext(globalDir, { cwd: projectDir });
319
- vi.spyOn(process, 'cwd').mockReturnValue(projectDir);
320
-
321
- await cmdPmMilestone(ctx, ['add', 'Phase 1']);
322
- await cmdPmMilestone(ctx, ['add', 'Phase 2']);
323
- await cmdPmMilestone(ctx, ['list']);
324
-
325
- expect(ctx.ui.table).toHaveBeenCalled();
326
- });
327
-
328
- it('marks milestone as done', async () => {
329
- const ctx = createMockContext(globalDir, { cwd: projectDir });
330
- vi.spyOn(process, 'cwd').mockReturnValue(projectDir);
331
-
332
- await cmdPmMilestone(ctx, ['add', 'Sprint 1']);
333
- await cmdPmMilestone(ctx, ['done', '1']);
334
-
335
- expect(ctx.ui.logs.some((l) => l.includes('done'))).toBe(true);
336
-
337
- // Verify status was updated
338
- const milestonePath = path.join(globalDir, 'teams', teamId, 'milestones', '1.json');
339
- const milestone = JSON.parse(fs.readFileSync(milestonePath, 'utf-8'));
340
- expect(milestone.status).toBe('done');
341
- });
342
-
343
- it('exits with error for non-existent milestone', async () => {
344
- const ctx = createMockContext(globalDir, { cwd: projectDir });
345
- vi.spyOn(process, 'cwd').mockReturnValue(projectDir);
346
-
347
- await expect(cmdPmMilestone(ctx, ['done', '999'])).rejects.toThrow('Exit');
348
- expect(ctx.ui.errors[0]).toContain('not found');
349
- });
350
-
351
- it('routes "pm m add" to milestone add', async () => {
352
- const ctx = createMockContext(globalDir, { cwd: projectDir });
353
- vi.spyOn(process, 'cwd').mockReturnValue(projectDir);
354
-
355
- await cmdPm(ctx, ['m', 'add', 'Shorthand Test']);
356
-
357
- expect(ctx.ui.logs.some((l) => l.includes('Shorthand Test'))).toBe(true);
358
- });
359
- });
360
-
361
- // ─────────────────────────────────────────────────────────────
362
- // cmdPmTask tests
363
- // ─────────────────────────────────────────────────────────────
364
-
365
- describe('cmdPmTask', () => {
366
- let testDir: string;
367
- let globalDir: string;
368
- let projectDir: string;
369
- let teamId: string;
370
-
371
- beforeEach(async () => {
372
- testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tmux-team-test-'));
373
- globalDir = path.join(testDir, 'global');
374
- projectDir = path.join(testDir, 'project');
375
- fs.mkdirSync(globalDir, { recursive: true });
376
- fs.mkdirSync(projectDir, { recursive: true });
377
-
378
- // Initialize a team
379
- const ctx = createMockContext(globalDir, { cwd: projectDir });
380
- vi.spyOn(process, 'cwd').mockReturnValue(projectDir);
381
- await cmdPmInit(ctx, ['--name', 'Test Project']);
382
- vi.restoreAllMocks();
383
-
384
- teamId = fs.readFileSync(path.join(projectDir, '.tmux-team-id'), 'utf-8').trim();
385
- });
386
-
387
- afterEach(() => {
388
- vi.restoreAllMocks();
389
- if (fs.existsSync(testDir)) {
390
- fs.rmSync(testDir, { recursive: true, force: true });
391
- }
392
- });
393
-
394
- it('creates task with given title', async () => {
395
- const ctx = createMockContext(globalDir, { cwd: projectDir });
396
- vi.spyOn(process, 'cwd').mockReturnValue(projectDir);
397
-
398
- await cmdPmTask(ctx, ['add', 'Implement login']);
399
-
400
- expect(ctx.ui.logs.some((l) => l.includes('Implement login'))).toBe(true);
401
- });
402
-
403
- it('creates task with milestone reference', async () => {
404
- const ctx = createMockContext(globalDir, { cwd: projectDir });
405
- vi.spyOn(process, 'cwd').mockReturnValue(projectDir);
406
-
407
- // Create milestone first
408
- await cmdPmMilestone(ctx, ['add', 'Sprint 1']);
409
- await cmdPmTask(ctx, ['add', 'Task with milestone', '--milestone', '1']);
410
-
411
- const taskPath = path.join(globalDir, 'teams', teamId, 'tasks', '1.json');
412
- const task = JSON.parse(fs.readFileSync(taskPath, 'utf-8'));
413
- expect(task.milestone).toBe('1');
414
- });
415
-
416
- it('creates task with assignee', async () => {
417
- const ctx = createMockContext(globalDir, { cwd: projectDir });
418
- vi.spyOn(process, 'cwd').mockReturnValue(projectDir);
419
-
420
- await cmdPmTask(ctx, ['add', 'Assigned task', '--assignee', 'claude']);
421
-
422
- const taskPath = path.join(globalDir, 'teams', teamId, 'tasks', '1.json');
423
- const task = JSON.parse(fs.readFileSync(taskPath, 'utf-8'));
424
- expect(task.assignee).toBe('claude');
425
- });
426
-
427
- it('lists all tasks in table format', async () => {
428
- const ctx = createMockContext(globalDir, { cwd: projectDir });
429
- vi.spyOn(process, 'cwd').mockReturnValue(projectDir);
430
-
431
- await cmdPmTask(ctx, ['add', 'Task 1']);
432
- await cmdPmTask(ctx, ['add', 'Task 2']);
433
- await cmdPmTask(ctx, ['list']);
434
-
435
- expect(ctx.ui.table).toHaveBeenCalled();
436
- });
437
-
438
- it('filters task list by status', async () => {
439
- const ctx = createMockContext(globalDir, { json: true, cwd: projectDir });
440
- vi.spyOn(process, 'cwd').mockReturnValue(projectDir);
441
-
442
- await cmdPmTask(ctx, ['add', 'Pending task']);
443
- await cmdPmTask(ctx, ['add', 'Another task']);
444
- await cmdPmTask(ctx, ['done', '1']);
445
- await cmdPmTask(ctx, ['list', '--status', 'pending']);
446
-
447
- const lastJson = ctx.ui.jsonData[ctx.ui.jsonData.length - 1] as { id: string }[];
448
- expect(lastJson).toHaveLength(1);
449
- expect(lastJson[0].id).toBe('2');
450
- });
451
-
452
- it('filters task list by milestone', async () => {
453
- const ctx = createMockContext(globalDir, { json: true, cwd: projectDir });
454
- vi.spyOn(process, 'cwd').mockReturnValue(projectDir);
455
-
456
- await cmdPmMilestone(ctx, ['add', 'Sprint 1']);
457
- await cmdPmMilestone(ctx, ['add', 'Sprint 2']);
458
- await cmdPmTask(ctx, ['add', 'Task in Sprint 1', '--milestone', '1']);
459
- await cmdPmTask(ctx, ['add', 'Task in Sprint 2', '--milestone', '2']);
460
- await cmdPmTask(ctx, ['list', '--milestone', '1']);
461
-
462
- const lastJson = ctx.ui.jsonData[ctx.ui.jsonData.length - 1] as { milestone: string }[];
463
- expect(lastJson).toHaveLength(1);
464
- expect(lastJson[0].milestone).toBe('1');
465
- });
466
-
467
- it('displays task details', async () => {
468
- const ctx = createMockContext(globalDir, { json: true, cwd: projectDir });
469
- vi.spyOn(process, 'cwd').mockReturnValue(projectDir);
470
-
471
- await cmdPmTask(ctx, ['add', 'Show me', '--assignee', 'claude']);
472
- await cmdPmTask(ctx, ['show', '1']);
473
-
474
- const lastJson = ctx.ui.jsonData[ctx.ui.jsonData.length - 1] as { title: string };
475
- expect(lastJson.title).toBe('Show me');
476
- });
477
-
478
- it('updates task status', async () => {
479
- const ctx = createMockContext(globalDir, { cwd: projectDir });
480
- vi.spyOn(process, 'cwd').mockReturnValue(projectDir);
481
-
482
- await cmdPmTask(ctx, ['add', 'Update me']);
483
- await cmdPmTask(ctx, ['update', '1', '--status', 'in_progress']);
484
-
485
- const taskPath = path.join(globalDir, 'teams', teamId, 'tasks', '1.json');
486
- const task = JSON.parse(fs.readFileSync(taskPath, 'utf-8'));
487
- expect(task.status).toBe('in_progress');
488
- });
489
-
490
- it('updates task assignee', async () => {
491
- const ctx = createMockContext(globalDir, { cwd: projectDir });
492
- vi.spyOn(process, 'cwd').mockReturnValue(projectDir);
493
-
494
- await cmdPmTask(ctx, ['add', 'Reassign me']);
495
- await cmdPmTask(ctx, ['update', '1', '--assignee', 'codex']);
496
-
497
- const taskPath = path.join(globalDir, 'teams', teamId, 'tasks', '1.json');
498
- const task = JSON.parse(fs.readFileSync(taskPath, 'utf-8'));
499
- expect(task.assignee).toBe('codex');
500
- });
501
-
502
- it('marks task as done', async () => {
503
- const ctx = createMockContext(globalDir, { cwd: projectDir });
504
- vi.spyOn(process, 'cwd').mockReturnValue(projectDir);
505
-
506
- await cmdPmTask(ctx, ['add', 'Complete me']);
507
- await cmdPmTask(ctx, ['done', '1']);
508
-
509
- const taskPath = path.join(globalDir, 'teams', teamId, 'tasks', '1.json');
510
- const task = JSON.parse(fs.readFileSync(taskPath, 'utf-8'));
511
- expect(task.status).toBe('done');
512
- });
513
-
514
- it('exits with error for non-existent task', async () => {
515
- const ctx = createMockContext(globalDir, { cwd: projectDir });
516
- vi.spyOn(process, 'cwd').mockReturnValue(projectDir);
517
-
518
- await expect(cmdPmTask(ctx, ['show', '999'])).rejects.toThrow('Exit');
519
- expect(ctx.ui.errors[0]).toContain('not found');
520
- });
521
-
522
- it('routes "pm t add" to task add', async () => {
523
- const ctx = createMockContext(globalDir, { cwd: projectDir });
524
- vi.spyOn(process, 'cwd').mockReturnValue(projectDir);
525
-
526
- await cmdPm(ctx, ['t', 'add', 'Shorthand task']);
527
-
528
- expect(ctx.ui.logs.some((l) => l.includes('Shorthand task'))).toBe(true);
529
- });
530
- });
531
-
532
- // ─────────────────────────────────────────────────────────────
533
- // cmdPmDoc tests
534
- // ─────────────────────────────────────────────────────────────
535
-
536
- describe('cmdPmDoc', () => {
537
- let testDir: string;
538
- let globalDir: string;
539
- let projectDir: string;
540
-
541
- beforeEach(async () => {
542
- testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tmux-team-test-'));
543
- globalDir = path.join(testDir, 'global');
544
- projectDir = path.join(testDir, 'project');
545
- fs.mkdirSync(globalDir, { recursive: true });
546
- fs.mkdirSync(projectDir, { recursive: true });
547
-
548
- // Initialize a team and create a task
549
- const ctx = createMockContext(globalDir, { cwd: projectDir });
550
- vi.spyOn(process, 'cwd').mockReturnValue(projectDir);
551
- await cmdPmInit(ctx, ['--name', 'Test Project']);
552
- await cmdPmTask(ctx, ['add', 'Test Task']);
553
- vi.restoreAllMocks();
554
- });
555
-
556
- afterEach(() => {
557
- vi.restoreAllMocks();
558
- if (fs.existsSync(testDir)) {
559
- fs.rmSync(testDir, { recursive: true, force: true });
560
- }
561
- });
562
-
563
- it('prints task documentation with --print flag', async () => {
564
- const ctx = createMockContext(globalDir, { cwd: projectDir });
565
- vi.spyOn(process, 'cwd').mockReturnValue(projectDir);
566
-
567
- // Capture console.log output
568
- const logs: string[] = [];
569
- const originalLog = console.log;
570
- console.log = (msg: string) => logs.push(msg);
571
-
572
- await cmdPmDoc(ctx, ['1', '--print']);
573
-
574
- console.log = originalLog;
575
-
576
- expect(logs.some((l) => l.includes('Test Task'))).toBe(true);
577
- });
578
-
579
- it('opens documentation in $EDITOR', async () => {
580
- // This test is tricky because it spawns an editor
581
- // We'll just verify the command doesn't throw
582
- const ctx = createMockContext(globalDir, { cwd: projectDir });
583
- vi.spyOn(process, 'cwd').mockReturnValue(projectDir);
584
-
585
- // Set a no-op editor
586
- const originalEditor = process.env.EDITOR;
587
- process.env.EDITOR = 'true'; // 'true' command exists and does nothing
588
-
589
- try {
590
- await cmdPmDoc(ctx, ['1']);
591
- expect(ctx.ui.logs.some((l) => l.includes('Saved'))).toBe(true);
592
- } finally {
593
- if (originalEditor) {
594
- process.env.EDITOR = originalEditor;
595
- } else {
596
- delete process.env.EDITOR;
597
- }
598
- }
599
- });
600
-
601
- it('exits with error for non-existent task', async () => {
602
- const ctx = createMockContext(globalDir, { cwd: projectDir });
603
- vi.spyOn(process, 'cwd').mockReturnValue(projectDir);
604
-
605
- await expect(cmdPmDoc(ctx, ['999'])).rejects.toThrow('Exit');
606
- expect(ctx.ui.errors[0]).toContain('not found');
607
- });
608
- });
609
-
610
- // ─────────────────────────────────────────────────────────────
611
- // cmdPmLog tests
612
- // ─────────────────────────────────────────────────────────────
613
-
614
- describe('cmdPmLog', () => {
615
- let testDir: string;
616
- let globalDir: string;
617
- let projectDir: string;
618
-
619
- beforeEach(async () => {
620
- testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tmux-team-test-'));
621
- globalDir = path.join(testDir, 'global');
622
- projectDir = path.join(testDir, 'project');
623
- fs.mkdirSync(globalDir, { recursive: true });
624
- fs.mkdirSync(projectDir, { recursive: true });
625
-
626
- // Initialize a team
627
- const ctx = createMockContext(globalDir, { cwd: projectDir });
628
- vi.spyOn(process, 'cwd').mockReturnValue(projectDir);
629
- await cmdPmInit(ctx, ['--name', 'Test Project']);
630
- vi.restoreAllMocks();
631
- });
632
-
633
- afterEach(() => {
634
- vi.restoreAllMocks();
635
- if (fs.existsSync(testDir)) {
636
- fs.rmSync(testDir, { recursive: true, force: true });
637
- }
638
- });
639
-
640
- it('displays audit events', async () => {
641
- const ctx = createMockContext(globalDir, { cwd: projectDir });
642
- vi.spyOn(process, 'cwd').mockReturnValue(projectDir);
643
-
644
- // Create some events
645
- await cmdPmTask(ctx, ['add', 'Task 1']);
646
- await cmdPmTask(ctx, ['done', '1']);
647
-
648
- const logs: string[] = [];
649
- const originalLog = console.log;
650
- console.log = (msg: string) => logs.push(String(msg));
651
-
652
- await cmdPmLog(ctx, []);
653
-
654
- console.log = originalLog;
655
-
656
- // Should show team_created and task events
657
- expect(logs.some((l) => l.includes('team_created') || l.includes('task'))).toBe(true);
658
- });
659
-
660
- it('limits number of events displayed', async () => {
661
- const ctx = createMockContext(globalDir, { json: true, cwd: projectDir });
662
- vi.spyOn(process, 'cwd').mockReturnValue(projectDir);
663
-
664
- // Create multiple events
665
- await cmdPmTask(ctx, ['add', 'Task 1']);
666
- await cmdPmTask(ctx, ['add', 'Task 2']);
667
- await cmdPmTask(ctx, ['add', 'Task 3']);
668
- await cmdPmLog(ctx, ['--limit', '2']);
669
-
670
- const lastJson = ctx.ui.jsonData[ctx.ui.jsonData.length - 1] as unknown[];
671
- expect(lastJson.length).toBe(2);
672
- });
673
-
674
- it('outputs events in JSON when --json flag set', async () => {
675
- const ctx = createMockContext(globalDir, { json: true, cwd: projectDir });
676
- vi.spyOn(process, 'cwd').mockReturnValue(projectDir);
677
-
678
- await cmdPmLog(ctx, []);
679
-
680
- expect(ctx.ui.jsonData.length).toBeGreaterThan(0);
681
- expect(Array.isArray(ctx.ui.jsonData[ctx.ui.jsonData.length - 1])).toBe(true);
682
- });
683
-
684
- it('shows info message when no events', async () => {
685
- // Create a new project without events
686
- const newProjectDir = path.join(testDir, 'empty-project');
687
- fs.mkdirSync(newProjectDir, { recursive: true });
688
-
689
- const initCtx = createMockContext(globalDir, { cwd: newProjectDir });
690
- vi.spyOn(process, 'cwd').mockReturnValue(newProjectDir);
691
- await cmdPmInit(initCtx, ['--name', 'Empty']);
692
- vi.restoreAllMocks();
693
-
694
- // Clear events file
695
- const teamId = fs.readFileSync(path.join(newProjectDir, '.tmux-team-id'), 'utf-8').trim();
696
- const eventsFile = path.join(globalDir, 'teams', teamId, 'events.jsonl');
697
- fs.writeFileSync(eventsFile, '');
698
-
699
- const ctx = createMockContext(globalDir, { cwd: newProjectDir });
700
- vi.spyOn(process, 'cwd').mockReturnValue(newProjectDir);
701
-
702
- await cmdPmLog(ctx, []);
703
-
704
- expect(ctx.ui.logs.some((l) => l.includes('No events'))).toBe(true);
705
- });
706
- });
707
-
708
- // ─────────────────────────────────────────────────────────────
709
- // cmdPmList tests
710
- // ─────────────────────────────────────────────────────────────
711
-
712
- describe('cmdPmList', () => {
713
- let testDir: string;
714
- let globalDir: string;
715
-
716
- beforeEach(() => {
717
- testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tmux-team-test-'));
718
- globalDir = path.join(testDir, 'global');
719
- fs.mkdirSync(globalDir, { recursive: true });
720
- });
721
-
722
- afterEach(() => {
723
- vi.restoreAllMocks();
724
- if (fs.existsSync(testDir)) {
725
- fs.rmSync(testDir, { recursive: true, force: true });
726
- }
727
- });
728
-
729
- it('lists all teams in table format', async () => {
730
- // Create multiple teams
731
- const project1 = path.join(testDir, 'project1');
732
- const project2 = path.join(testDir, 'project2');
733
- fs.mkdirSync(project1, { recursive: true });
734
- fs.mkdirSync(project2, { recursive: true });
735
-
736
- const ctx1 = createMockContext(globalDir, { cwd: project1 });
737
- vi.spyOn(process, 'cwd').mockReturnValue(project1);
738
- await cmdPmInit(ctx1, ['--name', 'Project 1']);
739
- vi.restoreAllMocks();
740
-
741
- const ctx2 = createMockContext(globalDir, { cwd: project2 });
742
- vi.spyOn(process, 'cwd').mockReturnValue(project2);
743
- await cmdPmInit(ctx2, ['--name', 'Project 2']);
744
- vi.restoreAllMocks();
745
-
746
- const ctx = createMockContext(globalDir);
747
- await cmdPmList(ctx, []);
748
-
749
- expect(ctx.ui.table).toHaveBeenCalled();
750
- });
751
-
752
- it('shows info message when no teams', async () => {
753
- const ctx = createMockContext(globalDir);
754
- await cmdPmList(ctx, []);
755
-
756
- expect(ctx.ui.logs.some((l) => l.includes('No teams'))).toBe(true);
757
- });
758
-
759
- it('outputs teams in JSON when --json flag set', async () => {
760
- // Create a team first
761
- const project = path.join(testDir, 'project');
762
- fs.mkdirSync(project, { recursive: true });
763
-
764
- const initCtx = createMockContext(globalDir, { cwd: project });
765
- vi.spyOn(process, 'cwd').mockReturnValue(project);
766
- await cmdPmInit(initCtx, ['--name', 'JSON Team']);
767
- vi.restoreAllMocks();
768
-
769
- const ctx = createMockContext(globalDir, { json: true });
770
- await cmdPmList(ctx, []);
771
-
772
- expect(ctx.ui.jsonData.length).toBe(1);
773
- const data = ctx.ui.jsonData[0] as { teams: unknown[]; currentTeamId: string | null };
774
- expect(data).toHaveProperty('teams');
775
- expect(data).toHaveProperty('currentTeamId');
776
- expect(Array.isArray(data.teams)).toBe(true);
777
- });
778
- });
779
-
780
- // ─────────────────────────────────────────────────────────────
781
- // cmdPm router tests
782
- // ─────────────────────────────────────────────────────────────
783
-
784
- describe('cmdPm router', () => {
785
- let testDir: string;
786
- let globalDir: string;
787
- let projectDir: string;
788
-
789
- beforeEach(async () => {
790
- testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tmux-team-test-'));
791
- globalDir = path.join(testDir, 'global');
792
- projectDir = path.join(testDir, 'project');
793
- fs.mkdirSync(globalDir, { recursive: true });
794
- fs.mkdirSync(projectDir, { recursive: true });
795
-
796
- // Initialize a team
797
- const ctx = createMockContext(globalDir, { cwd: projectDir });
798
- vi.spyOn(process, 'cwd').mockReturnValue(projectDir);
799
- await cmdPmInit(ctx, ['--name', 'Test Project']);
800
- vi.restoreAllMocks();
801
- });
802
-
803
- afterEach(() => {
804
- vi.restoreAllMocks();
805
- if (fs.existsSync(testDir)) {
806
- fs.rmSync(testDir, { recursive: true, force: true });
807
- }
808
- });
809
-
810
- it('routes to correct subcommand', async () => {
811
- const ctx = createMockContext(globalDir, { cwd: projectDir });
812
- vi.spyOn(process, 'cwd').mockReturnValue(projectDir);
813
-
814
- await cmdPm(ctx, ['task', 'add', 'Routed task']);
815
-
816
- expect(ctx.ui.logs.some((l) => l.includes('Routed task'))).toBe(true);
817
- });
818
-
819
- it('expands m to milestone, t to task', async () => {
820
- const ctx = createMockContext(globalDir, { cwd: projectDir });
821
- vi.spyOn(process, 'cwd').mockReturnValue(projectDir);
822
-
823
- await cmdPm(ctx, ['m', 'add', 'Milestone via m']);
824
- await cmdPm(ctx, ['t', 'add', 'Task via t']);
825
-
826
- expect(ctx.ui.logs.some((l) => l.includes('Milestone via m'))).toBe(true);
827
- expect(ctx.ui.logs.some((l) => l.includes('Task via t'))).toBe(true);
828
- });
829
-
830
- it('exits with error for unknown subcommand', async () => {
831
- const ctx = createMockContext(globalDir, { cwd: projectDir });
832
- vi.spyOn(process, 'cwd').mockReturnValue(projectDir);
833
-
834
- await expect(cmdPm(ctx, ['unknown'])).rejects.toThrow('Exit');
835
- expect(ctx.ui.errors[0]).toContain('Unknown pm command');
836
- });
837
-
838
- it('displays help for pm help', async () => {
839
- const ctx = createMockContext(globalDir, { cwd: projectDir });
840
-
841
- const logs: string[] = [];
842
- const originalLog = console.log;
843
- console.log = (msg: string) => logs.push(String(msg));
844
-
845
- await cmdPm(ctx, ['help']);
846
-
847
- console.log = originalLog;
848
-
849
- expect(logs.some((l) => l.includes('tmux-team pm'))).toBe(true);
850
- });
851
- });
852
-
853
- // ─────────────────────────────────────────────────────────────
854
- // parseStatus tests
855
- // ─────────────────────────────────────────────────────────────
856
-
857
- describe('parseStatus', () => {
858
- let testDir: string;
859
- let globalDir: string;
860
- let projectDir: string;
861
-
862
- beforeEach(async () => {
863
- testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tmux-team-test-'));
864
- globalDir = path.join(testDir, 'global');
865
- projectDir = path.join(testDir, 'project');
866
- fs.mkdirSync(globalDir, { recursive: true });
867
- fs.mkdirSync(projectDir, { recursive: true });
868
-
869
- // Initialize a team
870
- const ctx = createMockContext(globalDir, { cwd: projectDir });
871
- vi.spyOn(process, 'cwd').mockReturnValue(projectDir);
872
- await cmdPmInit(ctx, ['--name', 'Test Project']);
873
- await cmdPmTask(ctx, ['add', 'Test task']);
874
- vi.restoreAllMocks();
875
- });
876
-
877
- afterEach(() => {
878
- vi.restoreAllMocks();
879
- if (fs.existsSync(testDir)) {
880
- fs.rmSync(testDir, { recursive: true, force: true });
881
- }
882
- });
883
-
884
- it('parses pending, in_progress, done', async () => {
885
- const ctx = createMockContext(globalDir, { cwd: projectDir });
886
- vi.spyOn(process, 'cwd').mockReturnValue(projectDir);
887
-
888
- await cmdPmTask(ctx, ['update', '1', '--status', 'pending']);
889
- await cmdPmTask(ctx, ['update', '1', '--status', 'in_progress']);
890
- await cmdPmTask(ctx, ['update', '1', '--status', 'done']);
891
-
892
- // If we got here without errors, parsing worked
893
- expect(true).toBe(true);
894
- });
895
-
896
- it('normalizes in-progress to in_progress', async () => {
897
- const ctx = createMockContext(globalDir, { cwd: projectDir });
898
- vi.spyOn(process, 'cwd').mockReturnValue(projectDir);
899
-
900
- await cmdPmTask(ctx, ['update', '1', '--status', 'in-progress']);
901
-
902
- const teamId = fs.readFileSync(path.join(projectDir, '.tmux-team-id'), 'utf-8').trim();
903
- const taskPath = path.join(globalDir, 'teams', teamId, 'tasks', '1.json');
904
- const task = JSON.parse(fs.readFileSync(taskPath, 'utf-8'));
905
- expect(task.status).toBe('in_progress');
906
- });
907
-
908
- it('handles case insensitive input', async () => {
909
- const ctx = createMockContext(globalDir, { cwd: projectDir });
910
- vi.spyOn(process, 'cwd').mockReturnValue(projectDir);
911
-
912
- await cmdPmTask(ctx, ['update', '1', '--status', 'DONE']);
913
-
914
- const teamId = fs.readFileSync(path.join(projectDir, '.tmux-team-id'), 'utf-8').trim();
915
- const taskPath = path.join(globalDir, 'teams', teamId, 'tasks', '1.json');
916
- const task = JSON.parse(fs.readFileSync(taskPath, 'utf-8'));
917
- expect(task.status).toBe('done');
918
- });
919
-
920
- it('throws error for invalid status', async () => {
921
- const ctx = createMockContext(globalDir, { cwd: projectDir });
922
- vi.spyOn(process, 'cwd').mockReturnValue(projectDir);
923
-
924
- await expect(cmdPmTask(ctx, ['update', '1', '--status', 'invalid'])).rejects.toThrow(
925
- 'Invalid status'
926
- );
927
- });
928
- });
929
-
930
- // ─────────────────────────────────────────────────────────────
931
- // Permission Integration Tests
932
- // ─────────────────────────────────────────────────────────────
933
-
934
- describe('Permission integration', () => {
935
- let testDir: string;
936
- let globalDir: string;
937
- let projectDir: string;
938
- const originalEnv = { ...process.env };
939
-
940
- beforeEach(async () => {
941
- // Disable pane detection in tests
942
- delete process.env.TMUX;
943
-
944
- testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tmux-team-test-'));
945
- globalDir = path.join(testDir, 'global');
946
- projectDir = path.join(testDir, 'project');
947
- fs.mkdirSync(globalDir, { recursive: true });
948
- fs.mkdirSync(projectDir, { recursive: true });
949
-
950
- // Initialize a team and create a task
951
- const ctx = createMockContext(globalDir, { cwd: projectDir });
952
- vi.spyOn(process, 'cwd').mockReturnValue(projectDir);
953
- await cmdPmInit(ctx, ['--name', 'Test Project']);
954
- await cmdPmTask(ctx, ['add', 'Test task']);
955
- await cmdPmMilestone(ctx, ['add', 'Test milestone']);
956
- vi.restoreAllMocks();
957
- });
958
-
959
- afterEach(() => {
960
- process.env = { ...originalEnv };
961
- vi.restoreAllMocks();
962
- if (fs.existsSync(testDir)) {
963
- fs.rmSync(testDir, { recursive: true, force: true });
964
- }
965
- });
966
-
967
- it('allows human to perform any action', async () => {
968
- delete process.env.TMT_AGENT_NAME;
969
- delete process.env.TMUX_TEAM_ACTOR;
970
-
971
- const ctx = createMockContext(globalDir, {
972
- cwd: projectDir,
973
- agents: { codex: { deny: ['pm:task:update(status)'] } },
974
- });
975
- vi.spyOn(process, 'cwd').mockReturnValue(projectDir);
976
-
977
- // Human should be able to update status even with deny pattern for codex
978
- await cmdPmTask(ctx, ['update', '1', '--status', 'in_progress']);
979
- expect(ctx.ui.logs.some((l) => l.includes('Updated'))).toBe(true);
980
- });
981
-
982
- it('blocks agent when deny pattern matches status update', async () => {
983
- process.env.TMT_AGENT_NAME = 'codex';
984
-
985
- const ctx = createMockContext(globalDir, {
986
- cwd: projectDir,
987
- agents: { codex: { deny: ['pm:task:update(status)'] } },
988
- });
989
- vi.spyOn(process, 'cwd').mockReturnValue(projectDir);
990
-
991
- await expect(cmdPmTask(ctx, ['update', '1', '--status', 'done'])).rejects.toThrow('Exit');
992
- expect(ctx.ui.errors[0]).toContain('Permission denied');
993
- expect(ctx.ui.errors[0]).toContain('pm:task:update(status)');
994
- });
995
-
996
- it('blocks agent when deny pattern matches task done command', async () => {
997
- process.env.TMT_AGENT_NAME = 'codex';
998
-
999
- const ctx = createMockContext(globalDir, {
1000
- cwd: projectDir,
1001
- agents: { codex: { deny: ['pm:task:update(status)'] } },
1002
- });
1003
- vi.spyOn(process, 'cwd').mockReturnValue(projectDir);
1004
-
1005
- // 'task done' is equivalent to 'task update --status done'
1006
- await expect(cmdPmTask(ctx, ['done', '1'])).rejects.toThrow('Exit');
1007
- expect(ctx.ui.errors[0]).toContain('Permission denied');
1008
- });
1009
-
1010
- it('allows agent to update assignee when only status is denied', async () => {
1011
- process.env.TMT_AGENT_NAME = 'codex';
1012
-
1013
- const ctx = createMockContext(globalDir, {
1014
- cwd: projectDir,
1015
- agents: { codex: { deny: ['pm:task:update(status)'] } },
1016
- });
1017
- vi.spyOn(process, 'cwd').mockReturnValue(projectDir);
1018
-
1019
- // Assignee update should be allowed
1020
- await cmdPmTask(ctx, ['update', '1', '--assignee', 'gemini']);
1021
- expect(ctx.ui.logs.some((l) => l.includes('Updated'))).toBe(true);
1022
- });
1023
-
1024
- it('blocks agent when wildcard deny pattern matches any field update', async () => {
1025
- process.env.TMT_AGENT_NAME = 'codex';
1026
-
1027
- const ctx = createMockContext(globalDir, {
1028
- cwd: projectDir,
1029
- agents: { codex: { deny: ['pm:task:update(*)'] } },
1030
- });
1031
- vi.spyOn(process, 'cwd').mockReturnValue(projectDir);
1032
-
1033
- await expect(cmdPmTask(ctx, ['update', '1', '--assignee', 'gemini'])).rejects.toThrow('Exit');
1034
- expect(ctx.ui.errors[0]).toContain('Permission denied');
1035
- });
1036
-
1037
- it('blocks agent when entire action is denied (no fields)', async () => {
1038
- process.env.TMT_AGENT_NAME = 'codex';
1039
-
1040
- const ctx = createMockContext(globalDir, {
1041
- cwd: projectDir,
1042
- agents: { codex: { deny: ['pm:task:create'] } },
1043
- });
1044
- vi.spyOn(process, 'cwd').mockReturnValue(projectDir);
1045
-
1046
- await expect(cmdPmTask(ctx, ['add', 'New task'])).rejects.toThrow('Exit');
1047
- expect(ctx.ui.errors[0]).toContain('Permission denied');
1048
- expect(ctx.ui.errors[0]).toContain('pm:task:create');
1049
- });
1050
-
1051
- it('allows agent without deny patterns', async () => {
1052
- process.env.TMT_AGENT_NAME = 'gemini';
1053
-
1054
- const ctx = createMockContext(globalDir, {
1055
- cwd: projectDir,
1056
- agents: { codex: { deny: ['pm:task:update(status)'] } }, // Only codex is restricted
1057
- });
1058
- vi.spyOn(process, 'cwd').mockReturnValue(projectDir);
1059
-
1060
- // gemini should be allowed
1061
- await cmdPmTask(ctx, ['update', '1', '--status', 'done']);
1062
- expect(ctx.ui.logs.some((l) => l.includes('Updated'))).toBe(true);
1063
- });
1064
-
1065
- it('blocks milestone status update when denied', async () => {
1066
- process.env.TMT_AGENT_NAME = 'codex';
1067
-
1068
- const ctx = createMockContext(globalDir, {
1069
- cwd: projectDir,
1070
- agents: { codex: { deny: ['pm:milestone:update(status)'] } },
1071
- });
1072
- vi.spyOn(process, 'cwd').mockReturnValue(projectDir);
1073
-
1074
- await expect(cmdPmMilestone(ctx, ['done', '1'])).rejects.toThrow('Exit');
1075
- expect(ctx.ui.errors[0]).toContain('Permission denied');
1076
- });
1077
-
1078
- it('blocks team creation when denied', async () => {
1079
- process.env.TMT_AGENT_NAME = 'codex';
1080
-
1081
- const newProjectDir = path.join(testDir, 'new-project');
1082
- fs.mkdirSync(newProjectDir, { recursive: true });
1083
-
1084
- const ctx = createMockContext(globalDir, {
1085
- cwd: newProjectDir,
1086
- agents: { codex: { deny: ['pm:team:create'] } },
1087
- });
1088
- vi.spyOn(process, 'cwd').mockReturnValue(newProjectDir);
1089
-
1090
- await expect(cmdPmInit(ctx, ['--name', 'New Project'])).rejects.toThrow('Exit');
1091
- expect(ctx.ui.errors[0]).toContain('Permission denied');
1092
- });
1093
-
1094
- it('blocks doc update but allows doc read', async () => {
1095
- process.env.TMT_AGENT_NAME = 'codex';
1096
-
1097
- const ctx = createMockContext(globalDir, {
1098
- cwd: projectDir,
1099
- agents: { codex: { deny: ['pm:doc:update'] } },
1100
- });
1101
- vi.spyOn(process, 'cwd').mockReturnValue(projectDir);
1102
-
1103
- // Read should work
1104
- const logs: string[] = [];
1105
- const originalLog = console.log;
1106
- console.log = (msg: string) => logs.push(msg);
1107
-
1108
- await cmdPmDoc(ctx, ['1', '--print']);
1109
-
1110
- console.log = originalLog;
1111
- expect(logs.some((l) => l.includes('Test task'))).toBe(true);
1112
- });
1113
-
1114
- it('uses TMUX_TEAM_ACTOR when TMT_AGENT_NAME is not set', async () => {
1115
- delete process.env.TMT_AGENT_NAME;
1116
- process.env.TMUX_TEAM_ACTOR = 'codex';
1117
-
1118
- const ctx = createMockContext(globalDir, {
1119
- cwd: projectDir,
1120
- agents: { codex: { deny: ['pm:task:update(status)'] } },
1121
- });
1122
- vi.spyOn(process, 'cwd').mockReturnValue(projectDir);
1123
-
1124
- await expect(cmdPmTask(ctx, ['update', '1', '--status', 'done'])).rejects.toThrow('Exit');
1125
- expect(ctx.ui.errors[0]).toContain('Permission denied');
1126
- expect(ctx.ui.errors[0]).toContain('codex');
1127
- });
1128
- });