import test from "node:test"; import assert from "node:assert/strict"; import { MAX_FREE_MESSAGES, createTrialStore } from "../src/services/anonTrial.js"; test("new trial starts with a full budget", () => { const store = createTrialStore(); const t = store.createTrial(); assert.ok(t.id, "trial has an id"); assert.equal(t.count, 0); assert.equal(store.countRemaining(t.id), MAX_FREE_MESSAGES); assert.equal(store.isExhausted(t.id), false); }); test("trial ids are unique and unguessable", () => { const store = createTrialStore(); const ids = new Set(Array.from({ length: 50 }, () => store.createTrial().id)); assert.equal(ids.size, 50, "all 50 ids unique"); const uuidRe = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/; for (const id of ids) assert.match(id, uuidRe, "ids are crypto UUIDs"); }); test("only user messages consume the budget; assistant messages are free", () => { const store = createTrialStore(); const t = store.createTrial(); store.addMessage(t.id, "user", "q1"); store.addMessage(t.id, "assistant", "a1"); store.addMessage(t.id, "user", "q2"); assert.equal(store.countRemaining(t.id), MAX_FREE_MESSAGES - 2); }); test("budget exhausts exactly at MAX_FREE_MESSAGES and never goes negative", () => { const store = createTrialStore(); const t = store.createTrial(); for (let i = 1; i <= MAX_FREE_MESSAGES; i++) store.addMessage(t.id, "user", `q${i}`); assert.equal(store.countRemaining(t.id), 0); assert.equal(store.isExhausted(t.id), true); store.addMessage(t.id, "user", "q-over"); assert.equal(store.countRemaining(t.id), 0); assert.equal(store.isExhausted(t.id), true); }); test("unknown trial is treated as exhausted with zero remaining", () => { const store = createTrialStore(); assert.equal(store.isExhausted("does-not-exist"), true); assert.equal(store.countRemaining("does-not-exist"), 0); }); test("context window returns the last N messages only; stored history bounded", () => { const store = createTrialStore({ maxHistory: 10 }); const t = store.createTrial(); for (let i = 0; i < 30; i++) store.addMessage(t.id, "user", `q${i}`); const ctx = store.getContextMessages(t.id); assert.equal(ctx.length, 10, "context capped at maxHistory"); assert.equal(ctx[ctx.length - 1].text, "q29"); assert.ok(!ctx.some((m) => m.text === "q0"), "oldest message trimmed"); assert.ok(t.messages.length <= 20, `stored history bounded, got ${t.messages.length}`); }); test("stale trials are reaped; active trials survive", () => { const store = createTrialStore({ idleTtlMs: 1_000 }); const stale = store.createTrial(); stale.lastActive = Date.now() - 5_000; // simulate 5s idle const fresh = store.createTrial(); store.addMessage(fresh.id, "user", "recent"); store.cleanupStale(Date.now()); assert.equal(store.getTrial(stale.id), null, "idle trial evicted"); assert.ok(store.getTrial(fresh.id), "recently active trial kept"); }); test("trial store is bounded by LRU eviction at capacity", () => { const store = createTrialStore({ maxTrials: 10 }); const created = []; for (let i = 0; i < 25; i++) created.push(store.createTrial().id); assert.equal(store.stats().activeTrials, 10, "store keeps at most maxTrials"); // only the newest survive; the oldest are evicted for (const id of created.slice(0, 15)) { assert.equal(store.getTrial(id), null, `oldest trial ${id} evicted`); } for (const id of created.slice(15)) { assert.ok(store.getTrial(id), `newest trial ${id} kept`); } }); test("getTrial on a continued trial keeps LRU ordering (does not evict active)", () => { const store = createTrialStore({ maxTrials: 3 }); const a = store.createTrial(); const b = store.createTrial(); const c = store.createTrial(); // Control recency explicitly (creation timestamps can tie within a millisecond). a.lastActive = Date.now() - 200; b.lastActive = Date.now() - 100; c.lastActive = Date.now(); store.createTrial(); // evicts the least-recently-active: A assert.equal(store.getTrial(a.id), null, "A evicted (LRU)"); assert.ok(store.getTrial(b.id) && store.getTrial(c.id), "B and C kept"); }); test("cap holds under worst-case concurrent interleaving", async () => { const store = createTrialStore({ maxTrials: 1000 }); const trial = store.createTrial(); let passed = 0; await Promise.all( Array.from({ length: 100 }, async (_, i) => { if (store.isExhausted(trial.id)) return; // Force an interleave point between check and increment. If the // check-then-increment were non-atomic, more than MAX_FREE_MESSAGES // requests would slip through. await new Promise((r) => setImmediate(r)); if (store.isExhausted(trial.id)) return; store.addMessage(trial.id, "user", `race-${i}`); passed++; }) ); assert.ok(passed <= MAX_FREE_MESSAGES, `at most ${MAX_FREE_MESSAGES} pass, got ${passed}`); assert.equal(trial.count, passed, "stored count matches passed messages"); assert.equal(store.isExhausted(trial.id), true, "trial exhausted after the race"); });