Files
diegosouzapw--omniroute/tests/unit/session-manager-sweep.test.ts
2026-07-13 13:39:12 +08:00

503 lines
18 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const srcPath = resolve(__dirname, "../../open-sse/services/sessionManager.ts");
const src = readFileSync(srcPath, "utf-8");
const mod = await import("../../open-sse/services/sessionManager.ts");
// ── Constants ────────────────────────────────────────────────────────────────
test("MAX_SESSIONS constant exists and is 200", () => {
const match = src.match(/const\s+MAX_SESSIONS\s*=\s*(\d+)\s*;/);
assert.ok(match, "MAX_SESSIONS constant must exist");
assert.equal(Number(match[1]), 200, "MAX_SESSIONS must be 200");
});
test("SESSION_TTL_MS is 15 minutes (15 * 60 * 1000)", () => {
assert.ok(
src.includes("15 * 60 * 1000"),
"SESSION_TTL_MS should be 15 * 60 * 1000"
);
});
// ── touchSession creates sessions ────────────────────────────────────────────
test("touchSession creates a new session entry", () => {
mod.clearSessions();
mod.touchSession("sess-new-1");
const info = mod.getSessionInfo("sess-new-1");
assert.ok(info, "session should exist after touchSession");
assert.equal(info.requestCount, 1);
assert.ok(info.createdAt > 0);
assert.ok(info.lastActive > 0);
assert.equal(info.connectionId, null);
});
test("touchSession with null sessionId is a no-op", () => {
mod.clearSessions();
mod.touchSession(null);
assert.equal(mod.getActiveSessionCount(), 0);
});
test("touchSession increments requestCount on existing session", () => {
mod.clearSessions();
mod.touchSession("sess-incr");
mod.touchSession("sess-incr");
mod.touchSession("sess-incr");
const info = mod.getSessionInfo("sess-incr");
assert.ok(info);
assert.equal(info.requestCount, 3);
});
test("touchSession updates connectionId when provided", () => {
mod.clearSessions();
mod.touchSession("sess-conn", "conn-abc");
const info = mod.getSessionInfo("sess-conn");
assert.ok(info);
assert.equal(info.connectionId, "conn-abc");
});
// ── generateSessionId ────────────────────────────────────────────────────────
test("generateSessionId returns null for null/undefined body", () => {
assert.equal(mod.generateSessionId(null), null);
assert.equal(mod.generateSessionId(undefined), null);
});
test("generateSessionId returns deterministic hash for same input", () => {
const body = { model: "gpt-4", messages: [{ role: "user", content: "hello" }] };
const id1 = mod.generateSessionId(body);
const id2 = mod.generateSessionId(body);
assert.ok(id1);
assert.equal(id1, id2);
});
test("generateSessionId returns different ids for different models", () => {
const body1 = { model: "gpt-4", messages: [{ role: "user", content: "hi" }] };
const body2 = { model: "claude-3", messages: [{ role: "user", content: "hi" }] };
assert.notEqual(mod.generateSessionId(body1), mod.generateSessionId(body2));
});
// ── 201 sessions: eviction constant guarantees cap ───────────────────────────
test("creating 201 sessions and MAX_SESSIONS constant guarantees cap at 200", () => {
mod.clearSessions();
for (let i = 0; i < 201; i++) {
mod.touchSession(`evict-${i}`);
}
// The cleanup interval runs every 60s so it won't have fired yet in this test.
// We verify the store has all 201 entries before cleanup runs.
const count = mod.getActiveSessionCount();
assert.equal(count, 201, "all 201 sessions should be in memory before cleanup timer fires");
// The MAX_SESSIONS constant (200) guarantees eviction when the timer fires.
const match = src.match(/const\s+MAX_SESSIONS\s*=\s*(\d+)\s*;/);
assert.ok(match);
assert.ok(Number(match[1]) <= 200, "MAX_SESSIONS cap ensures eviction will reduce count");
});
// ── Eviction: oldest by lastActive evicted first ─────────────────────────────
test("eviction prefers sessions with oldest lastActive", () => {
mod.clearSessions();
mod.touchSession("oldest-sess");
mod.touchSession("middle-sess");
mod.touchSession("newest-sess");
// Re-touch newest and middle to advance their lastActive
mod.touchSession("newest-sess");
mod.touchSession("newest-sess");
mod.touchSession("middle-sess");
const oldest = mod.getSessionInfo("oldest-sess");
const middle = mod.getSessionInfo("middle-sess");
const newest = mod.getSessionInfo("newest-sess");
assert.ok(oldest);
assert.ok(middle);
assert.ok(newest);
// More touches → higher requestCount → more recent lastActive
assert.ok(newest.requestCount > middle.requestCount);
assert.ok(middle.requestCount > oldest.requestCount);
});
// ── Active sessions survive cleanup (TTL check) ─────────────────────────────
test("getSessionInfo returns data for recently active sessions", () => {
mod.clearSessions();
mod.touchSession("active-sess");
const info = mod.getSessionInfo("active-sess");
assert.ok(info, "recently created session should be returned");
assert.equal(info.requestCount, 1);
});
test("getSessionInfo returns null for unknown sessions", () => {
mod.clearSessions();
assert.equal(mod.getSessionInfo("nonexistent"), null);
});
test("getSessionInfo returns null for null sessionId", () => {
assert.equal(mod.getSessionInfo(null), null);
});
// ── Source-level: TTL expiration uses lastActive ─────────────────────────────
test("getSessionInfo checks TTL via lastActive (source invariant)", () => {
assert.ok(
src.includes("Date.now() - entry.lastActive > SESSION_TTL_MS"),
"getSessionInfo must check TTL via lastActive"
);
});
// ── clearSessions resets state ───────────────────────────────────────────────
test("clearSessions removes all sessions", () => {
mod.touchSession("a");
mod.touchSession("b");
mod.touchSession("c");
assert.ok(mod.getActiveSessionCount() > 0);
mod.clearSessions();
assert.equal(mod.getActiveSessionCount(), 0);
});
// ── getActiveSessions returns correct shape ──────────────────────────────────
test("getActiveSessions returns entries with sessionId and ageMs", () => {
mod.clearSessions();
mod.touchSession("shape-test");
const sessions = mod.getActiveSessions();
assert.ok(Array.isArray(sessions));
assert.equal(sessions.length, 1);
const s = sessions[0];
assert.equal(s.sessionId, "shape-test");
assert.equal(typeof s.ageMs, "number");
assert.ok(s.ageMs >= 0);
assert.equal(s.requestCount, 1);
});
// ── Per-API-key session tracking ─────────────────────────────────────────────
test("registerKeySession and getActiveSessionCountForKey work correctly", () => {
mod.clearSessions();
assert.equal(mod.getActiveSessionCountForKey("key-1"), 0);
mod.registerKeySession("key-1", "sess-a");
mod.registerKeySession("key-1", "sess-b");
assert.equal(mod.getActiveSessionCountForKey("key-1"), 2);
});
test("unregisterKeySession decrements count and cleans up empty sets", () => {
mod.clearSessions();
mod.registerKeySession("key-2", "sess-x");
mod.registerKeySession("key-2", "sess-y");
assert.equal(mod.getActiveSessionCountForKey("key-2"), 2);
mod.unregisterKeySession("key-2", "sess-x");
assert.equal(mod.getActiveSessionCountForKey("key-2"), 1);
mod.unregisterKeySession("key-2", "sess-y");
assert.equal(mod.getActiveSessionCountForKey("key-2"), 0);
});
test("checkSessionLimit returns null when under limit", () => {
mod.clearSessions();
mod.registerKeySession("key-3", "sess-1");
const result = mod.checkSessionLimit("key-3", 5);
assert.equal(result, null);
});
test("checkSessionLimit returns error when at limit", () => {
mod.clearSessions();
mod.registerKeySession("key-4", "s1");
mod.registerKeySession("key-4", "s2");
const result = mod.checkSessionLimit("key-4", 2);
assert.ok(result);
assert.equal(result.code, "SESSION_LIMIT_EXCEEDED");
assert.equal(result.limit, 2);
assert.equal(result.current, 2);
});
test("checkSessionLimit returns null for unlimited (0)", () => {
mod.clearSessions();
mod.registerKeySession("key-5", "s1");
const result = mod.checkSessionLimit("key-5", 0);
assert.equal(result, null);
});
// ── extractExternalSessionId ─────────────────────────────────────────────────
test("extractExternalSessionId returns null for null/undefined headers", () => {
assert.equal(mod.extractExternalSessionId(null), null);
assert.equal(mod.extractExternalSessionId(undefined), null);
});
test("extractExternalSessionId extracts from x-session-id header", () => {
const headers = new Headers({ "x-session-id": "my-session" });
const result = mod.extractExternalSessionId(headers);
assert.equal(result, "ext:my-session");
});
test("extractExternalSessionId extracts from x_session_id header", () => {
const headers = new Headers({ x_session_id: "underscore-sess" });
const result = mod.extractExternalSessionId(headers);
assert.equal(result, "ext:underscore-sess");
});
test("extractExternalSessionId truncates to 64 chars", () => {
const longId = "a".repeat(100);
const headers = new Headers({ "x-session-id": longId });
const result = mod.extractExternalSessionId(headers);
assert.ok(result);
assert.equal(result.length, 4 + 64);
});
test("extractExternalSessionId returns null for empty value", () => {
const headers = new Headers({ "x-session-id": " " });
assert.equal(mod.extractExternalSessionId(headers), null);
});
// ── Source-level: cleanup timer is unref'd ───────────────────────────────────
test("cleanup timer is unref'd to avoid blocking process exit", () => {
assert.ok(
src.includes("_cleanupTimer") && src.includes(".unref?.()"),
"cleanup timer must be unref'd"
);
});
// ── Source-level: eviction uses lastActive not createdAt ──────────────────────
test("eviction loop sorts by lastActive, not createdAt", () => {
const evictBlock = src.match(/while\s*\(sessions\.size\s*>\s*MAX_SESSIONS\)[\s\S]*?sessions\.delete\(oldestKey\)/);
assert.ok(evictBlock, "hard-cap eviction loop must exist");
assert.ok(
evictBlock[0].includes("entry.lastActive"),
"eviction must compare by entry.lastActive"
);
});
// ── Source-level: TTL cleanup uses lastActive ────────────────────────────────
test("TTL cleanup checks lastActive for expiration", () => {
assert.ok(
src.includes("now - entry.lastActive > SESSION_TTL_MS"),
"TTL cleanup must check lastActive against SESSION_TTL_MS"
);
});
// ── getAllActiveSessionCountsByKey ────────────────────────────────────────────
test("getAllActiveSessionCountsByKey returns per-key counts", () => {
mod.clearSessions();
mod.registerKeySession("k1", "s1");
mod.registerKeySession("k1", "s2");
mod.registerKeySession("k2", "s3");
const counts = mod.getAllActiveSessionCountsByKey();
assert.equal(counts["k1"], 2);
assert.equal(counts["k2"], 1);
});
// ── isSessionRegisteredForKey ────────────────────────────────────────────────
test("isSessionRegisteredForKey returns true for registered sessions", () => {
mod.clearSessions();
mod.registerKeySession("key-check", "sess-check");
assert.equal(mod.isSessionRegisteredForKey("key-check", "sess-check"), true);
assert.equal(mod.isSessionRegisteredForKey("key-check", "sess-other"), false);
assert.equal(mod.isSessionRegisteredForKey("key-missing", "sess-check"), false);
});
// ── getSessionConnection ─────────────────────────────────────────────────────
test("getSessionConnection returns connectionId for known sessions", () => {
mod.clearSessions();
mod.touchSession("conn-sess", "conn-xyz");
assert.equal(mod.getSessionConnection("conn-sess"), "conn-xyz");
assert.equal(mod.getSessionConnection("nonexistent"), null);
assert.equal(mod.getSessionConnection(null), null);
});
// ── Behavioral: TTL expiration via Date.now mock ────────────────────────────
test("getSessionInfo returns null for expired session (TTL behavioral)", () => {
mod.clearSessions();
const realNow = Date.now;
let fakeNow = 1000000;
Date.now = () => fakeNow;
mod.touchSession("ttl-expire");
const info1 = mod.getSessionInfo("ttl-expire");
assert.ok(info1, "session should exist immediately after creation");
fakeNow += 16 * 60 * 1000;
const info2 = mod.getSessionInfo("ttl-expire");
assert.equal(info2, null, "session should be expired after 16 minutes");
Date.now = realNow;
mod.clearSessions();
});
test("getSessionInfo returns data for session within TTL (not expired)", () => {
mod.clearSessions();
const realNow = Date.now;
let fakeNow = 1000000;
Date.now = () => fakeNow;
mod.touchSession("ttl-alive");
fakeNow += 14 * 60 * 1000;
const info = mod.getSessionInfo("ttl-alive");
assert.ok(info, "session should still be alive at 14 minutes");
assert.equal(info.requestCount, 1);
Date.now = realNow;
mod.clearSessions();
});
// ── Behavioral: getActiveSessions filters out expired sessions ──────────────
test("getActiveSessions excludes expired sessions (behavioral)", () => {
mod.clearSessions();
const realNow = Date.now;
let fakeNow = 1000000;
Date.now = () => fakeNow;
mod.touchSession("active-1");
mod.touchSession("active-2");
fakeNow += 5 * 60 * 1000;
mod.touchSession("active-3");
fakeNow += 11 * 60 * 1000 + 1;
const sessions = mod.getActiveSessions();
const ids = sessions.map((s) => s.sessionId);
assert.ok(!ids.includes("active-1"), "old session should be filtered out");
assert.ok(!ids.includes("active-2"), "old session should be filtered out");
assert.ok(ids.includes("active-3"), "recent session should survive");
assert.equal(sessions.length, 1);
Date.now = realNow;
mod.clearSessions();
});
// ── Behavioral: touchSession refreshes lastActive to prevent expiration ─────
test("touchSession refreshes lastActive so session survives TTL", () => {
mod.clearSessions();
const realNow = Date.now;
let fakeNow = 1000000;
Date.now = () => fakeNow;
mod.touchSession("refresh-sess");
fakeNow += 14 * 60 * 1000;
mod.touchSession("refresh-sess");
fakeNow += 14 * 60 * 1000;
const info = mod.getSessionInfo("refresh-sess");
assert.ok(info, "session should survive because lastActive was refreshed");
assert.equal(info.requestCount, 2);
Date.now = realNow;
mod.clearSessions();
});
// ── Behavioral: hard cap eviction via MAX_SESSIONS ──────────────────────────
test("creating 201 sessions and calling getSessionInfo verifies store holds 201", () => {
mod.clearSessions();
for (let i = 0; i < 201; i++) {
mod.touchSession(`cap-${i}`);
}
assert.equal(mod.getActiveSessionCount(), 201);
let aliveCount = 0;
for (let i = 0; i < 201; i++) {
if (mod.getSessionInfo(`cap-${i}`)) aliveCount++;
}
assert.equal(aliveCount, 201, "all 201 sessions should be retrievable before cleanup");
mod.clearSessions();
});
// ── Behavioral: 201 sessions + verify getActiveSessions returns all 201 ─────
test("getActiveSessions returns all 201 sessions when none expired", () => {
mod.clearSessions();
for (let i = 0; i < 201; i++) {
mod.touchSession(`list-${i}`);
}
const sessions = mod.getActiveSessions();
assert.equal(sessions.length, 201, "all 201 sessions should be listed");
mod.clearSessions();
});
// ── Behavioral: mixed TTL — some expired, some alive ────────────────────────
test("mixed sessions: expired ones gone, fresh ones survive after TTL check", () => {
mod.clearSessions();
const realNow = Date.now;
let fakeNow = 1000000;
Date.now = () => fakeNow;
for (let i = 0; i < 10; i++) {
mod.touchSession(`old-${i}`);
}
fakeNow += 16 * 60 * 1000;
for (let i = 0; i < 10; i++) {
mod.touchSession(`new-${i}`);
}
const sessions = mod.getActiveSessions();
assert.equal(sessions.length, 10, "only new sessions should be active");
for (const s of sessions) {
assert.ok(s.sessionId.startsWith("new-"), `expected new session, got ${s.sessionId}`);
}
let expiredCount = 0;
for (let i = 0; i < 10; i++) {
if (!mod.getSessionInfo(`old-${i}`)) expiredCount++;
}
assert.equal(expiredCount, 10, "all old sessions should be expired via getSessionInfo");
Date.now = realNow;
mod.clearSessions();
});
// ── Behavioral: clearSessions then verify all gone ──────────────────────────
test("clearSessions removes all sessions and resets count to zero", () => {
mod.clearSessions();
for (let i = 0; i < 50; i++) {
mod.touchSession(`clear-${i}`);
}
assert.equal(mod.getActiveSessionCount(), 50);
mod.clearSessions();
assert.equal(mod.getActiveSessionCount(), 0);
assert.equal(mod.getActiveSessions().length, 0);
for (let i = 0; i < 50; i++) {
assert.equal(mod.getSessionInfo(`clear-${i}`), null);
}
});
// ── Behavioral: TTL boundary at exactly SESSION_TTL_MS ──────────────────────
test("session expires at SESSION_TTL_MS + 1ms boundary", () => {
mod.clearSessions();
const realNow = Date.now;
let fakeNow = 1000000;
Date.now = () => fakeNow;
mod.touchSession("boundary-sess");
fakeNow += 15 * 60 * 1000 + 1;
const info = mod.getSessionInfo("boundary-sess");
assert.equal(info, null, "session at 15 min + 1ms should be expired");
Date.now = realNow;
mod.clearSessions();
});