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:
@@ -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",
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user