121 lines
5.0 KiB
JavaScript
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");
|
|
}); |