test/trial.test.js
Rama main · réplica de lectura del repositorio original.
import { test } from "node:test";
import assert from "node:assert/strict";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
/**
* The free tier is a TRIAL, not an allowance.
*
* Every other cap in src/limits.js is a rolling window, and a rolling window
* RESETS — a user who waits gets unlimited decks forever, so free exposure per
* account is unbounded and cannot be budgeted. For a one-person operation
* paying a commercial API bill that is the difference between a number you can
* write down in advance and one you cannot (docs/ECONOMICS.md: ~₹5-21 per
* signup once, against ₹110-439 per user per semester and climbing).
*
* Its own file because the trial and the rolling caps cannot both be the
* binding limit in one fixture, and a test where the wrong one refuses is a
* test about nothing.
*/
const scratch = await mkdtemp(path.join(tmpdir(), "forge-trial-"));
process.env.FORGE_DB_PATH = path.join(scratch, "forge.db");
process.env.FORGE_CONFIG_DIR = scratch;
// The rolling caps are lifted so the TRIAL is always what refuses.
process.env.FORGE_AUTO_WINDOW_REQUESTS = "10000";
process.env.FORGE_AUTO_WEEKLY_REQUESTS = "10000";
process.env.FORGE_AUTO_WINDOW_SLIDES = "100000";
process.env.FORGE_AUTO_WEEKLY_SLIDES = "100000";
process.env.FORGE_AUTO_WEEKLY_TOKENS = String(10 ** 9);
process.env.FORGE_AUTO_TRIAL_TOKENS = "50000";
const {
reserveAuto, settleAuto, getUsage, checkAutoLimits, clearAutoEvents,
limitConfig, setPlan, lifetimeTokens,
} = await import("../src/limits.js");
const auth = await import("../src/auth.js");
const email = "trial@example.test";
await auth.register({ name: "T", email, password: "test-password-123" });
const userId = auth.getUserId(email);
test.after(() => rm(scratch, { recursive: true, force: true }));
const reset = () => clearAutoEvents({ userId });
/* ------------------------------------------------------------ the trial */
/**
* The free tier is a TRIAL, not an allowance. Every other cap here is a
* rolling window, and a rolling window resets — a user who waits gets
* unlimited decks forever, so free exposure per account is unbounded and
* cannot be budgeted. These tests hold the one property that makes it a
* trial: it does not come back.
*/
const trialCfg = () => limitConfig("free");
test("the free tier carries a lifetime cap and the paid tiers do not", () => {
assert.ok(Number.isFinite(trialCfg().lifetimeTokens), "free must be capped for life");
assert.equal(trialCfg().trial, true);
for (const paid of ["plus", "pro", "unlimited"]) {
assert.equal(limitConfig(paid).lifetimeTokens, Infinity, `${paid} must not carry a trial cap`);
assert.equal(limitConfig(paid).trial, false);
}
});
test("the trial does not reset when the rolling windows do", async () => {
reset();
const cap = trialCfg().lifetimeTokens;
// Spend the trial, then wind every event out of both rolling windows by
// deleting the rows the way the 30-day prune eventually does.
reserveAuto({ userId, upcomingTokens: cap });
assert.equal(checkAutoLimits({ userId, upcomingTokens: 1000 }).allowed, false);
const { getDb } = await import("../src/db.js");
getDb().prepare("DELETE FROM auto_events WHERE user_id=?").run(userId);
// Both windows now read zero — and the trial still says no. This is the
// whole point: a row-derived trial would have refilled here.
assert.equal(getUsage({ userId }).week.tokens, 0, "the rolling window should be empty");
assert.equal(lifetimeTokens(userId), cap, "the trial counter must survive the rows");
const chk = checkAutoLimits({ userId, upcomingTokens: 1000 });
assert.equal(chk.allowed, false, "an emptied window must not refill the trial");
assert.ok(chk.errors.some((e) => /free trial/i.test(e)), chk.errors.join("; "));
});
test("the refusal explains that this one does not come back", () => {
reset();
reserveAuto({ userId, upcomingTokens: trialCfg().lifetimeTokens });
const [msg] = checkAutoLimits({ userId, upcomingTokens: 1000 }).errors;
assert.match(msg, /does not reset/i, "a trial refusal must not read like 'come back later'");
assert.match(msg, /own API key|Cloud/i, "and should name the unmetered way out");
});
test("settling gives back what a cheap run did not spend", async () => {
reset();
const r = reserveAuto({ userId, upcomingTokens: 40_000 });
assert.equal(r.allowed, true);
assert.equal(lifetimeTokens(userId), 40_000);
settleAuto({ eventId: r.eventId, tokens: 12_000 });
assert.equal(lifetimeTokens(userId), 12_000, "an over-estimate must not eat the trial");
// With the estimate given back there is room for another run.
const r2 = reserveAuto({ userId, upcomingTokens: 5_000 });
assert.equal(r2.allowed, true, "the returned budget should be spendable");
settleAuto({ eventId: r2.eventId, tokens: 9_000 });
assert.equal(lifetimeTokens(userId), 21_000, "and an under-estimate must still be charged");
});
test("moving an account onto a paid tier lifts the trial", () => {
reset();
reserveAuto({ userId, upcomingTokens: trialCfg().lifetimeTokens });
assert.equal(checkAutoLimits({ userId, upcomingTokens: 1000 }).allowed, false);
setPlan(userId, "plus");
assert.equal(
checkAutoLimits({ userId, upcomingTokens: 1000 }).allowed, true,
"paying must be the way out of a spent trial",
);
setPlan(userId, "free");
});
test("clearing an account's usage clears the trial with it", async () => {
reset();
reserveAuto({ userId, upcomingTokens: trialCfg().lifetimeTokens });
assert.equal(checkAutoLimits({ userId, upcomingTokens: 1 }).allowed, false);
clearAutoEvents({ userId });
assert.equal(lifetimeTokens(userId), 0);
assert.equal(
checkAutoLimits({ userId, upcomingTokens: 1000 }).allowed, true,
"the operator's one remedy must work on the one limit that never resets",
);
});
test("a check reports how much trial is left, so it can be shown before it runs out", () => {
reset();
const chk = checkAutoLimits({ userId });
assert.equal(chk.trial.cap, trialCfg().lifetimeTokens);
assert.equal(chk.trial.spent, 0);
assert.equal(chk.remaining.lifetimeTokens, trialCfg().lifetimeTokens);
assert.equal(checkAutoLimits({ userId, plan: "plus" }).trial, null);
});
test("the trial is worth about three decks", async () => {
const { settingDefault } = await import("../src/runtime.js");
const { estimateTokens } = await import("../src/usage.js");
const decks = settingDefault("autoTrialTokens") / estimateTokens({ slides: 22, research: true });
// Enough to finish one real assignment and discover the tool is good; a free
// tier that cannot complete one task converts nobody.
assert.ok(decks >= 2 && decks <= 6, `the trial is worth ${decks.toFixed(1)} decks`);
});