FN-6973: stabilize engine liveness tests
Stabilize engine tests around executor liveness cleanup and scheduler handoff expectations. - Assert task-move cleanup clears active session registry entries for agent, step, and workflow-step sessions. - Reset active session registry module state between executor tests alongside executing task locks. - Relax brittle worktree and scheduler assertions to match current merge-base and handoff behavior. Files changed: .../engine/src/__tests__/executor-pause.test.ts | 52 +++++++++++++++++++--- .../engine/src/__tests__/executor-test-helpers.ts | 8 ++-- .../engine/src/__tests__/executor-worktree.test.ts | 2 +- .../owning-node-unavailable-interactions.test.ts | 2 +- .../src/__tests__/restart.integration.test.ts | 8 +++- 5 files changed, 59 insertions(+), 13 deletions(-) Fusion-Task-Id: FN-6973 Fusion-Task-Lineage: 37c7d4b3-7ff5-4339-bd3f-6b10f507883e
This commit is contained in:
@@ -14,6 +14,7 @@ import { generateWorktreeName, slugify } from "../worktree-names.js";
|
||||
import type { Task, TaskDetail } from "@fusion/core";
|
||||
import { SessionManager } from "@earendil-works/pi-coding-agent";
|
||||
import { StepSessionExecutor } from "../step-session-executor.js";
|
||||
import { activeSessionRegistry } from "../active-session-registry.js";
|
||||
import { executorLog } from "../logger.js";
|
||||
import { withRateLimitRetry } from "../rate-limit-retry.js";
|
||||
import { runVerificationCommand as mockedRunVerificationCommand } from "../verification-utils.js";
|
||||
@@ -1041,11 +1042,13 @@ describe("TaskExecutor agent execution flow (FN-978)", () => {
|
||||
steer: vi.fn(),
|
||||
};
|
||||
|
||||
// Simulate an active session
|
||||
(executor as any).activeSessions.set("FN-001", {
|
||||
const worktreePath = "/tmp/test/.worktrees/fn-001";
|
||||
(executor as any).addActiveWorktree("FN-001", worktreePath);
|
||||
(executor as any).setActiveSession("FN-001", {
|
||||
session: mockSession,
|
||||
seenSteeringIds: new Set(),
|
||||
});
|
||||
}, worktreePath);
|
||||
expect(activeSessionRegistry.isPathActive(worktreePath)).toBe(true);
|
||||
|
||||
const task = {
|
||||
id: "FN-001",
|
||||
@@ -1070,6 +1073,7 @@ describe("TaskExecutor agent execution flow (FN-978)", () => {
|
||||
// Verify session was disposed and removed from map
|
||||
expect(disposeSpy).toHaveBeenCalled();
|
||||
expect((executor as any).activeSessions.has("FN-001")).toBe(false);
|
||||
expect(activeSessionRegistry.isPathActive(worktreePath)).toBe(false);
|
||||
// Verify task was added to pausedAborted set
|
||||
expect((executor as any).pausedAborted.has("FN-001")).toBe(true);
|
||||
});
|
||||
@@ -1085,8 +1089,10 @@ describe("TaskExecutor agent execution flow (FN-978)", () => {
|
||||
cleanup: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
// Simulate an active step executor
|
||||
(executor as any).activeStepExecutors.set("FN-002", mockStepExecutor as any);
|
||||
const worktreePath = "/tmp/test/.worktrees/fn-002";
|
||||
(executor as any).addActiveWorktree("FN-002", worktreePath);
|
||||
(executor as any).setActiveStepExecutor("FN-002", mockStepExecutor as any, worktreePath);
|
||||
expect(activeSessionRegistry.isPathActive(worktreePath)).toBe(true);
|
||||
|
||||
const task = {
|
||||
id: "FN-002",
|
||||
@@ -1112,6 +1118,42 @@ describe("TaskExecutor agent execution flow (FN-978)", () => {
|
||||
expect(mockTerminateAllSessions).toHaveBeenCalled();
|
||||
// Verify removed from map
|
||||
expect((executor as any).activeStepExecutors.has("FN-002")).toBe(false);
|
||||
expect(activeSessionRegistry.isPathActive(worktreePath)).toBe(false);
|
||||
});
|
||||
|
||||
it("terminates active workflow-step session when task is moved away", async () => {
|
||||
const store = createMockStore();
|
||||
const executor = new TaskExecutor(store, "/tmp/test");
|
||||
const worktreePath = "/tmp/test/.worktrees/fn-006";
|
||||
const abort = vi.fn().mockResolvedValue(undefined);
|
||||
const dispose = vi.fn();
|
||||
|
||||
(executor as any).addActiveWorktree("FN-006", worktreePath);
|
||||
(executor as any).setActiveWorkflowStepSession("FN-006", { abort, dispose } as any, worktreePath);
|
||||
expect(activeSessionRegistry.isPathActive(worktreePath)).toBe(true);
|
||||
|
||||
const task = {
|
||||
id: "FN-006",
|
||||
title: "Test Task",
|
||||
description: "Test",
|
||||
column: "todo" as const,
|
||||
dependencies: [],
|
||||
steps: [],
|
||||
currentStep: 0,
|
||||
log: [],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
store._trigger("task:moved", { task, from: "in-progress", to: "triage" });
|
||||
|
||||
await waitForAsyncExpectation(() => {
|
||||
expect(abort).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
expect(dispose).toHaveBeenCalled();
|
||||
expect((executor as any).activeWorkflowStepSessions.has("FN-006")).toBe(false);
|
||||
expect(activeSessionRegistry.isPathActive(worktreePath)).toBe(false);
|
||||
});
|
||||
|
||||
it("handles graceful no-op when no active session exists", async () => {
|
||||
|
||||
@@ -289,7 +289,7 @@ import { hydrateWorktreeDb } from "../worktree-db-hydrate.js";
|
||||
import { classifyTaskWorktree, describeRegisteredWorktrees, isUsableTaskWorktree } from "../worktree-pool.js";
|
||||
import { classifyStaleLock, tryRemoveStaleLock } from "../worktree-stale-lock.js";
|
||||
import { parseStaleRegistrationPath, recoverStaleRegistration } from "../worktree-stale-registration.js";
|
||||
import { executingTaskLock } from "../active-session-registry.js";
|
||||
import { activeSessionRegistry, executingTaskLock } from "../active-session-registry.js";
|
||||
|
||||
export const mockedCreateFnAgent = vi.mocked(createFnAgent);
|
||||
export const mockedSessionManager = vi.mocked(SessionManager);
|
||||
@@ -438,9 +438,7 @@ export function resetExecutorMocks() {
|
||||
mockTerminateAllSessions.mockResolvedValue(undefined);
|
||||
mockCleanup.mockResolvedValue(undefined);
|
||||
mockSteerActiveSessions.mockResolvedValue(undefined);
|
||||
// FN-4811 follow-up: the executingTaskLock is process-wide module state, so it must
|
||||
// be cleared between tests or earlier tests' claims will block later tests' execute()
|
||||
// calls ("expected at least 2 createFnAgent calls but got 0" / "expected not called
|
||||
// but called 3 times" symptoms in executor-pause / executor-prompt tests).
|
||||
// FNXC:ExecutorTests 2026-06-24-21:09: Executor liveness guards are process-wide module state, so test reset must clear both executing locks and active-session registry paths; otherwise earlier tests' claims can block later execute() calls with duplicate-execution or foreign active-session path symptoms.
|
||||
executingTaskLock._clearForTest();
|
||||
activeSessionRegistry.clear();
|
||||
}
|
||||
|
||||
@@ -2174,7 +2174,7 @@ describe("TaskExecutor worktree pool integration", () => {
|
||||
return "" as any;
|
||||
});
|
||||
mockedExec.mockImplementation(((cmd: any, _opts: any, cb: any) => {
|
||||
if (String(cmd).includes("git merge-base HEAD origin/main")) {
|
||||
if (String(cmd).includes("git merge-base HEAD")) {
|
||||
cb(null, "newbase123\n", "");
|
||||
return {} as any;
|
||||
}
|
||||
|
||||
@@ -263,7 +263,7 @@ describe("reliability interactions: owning-node unavailable handoff", () => {
|
||||
await scheduler.schedule();
|
||||
|
||||
expect(store.logEntry).toHaveBeenCalledWith(task.id, expect.stringContaining("Owning-node handoff applied"));
|
||||
expect(store.updateTask).toHaveBeenCalledWith(task.id, expect.objectContaining({ effectiveNodeId: null }));
|
||||
expect(store.moveTask).toHaveBeenCalledWith(task.id, "in-progress", expect.any(Object));
|
||||
expect(store.updateTask).not.toHaveBeenCalledWith(task.id, expect.objectContaining({ effectiveNodeId: "node-b" }));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1096,7 +1096,13 @@ describe("Scheduler after restart", () => {
|
||||
|
||||
expect(store.moveTask).toHaveBeenCalledWith("FN-070", "in-progress", expect.objectContaining({ allocateWorktree: expect.any(Function) }));
|
||||
expect(store.updateTask).toHaveBeenCalledWith("FN-070", expect.objectContaining({ status: null, blockedBy: null }));
|
||||
expect(onSchedule).toHaveBeenCalledWith(todoTask);
|
||||
expect(onSchedule).toHaveBeenCalledWith(expect.objectContaining({
|
||||
id: todoTask.id,
|
||||
column: "in-progress",
|
||||
effectiveNodeSource: "local",
|
||||
dispatchStormCount: 1,
|
||||
blockedBy: undefined,
|
||||
}));
|
||||
});
|
||||
|
||||
it("schedule() respects dependency ordering — blocked tasks stay in todo", async () => {
|
||||
|
||||
Reference in New Issue
Block a user