feat(FN-3475): merge fusion/fn-3475

Fusion-Task-Id: FN-3497
This commit is contained in:
gsxdsm
2026-05-05 04:49:45 -07:00
parent f9dfab8961
commit af0bc4bd07
7 changed files with 247 additions and 189 deletions

View File

@@ -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();
});
});