@ronkovic/aad 0.3.9 → 0.4.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.
Files changed (114) hide show
  1. package/README.md +292 -12
  2. package/package.json +6 -1
  3. package/src/__tests__/e2e/pipeline-e2e.test.ts +1 -0
  4. package/src/__tests__/e2e/resume-e2e.test.ts +2 -0
  5. package/src/__tests__/integration/pipeline.test.ts +1 -0
  6. package/src/modules/claude-provider/__tests__/claude-sdk-real-env.test.ts +1 -1
  7. package/src/modules/claude-provider/__tests__/claude-sdk.adapter.test.ts +2 -0
  8. package/src/modules/claude-provider/__tests__/provider-registry.test.ts +1 -0
  9. package/src/modules/cli/__tests__/cleanup.test.ts +72 -0
  10. package/src/modules/cli/__tests__/resume.test.ts +1 -0
  11. package/src/modules/cli/__tests__/run.test.ts +1 -0
  12. package/src/modules/cli/commands/__tests__/task-dispatch-handler.test.ts +145 -0
  13. package/src/modules/cli/commands/cleanup.ts +26 -11
  14. package/src/modules/cli/commands/resume.ts +3 -2
  15. package/src/modules/cli/commands/run.ts +57 -7
  16. package/src/modules/cli/commands/task-dispatch-handler.ts +73 -3
  17. package/src/modules/dashboard/__tests__/api-graph.test.ts +332 -0
  18. package/src/modules/dashboard/__tests__/api-timeline.test.ts +461 -0
  19. package/src/modules/dashboard/routes/sse.ts +3 -2
  20. package/src/modules/dashboard/server.ts +1 -0
  21. package/src/modules/dashboard/services/sse-broadcaster.ts +29 -0
  22. package/src/modules/dashboard/ui/dashboard.html +143 -18
  23. package/src/modules/git-workspace/__tests__/branch-manager.test.ts +52 -0
  24. package/src/modules/git-workspace/__tests__/dependency-installer.test.ts +77 -0
  25. package/src/modules/git-workspace/__tests__/git-exec.test.ts +26 -0
  26. package/src/modules/git-workspace/__tests__/merge-service.test.ts +19 -0
  27. package/src/modules/git-workspace/__tests__/pr-manager.test.ts +80 -0
  28. package/src/modules/git-workspace/__tests__/template-copy.test.ts +189 -0
  29. package/src/modules/git-workspace/__tests__/worktree-cleanup.test.ts +29 -2
  30. package/src/modules/git-workspace/__tests__/worktree-manager.test.ts +64 -4
  31. package/src/modules/git-workspace/branch-manager.ts +24 -3
  32. package/src/modules/git-workspace/dependency-installer.ts +113 -0
  33. package/src/modules/git-workspace/git-exec.ts +3 -2
  34. package/src/modules/git-workspace/index.ts +10 -1
  35. package/src/modules/git-workspace/merge-service.ts +36 -2
  36. package/src/modules/git-workspace/pr-manager.ts +278 -0
  37. package/src/modules/git-workspace/template-copy.ts +302 -0
  38. package/src/modules/git-workspace/worktree-manager.ts +37 -11
  39. package/src/modules/planning/__tests__/planning-service.test.ts +1 -0
  40. package/src/modules/planning/__tests__/planning.service.test.ts +149 -0
  41. package/src/modules/planning/__tests__/project-detection.test.ts +7 -1
  42. package/src/modules/planning/planning.service.ts +16 -2
  43. package/src/modules/planning/project-detection.ts +4 -1
  44. package/src/modules/process-manager/__tests__/process-manager.test.ts +1 -0
  45. package/src/modules/task-execution/__tests__/executor.test.ts +86 -0
  46. package/src/modules/task-execution/__tests__/tester-verify.test.ts +4 -3
  47. package/src/modules/task-execution/executor.ts +87 -4
  48. package/src/modules/task-execution/phases/implementer-green.ts +22 -5
  49. package/src/modules/task-execution/phases/merge.ts +44 -2
  50. package/src/modules/task-execution/phases/tester-red.ts +22 -5
  51. package/src/modules/task-execution/phases/tester-verify.ts +22 -6
  52. package/src/modules/task-queue/dispatcher.ts +50 -1
  53. package/src/shared/__tests__/prerequisites.test.ts +176 -0
  54. package/src/shared/config.ts +6 -0
  55. package/src/shared/prerequisites.ts +190 -0
  56. package/src/shared/types.ts +13 -0
  57. package/templates/CLAUDE.md +122 -0
  58. package/templates/settings.json +117 -0
  59. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/progress.json +0 -10
  60. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/completed/task-getall-2.json +0 -10
  61. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-1.json +0 -13
  62. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-getall-1.json +0 -10
  63. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-status-change.json +0 -10
  64. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/workers/worker-1.json +0 -5
  65. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/workers/worker-2.json +0 -5
  66. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/progress.json +0 -10
  67. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/completed/task-getall-2.json +0 -10
  68. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-1.json +0 -13
  69. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-getall-1.json +0 -10
  70. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-status-change.json +0 -10
  71. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/workers/worker-1.json +0 -5
  72. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/workers/worker-2.json +0 -5
  73. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/progress.json +0 -10
  74. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/completed/task-getall-2.json +0 -10
  75. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-1.json +0 -13
  76. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-getall-1.json +0 -10
  77. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-status-change.json +0 -10
  78. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/workers/worker-1.json +0 -5
  79. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/workers/worker-2.json +0 -5
  80. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/progress.json +0 -10
  81. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/completed/task-getall-2.json +0 -10
  82. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-1.json +0 -13
  83. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-getall-1.json +0 -10
  84. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-status-change.json +0 -10
  85. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/workers/worker-1.json +0 -5
  86. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/workers/worker-2.json +0 -5
  87. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/progress.json +0 -10
  88. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/completed/task-getall-2.json +0 -10
  89. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-1.json +0 -13
  90. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-getall-1.json +0 -10
  91. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-status-change.json +0 -10
  92. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/workers/worker-1.json +0 -5
  93. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/workers/worker-2.json +0 -5
  94. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/progress.json +0 -10
  95. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/completed/task-getall-2.json +0 -10
  96. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-1.json +0 -13
  97. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-getall-1.json +0 -10
  98. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-status-change.json +0 -10
  99. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/workers/worker-1.json +0 -5
  100. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/workers/worker-2.json +0 -5
  101. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/progress.json +0 -10
  102. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/completed/task-getall-2.json +0 -10
  103. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-1.json +0 -13
  104. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-getall-1.json +0 -10
  105. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-status-change.json +0 -10
  106. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/workers/worker-1.json +0 -5
  107. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/workers/worker-2.json +0 -5
  108. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/progress.json +0 -10
  109. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/completed/task-getall-2.json +0 -10
  110. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-1.json +0 -13
  111. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-getall-1.json +0 -10
  112. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-status-change.json +0 -10
  113. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/workers/worker-1.json +0 -5
  114. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/workers/worker-2.json +0 -5
