fix(FN-2351): structure startup AI session cleanup diagnostics

- Add normalizeErrorForLog helper and use it to emit consistent structured error fields
- Replace string-interpolated rehydrate and cleanup logs with structured summary payloads including source, ttl, and totals
- Emit structured warning/error diagnostics for settings fallback, initial/scheduled cleanup failures, and dev-server shutdown cleanup failures
- Add server startup tests covering structured cleanup success, failure, and settings-fallback logging behavior
This commit is contained in:
Fusion
2026-04-23 14:40:13 -07:00
committed by gsxdsm
parent dbb3d9911f
commit c8b46bfeb2
2 changed files with 227 additions and 18 deletions

View File

@@ -93,6 +93,165 @@ async function REQUEST(
return performRequest(app, method, path, body, headers);
}
async function flushStartupCleanupTasks(): Promise<void> {
await Promise.resolve();
await Promise.resolve();
await Promise.resolve();
}
describe("createServer AI session startup cleanup diagnostics", () => {
const originalNodeEnv = process.env.NODE_ENV;
beforeEach(() => {
vi.useFakeTimers();
process.env.NODE_ENV = "development";
});
afterEach(() => {
process.env.NODE_ENV = originalNodeEnv;
vi.useRealTimers();
vi.restoreAllMocks();
});
function createRuntimeLoggerMock() {
const logger = {
scope: "server",
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
child: vi.fn(),
};
logger.child.mockReturnValue(logger);
return logger;
}
function createAiSessionStoreMock(overrides: Record<string, unknown> = {}) {
return {
on: vi.fn(),
off: vi.fn(),
recoverStaleSessions: vi.fn(),
listRecoverable: vi.fn().mockReturnValue([]),
cleanupStaleSessions: vi.fn().mockReturnValue({
terminalDeleted: 0,
orphanedDeleted: 0,
totalDeleted: 0,
}),
stopScheduledCleanup: vi.fn(),
...overrides,
};
}
it("logs structured startup cleanup summary on success", async () => {
const runtimeLogger = createRuntimeLoggerMock();
const aiSessionStore = createAiSessionStoreMock({
cleanupStaleSessions: vi.fn().mockReturnValue({
terminalDeleted: 2,
orphanedDeleted: 1,
totalDeleted: 3,
}),
});
const store = createMockStore({
getSettings: vi.fn().mockResolvedValue({
aiSessionTtlMs: 700_000,
aiSessionCleanupIntervalMs: 120_000,
}),
});
createServer(store, {
runtimeLogger: runtimeLogger as any,
aiSessionStore: aiSessionStore as any,
headless: true,
});
await flushStartupCleanupTasks();
expect(aiSessionStore.cleanupStaleSessions).toHaveBeenCalledWith(700_000);
expect(runtimeLogger.info).toHaveBeenCalledWith(
"AI session cleanup summary",
expect.objectContaining({
message: "Removed stale AI sessions",
source: "initial",
ttlMs: 700_000,
terminalDeleted: 2,
orphanedDeleted: 1,
totalDeleted: 3,
}),
);
});
it("logs structured startup cleanup failure and keeps server creation non-fatal", async () => {
const runtimeLogger = createRuntimeLoggerMock();
const aiSessionStore = createAiSessionStoreMock({
cleanupStaleSessions: vi.fn().mockImplementation(() => {
throw new Error("cleanup exploded");
}),
});
const store = createMockStore({
getSettings: vi.fn().mockResolvedValue({
aiSessionTtlMs: 900_000,
aiSessionCleanupIntervalMs: 180_000,
}),
});
const app = createServer(store, {
runtimeLogger: runtimeLogger as any,
aiSessionStore: aiSessionStore as any,
headless: true,
});
await flushStartupCleanupTasks();
expect(app).toBeDefined();
expect(runtimeLogger.error).toHaveBeenCalledWith(
"AI session cleanup failed",
expect.objectContaining({
message: "Initial AI session cleanup failed",
source: "initial",
ttlMs: 900_000,
errorName: "Error",
errorMessage: "cleanup exploded",
error: "cleanup exploded",
}),
);
});
it("logs structured settings fallback warning and continues with default cleanup values", async () => {
const runtimeLogger = createRuntimeLoggerMock();
const aiSessionStore = createAiSessionStoreMock({
cleanupStaleSessions: vi.fn().mockReturnValue({
terminalDeleted: 1,
orphanedDeleted: 0,
totalDeleted: 1,
}),
});
const store = createMockStore({
getSettings: vi.fn().mockRejectedValue(new Error("settings unavailable")),
});
const app = createServer(store, {
runtimeLogger: runtimeLogger as any,
aiSessionStore: aiSessionStore as any,
headless: true,
});
await flushStartupCleanupTasks();
expect(app).toBeDefined();
expect(runtimeLogger.warn).toHaveBeenCalledWith(
"AI session cleanup settings fallback",
expect.objectContaining({
message: "Failed to load settings for AI session cleanup; using defaults",
fallbackTtlMs: 7 * 24 * 60 * 60 * 1000,
fallbackCleanupIntervalMs: 6 * 60 * 60 * 1000,
errorName: "Error",
errorMessage: "settings unavailable",
error: "settings unavailable",
}),
);
expect(aiSessionStore.cleanupStaleSessions).toHaveBeenCalledWith(7 * 24 * 60 * 60 * 1000);
});
});
describe("createServer health and headless mode", () => {
it("returns liveness payload from /api/health", async () => {
const store = createMockStore();