@ronkovic/aad 0.6.0 → 0.6.2

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ronkovic/aad",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "Autonomous Agent Development Orchestrator - Multi-agent TDD pipeline powered by Claude",
5
5
  "module": "src/main.ts",
6
6
  "type": "module",
@@ -586,14 +586,14 @@ tr:hover td {
586
586
  <div class="log-entries" id="log-entries"></div>
587
587
  </div>
588
588
 
589
- <script type="module">class f{baseUrl;constructor(t){this.baseUrl=t}async getProgress(){let t=await fetch(`${this.baseUrl}/api/progress`);if(!t.ok)throw Error(`Failed to fetch progress: ${t.statusText}`);return t.json()}async getWorkers(){let t=await fetch(`${this.baseUrl}/api/workers`);if(!t.ok)throw Error(`Failed to fetch workers: ${t.statusText}`);return t.json()}async getTasks(){let t=await fetch(`${this.baseUrl}/api/tasks`);if(!t.ok)throw Error(`Failed to fetch tasks: ${t.statusText}`);return t.json()}async getGraph(){let t=await fetch(`${this.baseUrl}/api/graph`);if(!t.ok)throw Error(`Failed to fetch graph: ${t.statusText}`);return t.json()}async getTimeline(){let t=await fetch(`${this.baseUrl}/api/timeline`);if(!t.ok)throw Error(`Failed to fetch timeline: ${t.statusText}`);return t.json()}async getLogs(){let t=await fetch(`${this.baseUrl}/api/logs`);if(!t.ok)throw Error(`Failed to fetch logs: ${t.statusText}`);let i=await t.json();return Array.isArray(i)?i:i.entries||[]}async getTaskLogs(t){let i=await fetch(`${this.baseUrl}/api/tasks/${t}/logs`);if(!i.ok)throw Error(`Failed to fetch task logs: ${i.statusText}`);return i.json()}}class c{url;onEvent;eventSource=null;status="disconnected";reconnectAttempts=0;reconnectTimer=null;connectionChangeListeners=[];constructor(t,i){this.url=t;this.onEvent=i}connect(){if(this.eventSource)try{this.eventSource.close()}catch(t){}this.eventSource=new EventSource(this.url),this.eventSource.addEventListener("open",()=>{this.reconnectAttempts=0,this.setStatus("connected")}),this.eventSource.addEventListener("heartbeat",()=>{this.setStatus("connected")}),this.eventSource.addEventListener("message",(t)=>{try{let i=JSON.parse(t.data);this.onEvent(i)}catch(i){console.error("SSE parse error:",i)}}),this.eventSource.addEventListener("error",()=>{this.reconnectAttempts++;let t=this.reconnectAttempts>3?"disconnected":"reconnecting";if(this.setStatus(t),this.eventSource)this.eventSource.close();let i=Math.min(this.reconnectAttempts*3000,9000);this.reconnectTimer=setTimeout(()=>{this.connect()},i)})}disconnect(){if(this.reconnectTimer)clearTimeout(this.reconnectTimer),this.reconnectTimer=null;if(this.eventSource){try{this.eventSource.close()}catch(t){}this.eventSource=null}this.setStatus("disconnected")}isConnected(){return this.status==="connected"}getStatus(){return this.status}getReconnectAttempts(){return this.reconnectAttempts}onConnectionChange(t){this.connectionChangeListeners.push(t)}setStatus(t){if(this.status!==t)this.status=t,this.connectionChangeListeners.forEach((i)=>{try{i(t)}catch(n){console.error("Connection change listener error:",n)}})}}class x{state={progress:{pending:0,running:0,completed:0,failed:0,total:0},tasks:[],workers:[],graph:{nodes:[],edges:[]},timeline:{tasks:[]},logs:[],taskPhases:{}};listeners=[];getState(){return{...this.state}}updateProgress(t){this.state.progress=t,this.notify()}updateTasks(t){this.state.tasks=t,this.notify()}updateTask(t,i){let n=this.state.tasks.findIndex((e)=>e.taskId===t);if(n>=0)this.state.tasks[n]={...this.state.tasks[n],...i},this.notify()}updateWorkers(t){this.state.workers=t,this.notify()}updateGraph(t){this.state.graph=t,this.notify()}updateTimeline(t){this.state.timeline=t,this.notify()}addLog(t){if(this.state.logs.unshift(t),this.state.logs.length>200)this.state.logs=this.state.logs.slice(0,200);this.notify()}updateTaskPhase(t,i,n){this.state.taskPhases[t]={phase:i,status:n},this.notify()}subscribe(t){return this.listeners.push(t),()=>{let i=this.listeners.indexOf(t);if(i>=0)this.listeners.splice(i,1)}}notify(){let t=this.getState();this.listeners.forEach((i)=>{try{i(t)}catch(n){console.error("State listener error:",n)}})}}function v(t){let i=t.total||1,n=i>0?Math.round(t.completed/i*100):0,e=document.getElementById("pct-display"),o=document.getElementById("progress-fill"),d=document.getElementById("s-pending"),s=document.getElementById("s-running"),l=document.getElementById("s-completed"),p=document.getElementById("s-failed");if(e)e.textContent=`${n}%`;if(o)o.style.width=`${n}%`;if(d)d.textContent=String(t.pending||0);if(s)s.textContent=String(t.running||0);if(l)l.textContent=String(t.completed||0);if(p)p.textContent=String(t.failed||0)}function E(t){let i=document.createElement("div");return i.textContent=t,i.innerHTML}function $(t){let i=document.getElementById("worker-list"),n=document.getElementById("workers-count");if(!i)return;if(n)n.textContent=`(${t.length})`;if(!t.length){i.innerHTML='<div class="empty-msg">No workers</div>';return}i.innerHTML=t.map((e)=>{let o=e.currentTask?`<span class="worker-task">Task: ${E(String(e.currentTask))}</span>`:"";return`
589
+ <script type="module">class z{baseUrl;constructor(t){this.baseUrl=t}async getProgress(){let t=await fetch(`${this.baseUrl}/api/progress`);if(!t.ok)throw Error(`Failed to fetch progress: ${t.statusText}`);return t.json()}async getWorkers(){let t=await fetch(`${this.baseUrl}/api/workers`);if(!t.ok)throw Error(`Failed to fetch workers: ${t.statusText}`);return t.json()}async getTasks(){let t=await fetch(`${this.baseUrl}/api/tasks`);if(!t.ok)throw Error(`Failed to fetch tasks: ${t.statusText}`);return t.json()}async getGraph(){let t=await fetch(`${this.baseUrl}/api/graph`);if(!t.ok)throw Error(`Failed to fetch graph: ${t.statusText}`);return t.json()}async getTimeline(){let t=await fetch(`${this.baseUrl}/api/timeline`);if(!t.ok)throw Error(`Failed to fetch timeline: ${t.statusText}`);return t.json()}async getLogs(){let t=await fetch(`${this.baseUrl}/api/logs`);if(!t.ok)throw Error(`Failed to fetch logs: ${t.statusText}`);let i=await t.json();return Array.isArray(i)?i:i.entries||[]}async getTaskLogs(t){let i=await fetch(`${this.baseUrl}/api/tasks/${t}/logs`);if(!i.ok)throw Error(`Failed to fetch task logs: ${i.statusText}`);return i.json()}}class D{url;onEvent;eventSource=null;status="disconnected";reconnectAttempts=0;reconnectTimer=null;connectionChangeListeners=[];constructor(t,i){this.url=t;this.onEvent=i}connect(){if(this.eventSource)try{this.eventSource.close()}catch(t){}this.eventSource=new EventSource(this.url),this.eventSource.addEventListener("open",()=>{this.reconnectAttempts=0,this.setStatus("connected")}),this.eventSource.addEventListener("heartbeat",()=>{this.setStatus("connected")}),this.eventSource.addEventListener("message",(t)=>{try{let i=JSON.parse(t.data);this.onEvent(i)}catch(i){console.error("SSE parse error:",i)}}),this.eventSource.addEventListener("error",()=>{this.reconnectAttempts++;let t=this.reconnectAttempts>3?"disconnected":"reconnecting";if(this.setStatus(t),this.eventSource)this.eventSource.close();let i=Math.min(this.reconnectAttempts*3000,9000);this.reconnectTimer=setTimeout(()=>{this.connect()},i)})}disconnect(){if(this.reconnectTimer)clearTimeout(this.reconnectTimer),this.reconnectTimer=null;if(this.eventSource){try{this.eventSource.close()}catch(t){}this.eventSource=null}this.setStatus("disconnected")}isConnected(){return this.status==="connected"}getStatus(){return this.status}getReconnectAttempts(){return this.reconnectAttempts}onConnectionChange(t){this.connectionChangeListeners.push(t)}setStatus(t){if(this.status!==t)this.status=t,this.connectionChangeListeners.forEach((i)=>{try{i(t)}catch(d){console.error("Connection change listener error:",d)}})}}class L{state={progress:{pending:0,running:0,completed:0,failed:0,total:0},tasks:[],workers:[],graph:{nodes:[],edges:[]},timeline:{tasks:[]},logs:[],taskPhases:{}};listeners=[];getState(){return{...this.state}}updateProgress(t){this.state.progress=t,this.notify()}updateTasks(t){this.state.tasks=t,this.notify()}updateTask(t,i){let d=this.state.tasks.findIndex((n)=>n.taskId===t);if(d>=0)this.state.tasks[d]={...this.state.tasks[d],...i},this.notify()}updateWorkers(t){this.state.workers=t,this.notify()}updateGraph(t){this.state.graph=t,this.notify()}updateTimeline(t){this.state.timeline=t,this.notify()}addLog(t){if(this.state.logs.unshift(t),this.state.logs.length>200)this.state.logs=this.state.logs.slice(0,200);this.notify()}updateTaskPhase(t,i,d){this.state.taskPhases[t]={phase:i,status:d},this.notify()}subscribe(t){return this.listeners.push(t),()=>{let i=this.listeners.indexOf(t);if(i>=0)this.listeners.splice(i,1)}}notify(){let t=this.getState();this.listeners.forEach((i)=>{try{i(t)}catch(d){console.error("State listener error:",d)}})}}function j(t){let i=t.total||1,d=i>0?Math.round(t.completed/i*100):0,n=document.getElementById("pct-display"),a=document.getElementById("progress-fill"),e=document.getElementById("s-pending"),s=document.getElementById("s-running"),p=document.getElementById("s-completed"),o=document.getElementById("s-failed");if(n)n.textContent=`${d}%`;if(a)a.style.width=`${d}%`;if(e)e.textContent=String(t.pending||0);if(s)s.textContent=String(t.running||0);if(p)p.textContent=String(t.completed||0);if(o)o.textContent=String(t.failed||0)}function q(t){let i=document.createElement("div");return i.textContent=t,i.innerHTML}function P(t){let i=document.getElementById("worker-list"),d=document.getElementById("workers-count");if(!i)return;if(d)d.textContent=`(${t.length})`;if(!t.length){i.innerHTML='<div class="empty-msg">No workers</div>';return}i.innerHTML=t.map((n)=>{let a=n.currentTask?`<span class="worker-task">Task: ${q(String(n.currentTask))}</span>`:"";return`
590
590
  <div class="worker-item">
591
- <span class="worker-dot ${e.status}"></span>
592
- <span class="worker-name">${E(String(e.id))}</span>
593
- ${o}
594
- <span class="badge badge-${e.status}" style="margin-left:auto">${e.status}</span>
591
+ <span class="worker-dot ${n.status}"></span>
592
+ <span class="worker-name">${q(String(n.id))}</span>
593
+ ${a}
594
+ <span class="badge badge-${n.status}" style="margin-left:auto">${n.status}</span>
595
595
  </div>
596
- `}).join("")}function y(t){let i=document.createElement("div");return i.textContent=t,i.innerHTML}class u{panelEl=null;overlayEl=null;constructor(){this.createPanel(),this.setupEventListeners()}createPanel(){this.overlayEl=document.createElement("div"),this.overlayEl.className="task-detail-overlay",this.overlayEl.style.cssText=`
596
+ `}).join("")}function $(t){let i=document.createElement("div");return i.textContent=t,i.innerHTML}class M{panelEl=null;overlayEl=null;constructor(){this.createPanel(),this.setupEventListeners()}createPanel(){this.overlayEl=document.createElement("div"),this.overlayEl.className="task-detail-overlay",this.overlayEl.style.cssText=`
597
597
  display: none;
598
598
  position: fixed;
599
599
  top: 0;
@@ -618,13 +618,13 @@ tr:hover td {
618
618
  overflow-y: auto;
619
619
  z-index: 1001;
620
620
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
621
- `,document.body.appendChild(this.overlayEl),document.body.appendChild(this.panelEl)}setupEventListeners(){this.overlayEl?.addEventListener("click",()=>this.close()),document.addEventListener("keydown",(t)=>{if(t.key==="Escape"&&this.isOpen())this.close()})}show(t,i){if(!this.panelEl||!this.overlayEl)return;let n={red:"Red (Tests)",green:"Green (Implementation)",verify:"Verify",review:"Review",merge:"Merge"},e=i?`<div class="phase-info">
621
+ `,document.body.appendChild(this.overlayEl),document.body.appendChild(this.panelEl)}setupEventListeners(){this.overlayEl?.addEventListener("click",()=>this.close()),document.addEventListener("keydown",(t)=>{if(t.key==="Escape"&&this.isOpen())this.close()})}show(t,i){if(!this.panelEl||!this.overlayEl)return;let d={red:"Red (Tests)",green:"Green (Implementation)",verify:"Verify",review:"Review",merge:"Merge"},n=i?`<div class="phase-info">
622
622
  <strong>Current Phase:</strong>
623
623
  <span class="activity-phase-${i.phase}">
624
- ${y(n[i.phase]||i.phase)}
624
+ ${$(d[i.phase]||i.phase)}
625
625
  (${i.status})
626
626
  </span>
627
- </div>`:"",o=t.startTime?new Date(t.startTime).toLocaleString():"—",d=t.endTime?new Date(t.endTime).toLocaleString():"—",s=this.calculateDuration(t);this.panelEl.innerHTML=`
627
+ </div>`:"",a=t.startTime?new Date(t.startTime).toLocaleString():"—",e=t.endTime?new Date(t.endTime).toLocaleString():"—",s=this.calculateDuration(t);this.panelEl.innerHTML=`
628
628
  <div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 20px;">
629
629
  <h2 style="color: #4fc3f7; margin: 0; font-size: 18px;">Task Details</h2>
630
630
  <button class="close-btn" style="background: #ef5350; color: white; border: none; border-radius: 4px; padding: 6px 12px; cursor: pointer; font-size: 14px; font-weight: 600;">Close</button>
@@ -633,12 +633,12 @@ tr:hover td {
633
633
  <div style="display: flex; flex-direction: column; gap: 16px;">
634
634
  <div>
635
635
  <div style="color: #9e9e9e; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px;">Task ID</div>
636
- <div style="font-family: monospace; font-size: 13px; color: #e0e0e0;">${y(String(t.taskId))}</div>
636
+ <div style="font-family: monospace; font-size: 13px; color: #e0e0e0;">${$(String(t.taskId))}</div>
637
637
  </div>
638
638
 
639
639
  <div>
640
640
  <div style="color: #9e9e9e; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px;">Title</div>
641
- <div style="font-size: 15px; color: #e0e0e0; font-weight: 500;">${y(t.title||"—")}</div>
641
+ <div style="font-size: 15px; color: #e0e0e0; font-weight: 500;">${$(t.title||"—")}</div>
642
642
  </div>
643
643
 
644
644
  <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px;">
@@ -652,23 +652,23 @@ tr:hover td {
652
652
  </div>
653
653
  </div>
654
654
 
655
- ${e}
655
+ ${n}
656
656
 
657
657
  <div>
658
658
  <div style="color: #9e9e9e; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px;">Dependencies</div>
659
659
  <div style="font-size: 13px; color: #e0e0e0;">
660
- ${t.dependsOn&&t.dependsOn.length>0?t.dependsOn.map((p)=>`<code style="background: #3a3a3a; padding: 2px 6px; border-radius: 3px; font-family: monospace; font-size: 11px;">${y(String(p))}</code>`).join(" "):"—"}
660
+ ${t.dependsOn&&t.dependsOn.length>0?t.dependsOn.map((o)=>`<code style="background: #3a3a3a; padding: 2px 6px; border-radius: 3px; font-family: monospace; font-size: 11px;">${$(String(o))}</code>`).join(" "):"—"}
661
661
  </div>
662
662
  </div>
663
663
 
664
664
  <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px;">
665
665
  <div>
666
666
  <div style="color: #9e9e9e; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px;">Start Time</div>
667
- <div style="font-size: 12px; color: #e0e0e0;">${o}</div>
667
+ <div style="font-size: 12px; color: #e0e0e0;">${a}</div>
668
668
  </div>
669
669
  <div>
670
670
  <div style="color: #9e9e9e; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px;">End Time</div>
671
- <div style="font-size: 12px; color: #e0e0e0;">${d}</div>
671
+ <div style="font-size: 12px; color: #e0e0e0;">${e}</div>
672
672
  </div>
673
673
  </div>
674
674
 
@@ -677,20 +677,20 @@ tr:hover td {
677
677
  <div style="font-size: 13px; color: #66bb6a; font-weight: 600;">${s}</div>
678
678
  </div>`:""}
679
679
  </div>
680
- `;let l=this.panelEl.querySelector(".close-btn");if(l)l.addEventListener("click",()=>this.close());this.overlayEl.style.display="block",this.panelEl.style.display="block"}close(){if(this.overlayEl)this.overlayEl.style.display="none";if(this.panelEl)this.panelEl.style.display="none"}isOpen(){return this.panelEl?.style.display==="block"}calculateDuration(t){if(!t.startTime)return null;let i=new Date(t.startTime).getTime(),e=(t.endTime?new Date(t.endTime).getTime():Date.now())-i,o=Math.floor(e/1000),d=Math.floor(o/60),s=Math.floor(d/60);if(s>0)return`${s}h ${d%60}m`;else if(d>0)return`${d}m ${o%60}s`;else return`${o}s`}}function m(t){let i=document.createElement("div");return i.textContent=t,i.innerHTML}function L(t,i,n){if(t.status==="running"){let e=i[t.taskId];if(e){let d={red:"Red (Tests)",green:"Green (Impl)",verify:"Verify",review:"Review",merge:"Merge"}[e.phase]||e.phase,s=e.status==="completed"?"✓ ":e.status==="failed"?"✗ ":"";return`<span class="activity-phase-${e.phase}">${s}${m(d)}</span>`}return'<span class="activity-phase-green">Running</span>'}else if(n==="connected")return'<span class="activity-connected">Connected</span>';else if(n==="reconnecting")return'<span class="activity-reconnecting">Reconnecting...</span>';else return'<span class="activity-disconnected">Disconnected</span>'}var M=new u;function g(t,i,n){let e=document.getElementById("task-tbody"),o=document.getElementById("tasks-count");if(!e)return;if(o)o.textContent=`(${t.length})`;if(!t.length){e.innerHTML='<tr><td colspan="6" class="empty-msg">No tasks</td></tr>';return}e.innerHTML=t.map((d)=>{let s=(d.dependsOn||[]).map((p)=>m(String(p))).join(", ")||"—",l=L(d,i,n);return`
681
- <tr data-task-id="${m(String(d.taskId))}" style="cursor: pointer;">
682
- <td style="font-family:monospace;font-size:11px">${m(String(d.taskId))}</td>
683
- <td>${m(d.title||"—")}</td>
684
- <td><span class="badge badge-${d.status}">${d.status}</span></td>
685
- <td class="activity-cell">${l}</td>
686
- <td class="priority">${d.priority||0}</td>
680
+ `;let p=this.panelEl.querySelector(".close-btn");if(p)p.addEventListener("click",()=>this.close());this.overlayEl.style.display="block",this.panelEl.style.display="block"}close(){if(this.overlayEl)this.overlayEl.style.display="none";if(this.panelEl)this.panelEl.style.display="none"}isOpen(){return this.panelEl?.style.display==="block"}calculateDuration(t){if(!t.startTime)return null;let i=new Date(t.startTime).getTime(),n=(t.endTime?new Date(t.endTime).getTime():Date.now())-i,a=Math.floor(n/1000),e=Math.floor(a/60),s=Math.floor(e/60);if(s>0)return`${s}h ${e%60}m`;else if(e>0)return`${e}m ${a%60}s`;else return`${a}s`}}function h(t){let i=document.createElement("div");return i.textContent=t,i.innerHTML}function X(t,i,d){if(t.status==="running"){let n=i[t.taskId];if(n){let e={red:"Red (Tests)",green:"Green (Impl)",verify:"Verify",review:"Review",merge:"Merge"}[n.phase]||n.phase,s=n.status==="completed"?"✓ ":n.status==="failed"?"✗ ":"";return`<span class="activity-phase-${n.phase}">${s}${h(e)}</span>`}return'<span class="activity-phase-green">Running</span>'}else if(d==="connected")return'<span class="activity-connected">Connected</span>';else if(d==="reconnecting")return'<span class="activity-reconnecting">Reconnecting...</span>';else return'<span class="activity-disconnected">Disconnected</span>'}var Y=new M;function S(t,i,d){let n=document.getElementById("task-tbody"),a=document.getElementById("tasks-count");if(!n)return;if(a)a.textContent=`(${t.length})`;if(!t.length){n.innerHTML='<tr><td colspan="6" class="empty-msg">No tasks</td></tr>';return}n.innerHTML=t.map((e)=>{let s=(e.dependsOn||[]).map((o)=>h(String(o))).join(", ")||"—",p=X(e,i,d);return`
681
+ <tr data-task-id="${h(String(e.taskId))}" style="cursor: pointer;">
682
+ <td style="font-family:monospace;font-size:11px">${h(String(e.taskId))}</td>
683
+ <td>${h(e.title||"—")}</td>
684
+ <td><span class="badge badge-${e.status}">${e.status}</span></td>
685
+ <td class="activity-cell">${p}</td>
686
+ <td class="priority">${e.priority||0}</td>
687
687
  <td class="deps">${s}</td>
688
688
  </tr>
689
- `}).join(""),e.querySelectorAll("tr[data-task-id]").forEach((d)=>{d.addEventListener("click",()=>{let s=d.dataset.taskId,l=t.find((p)=>String(p.taskId)===s);if(l){let p=i[l.taskId];M.show(l,p)}})})}function b(t){let i=document.createElement("div");return i.textContent=t,i.innerHTML}class h{filters={info:!0,warn:!0,error:!0,service:"",search:""};autoScroll=!0;constructor(){this.setupEventListeners(),this.loadFiltersFromStorage()}setupEventListeners(){document.querySelectorAll(".log-filters input[data-level]").forEach((o)=>{o.addEventListener("change",()=>{let d=o,s=d.dataset.level;this.filters[s]=d.checked,this.saveFiltersToStorage()})});let t=document.getElementById("svc-filter");if(t)t.addEventListener("change",()=>{this.filters.service=t.value,this.saveFiltersToStorage()});let i=document.getElementById("log-search");if(i)i.addEventListener("input",()=>{this.filters.search=i.value,this.saveFiltersToStorage()});let n=document.getElementById("filter-reset");if(n)n.addEventListener("click",()=>{this.resetFilters()});let e=document.getElementById("log-entries");if(e)e.addEventListener("scroll",()=>{let o=e.scrollHeight-e.scrollTop<=e.clientHeight+10;this.autoScroll=o})}renderLog(t,i=!0){let n=document.getElementById("log-entries");if(!n)return;let e=document.createElement("div"),o=t.level||"info";e.className=`log-entry log-${o}`,e.dataset.level=o,e.dataset.service=t.service||"";let d=t.timestamp?new Date(t.timestamp).toLocaleTimeString():"";if(e.innerHTML=`
690
- <span class="log-time">${d}</span>
691
- <span class="log-svc">${b(t.service||"")}</span>
692
- <span>${b(t.message||"")}</span>
693
- `,i)n.insertBefore(e,n.firstChild);else n.appendChild(e);if(this.applyFilterToEntry(e),this.autoScroll&&i)n.scrollTop=0;this.updateLogCount()}renderAllLogs(t){let i=document.getElementById("log-entries");if(!i)return;i.innerHTML="",t.forEach((n)=>this.renderLog(n,!1)),this.updateLogCount(),this.updateServiceFilter(t)}applyFilterToEntry(t){let i=t.dataset.level,n=i?this.filters[i]:!1;if(n&&this.filters.service)n=t.dataset.service===this.filters.service;if(n&&this.filters.search){let e=this.filters.search.toLowerCase();n=(t.textContent?.toLowerCase()||"").includes(e)}t.classList.toggle("log-hidden",!n)}updateLogCount(){let t=document.getElementById("log-entries"),i=document.getElementById("log-count");if(!t||!i)return;let n=t.querySelectorAll(".log-entry").length,e=t.querySelectorAll(".log-entry:not(.log-hidden)").length;i.textContent=`(Showing ${e}/${n})`}updateServiceFilter(t){let i=document.getElementById("svc-filter");if(!i)return;let n=new Set(t.map((o)=>o.service).filter(Boolean)),e=i.value;if(i.innerHTML='<option value="">All Services</option>',n.forEach((o)=>{let d=document.createElement("option");d.value=o,d.textContent=o,i.appendChild(d)}),e)i.value=e}resetFilters(){this.filters={info:!0,warn:!0,error:!0,service:"",search:""},document.querySelectorAll(".log-filters input[data-level]").forEach((n)=>{n.checked=!0});let t=document.getElementById("svc-filter");if(t)t.value="";let i=document.getElementById("log-search");if(i)i.value="";this.saveFiltersToStorage(),this.reapplyFilters()}reapplyFilters(){let t=document.getElementById("log-entries");if(!t)return;t.querySelectorAll(".log-entry").forEach((i)=>{this.applyFilterToEntry(i)}),this.updateLogCount()}loadFiltersFromStorage(){try{let t=localStorage.getItem("aad-log-filters");if(t){let i=JSON.parse(t);this.filters={...this.filters,...i},document.querySelectorAll(".log-filters input[data-level]").forEach((o)=>{let d=o,s=d.dataset.level;d.checked=this.filters[s]!==!1});let n=document.getElementById("svc-filter");if(n&&this.filters.service)n.value=this.filters.service;let e=document.getElementById("log-search");if(e&&this.filters.search)e.value=this.filters.search}}catch(t){console.error("Failed to load filters:",t)}}saveFiltersToStorage(){try{localStorage.setItem("aad-log-filters",JSON.stringify(this.filters))}catch(t){console.error("Failed to save filters:",t)}this.reapplyFilters()}}var z="http://localhost:7333",r=new f(z),a=new x,T=new c(`${z}/events/all`,P),D=new h;a.subscribe((t)=>{v(t.progress),$(t.workers),g(t.tasks,t.taskPhases,T.getStatus())});function P(t){switch(t.type){case"log:entry":a.addLog(t.entry),D.renderLog(t.entry,!0);break;case"progress:updated":a.updateProgress(t.state);break;case"execution:phase:started":a.updateTaskPhase(t.taskId,t.phase,"running");break;case"execution:phase:completed":a.updateTaskPhase(t.taskId,t.phase,"completed");break;case"execution:phase:failed":a.updateTaskPhase(t.taskId,t.phase,"failed");break;case"task:dispatched":case"task:completed":case"task:failed":if(t.task)a.updateTask(t.task.taskId,t.task);O(),G();break;case"worker:idle":case"worker:busy":B();break;case"heartbeat":break}}async function S(){try{let[t,i,n,e,o,d]=await Promise.all([r.getProgress(),r.getWorkers(),r.getTasks(),r.getGraph(),r.getTimeline(),r.getLogs()]);a.updateProgress(t.progress),a.updateWorkers(i.workers),a.updateTasks(n.tasks),a.updateGraph(e),a.updateTimeline(o),d.forEach((s)=>a.addLog(s)),D.renderAllLogs(d)}catch(t){console.error("Failed to load initial data:",t)}}async function O(){try{let t=await r.getProgress();a.updateProgress(t.progress)}catch(t){console.error("Failed to refresh progress:",t)}}async function B(){try{let t=await r.getWorkers();a.updateWorkers(t.workers)}catch(t){console.error("Failed to refresh workers:",t)}}async function G(){try{let t=await r.getTimeline();a.updateTimeline(t)}catch(t){console.error("Failed to refresh timeline:",t)}}S();T.connect();T.onConnectionChange((t)=>{console.log("SSE connection status:",t),g(a.getState().tasks,a.getState().taskPhases,t)});export{a as store,T as sseClient,r as apiClient};
689
+ `}).join(""),n.querySelectorAll("tr[data-task-id]").forEach((e)=>{e.addEventListener("click",()=>{let s=e.dataset.taskId,p=t.find((o)=>String(o.taskId)===s);if(p){let o=i[p.taskId];Y.show(p,o)}})})}var Z={pending:"#9e9e9e",running:"#4fc3f7",completed:"#66bb6a",failed:"#ef5350"};function H(t){let i=document.getElementById("graph-container");if(!i)return;if(i.innerHTML="",!t.nodes.length){i.innerHTML='<div class="empty-msg">No graph data</div>';return}let d=i.clientWidth||600,n=400,a=20,e=Math.ceil(Math.sqrt(t.nodes.length)),s=Math.ceil(t.nodes.length/e),p=d/(e+1),o=n/(s+1),g=new Map;t.nodes.forEach((f,y)=>{let T=y%e,l=Math.floor(y/e);g.set(f.id,{x:(T+1)*p,y:(l+1)*o})});let r=document.createElementNS("http://www.w3.org/2000/svg","svg");r.setAttribute("width",String(d)),r.setAttribute("height",String(n)),r.style.background="#1a1a1a";let v=document.createElementNS("http://www.w3.org/2000/svg","g");v.setAttribute("class","graph-edges"),t.edges.forEach((f)=>{let y=g.get(f.from),T=g.get(f.to);if(!y||!T)return;let l=document.createElementNS("http://www.w3.org/2000/svg","line");l.setAttribute("x1",String(y.x)),l.setAttribute("y1",String(y.y)),l.setAttribute("x2",String(T.x)),l.setAttribute("y2",String(T.y)),l.setAttribute("stroke","#424242"),l.setAttribute("stroke-width","2"),l.setAttribute("marker-end","url(#arrowhead)"),v.appendChild(l)});let N=document.createElementNS("http://www.w3.org/2000/svg","defs"),u=document.createElementNS("http://www.w3.org/2000/svg","marker");u.setAttribute("id","arrowhead"),u.setAttribute("markerWidth","10"),u.setAttribute("markerHeight","10"),u.setAttribute("refX","8"),u.setAttribute("refY","3"),u.setAttribute("orient","auto"),u.setAttribute("markerUnits","strokeWidth");let b=document.createElementNS("http://www.w3.org/2000/svg","polygon");b.setAttribute("points","0 0, 10 3, 0 6"),b.setAttribute("fill","#424242"),u.appendChild(b),N.appendChild(u),r.appendChild(N),r.appendChild(v);let E=document.createElementNS("http://www.w3.org/2000/svg","g");E.setAttribute("class","graph-nodes"),t.nodes.forEach((f)=>{let y=g.get(f.id);if(!y)return;let T=Z[f.status],l=document.createElementNS("http://www.w3.org/2000/svg","circle");l.setAttribute("cx",String(y.x)),l.setAttribute("cy",String(y.y)),l.setAttribute("r",String(a)),l.setAttribute("fill",T),l.setAttribute("stroke","#2d2d2d"),l.setAttribute("stroke-width","2");let c=document.createElementNS("http://www.w3.org/2000/svg","text");c.setAttribute("x",String(y.x)),c.setAttribute("y",String(y.y+4)),c.setAttribute("text-anchor","middle"),c.setAttribute("fill","#1a1a1a"),c.setAttribute("font-size","10"),c.setAttribute("font-weight","600"),c.textContent=f.id.slice(0,3);let W=document.createElementNS("http://www.w3.org/2000/svg","title");W.textContent=`${f.id} (${f.status})`,l.appendChild(W),E.appendChild(l),E.appendChild(c)}),r.appendChild(E),i.appendChild(r)}function J(t){let i=document.createElement("div");return i.textContent=t,i.innerHTML}class B{filters={info:!0,warn:!0,error:!0,service:"",search:""};autoScroll=!0;constructor(){this.setupEventListeners(),this.loadFiltersFromStorage()}setupEventListeners(){document.querySelectorAll(".log-filters input[data-level]").forEach((a)=>{a.addEventListener("change",()=>{let e=a,s=e.dataset.level;this.filters[s]=e.checked,this.saveFiltersToStorage()})});let t=document.getElementById("svc-filter");if(t)t.addEventListener("change",()=>{this.filters.service=t.value,this.saveFiltersToStorage()});let i=document.getElementById("log-search");if(i)i.addEventListener("input",()=>{this.filters.search=i.value,this.saveFiltersToStorage()});let d=document.getElementById("filter-reset");if(d)d.addEventListener("click",()=>{this.resetFilters()});let n=document.getElementById("log-entries");if(n)n.addEventListener("scroll",()=>{let a=n.scrollHeight-n.scrollTop<=n.clientHeight+10;this.autoScroll=a})}renderLog(t,i=!0){let d=document.getElementById("log-entries");if(!d)return;let n=document.createElement("div"),a=t.level||"info";n.className=`log-entry log-${a}`,n.dataset.level=a,n.dataset.service=t.service||"";let e=t.timestamp?new Date(t.timestamp).toLocaleTimeString():"";if(n.innerHTML=`
690
+ <span class="log-time">${e}</span>
691
+ <span class="log-svc">${J(t.service||"")}</span>
692
+ <span>${J(t.message||"")}</span>
693
+ `,i)d.insertBefore(n,d.firstChild);else d.appendChild(n);if(this.applyFilterToEntry(n),this.autoScroll&&i)d.scrollTop=0;this.updateLogCount()}renderAllLogs(t){let i=document.getElementById("log-entries");if(!i)return;i.innerHTML="",t.forEach((d)=>this.renderLog(d,!1)),this.updateLogCount(),this.updateServiceFilter(t)}applyFilterToEntry(t){let i=t.dataset.level,d=i?this.filters[i]:!1;if(d&&this.filters.service)d=t.dataset.service===this.filters.service;if(d&&this.filters.search){let n=this.filters.search.toLowerCase();d=(t.textContent?.toLowerCase()||"").includes(n)}t.classList.toggle("log-hidden",!d)}updateLogCount(){let t=document.getElementById("log-entries"),i=document.getElementById("log-count");if(!t||!i)return;let d=t.querySelectorAll(".log-entry").length,n=t.querySelectorAll(".log-entry:not(.log-hidden)").length;i.textContent=`(Showing ${n}/${d})`}updateServiceFilter(t){let i=document.getElementById("svc-filter");if(!i)return;let d=new Set(t.map((a)=>a.service).filter(Boolean)),n=i.value;if(i.innerHTML='<option value="">All Services</option>',d.forEach((a)=>{let e=document.createElement("option");e.value=a,e.textContent=a,i.appendChild(e)}),n)i.value=n}resetFilters(){this.filters={info:!0,warn:!0,error:!0,service:"",search:""},document.querySelectorAll(".log-filters input[data-level]").forEach((d)=>{d.checked=!0});let t=document.getElementById("svc-filter");if(t)t.value="";let i=document.getElementById("log-search");if(i)i.value="";this.saveFiltersToStorage(),this.reapplyFilters()}reapplyFilters(){let t=document.getElementById("log-entries");if(!t)return;t.querySelectorAll(".log-entry").forEach((i)=>{this.applyFilterToEntry(i)}),this.updateLogCount()}loadFiltersFromStorage(){try{let t=localStorage.getItem("aad-log-filters");if(t){let i=JSON.parse(t);this.filters={...this.filters,...i},document.querySelectorAll(".log-filters input[data-level]").forEach((a)=>{let e=a,s=e.dataset.level;e.checked=this.filters[s]!==!1});let d=document.getElementById("svc-filter");if(d&&this.filters.service)d.value=this.filters.service;let n=document.getElementById("log-search");if(n&&this.filters.search)n.value=this.filters.search}}catch(t){console.error("Failed to load filters:",t)}}saveFiltersToStorage(){try{localStorage.setItem("aad-log-filters",JSON.stringify(this.filters))}catch(t){console.error("Failed to save filters:",t)}this.reapplyFilters()}}var Q="http://localhost:7333",x=new z(Q),m=new L,O=new D(`${Q}/events/all`,K),R=new B;m.subscribe((t)=>{j(t.progress),P(t.workers),S(t.tasks,t.taskPhases,O.getStatus()),H(t.graph)});function K(t){switch(t.type){case"log:entry":m.addLog(t.entry),R.renderLog(t.entry,!0);break;case"progress:updated":m.updateProgress(t.state);break;case"execution:phase:started":m.updateTaskPhase(t.taskId,t.phase,"running");break;case"execution:phase:completed":m.updateTaskPhase(t.taskId,t.phase,"completed");break;case"execution:phase:failed":m.updateTaskPhase(t.taskId,t.phase,"failed");break;case"task:dispatched":case"task:completed":case"task:failed":if(t.task)m.updateTask(t.task.taskId,t.task);_(),U(),w();break;case"worker:idle":case"worker:busy":G();break;case"heartbeat":break}}async function V(){try{let[t,i,d,n,a,e]=await Promise.all([x.getProgress(),x.getWorkers(),x.getTasks(),x.getGraph(),x.getTimeline(),x.getLogs()]);m.updateProgress(t.progress),m.updateWorkers(i.workers),m.updateTasks(d.tasks),m.updateGraph(n),m.updateTimeline(a),e.forEach((s)=>m.addLog(s)),R.renderAllLogs(e)}catch(t){console.error("Failed to load initial data:",t)}}async function _(){try{let t=await x.getProgress();m.updateProgress(t.progress)}catch(t){console.error("Failed to refresh progress:",t)}}async function G(){try{let t=await x.getWorkers();m.updateWorkers(t.workers)}catch(t){console.error("Failed to refresh workers:",t)}}async function U(){try{let t=await x.getTimeline();m.updateTimeline(t)}catch(t){console.error("Failed to refresh timeline:",t)}}async function w(){try{let t=await x.getGraph();m.updateGraph(t)}catch(t){console.error("Failed to refresh graph:",t)}}V();O.connect();O.onConnectionChange((t)=>{console.log("SSE connection status:",t),S(m.getState().tasks,m.getState().taskPhases,t)});export{m as store,O as sseClient,x as apiClient};
694
694
  </script>
695
695
  </body>
696
696
  </html>
@@ -94,3 +94,42 @@ describe("resolveAvailableCommand", () => {
94
94
  expect(result).toBe("npx");
95
95
  });
96
96
  });
97
+
98
+ describe("installDependencies integration", () => {
99
+ test("integrates with resolveAvailableCommand", async () => {
100
+ // This test verifies that installDependencies calls resolveAvailableCommand
101
+ // Actual fallback behavior is tested in resolveAvailableCommand tests above
102
+ const { buildInstallCommand } = await import("../dependency-installer");
103
+
104
+ const workspace: WorkspaceInfo = {
105
+ path: "/tmp/test",
106
+ language: "javascript",
107
+ packageManager: "yarn",
108
+ framework: "react",
109
+ testFramework: "jest",
110
+ };
111
+
112
+ const command = buildInstallCommand(workspace);
113
+ expect(command).toEqual(["yarn", "install", "--frozen-lockfile"]);
114
+
115
+ // The actual fallback is applied at runtime in installDependencies
116
+ // when yarn is not available in PATH, resolveAvailableCommand returns "npx"
117
+ });
118
+
119
+ test("constructs correct npx command when fallback is triggered", async () => {
120
+ // Test that when resolveAvailableCommand returns "npx",
121
+ // the spawn args preserve the original command name
122
+ const { resolveAvailableCommand } = await import("../dependency-installer");
123
+
124
+ // Simulate fallback scenario
125
+ const rawCmd = "yarn";
126
+ const args = ["install", "--frozen-lockfile"];
127
+ const resolved = await resolveAvailableCommand("nonexistent-binary-xyz123"); // Will return "npx"
128
+
129
+ // Construct spawn args as done in installDependencies
130
+ const spawnArgs = resolved === "npx" ? ["npx", rawCmd, ...args] : [resolved, ...args];
131
+
132
+ // Should be: ["npx", "yarn", "install", "--frozen-lockfile"]
133
+ expect(spawnArgs).toEqual(["npx", "yarn", "install", "--frozen-lockfile"]);
134
+ });
135
+ });
@@ -76,11 +76,18 @@ export async function installDependencies(
76
76
  return { success: true, output: "", duration: 0, skipped: true };
77
77
  }
78
78
 
79
- const [cmd, ...args] = command;
80
- logger.info({ cmd, args, cwd: workspace.path }, "Installing dependencies");
79
+ const [rawCmd, ...args] = command;
80
+
81
+ // Resolve command with fallback (e.g., yarn → npx if yarn not found)
82
+ const cmd = await resolveAvailableCommand(rawCmd!);
83
+
84
+ // If resolved to npx, preserve original command name as first argument
85
+ const spawnArgs = cmd === "npx" ? ["npx", rawCmd!, ...args] : [cmd, ...args];
86
+
87
+ logger.info({ cmd: spawnArgs[0], args: spawnArgs.slice(1), cwd: workspace.path }, "Installing dependencies");
81
88
 
82
89
  // Bun.spawn で実行(default-spawner.ts と同パターン)
83
- const proc = Bun.spawn([cmd!, ...args], {
90
+ const proc = Bun.spawn(spawnArgs, {
84
91
  cwd: workspace.path,
85
92
  stdout: "pipe",
86
93
  stderr: "pipe",
@@ -1,13 +1,13 @@
1
1
  import { describe, test, expect } from "bun:test";
2
2
  import {
3
- buildTestCommand,
3
+ buildTestCommandWithFallback,
4
4
  runTests,
5
5
  type ProcessSpawner,
6
6
  } from "../phases/tester-verify";
7
7
  import type { WorkspaceInfo } from "@aad/shared/types";
8
8
 
9
- describe("buildTestCommand", () => {
10
- test("builds bun test command", () => {
9
+ describe("buildTestCommandWithFallback", () => {
10
+ test("builds bun test command", async () => {
11
11
  const workspace: WorkspaceInfo = {
12
12
  path: "/path/to/workspace",
13
13
  language: "typescript",
@@ -16,10 +16,10 @@ describe("buildTestCommand", () => {
16
16
  testFramework: "bun-test",
17
17
  };
18
18
 
19
- expect(buildTestCommand(workspace)).toEqual(["bun", "test"]);
19
+ expect(await buildTestCommandWithFallback(workspace)).toEqual(["bun", "test"]);
20
20
  });
21
21
 
22
- test("builds vitest command", () => {
22
+ test("builds vitest command", async () => {
23
23
  const workspace: WorkspaceInfo = {
24
24
  path: "/path/to/workspace",
25
25
  language: "typescript",
@@ -28,10 +28,10 @@ describe("buildTestCommand", () => {
28
28
  testFramework: "vitest",
29
29
  };
30
30
 
31
- expect(buildTestCommand(workspace)).toEqual(["npm", "run", "test"]);
31
+ expect(await buildTestCommandWithFallback(workspace)).toEqual(["npm", "run", "test"]);
32
32
  });
33
33
 
34
- test("builds jest command", () => {
34
+ test("builds jest command", async () => {
35
35
  const workspace: WorkspaceInfo = {
36
36
  path: "/path/to/workspace",
37
37
  language: "javascript",
@@ -40,10 +40,10 @@ describe("buildTestCommand", () => {
40
40
  testFramework: "jest",
41
41
  };
42
42
 
43
- expect(buildTestCommand(workspace)).toEqual(["yarn", "test"]);
43
+ expect(await buildTestCommandWithFallback(workspace)).toEqual(["yarn", "test"]);
44
44
  });
45
45
 
46
- test("builds pytest command", () => {
46
+ test("builds pytest command", async () => {
47
47
  const workspace: WorkspaceInfo = {
48
48
  path: "/path/to/workspace",
49
49
  language: "python",
@@ -52,10 +52,10 @@ describe("buildTestCommand", () => {
52
52
  testFramework: "pytest",
53
53
  };
54
54
 
55
- expect(buildTestCommand(workspace)).toEqual(["pytest", "-v"]);
55
+ expect(await buildTestCommandWithFallback(workspace)).toEqual(["pytest", "-v"]);
56
56
  });
57
57
 
58
- test("builds go test command", () => {
58
+ test("builds go test command", async () => {
59
59
  const workspace: WorkspaceInfo = {
60
60
  path: "/path/to/workspace",
61
61
  language: "go",
@@ -64,10 +64,10 @@ describe("buildTestCommand", () => {
64
64
  testFramework: "go-test",
65
65
  };
66
66
 
67
- expect(buildTestCommand(workspace)).toEqual(["go", "test", "./..."]);
67
+ expect(await buildTestCommandWithFallback(workspace)).toEqual(["go", "test", "./..."]);
68
68
  });
69
69
 
70
- test("builds cargo test command", () => {
70
+ test("builds cargo test command", async () => {
71
71
  const workspace: WorkspaceInfo = {
72
72
  path: "/path/to/workspace",
73
73
  language: "rust",
@@ -76,10 +76,10 @@ describe("buildTestCommand", () => {
76
76
  testFramework: "cargo",
77
77
  };
78
78
 
79
- expect(buildTestCommand(workspace)).toEqual(["cargo", "test"]);
79
+ expect(await buildTestCommandWithFallback(workspace)).toEqual(["cargo", "test"]);
80
80
  });
81
81
 
82
- test("builds vitest command with yarn", () => {
82
+ test("builds vitest command with yarn", async () => {
83
83
  const workspace: WorkspaceInfo = {
84
84
  path: "/path",
85
85
  language: "typescript",
@@ -87,10 +87,10 @@ describe("buildTestCommand", () => {
87
87
  framework: "vite",
88
88
  testFramework: "vitest",
89
89
  };
90
- expect(buildTestCommand(workspace)).toEqual(["yarn", "test"]);
90
+ expect(await buildTestCommandWithFallback(workspace)).toEqual(["yarn", "test"]);
91
91
  });
92
92
 
93
- test("builds vitest command with pnpm", () => {
93
+ test("builds vitest command with pnpm", async () => {
94
94
  const workspace: WorkspaceInfo = {
95
95
  path: "/path",
96
96
  language: "typescript",
@@ -98,10 +98,10 @@ describe("buildTestCommand", () => {
98
98
  framework: "vite",
99
99
  testFramework: "vitest",
100
100
  };
101
- expect(buildTestCommand(workspace)).toEqual(["pnpm", "test"]);
101
+ expect(await buildTestCommandWithFallback(workspace)).toEqual(["pnpm", "test"]);
102
102
  });
103
103
 
104
- test("builds vitest command with default (npx)", () => {
104
+ test("builds vitest command with default (npx)", async () => {
105
105
  const workspace: WorkspaceInfo = {
106
106
  path: "/path",
107
107
  language: "typescript",
@@ -109,10 +109,10 @@ describe("buildTestCommand", () => {
109
109
  framework: "vite",
110
110
  testFramework: "vitest",
111
111
  };
112
- expect(buildTestCommand(workspace)).toEqual(["npx", "vitest", "run"]);
112
+ expect(await buildTestCommandWithFallback(workspace)).toEqual(["npx", "vitest", "run"]);
113
113
  });
114
114
 
115
- test("builds jest command with npm", () => {
115
+ test("builds jest command with npm", async () => {
116
116
  const workspace: WorkspaceInfo = {
117
117
  path: "/path",
118
118
  language: "javascript",
@@ -120,10 +120,10 @@ describe("buildTestCommand", () => {
120
120
  framework: "react",
121
121
  testFramework: "jest",
122
122
  };
123
- expect(buildTestCommand(workspace)).toEqual(["npm", "test"]);
123
+ expect(await buildTestCommandWithFallback(workspace)).toEqual(["npm", "test"]);
124
124
  });
125
125
 
126
- test("builds jest command with pnpm", () => {
126
+ test("builds jest command with pnpm", async () => {
127
127
  const workspace: WorkspaceInfo = {
128
128
  path: "/path",
129
129
  language: "javascript",
@@ -131,10 +131,10 @@ describe("buildTestCommand", () => {
131
131
  framework: "react",
132
132
  testFramework: "jest",
133
133
  };
134
- expect(buildTestCommand(workspace)).toEqual(["pnpm", "test"]);
134
+ expect(await buildTestCommandWithFallback(workspace)).toEqual(["pnpm", "test"]);
135
135
  });
136
136
 
137
- test("builds jest command with default (npx)", () => {
137
+ test("builds jest command with default (npx)", async () => {
138
138
  const workspace: WorkspaceInfo = {
139
139
  path: "/path",
140
140
  language: "javascript",
@@ -142,10 +142,10 @@ describe("buildTestCommand", () => {
142
142
  framework: "react",
143
143
  testFramework: "jest",
144
144
  };
145
- expect(buildTestCommand(workspace)).toEqual(["npx", "jest"]);
145
+ expect(await buildTestCommandWithFallback(workspace)).toEqual(["npx", "jest"]);
146
146
  });
147
147
 
148
- test("builds mocha command with npm", () => {
148
+ test("builds mocha command with npm", async () => {
149
149
  const workspace: WorkspaceInfo = {
150
150
  path: "/path",
151
151
  language: "javascript",
@@ -153,10 +153,10 @@ describe("buildTestCommand", () => {
153
153
  framework: "express",
154
154
  testFramework: "mocha",
155
155
  };
156
- expect(buildTestCommand(workspace)).toEqual(["npm", "test"]);
156
+ expect(await buildTestCommandWithFallback(workspace)).toEqual(["npm", "test"]);
157
157
  });
158
158
 
159
- test("builds mocha command with yarn", () => {
159
+ test("builds mocha command with yarn", async () => {
160
160
  const workspace: WorkspaceInfo = {
161
161
  path: "/path",
162
162
  language: "javascript",
@@ -164,10 +164,10 @@ describe("buildTestCommand", () => {
164
164
  framework: "express",
165
165
  testFramework: "mocha",
166
166
  };
167
- expect(buildTestCommand(workspace)).toEqual(["yarn", "test"]);
167
+ expect(await buildTestCommandWithFallback(workspace)).toEqual(["yarn", "test"]);
168
168
  });
169
169
 
170
- test("builds mocha command with default (npx)", () => {
170
+ test("builds mocha command with default (npx)", async () => {
171
171
  const workspace: WorkspaceInfo = {
172
172
  path: "/path",
173
173
  language: "javascript",
@@ -175,10 +175,10 @@ describe("buildTestCommand", () => {
175
175
  framework: "express",
176
176
  testFramework: "mocha",
177
177
  };
178
- expect(buildTestCommand(workspace)).toEqual(["npx", "mocha"]);
178
+ expect(await buildTestCommandWithFallback(workspace)).toEqual(["npx", "mocha"]);
179
179
  });
180
180
 
181
- test("builds maven test command", () => {
181
+ test("builds maven test command", async () => {
182
182
  const workspace: WorkspaceInfo = {
183
183
  path: "/path",
184
184
  language: "java",
@@ -186,10 +186,10 @@ describe("buildTestCommand", () => {
186
186
  framework: "spring",
187
187
  testFramework: "maven",
188
188
  };
189
- expect(buildTestCommand(workspace)).toEqual(["mvn", "test"]);
189
+ expect(await buildTestCommandWithFallback(workspace)).toEqual(["mvn", "test"]);
190
190
  });
191
191
 
192
- test("builds gradle test command", () => {
192
+ test("builds gradle test command", async () => {
193
193
  const workspace: WorkspaceInfo = {
194
194
  path: "/path",
195
195
  language: "java",
@@ -197,10 +197,10 @@ describe("buildTestCommand", () => {
197
197
  framework: "spring",
198
198
  testFramework: "gradle",
199
199
  };
200
- expect(buildTestCommand(workspace)).toEqual(["./gradlew", "test"]);
200
+ expect(await buildTestCommandWithFallback(workspace)).toEqual(["./gradlew", "test"]);
201
201
  });
202
202
 
203
- test("returns fallback for unknown test framework", () => {
203
+ test("returns fallback for unknown test framework with npm", async () => {
204
204
  const workspace: WorkspaceInfo = {
205
205
  path: "/path/to/workspace",
206
206
  language: "unknown",
@@ -209,8 +209,89 @@ describe("buildTestCommand", () => {
209
209
  testFramework: "unknown",
210
210
  };
211
211
 
212
- // After fallback implementation, unknown should return npm test
213
- expect(buildTestCommand(workspace)).toEqual(["npm", "test"]);
212
+ expect(await buildTestCommandWithFallback(workspace)).toEqual(["npm", "test"]);
213
+ });
214
+
215
+ test("returns fallback for unknown test framework with bun", async () => {
216
+ const workspace: WorkspaceInfo = {
217
+ path: "/path/to/workspace",
218
+ language: "unknown",
219
+ packageManager: "bun",
220
+ framework: "unknown",
221
+ testFramework: "unknown",
222
+ };
223
+
224
+ expect(await buildTestCommandWithFallback(workspace)).toEqual(["bun", "test"]);
225
+ });
226
+
227
+ test("returns fallback for unknown test framework with yarn", async () => {
228
+ const workspace: WorkspaceInfo = {
229
+ path: "/path/to/workspace",
230
+ language: "unknown",
231
+ packageManager: "yarn",
232
+ framework: "unknown",
233
+ testFramework: "unknown",
234
+ };
235
+
236
+ expect(await buildTestCommandWithFallback(workspace)).toEqual(["yarn", "test"]);
237
+ });
238
+
239
+ test("returns fallback for unknown test framework with pnpm", async () => {
240
+ const workspace: WorkspaceInfo = {
241
+ path: "/path/to/workspace",
242
+ language: "unknown",
243
+ packageManager: "pnpm",
244
+ framework: "unknown",
245
+ testFramework: "unknown",
246
+ };
247
+
248
+ expect(await buildTestCommandWithFallback(workspace)).toEqual(["pnpm", "test"]);
249
+ });
250
+
251
+ test("builds unknown test with yarn fallback when yarn is unavailable", async () => {
252
+ const workspace: WorkspaceInfo = {
253
+ path: "/path",
254
+ language: "unknown",
255
+ packageManager: "yarn",
256
+ framework: "unknown",
257
+ testFramework: "unknown",
258
+ };
259
+
260
+ const originalWhich = Bun.which;
261
+ Bun.which = (cmd: string) => {
262
+ if (cmd === "yarn") return null;
263
+ return originalWhich(cmd);
264
+ };
265
+
266
+ try {
267
+ const result = await buildTestCommandWithFallback(workspace);
268
+ expect(result).toEqual(["npx", "yarn", "test"]);
269
+ } finally {
270
+ Bun.which = originalWhich;
271
+ }
272
+ });
273
+
274
+ test("builds unknown test with pnpm fallback when pnpm is unavailable", async () => {
275
+ const workspace: WorkspaceInfo = {
276
+ path: "/path",
277
+ language: "unknown",
278
+ packageManager: "pnpm",
279
+ framework: "unknown",
280
+ testFramework: "unknown",
281
+ };
282
+
283
+ const originalWhich = Bun.which;
284
+ Bun.which = (cmd: string) => {
285
+ if (cmd === "pnpm") return null;
286
+ return originalWhich(cmd);
287
+ };
288
+
289
+ try {
290
+ const result = await buildTestCommandWithFallback(workspace);
291
+ expect(result).toEqual(["npx", "pnpm", "test"]);
292
+ } finally {
293
+ Bun.which = originalWhich;
294
+ }
214
295
  });
215
296
 
216
297
  test("builds vitest with fallback when yarn is unavailable", async () => {
@@ -230,7 +311,7 @@ describe("buildTestCommand", () => {
230
311
  };
231
312
 
232
313
  try {
233
- const result = await (await import("../phases/tester-verify")).buildTestCommandWithFallback(workspace);
314
+ const result = await buildTestCommandWithFallback(workspace);
234
315
  expect(result).toEqual(["npx", "yarn", "test"]);
235
316
  } finally {
236
317
  Bun.which = originalWhich;
@@ -253,7 +334,7 @@ describe("buildTestCommand", () => {
253
334
  };
254
335
 
255
336
  try {
256
- const result = await (await import("../phases/tester-verify")).buildTestCommandWithFallback(workspace);
337
+ const result = await buildTestCommandWithFallback(workspace);
257
338
  expect(result).toEqual(["npx", "pnpm", "test"]);
258
339
  } finally {
259
340
  Bun.which = originalWhich;
@@ -14,7 +14,7 @@ export {
14
14
  } from "./phases/implementer-green";
15
15
  export type { ImplementerGreenOptions } from "./phases/implementer-green";
16
16
 
17
- export { runTests, buildTestCommand } from "./phases/tester-verify";
17
+ export { runTests, buildTestCommandWithFallback } from "./phases/tester-verify";
18
18
  export type {
19
19
  TestResult,
20
20
  ProcessSpawner,
@@ -94,8 +94,14 @@ export async function buildTestCommandWithFallback(workspace: WorkspaceInfo): Pr
94
94
  case "unknown": {
95
95
  if (packageManager === "bun") return ["bun", "test"];
96
96
  if (packageManager === "npm") return ["npm", "test"];
97
- if (packageManager === "yarn") return ["yarn", "test"];
98
- if (packageManager === "pnpm") return ["pnpm", "test"];
97
+ if (packageManager === "yarn") {
98
+ const cmd = await resolveAvailableCommand("yarn");
99
+ return cmd === "npx" ? ["npx", "yarn", "test"] : ["yarn", "test"];
100
+ }
101
+ if (packageManager === "pnpm") {
102
+ const cmd = await resolveAvailableCommand("pnpm");
103
+ return cmd === "npx" ? ["npx", "pnpm", "test"] : ["pnpm", "test"];
104
+ }
99
105
  if (packageManager === "uv") return ["uv", "run", "pytest", "-v"];
100
106
  if (packageManager === "poetry") return ["poetry", "run", "pytest", "-v"];
101
107
  return ["npm", "test"];
@@ -111,79 +117,6 @@ export async function buildTestCommandWithFallback(workspace: WorkspaceInfo): Pr
111
117
  }
112
118
  }
113
119
 
114
- /**
115
- * Build test command based on detected test framework
116
- */
117
- export function buildTestCommand(workspace: WorkspaceInfo): string[] {
118
- const { testFramework, packageManager } = workspace;
119
-
120
- switch (testFramework) {
121
- case "bun-test":
122
- return ["bun", "test"];
123
-
124
- case "vitest":
125
- if (packageManager === "npm") return ["npm", "run", "test"];
126
- if (packageManager === "yarn") return ["yarn", "test"];
127
- if (packageManager === "pnpm") return ["pnpm", "test"];
128
- return ["npx", "vitest", "run"];
129
-
130
- case "jest":
131
- if (packageManager === "npm") return ["npm", "test"];
132
- if (packageManager === "yarn") return ["yarn", "test"];
133
- if (packageManager === "pnpm") return ["pnpm", "test"];
134
- return ["npx", "jest"];
135
-
136
- case "mocha":
137
- if (packageManager === "npm") return ["npm", "test"];
138
- if (packageManager === "yarn") return ["yarn", "test"];
139
- return ["npx", "mocha"];
140
-
141
- case "pytest": {
142
- const { packageManager } = workspace;
143
- if (packageManager === "uv") return ["uv", "run", "pytest", "-v"];
144
- if (packageManager === "poetry") return ["poetry", "run", "pytest", "-v"];
145
- return ["pytest", "-v"];
146
- }
147
-
148
- case "go-test":
149
- return ["go", "test", "./..."];
150
-
151
- case "cargo":
152
- return ["cargo", "test"];
153
-
154
- case "maven":
155
- return ["mvn", "test"];
156
-
157
- case "gradle":
158
- return ["./gradlew", "test"];
159
-
160
- case "playwright":
161
- return ["npx", "playwright", "test"];
162
-
163
- case "terraform":
164
- return ["terraform", "validate"];
165
-
166
- case "unknown": {
167
- // Fallback: use package manager-based test command
168
- const { packageManager } = workspace;
169
- if (packageManager === "bun") return ["bun", "test"];
170
- if (packageManager === "npm") return ["npm", "test"];
171
- if (packageManager === "yarn") return ["yarn", "test"];
172
- if (packageManager === "pnpm") return ["pnpm", "test"];
173
- if (packageManager === "uv") return ["uv", "run", "pytest", "-v"];
174
- if (packageManager === "poetry") return ["poetry", "run", "pytest", "-v"];
175
- return ["npm", "test"];
176
- }
177
-
178
- default: {
179
- const exhaustive: never = testFramework;
180
- throw new TestRunnerError(
181
- `Unsupported test framework: ${exhaustive}`,
182
- { testFramework }
183
- );
184
- }
185
- }
186
- }
187
120
 
188
121
  /**
189
122
  * Run tests in workspace and return result
@@ -196,7 +129,7 @@ export async function runTests(
196
129
  spawner?: ProcessSpawner,
197
130
  timeout = 300000
198
131
  ): Promise<TestResult> {
199
- const command = buildTestCommand(workspace);
132
+ const command = await buildTestCommandWithFallback(workspace);
200
133
  const [cmd, ...args] = command;
201
134
 
202
135
  if (!cmd) {