@@ -10,9 +10,15 @@
10
10
  header{background:#2d2d2d;padding:20px 24px;border-radius:10px;margin-bottom:20px;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:10px}
11
11
  header h1{font-size:22px;font-weight:700;color:#4fc3f7}
12
12
  header p{color:#9e9e9e;font-size:13px}
13
- .conn-badge{font-size:11px;padding:4px 10px;border-radius:12px;font-weight:600}
14
- .conn-badge.connected{background:#66bb6a33;color:#66bb6a}
15
- .conn-badge.disconnected{background:#ef535033;color:#ef5350}
13
+ .activity-cell{font-size:11px;font-weight:600}
14
+ .activity-connected{color:#66bb6a}
15
+ .activity-reconnecting{color:#ffa726}
16
+ .activity-disconnected{color:#ef5350}
17
+ .activity-phase-red{color:#ef5350}
18
+ .activity-phase-green{color:#66bb6a}
19
+ .activity-phase-verify{color:#42a5f5}
20
+ .activity-phase-review{color:#ab47bc}
21
+ .activity-phase-merge{color:#ffa726}
16
22
 
17
23
  /* Progress bar */
18
24
  .progress-section{background:#2d2d2d;border-radius:10px;padding:20px 24px;margin-bottom:20px}
@@ -95,7 +101,6 @@
95
101
  <body>
96
102
  <header>
97
103
  <div><h1>AAD Dashboard</h1><p>Real-time Task Orchestrator Monitor</p></div>
98
- <span class="conn-badge disconnected" id="conn-status">Disconnected</span>
99
104
  </header>
100
105
 
101
106
  <!-- Progress Bar -->
@@ -130,8 +135,8 @@
130
135
  <h2>Tasks <span class="count" id="tasks-count"></span></h2>
131
136
  <div class="task-table-wrap">
132
137
  <table>
133
- <thead><tr><th>ID</th><th>Title</th><th>Status</th><th>Priority</th><th>Dependencies</th></tr></thead>
134
- <tbody id="task-tbody"><tr><td colspan="5" class="empty-msg">No tasks</td></tr></tbody>
138
+ <thead><tr><th>ID</th><th>Title</th><th>Status</th><th>Activity</th><th>Priority</th><th>Dependencies</th></tr></thead>
139
+ <tbody id="task-tbody"><tr><td colspan="6" class="empty-msg">No tasks</td></tr></tbody>
135
140
  </table>
136
141
  </div>
137
142
  </div>
@@ -150,6 +155,8 @@
150
155
  <label><input type="checkbox" checked data-level="warn"> Warn</label>
151
156
  <label><input type="checkbox" checked data-level="error"> Error</label>
152
157
  <select id="svc-filter"><option value="">All Services</option></select>
158
+ <input type="text" id="log-search" placeholder="Search logs..." style="background:#3a3a3a;color:#e0e0e0;border:1px solid #555;border-radius:4px;padding:4px 10px;font-size:12px;width:160px">
159
+ <button id="filter-reset" style="background:#4fc3f7;color:#1a1a1a;border:none;border-radius:4px;padding:4px 10px;font-size:12px;font-weight:600;cursor:pointer;transition:background .2s">Reset</button>
153
160
  </div>
154
161
  <div class="log-entries" id="log-entries"></div>
155
162
  </div>
@@ -159,7 +166,10 @@
159
166
  let allTasks = [];
160
167
  let logEntries = [];
161
168
  let knownServices = new Set();
162
- let logFilters = { info: true, warn: true, error: true, service: '' };
169
+ let logFilters = { info: true, warn: true, error: true, service: '', search: '' };
170
+ let taskPhases = {}; // { [taskId]: { phase, status } }
171
+ let sseStatus = 'disconnected'; // Global SSE connection state
172
+ let reconnectAttempts = 0;
163
173
 
164
174
  // --- Data fetching ---
165
175
  async function loadAll() {
@@ -212,13 +222,49 @@
212
222
  function renderTasks() {
213
223
  const tbody = document.getElementById('task-tbody');
214
224
  document.getElementById('tasks-count').textContent = '(' + allTasks.length + ')';
215
- if (!allTasks.length) { tbody.innerHTML = '<tr><td colspan="5" class="empty-msg">No tasks</td></tr>'; return; }
225
+ if (!allTasks.length) { tbody.innerHTML = '<tr><td colspan="6" class="empty-msg">No tasks</td></tr>'; return; }
216
226
  tbody.innerHTML = allTasks.map(t => {
217
227
  const deps = (t.dependsOn || []).map(d => esc(String(d))).join(', ') || '—';
218
- return '<tr><td style="font-family:monospace;font-size:11px">' + esc(String(t.taskId)) + '</td><td>' + esc(t.title || '—') + '</td><td><span class="badge badge-' + t.status + '">' + t.status + '</span></td><td class="priority">' + (t.priority || 0) + '</td><td class="deps">' + deps + '</td></tr>';
228
+ const activity = getActivityDisplay(t.taskId, t.status);
229
+ return '<tr><td style="font-family:monospace;font-size:11px">' + esc(String(t.taskId)) + '</td><td>' + esc(t.title || '—') + '</td><td><span class="badge badge-' + t.status + '">' + t.status + '</span></td><td class="activity-cell">' + activity + '</td><td class="priority">' + (t.priority || 0) + '</td><td class="deps">' + deps + '</td></tr>';
219
230
  }).join('');
220
231
  }
221
232
 
233
+ function getActivityDisplay(taskId, status) {
234
+ if (status === 'running') {
235
+ const phaseInfo = taskPhases[taskId];
236
+ if (phaseInfo) {
237
+ const phaseLabels = { red: 'Red (Tests)', green: 'Green (Impl)', verify: 'Verify', review: 'Review', merge: 'Merge' };
238
+ const phaseLabel = phaseLabels[phaseInfo.phase] || phaseInfo.phase;
239
+ if (phaseInfo.status === 'running') {
240
+ return '<span class="activity-phase-' + phaseInfo.phase + '">' + esc(phaseLabel) + '</span>';
241
+ } else if (phaseInfo.status === 'completed') {
242
+ return '<span class="activity-phase-' + phaseInfo.phase + '">✓ ' + esc(phaseLabel) + '</span>';
243
+ } else if (phaseInfo.status === 'failed') {
244
+ return '<span class="activity-phase-' + phaseInfo.phase + '">✗ ' + esc(phaseLabel) + '</span>';
245
+ }
246
+ }
247
+ return '<span class="activity-phase-green">Running</span>';
248
+ } else {
249
+ // Non-RUNNING: show SSE connection status
250
+ if (sseStatus === 'connected') {
251
+ return '<span class="activity-connected">Connected</span>';
252
+ } else if (sseStatus === 'reconnecting') {
253
+ return '<span class="activity-reconnecting">Reconnecting...</span>';
254
+ } else {
255
+ return '<span class="activity-disconnected">Disconnected</span>';
256
+ }
257
+ }
258
+ }
259
+
260
+ function updateActivityColumn(taskId) {
261
+ renderTasks();
262
+ }
263
+
264
+ function updateAllActivityColumns() {
265
+ renderTasks();
266
+ }
267
+
222
268
  function updateTaskInList(taskId, updates) {
223
269
  const idx = allTasks.findIndex(t => t.taskId === taskId);
224
270
  if (idx >= 0) Object.assign(allTasks[idx], updates);
@@ -340,30 +386,94 @@
340
386
  }
341
387
 
342
388
  function applyLogFilterToEntry(div) {
343
- const show = logFilters[div.dataset.level] && (!logFilters.service || div.dataset.service === logFilters.service);
389
+ let show = logFilters[div.dataset.level] && (!logFilters.service || div.dataset.service === logFilters.service);
390
+ if (show && logFilters.search) {
391
+ const searchLower = logFilters.search.toLowerCase();
392
+ const text = div.textContent.toLowerCase();
393
+ show = text.includes(searchLower);
394
+ }
344
395
  div.classList.toggle('log-hidden', !show);
345
- updateLogCount();
346
396
  }
347
397
 
348
398
  function updateLogCount() {
399
+ const total = logEntries.length;
349
400
  const visible = document.querySelectorAll('#log-entries .log-entry:not(.log-hidden)').length;
350
- document.getElementById('log-count').textContent = '(' + visible + ')';
401
+ document.getElementById('log-count').textContent = '(Showing ' + visible + '/' + total + ')';
402
+ }
403
+
404
+ // Load filters from localStorage
405
+ function loadFiltersFromStorage() {
406
+ try {
407
+ const saved = localStorage.getItem('aad-log-filters');
408
+ if (saved) {
409
+ const parsed = JSON.parse(saved);
410
+ logFilters = { ...logFilters, ...parsed };
411
+ document.querySelectorAll('.log-filters input[data-level]').forEach(cb => {
412
+ cb.checked = logFilters[cb.dataset.level] !== false;
413
+ });
414
+ const svcSelect = document.getElementById('svc-filter');
415
+ if (svcSelect && logFilters.service) svcSelect.value = logFilters.service;
416
+ const searchInput = document.getElementById('log-search');
417
+ if (searchInput && logFilters.search) searchInput.value = logFilters.search;
418
+ }
419
+ } catch (e) { console.error('Failed to load filters:', e); }
420
+ }
421
+
422
+ // Save filters to localStorage
423
+ function saveFiltersToStorage() {
424
+ try {
425
+ localStorage.setItem('aad-log-filters', JSON.stringify(logFilters));
426
+ } catch (e) { console.error('Failed to save filters:', e); }
427
+ }
428
+
429
+ // Reset filters
430
+ function resetFilters() {
431
+ logFilters = { info: true, warn: true, error: true, service: '', search: '' };
432
+ document.querySelectorAll('.log-filters input[data-level]').forEach(cb => {
433
+ cb.checked = true;
434
+ });
435
+ document.getElementById('svc-filter').value = '';
436
+ document.getElementById('log-search').value = '';
437
+ saveFiltersToStorage();
438
+ applyLogFilters();
351
439
  }
352
440
 
353
441
  // Filter event listeners
354
442
  document.querySelectorAll('.log-filters input[data-level]').forEach(cb => {
355
- cb.addEventListener('change', () => { logFilters[cb.dataset.level] = cb.checked; applyLogFilters(); });
443
+ cb.addEventListener('change', () => {
444
+ logFilters[cb.dataset.level] = cb.checked;
445
+ saveFiltersToStorage();
446
+ applyLogFilters();
447
+ });
448
+ });
449
+ document.getElementById('svc-filter').addEventListener('change', function() {
450
+ logFilters.service = this.value;
451
+ saveFiltersToStorage();
452
+ applyLogFilters();
356
453
  });
357
- document.getElementById('svc-filter').addEventListener('change', function() { logFilters.service = this.value; applyLogFilters(); });
454
+ document.getElementById('log-search').addEventListener('input', function() {
455
+ logFilters.search = this.value;
456
+ saveFiltersToStorage();
457
+ applyLogFilters();
458
+ });
459
+ document.getElementById('filter-reset').addEventListener('click', resetFilters);
358
460
 
359
461
  // --- SSE ---
360
462
  let evtSource;
361
463
  function connectSSE() {
362
464
  if (evtSource) { try { evtSource.close(); } catch(e){} }
363
465
  evtSource = new EventSource('/events/all');
364
- const badge = document.getElementById('conn-status');
365
466
 
366
- evtSource.addEventListener('open', () => { badge.textContent = 'Connected'; badge.className = 'conn-badge connected'; });
467
+ evtSource.addEventListener('open', () => {
468
+ reconnectAttempts = 0;
469
+ sseStatus = 'connected';
470
+ updateAllActivityColumns();
471
+ });
472
+
473
+ evtSource.addEventListener('heartbeat', () => {
474
+ sseStatus = 'connected';
475
+ updateAllActivityColumns();
476
+ });
367
477
 
368
478
  evtSource.addEventListener('message', (e) => {
369
479
  try {
@@ -371,6 +481,18 @@
371
481
  switch (ev.type) {
372
482
  case 'log:entry': addLogEntry(ev.entry || ev); break;
373
483
  case 'progress:updated': updateProgress(ev.state ? { progress: ev.state, percentComplete: ev.percentComplete } : ev); break;
484
+ case 'execution:phase:started':
485
+ taskPhases[ev.taskId] = { phase: ev.phase, status: 'running' };
486
+ updateActivityColumn(ev.taskId);
487
+ break;
488
+ case 'execution:phase:completed':
489
+ taskPhases[ev.taskId] = { phase: ev.phase, status: 'completed' };
490
+ updateActivityColumn(ev.taskId);
491
+ break;
492
+ case 'execution:phase:failed':
493
+ taskPhases[ev.taskId] = { phase: ev.phase, status: 'failed' };
494
+ updateActivityColumn(ev.taskId);
495
+ break;
374
496
  case 'task:dispatched':
375
497
  case 'task:completed':
376
498
  case 'task:failed':
@@ -388,9 +510,11 @@
388
510
  });
389
511
 
390
512
  evtSource.addEventListener('error', () => {
391
- badge.textContent = 'Disconnected'; badge.className = 'conn-badge disconnected';
513
+ reconnectAttempts++;
514
+ sseStatus = reconnectAttempts > 3 ? 'disconnected' : 'reconnecting';
515
+ updateAllActivityColumns();
392
516
  evtSource.close();
393
- setTimeout(connectSSE, 3000);
517
+ setTimeout(connectSSE, Math.min(reconnectAttempts * 3000, 9000));
394
518
  });
395
519
  }
396
520
 
@@ -398,6 +522,7 @@
398
522
  function esc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
399
523
 
400
524
  // Init
525
+ loadFiltersFromStorage();
401
526
  loadAll();
402
527
  connectSSE();
403
528
  </script>
@@ -199,6 +199,58 @@ describe("BranchManager", () => {
199
199
  );
200
200
  });
201
201
 
202
+ test("excludes branches matching excludePatterns (default: /parent)", async () => {
203
+ const branches = ["feat/run-001/task-001", "feat/run-001/parent", "aad-task-x"];
204
+ let callCount = 0;
205
+ mockGitOps.gitExec = mock(async (args: string[]) => {
206
+ callCount++;
207
+ if (args[0] === "branch" && args[1] === "--list") {
208
+ // Only return branches for the first conventional prefix pattern
209
+ if (callCount === 1) {
210
+ return { stdout: branches.slice(0, 2).join("\n"), stderr: "", exitCode: 0 };
211
+ }
212
+ // aad-* pattern
213
+ if (args[2] === "aad-*") {
214
+ return { stdout: branches[2] ?? "", stderr: "", exitCode: 0 };
215
+ }
216
+ return { stdout: "", stderr: "", exitCode: 0 };
217
+ }
218
+ return { stdout: "", stderr: "", exitCode: 0 };
219
+ });
220
+
221
+ const deleted = await branchManager.cleanupOrphanBranches();
222
+ // feat/run-001/parent はスキップ、他2つは削除
223
+ expect(deleted).not.toContain("feat/run-001/parent");
224
+ expect(deleted.length).toBe(2);
225
+ });
226
+
227
+ test("deletes all branches when excludePatterns is empty array", async () => {
228
+ const branches = ["feat/run-001/parent", "feat/run-001/task-001"];
229
+ mockGitOps.gitExec = mock(async (args: string[]) => {
230
+ if (args[0] === "branch" && args[1] === "--list") {
231
+ if (args[2]?.startsWith("feat/")) {
232
+ return { stdout: branches.join("\n"), stderr: "", exitCode: 0 };
233
+ }
234
+ return { stdout: "", stderr: "", exitCode: 0 };
235
+ }
236
+ return { stdout: "", stderr: "", exitCode: 0 };
237
+ });
238
+
239
+ const deleted = await branchManager.cleanupOrphanBranches(undefined, {
240
+ force: false,
241
+ excludePatterns: [],
242
+ });
243
+ // parent含めて全削除
244
+ expect(deleted).toContain("feat/run-001/parent");
245
+ });
246
+
247
+ test("supports legacy boolean argument (backward compat)", async () => {
248
+ mockGitOps.gitExec = mock(async () => ({ stdout: "", stderr: "", exitCode: 0 }));
249
+ const deleted = await branchManager.cleanupOrphanBranches(undefined, true);
250
+ expect(deleted).toEqual([]);
251
+ // No error = backward compat OK
252
+ });
253
+
202
254
  test("cleans up orphan branches for specific runId", async () => {
203
255
  const runId = createRunId("run-001");
204
256
  const branches = ["aad-task-run-001-01", "aad-run-run-001"];
@@ -0,0 +1,77 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { buildInstallCommand } from "../dependency-installer";
3
+ import type { WorkspaceInfo } from "../../../shared/types";
4
+
5
+ describe("buildInstallCommand", () => {
6
+ // Helper to create minimal WorkspaceInfo
7
+ const createWorkspace = (packageManager: WorkspaceInfo["packageManager"]): WorkspaceInfo => ({
8
+ path: "/tmp/test",
9
+ language: "typescript",
10
+ packageManager,
11
+ framework: "none",
12
+ testFramework: "bun-test",
13
+ });
14
+
15
+ describe("Node.js ecosystem", () => {
16
+ test("bun → bun install --frozen-lockfile", () => {
17
+ const result = buildInstallCommand(createWorkspace("bun"));
18
+ expect(result).toEqual(["bun", "install", "--frozen-lockfile"]);
19
+ });
20
+
21
+ test("npm → npm ci", () => {
22
+ const result = buildInstallCommand(createWorkspace("npm"));
23
+ expect(result).toEqual(["npm", "ci"]);
24
+ });
25
+
26
+ test("yarn → yarn install --frozen-lockfile", () => {
27
+ const result = buildInstallCommand(createWorkspace("yarn"));
28
+ expect(result).toEqual(["yarn", "install", "--frozen-lockfile"]);
29
+ });
30
+
31
+ test("pnpm → pnpm install --frozen-lockfile", () => {
32
+ const result = buildInstallCommand(createWorkspace("pnpm"));
33
+ expect(result).toEqual(["pnpm", "install", "--frozen-lockfile"]);
34
+ });
35
+ });
36
+
37
+ describe("Python ecosystem", () => {
38
+ test("uv → uv sync", () => {
39
+ const result = buildInstallCommand(createWorkspace("uv"));
40
+ expect(result).toEqual(["uv", "sync"]);
41
+ });
42
+
43
+ test("poetry → poetry install --no-interaction", () => {
44
+ const result = buildInstallCommand(createWorkspace("poetry"));
45
+ expect(result).toEqual(["poetry", "install", "--no-interaction"]);
46
+ });
47
+
48
+ test("pipenv → pipenv install", () => {
49
+ const result = buildInstallCommand(createWorkspace("pipenv"));
50
+ expect(result).toEqual(["pipenv", "install"]);
51
+ });
52
+
53
+ test("pip → pip install -r requirements.txt", () => {
54
+ const result = buildInstallCommand(createWorkspace("pip"));
55
+ expect(result).toEqual(["pip", "install", "-r", "requirements.txt"]);
56
+ });
57
+ });
58
+
59
+ describe("Go/Rust ecosystem", () => {
60
+ test("go → go mod download", () => {
61
+ const result = buildInstallCommand(createWorkspace("go"));
62
+ expect(result).toEqual(["go", "mod", "download"]);
63
+ });
64
+
65
+ test("cargo → cargo fetch", () => {
66
+ const result = buildInstallCommand(createWorkspace("cargo"));
67
+ expect(result).toEqual(["cargo", "fetch"]);
68
+ });
69
+ });
70
+
71
+ describe("Unknown/No-op package managers", () => {
72
+ test("unknown → null", () => {
73
+ const result = buildInstallCommand(createWorkspace("unknown"));
74
+ expect(result).toBeNull();
75
+ });
76
+ });
77
+ });
@@ -88,4 +88,30 @@ describe("git-exec", () => {
88
88
  expect(exists).toBe(false);
89
89
  });
90
90
  });
91
+
92
+ describe("allowNonZeroExit option", () => {
93
+ test("does not throw on non-zero exit when allowNonZeroExit is true", async () => {
94
+ // git diff --cached --quiet は変更なしで exit 0
95
+ const result = await gitExec(["diff", "--cached", "--quiet"], {
96
+ cwd: TEST_DIR,
97
+ allowNonZeroExit: true,
98
+ });
99
+ expect(result.exitCode).toBe(0);
100
+ });
101
+
102
+ test("returns non-zero exitCode without throwing", async () => {
103
+ // 存在しないref → exit 128
104
+ const result = await gitExec(["rev-parse", "--verify", "nonexistent-ref"], {
105
+ cwd: TEST_DIR,
106
+ allowNonZeroExit: true,
107
+ });
108
+ expect(result.exitCode).not.toBe(0);
109
+ });
110
+
111
+ test("still throws when allowNonZeroExit is false (default)", async () => {
112
+ await expect(
113
+ gitExec(["rev-parse", "--verify", "nonexistent-ref"], { cwd: TEST_DIR })
114
+ ).rejects.toThrow("Git command failed");
115
+ });
116
+ });
91
117
  });
@@ -111,6 +111,25 @@ describe("MergeService", () => {
111
111
  expect(result.message).toContain("Successfully merged");
112
112
  }, 60_000);
113
113
 
114
+ test("sets alreadyUpToDate when task branch has no new commits", async () => {
115
+ const mergeService = await resetWorktree();
116
+
117
+ // taskBranch = 現在のHEADと同じ → Already up to date
118
+ const taskBranch = "task-no-changes";
119
+ await gitExec(["checkout", DEFAULT_BRANCH], { cwd: PARENT_WORKTREE });
120
+ await gitExec(["branch", taskBranch], { cwd: PARENT_WORKTREE });
121
+
122
+ const result = await mergeService.mergeToParent(
123
+ createTaskId("task-noop"),
124
+ taskBranch,
125
+ DEFAULT_BRANCH,
126
+ PARENT_WORKTREE
127
+ );
128
+
129
+ expect(result.success).toBe(true);
130
+ expect(result.alreadyUpToDate).toBe(true);
131
+ }, 60_000);
132
+
114
133
  test("detects merge conflicts and aborts merge", async () => {
115
134
  const mergeService = await resetWorktree();
116
135
 
@@ -0,0 +1,80 @@
1
+ import { describe, test, expect, beforeEach } from "bun:test";
2
+ import { PrManager } from "../pr-manager";
3
+ import type { PrManagerOptions, DraftPrOptions } from "../pr-manager";
4
+ import { GitWorkspaceError } from "@aad/shared/errors";
5
+
6
+ describe("PrManager", () => {
7
+ let options: PrManagerOptions;
8
+
9
+ beforeEach(() => {
10
+ options = {
11
+ repoRoot: process.cwd(),
12
+ };
13
+ });
14
+
15
+ test("should create instance", () => {
16
+ const manager = new PrManager(options);
17
+ expect(manager).toBeDefined();
18
+ });
19
+
20
+ test("should check gh installation", async () => {
21
+ const manager = new PrManager(options);
22
+ const installed = await manager.checkGhInstalled();
23
+ // May be true or false depending on environment
24
+ expect(typeof installed).toBe("boolean");
25
+ });
26
+
27
+ test("should throw error if gh not installed when creating draft PR", async () => {
28
+ const manager = new PrManager(options);
29
+
30
+ // Mock checkGhInstalled to return false
31
+ manager.checkGhInstalled = async () => false;
32
+
33
+ const draftOptions: DraftPrOptions = {
34
+ title: "Test PR",
35
+ body: "Test body",
36
+ baseBranch: "main",
37
+ headBranch: "feature/test",
38
+ };
39
+
40
+ await expect(manager.createDraftPr(draftOptions)).rejects.toThrow(GitWorkspaceError);
41
+ });
42
+
43
+ test("should throw error if gh not installed when marking PR ready", async () => {
44
+ const manager = new PrManager(options);
45
+
46
+ // Mock checkGhInstalled to return false
47
+ manager.checkGhInstalled = async () => false;
48
+
49
+ await expect(manager.markPrReady(123)).rejects.toThrow(GitWorkspaceError);
50
+ });
51
+
52
+ test("should return null for getPrInfo if gh not installed", async () => {
53
+ const manager = new PrManager(options);
54
+
55
+ // Mock checkGhInstalled to return false
56
+ manager.checkGhInstalled = async () => false;
57
+
58
+ const result = await manager.getPrInfo(123);
59
+ expect(result).toBeNull();
60
+ });
61
+
62
+ test("should return null for findPrByBranch if gh not installed", async () => {
63
+ const manager = new PrManager(options);
64
+
65
+ // Mock checkGhInstalled to return false
66
+ manager.checkGhInstalled = async () => false;
67
+
68
+ const result = await manager.findPrByBranch("feature/test");
69
+ expect(result).toBeNull();
70
+ });
71
+
72
+ test("should throw error if gh not installed when closing PR", async () => {
73
+ const manager = new PrManager(options);
74
+
75
+ // Mock checkGhInstalled to return false
76
+ manager.checkGhInstalled = async () => false;
77
+
78
+ await expect(manager.closePr(123)).rejects.toThrow(GitWorkspaceError);
79
+ });
80
+ });