feat(FN-2088): merge fusion/fn-2088
This commit is contained in:
@@ -5,6 +5,7 @@ import { randomUUID } from "node:crypto";
|
||||
import type { Task, TaskStore, CentralCore, AgentStore, Agent } from "@fusion/core";
|
||||
import { InProcessRuntime } from "./in-process-runtime.js";
|
||||
import type { ProjectRuntimeConfig } from "../project-runtime.js";
|
||||
import { runtimeLog } from "../logger.js";
|
||||
|
||||
const {
|
||||
mockSelfHealingStart,
|
||||
@@ -642,6 +643,84 @@ describe("InProcessRuntime", () => {
|
||||
}, 30000);
|
||||
});
|
||||
|
||||
describe("agent cleanup failure diagnostics", () => {
|
||||
it("logs warning when agent state update fails on task completion", async () => {
|
||||
const warnSpy = vi.spyOn(runtimeLog, "warn");
|
||||
await runtime.start();
|
||||
|
||||
const store = getAgentStore(runtime);
|
||||
const updateStateSpy = vi.spyOn(store, "updateAgentState").mockImplementation(async (_agentId, state) => {
|
||||
if (state === "terminated") {
|
||||
throw new Error("state update failed");
|
||||
}
|
||||
return {} as Agent;
|
||||
});
|
||||
|
||||
const executorOptions = mockExecutorCtor.mock.calls.at(-1)?.[0] as {
|
||||
onStart?: (task: Task, worktreePath: string) => void;
|
||||
onComplete?: (task: Task) => void;
|
||||
};
|
||||
executorOptions.onStart?.({ id: "FN-DIAG-1" } as Task, join(testDir, "worktree-FN-DIAG-1"));
|
||||
|
||||
await vi.waitFor(async () => {
|
||||
const agents = await store.listAgents({ includeEphemeral: true });
|
||||
expect(agents.some((a: Agent) => a.name === "executor-FN-DIAG-1")).toBe(true);
|
||||
});
|
||||
|
||||
updateStateSpy.mockClear();
|
||||
executorOptions.onComplete?.({ id: "FN-DIAG-1" } as Task);
|
||||
await Promise.resolve();
|
||||
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Failed to update agent"),
|
||||
);
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining("terminated (completion)"),
|
||||
);
|
||||
|
||||
warnSpy.mockRestore();
|
||||
}, 30000);
|
||||
|
||||
it("logs warning when agent deletion fails after task error", async () => {
|
||||
vi.useFakeTimers();
|
||||
const warnSpy = vi.spyOn(runtimeLog, "warn");
|
||||
|
||||
try {
|
||||
await runtime.start();
|
||||
|
||||
const store = getAgentStore(runtime);
|
||||
const deleteAgentSpy = vi.spyOn(store, "deleteAgent").mockRejectedValue(new Error("delete failed"));
|
||||
|
||||
const executorOptions = mockExecutorCtor.mock.calls.at(-1)?.[0] as {
|
||||
onStart?: (task: Task, worktreePath: string) => void;
|
||||
onError?: (task: Task, error: Error) => void;
|
||||
};
|
||||
executorOptions.onStart?.({ id: "FN-DIAG-2" } as Task, join(testDir, "worktree-FN-DIAG-2"));
|
||||
|
||||
await vi.waitFor(async () => {
|
||||
const agents = await store.listAgents({ includeEphemeral: true });
|
||||
expect(agents.some((a: Agent) => a.name === "executor-FN-DIAG-2")).toBe(true);
|
||||
});
|
||||
|
||||
deleteAgentSpy.mockClear();
|
||||
executorOptions.onError?.({ id: "FN-DIAG-2" } as Task, new Error("Task failed"));
|
||||
|
||||
await vi.advanceTimersByTimeAsync(5000);
|
||||
await Promise.resolve();
|
||||
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Failed to delete agent"),
|
||||
);
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining("after error"),
|
||||
);
|
||||
} finally {
|
||||
warnSpy.mockRestore();
|
||||
vi.useRealTimers();
|
||||
}
|
||||
}, 30000);
|
||||
});
|
||||
|
||||
describe("configuration", () => {
|
||||
it("should store projectId in config", () => {
|
||||
// Access via the constructor params - runtime is created with testDir
|
||||
|
||||
@@ -355,12 +355,18 @@ export class InProcessRuntime
|
||||
// Update agent state to terminated (completed)
|
||||
const agentId = this.taskAgentMap.get(task.id);
|
||||
if (agentId && this.agentStore) {
|
||||
void this.agentStore.updateAgentState(agentId, "terminated").catch(() => {});
|
||||
void this.agentStore.updateAgentState(agentId, "terminated").catch((err: unknown) => {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
runtimeLog.warn(`Failed to update agent ${agentId} state to terminated (completion): ${msg}`);
|
||||
});
|
||||
this.taskAgentMap.delete(task.id);
|
||||
// Auto-delete the task-worker agent after a short delay so the UI
|
||||
// can observe the terminal state before the agent is removed.
|
||||
void setTimeout(() => {
|
||||
this.agentStore?.deleteAgent(agentId).catch(() => {});
|
||||
this.agentStore?.deleteAgent(agentId).catch((err: unknown) => {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
runtimeLog.warn(`Failed to delete agent ${agentId} after completion: ${msg}`);
|
||||
});
|
||||
}, 5000);
|
||||
}
|
||||
},
|
||||
@@ -387,12 +393,18 @@ export class InProcessRuntime
|
||||
// Update agent state to terminated (failed)
|
||||
const agentId = this.taskAgentMap.get(task.id);
|
||||
if (agentId && this.agentStore) {
|
||||
void this.agentStore.updateAgentState(agentId, "terminated").catch(() => {});
|
||||
void this.agentStore.updateAgentState(agentId, "terminated").catch((err: unknown) => {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
runtimeLog.warn(`Failed to update agent ${agentId} state to terminated (error): ${msg}`);
|
||||
});
|
||||
this.taskAgentMap.delete(task.id);
|
||||
// Auto-delete the task-worker agent after a short delay so the UI
|
||||
// can observe the terminal state before the agent is removed.
|
||||
void setTimeout(() => {
|
||||
this.agentStore?.deleteAgent(agentId).catch(() => {});
|
||||
this.agentStore?.deleteAgent(agentId).catch((err: unknown) => {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
runtimeLog.warn(`Failed to delete agent ${agentId} after error: ${msg}`);
|
||||
});
|
||||
}, 5000);
|
||||
}
|
||||
},
|
||||
@@ -1021,7 +1033,9 @@ export class InProcessRuntime
|
||||
try {
|
||||
const state = await this.centralCore.getGlobalConcurrencyState();
|
||||
return state.globalMaxConcurrent;
|
||||
} catch {
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
runtimeLog.warn(`Failed to fetch global concurrency from CentralCore, falling back to default (4): ${msg}`);
|
||||
// Fallback to default if CentralCore is unavailable
|
||||
return 4;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user