fix(FN-2200): log stale-session eviction failures

- Replace the silent stale-session eviction catch with a terminal error log in setupTerminalWebSocket
- Add regression tests that assert evictStaleSessions runs on each 60s interval tick
- Verify eviction errors are logged without stopping future ticks and interval cleanup runs on server close
This commit is contained in:
Fusion
2026-04-20 17:46:17 -07:00
committed by gsxdsm
parent dfcb0868a2
commit e6b2685e24
3 changed files with 74 additions and 4 deletions

View File

@@ -310,6 +310,8 @@ export class DevServerProcessManager extends EventEmitter {
}
private async handleClose(code: number): Promise<void> {
this.clearTimers();
const updated = await this.store.updateState({
status: "stopped",
exitCode: code,
@@ -318,7 +320,6 @@ export class DevServerProcessManager extends EventEmitter {
});
this.childProcess = null;
this.clearTimers();
this.resolveClosePromise?.(updated);
this.resolveClosePromise = null;
this.closePromise = null;
@@ -326,6 +327,8 @@ export class DevServerProcessManager extends EventEmitter {
}
private async handleFailure(error: Error): Promise<void> {
this.clearTimers();
const updated = await this.store.updateState({
status: "failed",
stoppedAt: new Date().toISOString(),
@@ -333,7 +336,6 @@ export class DevServerProcessManager extends EventEmitter {
});
this.childProcess = null;
this.clearTimers();
this.resolveClosePromise?.(updated);
this.resolveClosePromise = null;
this.closePromise = null;

View File

@@ -503,6 +503,74 @@ describe("Terminal WebSocket heartbeat", () => {
});
});
describe("Terminal stale-session eviction", () => {
let app: ReturnType<typeof express>;
let server: http.Server;
let store: TaskStore;
let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
beforeEach(() => {
app = express();
server = http.createServer(app);
store = createMockStore();
vi.useFakeTimers();
mockTerminalService.evictStaleSessions.mockReset().mockReturnValue(0);
consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
vi.spyOn(console, "log").mockImplementation(() => {});
});
afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
server.close();
});
it("calls evictStaleSessions on each 60s interval tick", () => {
setupTerminalWebSocket(app, server, store);
vi.advanceTimersByTime(60_000);
expect(mockTerminalService.evictStaleSessions).toHaveBeenCalledTimes(1);
vi.advanceTimersByTime(60_000);
expect(mockTerminalService.evictStaleSessions).toHaveBeenCalledTimes(2);
expect(consoleErrorSpy).not.toHaveBeenCalledWith(
expect.stringContaining("Stale session eviction failed"),
expect.anything(),
);
});
it("logs error and continues when evictStaleSessions throws", () => {
mockTerminalService.evictStaleSessions.mockImplementation(() => {
throw new Error("simulated eviction failure");
});
setupTerminalWebSocket(app, server, store);
vi.advanceTimersByTime(60_000);
expect(consoleErrorSpy).toHaveBeenCalled();
const failureCall = consoleErrorSpy.mock.calls.find(
(call) => typeof call[0] === "string" && call[0].includes("[terminal] Stale session eviction failed:"),
);
expect(failureCall).toBeDefined();
expect(failureCall?.[0]).toEqual(expect.stringContaining("[terminal] Stale session eviction failed:"));
expect(failureCall?.[1]).toBeInstanceOf(Error);
expect((failureCall?.[1] as Error).message).toContain("simulated eviction failure");
vi.advanceTimersByTime(60_000);
expect(mockTerminalService.evictStaleSessions).toHaveBeenCalledTimes(2);
});
it("stops eviction interval when server closes", () => {
setupTerminalWebSocket(app, server, store);
server.emit("close");
vi.advanceTimersByTime(120_000);
expect(mockTerminalService.evictStaleSessions).not.toHaveBeenCalled();
});
});
/**
* Scoped Scheduling Resolver Regression Tests
* ===========================================

View File

@@ -1051,8 +1051,8 @@ export function setupTerminalWebSocket(
const staleEvictionInterval = setInterval(() => {
try {
defaultTerminalService.evictStaleSessions();
} catch {
// Ignore errors during periodic eviction
} catch (err) {
console.error("[terminal] Stale session eviction failed:", err);
}
}, 60_000);