import fs from "node:fs" ;
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" ;
import { importFreshModule } from "../../test/helpers/import-fresh.js" ;
import {
emitDiagnosticEvent,
onDiagnosticEvent,
resetDiagnosticEventsForTest,
setDiagnosticsEnabledForProcess,
} from "../infra/diagnostic-events.js" ;
import {
diagnosticSessionStates,
getDiagnosticSessionStateCountForTest,
getDiagnosticSessionState,
pruneDiagnosticSessionStates,
resetDiagnosticSessionStateForTest,
} from "./diagnostic-session-state.js" ;
import { getDiagnosticStabilitySnapshot } from "./diagnostic-stability.js" ;
import {
logSessionStateChange,
resetDiagnosticStateForTest,
resolveStuckSessionWarnMs,
startDiagnosticHeartbeat,
} from "./diagnostic.js" ;
function createEmitMemorySampleMock() {
return vi.fn(() => ({
rssBytes: 100 ,
heapTotalBytes: 80 ,
heapUsedBytes: 40 ,
externalBytes: 10 ,
arrayBuffersBytes: 5 ,
}));
}
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:demo-channel:channel:c1" ,
});
const bySessionId = getDiagnosticSessionState({ sessionId: "s1" });
expect(bySessionId).toBe(keyed);
expect(bySessionId.sessionKey).toBe("agent:main:demo-channel: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();
const mkdirSpy = vi.spyOn(fs, "mkdirSync" );
await importFreshModule<typeof import ("./logger.js" )>(
import .meta.url,
"./logger.js?scope=diagnostic-mkdir" ,
);
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("starts and stops the stability recorder with the heartbeat lifecycle" , () => {
startDiagnosticHeartbeat({
diagnostics: {
enabled: true ,
},
});
logSessionStateChange({ sessionId: "s1" , sessionKey: "main" , state: "processing" });
expect(getDiagnosticStabilitySnapshot({ limit: 10 }).events).toContainEqual(
expect.objectContaining({
type: "session.state" ,
outcome: "processing" ,
}),
);
const [event] = getDiagnosticStabilitySnapshot({ limit: 10 }).events;
expect(event).not.toHaveProperty("sessionId" );
expect(event).not.toHaveProperty("sessionKey" );
resetDiagnosticStateForTest();
emitDiagnosticEvent({ type: "webhook.received" , channel: "telegram" });
expect(getDiagnosticStabilitySnapshot({ limit: 10 }).events).toEqual([]);
});
it("does not track session state when diagnostics are disabled" , () => {
const events: string[] = [];
const unsubscribe = onDiagnosticEvent((event) => events.push(event.type));
try {
setDiagnosticsEnabledForProcess(false );
logSessionStateChange({ sessionId: "s1" , sessionKey: "main" , state: "processing" });
} finally {
unsubscribe();
}
expect(events).toEqual([]);
expect(getDiagnosticSessionStateCountForTest()).toBe(0 );
});
it("checks memory pressure every tick without recording idle samples" , () => {
const emitMemorySample = createEmitMemorySampleMock();
startDiagnosticHeartbeat(
{
diagnostics: {
enabled: true ,
},
},
{ emitMemorySample },
);
vi.advanceTimersByTime(30 _000 );
expect(emitMemorySample).toHaveBeenLastCalledWith({ emitSample: false });
logSessionStateChange({ sessionId: "s1" , sessionKey: "main" , state: "processing" });
vi.advanceTimersByTime(30 _000 );
expect(emitMemorySample).toHaveBeenLastCalledWith({ emitSample: true });
});
it("does not start the heartbeat when diagnostics are disabled by config" , () => {
const emitMemorySample = createEmitMemorySampleMock();
startDiagnosticHeartbeat(
{
diagnostics: {
enabled: false ,
},
},
{ emitMemorySample },
);
vi.advanceTimersByTime(30 _000 );
expect(emitMemorySample).not.toHaveBeenCalled();
});
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 );
});
});
Messung V0.5 in Prozent C=100 H=95 G=97
¤ Dauer der Verarbeitung: 0.10 Sekunden
(vorverarbeitet am 2026-06-08)
¤
*© Formatika GbR, Deutschland