Skip to content

Commit e7db4f9

Browse files
committed
fix(workbench): sync engine bridge task statuses
1 parent c20b018 commit e7db4f9

File tree

2 files changed

+152
-2
lines changed

2 files changed

+152
-2
lines changed

apps/workbench/src/features/swarm/hooks/__tests__/use-engine-board-bridge.test.ts

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,4 +358,127 @@ describe("useEngineBoardBridge", () => {
358358
vi.useRealTimers();
359359
}
360360
});
361+
362+
it("refreshes the restore status when guard evaluation re-triggers after a status change", () => {
363+
vi.useFakeTimers();
364+
365+
const events = new TypedEventEmitter<SwarmEngineEventMap>();
366+
const engine = {
367+
getState: () => makeEngineState(),
368+
getEvents: () => events,
369+
};
370+
371+
const { unmount } = renderHook(() =>
372+
useEngineBoardBridge(engine as any),
373+
);
374+
375+
try {
376+
act(() => {
377+
events.emit("agent.status_changed", {
378+
agentId: "agt_pool_1",
379+
newStatus: "running",
380+
} as any);
381+
});
382+
383+
act(() => {
384+
events.emit("guard.evaluated", {
385+
action: { agentId: "agt_pool_1" },
386+
result: {
387+
verdict: "allow",
388+
guardResults: [],
389+
receipt: {
390+
signature: "beef".repeat(32),
391+
publicKey: "1234".repeat(16),
392+
},
393+
},
394+
} as any);
395+
});
396+
397+
act(() => {
398+
events.emit("agent.status_changed", {
399+
agentId: "agt_pool_1",
400+
newStatus: "idle",
401+
} as any);
402+
});
403+
404+
act(() => {
405+
events.emit("guard.evaluated", {
406+
action: { agentId: "agt_pool_1" },
407+
result: {
408+
verdict: "allow",
409+
guardResults: [],
410+
receipt: {
411+
signature: "cafe".repeat(32),
412+
publicKey: "5678".repeat(16),
413+
},
414+
},
415+
} as any);
416+
});
417+
418+
act(() => {
419+
vi.advanceTimersByTime(2000);
420+
});
421+
422+
expect(
423+
useSwarmBoardStore.getState().nodes.find((node) => node.id === "agt_pool_1")?.data.status,
424+
).toBe("idle");
425+
} finally {
426+
unmount();
427+
vi.useRealTimers();
428+
}
429+
});
430+
431+
it("maps created tasks from engine status updates instead of forcing them to running", () => {
432+
const events = new TypedEventEmitter<SwarmEngineEventMap>();
433+
const engine = {
434+
getState: () => makeEngineState(),
435+
getEvents: () => events,
436+
};
437+
438+
const { unmount } = renderHook(() =>
439+
useEngineBoardBridge(engine as any),
440+
);
441+
442+
try {
443+
act(() => {
444+
events.emit("task.created", {
445+
task: {
446+
id: "tsk_1",
447+
name: "Queue scan",
448+
type: "analysis",
449+
status: "created",
450+
assignedTo: null,
451+
},
452+
} as any);
453+
});
454+
455+
expect(
456+
useSwarmBoardStore.getState().nodes.find((node) => node.data.taskId === "tsk_1")?.data.status,
457+
).toBe("idle");
458+
459+
act(() => {
460+
events.emit("task.status_changed", {
461+
taskId: "tsk_1",
462+
newStatus: "paused",
463+
} as any);
464+
});
465+
466+
expect(
467+
useSwarmBoardStore.getState().nodes.find((node) => node.data.taskId === "tsk_1")?.data.status,
468+
).toBe("blocked");
469+
470+
act(() => {
471+
events.emit("task.status_changed", {
472+
taskId: "tsk_1",
473+
newStatus: "cancelled",
474+
} as any);
475+
});
476+
477+
expect(
478+
useSwarmBoardStore.getState().nodes.find((node) => node.data.taskId === "tsk_1")?.data.status,
479+
).toBe("completed");
480+
} finally {
481+
unmount();
482+
}
483+
});
361484
});

apps/workbench/src/features/swarm/hooks/use-engine-board-bridge.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,24 @@ const DEFAULT_LAYOUT_VIEWPORT = { width: 1200, height: 800 };
1717

1818
function mapEngineStatus(engineStatus: string): SessionStatus {
1919
switch (engineStatus) {
20+
case "created":
21+
case "queued":
22+
case "assigned":
23+
return "idle";
2024
case "running":
2125
return "running";
2226
case "idle":
2327
return "idle";
2428
case "busy":
2529
return "running";
30+
case "paused":
31+
return "blocked";
2632
case "terminated":
2733
return "completed";
34+
case "cancelled":
35+
return "completed";
36+
case "timeout":
37+
return "failed";
2838
case "offline":
2939
return "failed";
3040
case "completed":
@@ -290,7 +300,7 @@ export function useEngineBoardBridge(engine: SwarmOrchestrator | null): void {
290300
data: {
291301
nodeType: "terminalTask",
292302
title: event.task.type ?? event.task.name ?? "Task",
293-
status: "running",
303+
status: mapEngineStatus(event.task.status),
294304
taskId: event.task.id,
295305
agentId: event.task.assignedTo,
296306
engineManaged: true,
@@ -309,6 +319,21 @@ export function useEngineBoardBridge(engine: SwarmOrchestrator | null): void {
309319
}),
310320
);
311321

322+
unsubs.push(
323+
events.on("task.status_changed", (event: any) => {
324+
const { nodes, actions } = store();
325+
326+
const taskNode = nodes.find(
327+
(n: Node<SwarmBoardNodeData>) => n.data.taskId === event.taskId,
328+
);
329+
if (!taskNode) return;
330+
331+
actions.updateNode(taskNode.id, {
332+
status: mapEngineStatus(event.newStatus),
333+
});
334+
}),
335+
);
336+
312337
unsubs.push(
313338
events.on("task.completed", (event: any) => {
314339
const { nodes, actions } = store();
@@ -350,7 +375,9 @@ export function useEngineBoardBridge(engine: SwarmOrchestrator | null): void {
350375
const existingTimeout = timeouts.get(nodeId);
351376
if (existingTimeout != null) {
352377
clearTimeout(existingTimeout);
353-
} else {
378+
}
379+
380+
if (currentStatus !== "evaluating" || !restoreStatuses.has(nodeId)) {
354381
restoreStatuses.set(
355382
nodeId,
356383
currentStatus === "evaluating" ? "running" : currentStatus,

0 commit comments

Comments
 (0)