Files
2026-09-15 04:08:55 -04:00

121 lines
5.0 KiB
JavaScript

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");
});