feat(FN-3350): tokenize task detail danger/error styling

Fixes task detail modal danger/error styling by switching to token-based CSS variables for consistent theming across dark and light modes.

Fusion-Task-Id: FN-3350
This commit is contained in:
Fusion
2026-05-04 04:04:13 -07:00
committed by gsxdsm
parent fbb96b25a5
commit 8cb8055531
24 changed files with 487 additions and 53 deletions

View File

@@ -61,6 +61,9 @@ class MockStore extends EventEmitter {
getRunAuditEvents = mockGetRunAuditEvents;
getMutationsForRun = vi.fn().mockResolvedValue([]);
getAgentLogsByTimeRange = vi.fn().mockResolvedValue([]);
getTasksByAssignedAgent = vi.fn().mockResolvedValue([]);
getTask = vi.fn().mockResolvedValue({ id: "FN-1" });
pauseTask = vi.fn().mockImplementation(async (id: string, paused: boolean) => ({ id, paused }));
getRootDir(): string {
return "/tmp/fn-1059-test";
@@ -138,6 +141,70 @@ describe("Agent runs routes (without HeartbeatMonitor)", () => {
});
});
describe("POST /api/tasks/:id/pause and /unpause", () => {
it("returns 409 for pause on agent-assigned task", async () => {
(store.getTask as ReturnType<typeof vi.fn>).mockResolvedValueOnce({ id: "FN-1", assignedAgentId: "agent-1" });
const response = await request(
app,
"POST",
"/api/tasks/FN-1/pause",
JSON.stringify({}),
{ "content-type": "application/json" },
);
expect(response.status).toBe(409);
expect((response.body as any).error).toContain("Cannot manually pause/unpause task assigned to agent agent-1");
expect(store.pauseTask).not.toHaveBeenCalled();
});
it("allows pause for unassigned task", async () => {
(store.getTask as ReturnType<typeof vi.fn>).mockResolvedValueOnce({ id: "FN-2" });
const response = await request(
app,
"POST",
"/api/tasks/FN-2/pause",
JSON.stringify({}),
{ "content-type": "application/json" },
);
expect(response.status).toBe(200);
expect(store.pauseTask).toHaveBeenCalledWith("FN-2", true);
});
it("returns 409 for unpause on agent-assigned task", async () => {
(store.getTask as ReturnType<typeof vi.fn>).mockResolvedValueOnce({ id: "FN-3", assignedAgentId: "agent-2" });
const response = await request(
app,
"POST",
"/api/tasks/FN-3/unpause",
JSON.stringify({}),
{ "content-type": "application/json" },
);
expect(response.status).toBe(409);
expect((response.body as any).error).toContain("Cannot manually pause/unpause task assigned to agent agent-2");
expect(store.pauseTask).not.toHaveBeenCalled();
});
it("allows unpause for unassigned task", async () => {
(store.getTask as ReturnType<typeof vi.fn>).mockResolvedValueOnce({ id: "FN-4" });
const response = await request(
app,
"POST",
"/api/tasks/FN-4/unpause",
JSON.stringify({}),
{ "content-type": "application/json" },
);
expect(response.status).toBe(200);
expect(store.pauseTask).toHaveBeenCalledWith("FN-4", false);
});
});
describe("POST /api/agents/:id/runs", () => {
it("returns 201 with run record (fallback behavior without HeartbeatMonitor)", async () => {
const mockRun = createMockRun();
@@ -445,6 +512,55 @@ describe("Agent runs routes (with HeartbeatMonitor)", () => {
});
});
it("pausing agent auto-pauses only non-paused assigned tasks", async () => {
(store.getTasksByAssignedAgent as ReturnType<typeof vi.fn>).mockResolvedValueOnce([
{ id: "FN-1", paused: false },
{ id: "FN-2", paused: true },
{ id: "FN-3" },
]);
mockUpdateAgentState.mockResolvedValue({ id: "agent-001", state: "paused" });
const response = await request(
app,
"POST",
"/api/agents/agent-001/state",
JSON.stringify({ state: "paused" }),
{ "content-type": "application/json" },
);
expect(response.status).toBe(200);
await vi.waitFor(() => {
expect(store.pauseTask).toHaveBeenCalledTimes(2);
});
expect(store.pauseTask).toHaveBeenCalledWith("FN-1", true, undefined, { pausedByAgentId: "agent-001" });
expect(store.pauseTask).toHaveBeenCalledWith("FN-3", true, undefined, { pausedByAgentId: "agent-001" });
});
it("resuming agent only unpauses tasks paused by that same agent", async () => {
(store.getTasksByAssignedAgent as ReturnType<typeof vi.fn>).mockResolvedValueOnce([
{ id: "FN-1", paused: true, pausedByAgentId: "agent-001" },
{ id: "FN-2", paused: true, pausedByAgentId: "agent-002" },
{ id: "FN-3", paused: true },
]);
mockGetAgent.mockResolvedValue({ id: "agent-001", state: "paused" });
mockUpdateAgentState.mockResolvedValue({ id: "agent-001", state: "active" });
mockExecuteHeartbeat.mockResolvedValue(createMockRun({ id: "run-resume-1", status: "completed" }));
const response = await request(
app,
"POST",
"/api/agents/agent-001/state",
JSON.stringify({ state: "active" }),
{ "content-type": "application/json" },
);
expect(response.status).toBe(200);
await vi.waitFor(() => {
expect(store.pauseTask).toHaveBeenCalledWith("FN-1", false);
});
expect(store.pauseTask).toHaveBeenCalledTimes(1);
});
it("resuming to active triggers on-demand heartbeat exactly once", async () => {
mockGetAgent.mockResolvedValue({ id: "agent-001", state: "paused" });
mockUpdateAgentState.mockResolvedValue({ id: "agent-001", state: "active" });
@@ -475,6 +591,26 @@ describe("Agent runs routes (with HeartbeatMonitor)", () => {
});
});
it("terminated agent also unpauses tasks paused by that agent", async () => {
(store.getTasksByAssignedAgent as ReturnType<typeof vi.fn>).mockResolvedValueOnce([
{ id: "FN-9", paused: true, pausedByAgentId: "agent-001" },
]);
mockUpdateAgentState.mockResolvedValue({ id: "agent-001", state: "terminated" });
const response = await request(
app,
"POST",
"/api/agents/agent-001/state",
JSON.stringify({ state: "terminated" }),
{ "content-type": "application/json" },
);
expect(response.status).toBe(200);
await vi.waitFor(() => {
expect(store.pauseTask).toHaveBeenCalledWith("FN-9", false);
});
});
it("resuming to active does not auto-trigger heartbeat when disabled", async () => {
mockGetAgent.mockResolvedValue({
id: "agent-001",

View File

@@ -1623,6 +1623,7 @@ describe("Pause/Unpause endpoints", () => {
beforeEach(() => {
store = createMockStore({
getTask: vi.fn().mockResolvedValue({ id: "FN-001" }),
pauseTask: vi.fn().mockResolvedValue({ id: "FN-001", paused: true }),
});
});
@@ -1642,7 +1643,7 @@ describe("Pause/Unpause endpoints", () => {
});
it("POST /tasks/:id/pause — returns 500 on error", async () => {
(store.pauseTask as ReturnType<typeof vi.fn>).mockRejectedValue(new Error("not found"));
(store.getTask as ReturnType<typeof vi.fn>).mockRejectedValue(new Error("not found"));
const res = await REQUEST(buildApp(), "POST", "/api/tasks/KB-001/pause");
expect(res.status).toBe(500);
expect(res.body.error).toBe("not found");