feat(FN-3475): merge fusion/fn-3475
Fusion-Task-Id: FN-3497
This commit is contained in:
@@ -464,6 +464,8 @@ describe("Agent runs routes (with HeartbeatMonitor)", () => {
|
||||
let app: ReturnType<typeof import("../server.js").createServer>;
|
||||
let mockExecuteHeartbeat: ReturnType<typeof vi.fn>;
|
||||
let mockStopRun: ReturnType<typeof vi.fn>;
|
||||
let mockPauseAgent: ReturnType<typeof vi.fn>;
|
||||
let mockResumeAgent: ReturnType<typeof vi.fn>;
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
@@ -475,6 +477,8 @@ describe("Agent runs routes (with HeartbeatMonitor)", () => {
|
||||
|
||||
mockExecuteHeartbeat = vi.fn();
|
||||
mockStopRun = vi.fn();
|
||||
mockPauseAgent = vi.fn();
|
||||
mockResumeAgent = vi.fn();
|
||||
|
||||
store = new MockStore();
|
||||
const { createServer } = await import("../server.js");
|
||||
@@ -482,6 +486,8 @@ describe("Agent runs routes (with HeartbeatMonitor)", () => {
|
||||
heartbeatMonitor: {
|
||||
executeHeartbeat: mockExecuteHeartbeat,
|
||||
stopRun: mockStopRun,
|
||||
pauseAgent: mockPauseAgent,
|
||||
resumeAgent: mockResumeAgent,
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -491,10 +497,8 @@ describe("Agent runs routes (with HeartbeatMonitor)", () => {
|
||||
});
|
||||
|
||||
describe("POST /api/agents/:id/state", () => {
|
||||
it("pauses and stops active run asynchronously after responding", async () => {
|
||||
mockGetActiveHeartbeatRun.mockResolvedValue(createMockRun({ id: "run-pause-1" }));
|
||||
mockStopRun.mockResolvedValue(undefined);
|
||||
mockUpdateAgentState.mockResolvedValue({ id: "agent-001", state: "paused" });
|
||||
it("delegates pause transitions to heartbeat monitor lifecycle helper", async () => {
|
||||
mockPauseAgent.mockResolvedValue({ id: "agent-001", state: "paused", pauseReason: "manual" });
|
||||
|
||||
const response = await request(
|
||||
app,
|
||||
@@ -505,44 +509,42 @@ describe("Agent runs routes (with HeartbeatMonitor)", () => {
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual({ id: "agent-001", state: "paused" });
|
||||
expect(mockUpdateAgentState).toHaveBeenCalledWith("agent-001", "paused");
|
||||
await vi.waitFor(() => {
|
||||
expect(mockStopRun).toHaveBeenCalledWith("agent-001");
|
||||
});
|
||||
expect(response.body).toEqual({ id: "agent-001", state: "paused", pauseReason: "manual" });
|
||||
expect(mockPauseAgent).toHaveBeenCalledWith("agent-001", { pauseReason: undefined, stopActiveRun: true });
|
||||
expect(mockUpdateAgentState).not.toHaveBeenCalled();
|
||||
expect(store.pauseTask).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
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" });
|
||||
it("delegates resume transitions to heartbeat monitor lifecycle helper", async () => {
|
||||
mockResumeAgent.mockResolvedValue({ id: "agent-001", state: "active" });
|
||||
|
||||
const response = await request(
|
||||
app,
|
||||
"POST",
|
||||
"/api/agents/agent-001/state",
|
||||
JSON.stringify({ state: "paused" }),
|
||||
JSON.stringify({ state: "active" }),
|
||||
{ "content-type": "application/json" },
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
await vi.waitFor(() => {
|
||||
expect(store.pauseTask).toHaveBeenCalledTimes(2);
|
||||
expect(response.body).toEqual({ id: "agent-001", state: "active" });
|
||||
expect(mockResumeAgent).toHaveBeenCalledWith("agent-001", {
|
||||
triggerDetail: "Triggered from state resume",
|
||||
triggerSource: "state-resume",
|
||||
clearPauseReason: true,
|
||||
});
|
||||
expect(store.pauseTask).toHaveBeenCalledWith("FN-1", true, undefined, { pausedByAgentId: "agent-001" });
|
||||
expect(store.pauseTask).toHaveBeenCalledWith("FN-3", true, undefined, { pausedByAgentId: "agent-001" });
|
||||
expect(mockExecuteHeartbeat).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("resuming agent only unpauses tasks paused by that same agent", async () => {
|
||||
it("falls back to direct state update when monitor lacks lifecycle helpers", async () => {
|
||||
const { createServer } = await import("../server.js");
|
||||
app = createServer(store as any, {
|
||||
heartbeatMonitor: {
|
||||
executeHeartbeat: mockExecuteHeartbeat,
|
||||
stopRun: mockStopRun,
|
||||
},
|
||||
});
|
||||
(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" }));
|
||||
|
||||
@@ -558,39 +560,8 @@ describe("Agent runs routes (with HeartbeatMonitor)", () => {
|
||||
await vi.waitFor(() => {
|
||||
expect(store.pauseTask).toHaveBeenCalledWith("FN-1", false);
|
||||
});
|
||||
expect(store.pauseTask).toHaveBeenCalledTimes(1);
|
||||
expect(mockExecuteHeartbeat).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" });
|
||||
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);
|
||||
expect(response.body).toEqual({ id: "agent-001", state: "active" });
|
||||
await vi.waitFor(() => {
|
||||
expect(mockExecuteHeartbeat).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
expect(mockExecuteHeartbeat).toHaveBeenCalledWith({
|
||||
agentId: "agent-001",
|
||||
source: "on_demand",
|
||||
triggerDetail: "Triggered from state resume",
|
||||
contextSnapshot: {
|
||||
wakeReason: "on_demand",
|
||||
triggerDetail: "Triggered from state resume",
|
||||
triggerSource: "state-resume",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
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" },
|
||||
@@ -617,7 +588,7 @@ describe("Agent runs routes (with HeartbeatMonitor)", () => {
|
||||
state: "paused",
|
||||
runtimeConfig: { enabled: false },
|
||||
});
|
||||
mockUpdateAgentState.mockResolvedValue({ id: "agent-001", state: "active" });
|
||||
mockResumeAgent.mockResolvedValue({ id: "agent-001", state: "active" });
|
||||
|
||||
const response = await request(
|
||||
app,
|
||||
@@ -629,7 +600,7 @@ describe("Agent runs routes (with HeartbeatMonitor)", () => {
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual({ id: "agent-001", state: "active" });
|
||||
await Promise.resolve();
|
||||
expect(mockResumeAgent).toHaveBeenCalled();
|
||||
expect(mockExecuteHeartbeat).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user