| import fs from "node:fs"; |
| import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; |
| import { onDiagnosticEvent, resetDiagnosticEventsForTest } from "../infra/diagnostic-events.js"; |
| import { |
| diagnosticSessionStates, |
| getDiagnosticSessionStateCountForTest, |
| getDiagnosticSessionState, |
| pruneDiagnosticSessionStates, |
| resetDiagnosticSessionStateForTest, |
| } from "./diagnostic-session-state.js"; |
| import { |
| logSessionStateChange, |
| resetDiagnosticStateForTest, |
| resolveStuckSessionWarnMs, |
| startDiagnosticHeartbeat, |
| } from "./diagnostic.js"; |
|
|
| describe("diagnostic session state pruning", () => { |
| beforeEach(() => { |
| vi.useFakeTimers(); |
| resetDiagnosticSessionStateForTest(); |
| }); |
|
|
| afterEach(() => { |
| resetDiagnosticSessionStateForTest(); |
| vi.useRealTimers(); |
| }); |
|
|
| it("evicts stale idle session states", () => { |
| getDiagnosticSessionState({ sessionId: "stale-1" }); |
| expect(getDiagnosticSessionStateCountForTest()).toBe(1); |
|
|
| vi.advanceTimersByTime(31 * 60 * 1000); |
| getDiagnosticSessionState({ sessionId: "fresh-1" }); |
|
|
| expect(getDiagnosticSessionStateCountForTest()).toBe(1); |
| }); |
|
|
| it("caps tracked session states to a bounded max", () => { |
| const now = Date.now(); |
| for (let i = 0; i < 2001; i += 1) { |
| diagnosticSessionStates.set(`session-${i}`, { |
| sessionId: `session-${i}`, |
| lastActivity: now + i, |
| state: "idle", |
| queueDepth: 1, |
| }); |
| } |
| pruneDiagnosticSessionStates(now + 2002, true); |
|
|
| expect(getDiagnosticSessionStateCountForTest()).toBe(2000); |
| }); |
|
|
| it("reuses keyed session state when later looked up by sessionId", () => { |
| const keyed = getDiagnosticSessionState({ |
| sessionId: "s1", |
| sessionKey: "agent:main:discord:channel:c1", |
| }); |
| const bySessionId = getDiagnosticSessionState({ sessionId: "s1" }); |
|
|
| expect(bySessionId).toBe(keyed); |
| expect(bySessionId.sessionKey).toBe("agent:main:discord:channel:c1"); |
| expect(getDiagnosticSessionStateCountForTest()).toBe(1); |
| }); |
| }); |
|
|
| describe("logger import side effects", () => { |
| afterEach(() => { |
| vi.restoreAllMocks(); |
| vi.useRealTimers(); |
| }); |
|
|
| it("does not mkdir at import time", async () => { |
| vi.useRealTimers(); |
| vi.resetModules(); |
|
|
| const mkdirSpy = vi.spyOn(fs, "mkdirSync"); |
|
|
| await import("./logger.js"); |
|
|
| expect(mkdirSpy).not.toHaveBeenCalled(); |
| }); |
| }); |
|
|
| describe("stuck session diagnostics threshold", () => { |
| beforeEach(() => { |
| vi.useFakeTimers(); |
| resetDiagnosticStateForTest(); |
| resetDiagnosticEventsForTest(); |
| }); |
|
|
| afterEach(() => { |
| resetDiagnosticEventsForTest(); |
| resetDiagnosticStateForTest(); |
| vi.useRealTimers(); |
| }); |
|
|
| it("uses the configured diagnostics.stuckSessionWarnMs threshold", () => { |
| const events: Array<{ type: string }> = []; |
| const unsubscribe = onDiagnosticEvent((event) => { |
| events.push({ type: event.type }); |
| }); |
| try { |
| startDiagnosticHeartbeat({ |
| diagnostics: { |
| enabled: true, |
| stuckSessionWarnMs: 30_000, |
| }, |
| }); |
| logSessionStateChange({ sessionId: "s1", sessionKey: "main", state: "processing" }); |
| vi.advanceTimersByTime(61_000); |
| } finally { |
| unsubscribe(); |
| } |
|
|
| expect(events.filter((event) => event.type === "session.stuck")).toHaveLength(1); |
| }); |
|
|
| it("falls back to default threshold when config is absent", () => { |
| const events: Array<{ type: string }> = []; |
| const unsubscribe = onDiagnosticEvent((event) => { |
| events.push({ type: event.type }); |
| }); |
| try { |
| startDiagnosticHeartbeat(); |
| logSessionStateChange({ sessionId: "s2", sessionKey: "main", state: "processing" }); |
| vi.advanceTimersByTime(31_000); |
| } finally { |
| unsubscribe(); |
| } |
|
|
| expect(events.filter((event) => event.type === "session.stuck")).toHaveLength(0); |
| }); |
|
|
| it("uses default threshold for invalid values", () => { |
| expect(resolveStuckSessionWarnMs({ diagnostics: { stuckSessionWarnMs: -1 } })).toBe(120_000); |
| expect(resolveStuckSessionWarnMs({ diagnostics: { stuckSessionWarnMs: 0 } })).toBe(120_000); |
| expect(resolveStuckSessionWarnMs()).toBe(120_000); |
| }); |
| }); |
|
|