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

This commit is contained in:
gsxdsm
2026-04-25 12:21:33 -07:00
parent 800d0541a7
commit e15d01ad13
2 changed files with 77 additions and 7 deletions

View File

@@ -1042,7 +1042,52 @@ describe("InProcessRuntime", () => {
}
}, 30000);
it("warns on cleanup failure but does not throw", async () => {
it("does not warn when cleanup delete fails only because agent is already gone", async () => {
vi.useFakeTimers();
const warnSpy = vi.spyOn(runtimeLog, "warn");
try {
await runtime.start();
const store = getAgentStore(runtime);
// Create an ephemeral agent
const agent = await store.createAgent({
name: "executor-FN-BENIGN-1",
role: "executor",
metadata: {
agentKind: "task-worker",
},
runtimeConfig: { enabled: false },
});
const deleteAgentSpy = vi
.spyOn(store, "deleteAgent")
.mockRejectedValueOnce(new Error(`Agent ${agent.id} not found`));
// Emit termination event
store.emit("agent:stateChanged", agent.id, "running", "terminated");
// Wait for async handler, then fire delayed cleanup
await vi.advanceTimersByTimeAsync(0);
await vi.advanceTimersByTimeAsync(5000);
// Cleanup should still be attempted
expect(deleteAgentSpy).toHaveBeenCalledTimes(1);
expect(deleteAgentSpy).toHaveBeenCalledWith(agent.id);
// Benign not-found races should not produce warning-level noise
const emittedCleanupWarning = warnSpy.mock.calls.some(([msg]) =>
typeof msg === "string" && msg.includes("Failed to delete ephemeral agent"),
);
expect(emittedCleanupWarning).toBe(false);
} finally {
warnSpy.mockRestore();
vi.useRealTimers();
}
}, 30000);
it("warns on genuine cleanup failure but does not throw", async () => {
vi.useFakeTimers();
const warnSpy = vi.spyOn(runtimeLog, "warn");
@@ -1071,14 +1116,17 @@ describe("InProcessRuntime", () => {
// Advance timers to trigger deletion
await vi.advanceTimersByTimeAsync(5000);
// Should have logged a warning with concatenated message
expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining("Failed to delete ephemeral agent"),
);
// The warning message is a single concatenated string: "Failed to delete ephemeral agent {agentId} after termination: {error}"
// Should have attempted deletion
expect(deleteAgentSpy).toHaveBeenCalledTimes(1);
expect(deleteAgentSpy).toHaveBeenCalledWith(agent.id);
// Genuine failures still log warning-level context
const cleanupWarnings = warnSpy.mock.calls.filter(([msg]) =>
typeof msg === "string" && msg.includes("Failed to delete ephemeral agent"),
);
expect(cleanupWarnings).toHaveLength(1);
expect(cleanupWarnings[0]?.[0]).toContain(agent.id);
expect(cleanupWarnings[0]?.[0]).toContain("delete failed");
} finally {
warnSpy.mockRestore();
vi.useRealTimers();

View File

@@ -553,6 +553,9 @@ export class InProcessRuntime
try {
await this.agentStore?.deleteAgent(agentId);
} catch (err: unknown) {
if (this.isBenignEphemeralDeleteRaceError(agentId, err)) {
return;
}
const msg = err instanceof Error ? err.message : String(err);
runtimeLog.warn(`Failed to delete ephemeral agent ${agentId} after termination: ${msg}`);
}
@@ -1087,6 +1090,25 @@ export class InProcessRuntime
runtimeLog.log("Event forwarding setup complete");
}
/**
* Returns true when an ephemeral delete failure is expected due to cleanup races
* (for example the agent was already removed by a parallel cleanup path).
*/
private isBenignEphemeralDeleteRaceError(agentId: string, err: unknown): boolean {
const msg = err instanceof Error ? err.message : String(err);
const normalized = msg.toLowerCase();
if (normalized.includes("already deleted") || normalized.includes("already removed")) {
return true;
}
if (normalized.includes(`agent ${agentId.toLowerCase()} not found`)) {
return true;
}
return /^agent\s+.+\s+not found$/i.test(msg.trim());
}
/**
* Update status and emit health-changed event.
*/