tmux-team 2.2.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,512 +0,0 @@
1
- // ─────────────────────────────────────────────────────────────
2
- // FS Storage Adapter Tests
3
- // ─────────────────────────────────────────────────────────────
4
-
5
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
6
- import fs from 'fs';
7
- import path from 'path';
8
- import os from 'os';
9
- import { FSAdapter, createFSAdapter } from './fs.js';
10
-
11
- describe('FSAdapter', () => {
12
- let testDir: string;
13
- let adapter: FSAdapter;
14
-
15
- beforeEach(() => {
16
- // Create a unique temp directory for each test
17
- testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tmux-team-test-'));
18
- adapter = new FSAdapter(testDir);
19
- });
20
-
21
- afterEach(() => {
22
- // Clean up temp directory
23
- if (fs.existsSync(testDir)) {
24
- fs.rmSync(testDir, { recursive: true, force: true });
25
- }
26
- });
27
-
28
- describe('Team Operations', () => {
29
- it('creates team directory structure on initTeam', async () => {
30
- await adapter.initTeam('Test Project');
31
-
32
- expect(fs.existsSync(testDir)).toBe(true);
33
- expect(fs.existsSync(path.join(testDir, 'milestones'))).toBe(true);
34
- expect(fs.existsSync(path.join(testDir, 'tasks'))).toBe(true);
35
- });
36
-
37
- it('writes team.json with id, name, createdAt', async () => {
38
- const team = await adapter.initTeam('My Project', 'window-1');
39
-
40
- expect(team.name).toBe('My Project');
41
- expect(team.windowId).toBe('window-1');
42
- expect(team.id).toBe(path.basename(testDir));
43
- expect(team.createdAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
44
-
45
- // Verify file contents
46
- const teamFile = path.join(testDir, 'team.json');
47
- const saved = JSON.parse(fs.readFileSync(teamFile, 'utf-8'));
48
- expect(saved.name).toBe('My Project');
49
- });
50
-
51
- it('returns team data from team.json', async () => {
52
- await adapter.initTeam('Test Project');
53
- const team = await adapter.getTeam();
54
-
55
- expect(team).not.toBeNull();
56
- expect(team?.name).toBe('Test Project');
57
- });
58
-
59
- it('returns null when team.json does not exist', async () => {
60
- const team = await adapter.getTeam();
61
- expect(team).toBeNull();
62
- });
63
-
64
- it('updates team data', async () => {
65
- await adapter.initTeam('Original Name');
66
- const updated = await adapter.updateTeam({ name: 'New Name' });
67
-
68
- expect(updated.name).toBe('New Name');
69
-
70
- // Verify persisted
71
- const team = await adapter.getTeam();
72
- expect(team?.name).toBe('New Name');
73
- });
74
-
75
- it('throws error when updating non-existent team', async () => {
76
- await expect(adapter.updateTeam({ name: 'New' })).rejects.toThrow('Team not initialized');
77
- });
78
- });
79
-
80
- describe('Milestone Operations', () => {
81
- beforeEach(async () => {
82
- await adapter.initTeam('Test Project');
83
- });
84
-
85
- it('creates milestone with auto-incremented ID', async () => {
86
- const m1 = await adapter.createMilestone({ name: 'Phase 1' });
87
- const m2 = await adapter.createMilestone({ name: 'Phase 2' });
88
- const m3 = await adapter.createMilestone({ name: 'Phase 3' });
89
-
90
- expect(m1.id).toBe('1');
91
- expect(m2.id).toBe('2');
92
- expect(m3.id).toBe('3');
93
- });
94
-
95
- it('writes milestone to milestones/<id>.json', async () => {
96
- const milestone = await adapter.createMilestone({ name: 'MVP' });
97
- const filePath = path.join(testDir, 'milestones', `${milestone.id}.json`);
98
-
99
- expect(fs.existsSync(filePath)).toBe(true);
100
- const saved = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
101
- expect(saved.name).toBe('MVP');
102
- expect(saved.status).toBe('pending');
103
- });
104
-
105
- it('lists all milestones sorted by ID', async () => {
106
- await adapter.createMilestone({ name: 'Phase 3' });
107
- await adapter.createMilestone({ name: 'Phase 1' });
108
- await adapter.createMilestone({ name: 'Phase 2' });
109
-
110
- const milestones = await adapter.listMilestones();
111
-
112
- expect(milestones).toHaveLength(3);
113
- expect(milestones[0].id).toBe('1');
114
- expect(milestones[1].id).toBe('2');
115
- expect(milestones[2].id).toBe('3');
116
- });
117
-
118
- it('returns milestone by ID', async () => {
119
- await adapter.createMilestone({ name: 'Test' });
120
- const milestone = await adapter.getMilestone('1');
121
-
122
- expect(milestone).not.toBeNull();
123
- expect(milestone?.name).toBe('Test');
124
- });
125
-
126
- it('returns null for non-existent milestone', async () => {
127
- const milestone = await adapter.getMilestone('999');
128
- expect(milestone).toBeNull();
129
- });
130
-
131
- it('updates milestone status', async () => {
132
- await adapter.createMilestone({ name: 'Test' });
133
- const updated = await adapter.updateMilestone('1', { status: 'done' });
134
-
135
- expect(updated.status).toBe('done');
136
-
137
- const milestone = await adapter.getMilestone('1');
138
- expect(milestone?.status).toBe('done');
139
- });
140
-
141
- it('updates milestone updatedAt on change', async () => {
142
- const original = await adapter.createMilestone({ name: 'Test' });
143
- const originalUpdatedAt = original.updatedAt;
144
-
145
- // Small delay to ensure different timestamp
146
- await new Promise((r) => setTimeout(r, 10));
147
-
148
- const updated = await adapter.updateMilestone('1', { name: 'Updated' });
149
- expect(updated.updatedAt).not.toBe(originalUpdatedAt);
150
- });
151
-
152
- it('throws error when updating non-existent milestone', async () => {
153
- await expect(adapter.updateMilestone('999', { name: 'New' })).rejects.toThrow(
154
- 'Milestone 999 not found'
155
- );
156
- });
157
-
158
- it('deletes milestone', async () => {
159
- await adapter.createMilestone({ name: 'Test' });
160
- await adapter.deleteMilestone('1');
161
-
162
- const milestone = await adapter.getMilestone('1');
163
- expect(milestone).toBeNull();
164
- });
165
-
166
- it('creates markdown doc file for milestone', async () => {
167
- const milestone = await adapter.createMilestone({ name: 'Phase 1' });
168
- const docPath = path.join(testDir, 'milestones', `${milestone.id}.md`);
169
-
170
- expect(fs.existsSync(docPath)).toBe(true);
171
- const content = fs.readFileSync(docPath, 'utf-8');
172
- expect(content).toContain('# Phase 1');
173
- });
174
-
175
- it('creates milestone with description in doc file', async () => {
176
- const milestone = await adapter.createMilestone({
177
- name: 'Phase 1',
178
- description: 'Initial development phase',
179
- });
180
- const docPath = path.join(testDir, 'milestones', `${milestone.id}.md`);
181
-
182
- const content = fs.readFileSync(docPath, 'utf-8');
183
- expect(content).toContain('# Phase 1');
184
- expect(content).toContain('Initial development phase');
185
- });
186
-
187
- it('deletes both json and md files on milestone delete', async () => {
188
- const milestone = await adapter.createMilestone({ name: 'Cleanup' });
189
- const jsonPath = path.join(testDir, 'milestones', `${milestone.id}.json`);
190
- const mdPath = path.join(testDir, 'milestones', `${milestone.id}.md`);
191
-
192
- expect(fs.existsSync(jsonPath)).toBe(true);
193
- expect(fs.existsSync(mdPath)).toBe(true);
194
-
195
- await adapter.deleteMilestone(milestone.id);
196
-
197
- expect(fs.existsSync(jsonPath)).toBe(false);
198
- expect(fs.existsSync(mdPath)).toBe(false);
199
- });
200
- });
201
-
202
- describe('Milestone Documentation', () => {
203
- beforeEach(async () => {
204
- await adapter.initTeam('Test Project');
205
- });
206
-
207
- it('gets milestone documentation', async () => {
208
- await adapter.createMilestone({ name: 'Phase 1', description: 'Intro' });
209
- const doc = await adapter.getMilestoneDoc('1');
210
-
211
- expect(doc).toContain('# Phase 1');
212
- expect(doc).toContain('Intro');
213
- });
214
-
215
- it('sets milestone documentation', async () => {
216
- await adapter.createMilestone({ name: 'Test' });
217
- await adapter.setMilestoneDoc('1', '# Updated\n\nNew content');
218
-
219
- const doc = await adapter.getMilestoneDoc('1');
220
- expect(doc).toBe('# Updated\n\nNew content');
221
- });
222
-
223
- it('creates milestones dir when setting doc if missing', async () => {
224
- await adapter.createMilestone({ name: 'Test' });
225
- // Remove the milestones dir
226
- fs.rmSync(path.join(testDir, 'milestones'), { recursive: true, force: true });
227
-
228
- await adapter.setMilestoneDoc('1', '# Updated\n\nBody');
229
-
230
- expect(fs.existsSync(path.join(testDir, 'milestones', '1.md'))).toBe(true);
231
- const doc = await adapter.getMilestoneDoc('1');
232
- expect(doc).toBe('# Updated\n\nBody');
233
- });
234
-
235
- it('returns null for non-existent doc', async () => {
236
- const doc = await adapter.getMilestoneDoc('999');
237
- expect(doc).toBeNull();
238
- });
239
- });
240
-
241
- describe('Task Operations', () => {
242
- beforeEach(async () => {
243
- await adapter.initTeam('Test Project');
244
- });
245
-
246
- it('creates task with auto-incremented ID', async () => {
247
- const t1 = await adapter.createTask({ title: 'Task 1' });
248
- const t2 = await adapter.createTask({ title: 'Task 2' });
249
-
250
- expect(t1.id).toBe('1');
251
- expect(t2.id).toBe('2');
252
- });
253
-
254
- it('writes task to tasks/<id>.json', async () => {
255
- const task = await adapter.createTask({ title: 'Implement feature' });
256
- const filePath = path.join(testDir, 'tasks', `${task.id}.json`);
257
-
258
- expect(fs.existsSync(filePath)).toBe(true);
259
- const saved = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
260
- expect(saved.title).toBe('Implement feature');
261
- expect(saved.status).toBe('pending');
262
- });
263
-
264
- it('creates markdown doc file for task', async () => {
265
- const task = await adapter.createTask({ title: 'My Task' });
266
- const docPath = path.join(testDir, 'tasks', `${task.id}.md`);
267
-
268
- expect(fs.existsSync(docPath)).toBe(true);
269
- const content = fs.readFileSync(docPath, 'utf-8');
270
- expect(content).toContain('# My Task');
271
- });
272
-
273
- it('creates task with milestone and assignee', async () => {
274
- await adapter.createMilestone({ name: 'Phase 1' });
275
- const task = await adapter.createTask({
276
- title: 'Test',
277
- milestone: '1',
278
- assignee: 'claude',
279
- });
280
-
281
- expect(task.milestone).toBe('1');
282
- expect(task.assignee).toBe('claude');
283
- });
284
-
285
- it('lists all tasks sorted by ID', async () => {
286
- await adapter.createTask({ title: 'Third' });
287
- await adapter.createTask({ title: 'First' });
288
- await adapter.createTask({ title: 'Second' });
289
-
290
- const tasks = await adapter.listTasks();
291
-
292
- expect(tasks).toHaveLength(3);
293
- expect(tasks[0].id).toBe('1');
294
- expect(tasks[1].id).toBe('2');
295
- expect(tasks[2].id).toBe('3');
296
- });
297
-
298
- it('filters tasks by milestone', async () => {
299
- await adapter.createTask({ title: 'Task 1', milestone: '1' });
300
- await adapter.createTask({ title: 'Task 2', milestone: '2' });
301
- await adapter.createTask({ title: 'Task 3', milestone: '1' });
302
-
303
- const tasks = await adapter.listTasks({ milestone: '1' });
304
-
305
- expect(tasks).toHaveLength(2);
306
- expect(tasks.every((t) => t.milestone === '1')).toBe(true);
307
- });
308
-
309
- it('filters tasks by status', async () => {
310
- const t1 = await adapter.createTask({ title: 'Pending' });
311
- await adapter.createTask({ title: 'Also Pending' });
312
- await adapter.updateTask(t1.id, { status: 'done' });
313
-
314
- const pending = await adapter.listTasks({ status: 'pending' });
315
- const done = await adapter.listTasks({ status: 'done' });
316
-
317
- expect(pending).toHaveLength(1);
318
- expect(done).toHaveLength(1);
319
- });
320
-
321
- it('filters tasks by assignee', async () => {
322
- await adapter.createTask({ title: 'Task 1', assignee: 'claude' });
323
- await adapter.createTask({ title: 'Task 2', assignee: 'codex' });
324
-
325
- const tasks = await adapter.listTasks({ assignee: 'claude' });
326
-
327
- expect(tasks).toHaveLength(1);
328
- expect(tasks[0].assignee).toBe('claude');
329
- });
330
-
331
- it('excludes tasks in completed milestones by default', async () => {
332
- // Create milestones
333
- const m1 = await adapter.createMilestone({ name: 'Done Milestone' });
334
- const m2 = await adapter.createMilestone({ name: 'Open Milestone' });
335
- await adapter.updateMilestone(m1.id, { status: 'done' });
336
-
337
- // Create tasks in both milestones
338
- await adapter.createTask({ title: 'Task in done milestone', milestone: m1.id });
339
- await adapter.createTask({ title: 'Task in open milestone', milestone: m2.id });
340
- await adapter.createTask({ title: 'Task without milestone' });
341
-
342
- // Default: excludeCompletedMilestones = true
343
- const tasks = await adapter.listTasks();
344
- expect(tasks).toHaveLength(2);
345
- expect(tasks.map((t) => t.title)).not.toContain('Task in done milestone');
346
- });
347
-
348
- it('includes tasks in completed milestones when excludeCompletedMilestones is false', async () => {
349
- // Create milestones
350
- const m1 = await adapter.createMilestone({ name: 'Done Milestone' });
351
- await adapter.updateMilestone(m1.id, { status: 'done' });
352
-
353
- // Create task in done milestone
354
- await adapter.createTask({ title: 'Task in done milestone', milestone: m1.id });
355
- await adapter.createTask({ title: 'Task without milestone' });
356
-
357
- // Include all tasks
358
- const tasks = await adapter.listTasks({ excludeCompletedMilestones: false });
359
- expect(tasks).toHaveLength(2);
360
- expect(tasks.map((t) => t.title)).toContain('Task in done milestone');
361
- });
362
-
363
- it('hides tasks without milestones when hideOrphanTasks is true', async () => {
364
- const milestone = await adapter.createMilestone({ name: 'Has Tasks' });
365
-
366
- await adapter.createTask({ title: 'With milestone', milestone: milestone.id });
367
- await adapter.createTask({ title: 'Without milestone' });
368
-
369
- const tasks = await adapter.listTasks({ hideOrphanTasks: true });
370
- expect(tasks).toHaveLength(1);
371
- expect(tasks[0].title).toBe('With milestone');
372
- });
373
-
374
- it('includes tasks without milestones when hideOrphanTasks is false', async () => {
375
- const milestone = await adapter.createMilestone({ name: 'Has Tasks' });
376
-
377
- await adapter.createTask({ title: 'With milestone', milestone: milestone.id });
378
- await adapter.createTask({ title: 'Without milestone' });
379
-
380
- const tasks = await adapter.listTasks({ hideOrphanTasks: false });
381
- expect(tasks).toHaveLength(2);
382
- expect(tasks.map((t) => t.title)).toContain('Without milestone');
383
- });
384
-
385
- it('returns task by ID', async () => {
386
- await adapter.createTask({ title: 'Test Task' });
387
- const task = await adapter.getTask('1');
388
-
389
- expect(task).not.toBeNull();
390
- expect(task?.title).toBe('Test Task');
391
- });
392
-
393
- it('returns null for non-existent task', async () => {
394
- const task = await adapter.getTask('999');
395
- expect(task).toBeNull();
396
- });
397
-
398
- it('updates task status', async () => {
399
- await adapter.createTask({ title: 'Test' });
400
- const updated = await adapter.updateTask('1', { status: 'in_progress' });
401
-
402
- expect(updated.status).toBe('in_progress');
403
- });
404
-
405
- it('throws error when updating non-existent task', async () => {
406
- await expect(adapter.updateTask('999', { status: 'done' })).rejects.toThrow(
407
- 'Task 999 not found'
408
- );
409
- });
410
-
411
- it('deletes task and its doc file', async () => {
412
- await adapter.createTask({ title: 'Test' });
413
- await adapter.deleteTask('1');
414
-
415
- expect(await adapter.getTask('1')).toBeNull();
416
- expect(await adapter.getTaskDoc('1')).toBeNull();
417
- });
418
- });
419
-
420
- describe('Task Documentation', () => {
421
- beforeEach(async () => {
422
- await adapter.initTeam('Test Project');
423
- });
424
-
425
- it('gets task documentation', async () => {
426
- await adapter.createTask({ title: 'Test' });
427
- const doc = await adapter.getTaskDoc('1');
428
-
429
- expect(doc).toContain('# Test');
430
- });
431
-
432
- it('sets task documentation', async () => {
433
- await adapter.createTask({ title: 'Test' });
434
- await adapter.setTaskDoc('1', '# Updated\n\nNew content');
435
-
436
- const doc = await adapter.getTaskDoc('1');
437
- expect(doc).toBe('# Updated\n\nNew content');
438
- });
439
-
440
- it('returns null for non-existent doc', async () => {
441
- const doc = await adapter.getTaskDoc('999');
442
- expect(doc).toBeNull();
443
- });
444
- });
445
-
446
- describe('Audit Log', () => {
447
- beforeEach(async () => {
448
- await adapter.initTeam('Test Project');
449
- });
450
-
451
- it('appends event to events.jsonl', async () => {
452
- await adapter.appendEvent({
453
- event: 'task_created',
454
- id: '1',
455
- actor: 'human',
456
- ts: new Date().toISOString(),
457
- });
458
-
459
- const events = await adapter.getEvents();
460
- expect(events).toHaveLength(1);
461
- expect(events[0].event).toBe('task_created');
462
- });
463
-
464
- it('appends multiple events', async () => {
465
- await adapter.appendEvent({ event: 'event1', id: '1', actor: 'human', ts: '' });
466
- await adapter.appendEvent({ event: 'event2', id: '2', actor: 'claude', ts: '' });
467
- await adapter.appendEvent({ event: 'event3', id: '3', actor: 'codex', ts: '' });
468
-
469
- const events = await adapter.getEvents();
470
- expect(events).toHaveLength(3);
471
- });
472
-
473
- it('returns events with limit', async () => {
474
- for (let i = 0; i < 10; i++) {
475
- await adapter.appendEvent({ event: `event${i}`, id: String(i), actor: 'human', ts: '' });
476
- }
477
-
478
- const events = await adapter.getEvents(3);
479
- expect(events).toHaveLength(3);
480
- // Returns last 3 events
481
- expect(events[0].event).toBe('event7');
482
- expect(events[1].event).toBe('event8');
483
- expect(events[2].event).toBe('event9');
484
- });
485
-
486
- it('returns empty array when no events', async () => {
487
- const events = await adapter.getEvents();
488
- expect(events).toEqual([]);
489
- });
490
-
491
- it('handles malformed JSONL lines gracefully', async () => {
492
- // Write some valid and invalid lines directly
493
- const eventsFile = path.join(testDir, 'events.jsonl');
494
- fs.writeFileSync(
495
- eventsFile,
496
- `{"event":"valid","id":"1","actor":"h","ts":""}\n` +
497
- `not json\n` +
498
- `{"event":"also_valid","id":"2","actor":"c","ts":""}\n`
499
- );
500
-
501
- const events = await adapter.getEvents();
502
- expect(events).toHaveLength(2);
503
- });
504
- });
505
-
506
- describe('createFSAdapter factory', () => {
507
- it('creates FSAdapter instance', () => {
508
- const adapter = createFSAdapter('/tmp/test');
509
- expect(adapter).toBeInstanceOf(FSAdapter);
510
- });
511
- });
512
- });