feat(FN-3922): enforce task skip on credential-failure heartbeat
Credential-missing recovery is now documented and enforced in the agent heartbeat layer, with comprehensive test coverage for both timer-triggered and assignment-triggered credential failure scenarios. Fusion-Task-Id: FN-3922
This commit is contained in:
@@ -2545,11 +2545,84 @@ describe("executeHeartbeat", () => {
|
||||
expect(result.resultJson).toMatchObject({
|
||||
reason: "heartbeat_model_unavailable",
|
||||
source: "timer",
|
||||
detail: expect.stringContaining("No API key for provider: anthropic"),
|
||||
});
|
||||
expect(result.stderrExcerpt).toContain("No API key for provider: anthropic");
|
||||
expect(store.updateAgentState).toHaveBeenCalledWith("agent-001", "active");
|
||||
expect(store.updateAgentState).not.toHaveBeenCalledWith("agent-001", "error");
|
||||
});
|
||||
|
||||
it.each(["on_demand", "assignment"] as const)("pauses on %s heartbeat when model provider credentials are unavailable", async (source) => {
|
||||
const store = createStoreWithAgentForExec();
|
||||
mockedCreateFnAgent.mockRejectedValue(new Error("No API key for provider: anthropic"));
|
||||
|
||||
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
|
||||
|
||||
const result = await monitor.executeHeartbeat({ agentId: "agent-001", source });
|
||||
|
||||
expect(result.status).toBe("completed");
|
||||
expect(result.resultJson).toMatchObject({
|
||||
reason: "heartbeat_model_unavailable",
|
||||
source,
|
||||
actionRequired: true,
|
||||
detail: expect.stringContaining("Configure credentials for provider \"anthropic\""),
|
||||
});
|
||||
expect(result.stderrExcerpt).toContain("No API key for provider: anthropic");
|
||||
expect(store.updateAgentState).toHaveBeenCalledWith("agent-001", "paused");
|
||||
expect(store.updateAgent).toHaveBeenCalledWith("agent-001", {
|
||||
pauseReason: "heartbeat-model-unavailable",
|
||||
lastError: expect.stringContaining("No API key for provider: anthropic"),
|
||||
});
|
||||
expect(store.updateAgentState).not.toHaveBeenCalledWith("agent-001", "error");
|
||||
});
|
||||
|
||||
it("keeps timer-triggered credential failures in recoverable state across consecutive wakeups", async () => {
|
||||
const store = createStoreWithAgentForExec();
|
||||
mockedCreateFnAgent.mockRejectedValue(new Error("No API key for provider: anthropic"));
|
||||
|
||||
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
|
||||
|
||||
const first = await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
|
||||
const second = await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
|
||||
|
||||
for (const run of [first, second]) {
|
||||
expect(run.status).toBe("completed");
|
||||
expect(run.resultJson).toMatchObject({
|
||||
reason: "heartbeat_model_unavailable",
|
||||
source: "timer",
|
||||
detail: expect.stringContaining("No API key for provider: anthropic"),
|
||||
});
|
||||
expect(run.stderrExcerpt).toContain("No API key for provider: anthropic");
|
||||
}
|
||||
|
||||
expect(store.updateAgentState).toHaveBeenCalledWith("agent-001", "active");
|
||||
expect(store.updateAgentState).not.toHaveBeenCalledWith("agent-001", "error");
|
||||
});
|
||||
|
||||
it("keeps non-timer credential failures recoverable on consecutive wakeups", async () => {
|
||||
const store = createStoreWithAgentForExec();
|
||||
mockedCreateFnAgent.mockRejectedValue(new Error("No API key for provider: anthropic"));
|
||||
|
||||
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
|
||||
|
||||
const first = await monitor.executeHeartbeat({ agentId: "agent-001", source: "assignment" });
|
||||
const second = await monitor.executeHeartbeat({ agentId: "agent-001", source: "assignment" });
|
||||
|
||||
expect(first.status).toBe("completed");
|
||||
expect(first.resultJson).toMatchObject({
|
||||
reason: "heartbeat_model_unavailable",
|
||||
source: "assignment",
|
||||
actionRequired: true,
|
||||
});
|
||||
expect(second.status).toBe("completed");
|
||||
expect(second.resultJson).toMatchObject({
|
||||
reason: "heartbeat_model_unavailable",
|
||||
source: "assignment",
|
||||
actionRequired: true,
|
||||
});
|
||||
expect(store.updateAgentState).not.toHaveBeenCalledWith("agent-001", "error");
|
||||
});
|
||||
|
||||
it("completes run as failed when promptWithFallback throws", async () => {
|
||||
const store = createStoreWithAgentForExec();
|
||||
const mockSession = createMockAgentSession();
|
||||
|
||||
Reference in New Issue
Block a user