Files
recap/server/test/gemini-helpers.test.js
Keysat 0ae59f3550 Add multi-tenant cloud mode: self-serve purchase, credit metering, core-decoupling
Introduces RECAP_MODE=multi alongside single-mode self-host:
- Tenant auth + accounts (magic-link via System SMTP), per-tenant credit pool,
  anonymous trial minting with per-IP/-64 caps
- Self-serve Pro/Max purchase: inline Lightning (BTCPay) + card (Zaprite),
  prepaid 30-day periods, expiry-reminder emails
- Core-decoupling: relay owns cloud tier/expiry keyed by Recaps user-id
- SQLite (better-sqlite3) schema for multi-mode; filesystem unchanged for single
- StartOS actions/versions through 0.2.155
2026-06-13 14:25:05 -05:00

133 lines
4.4 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test, describe } from "node:test";
import { strict as assert } from "node:assert";
import { PRICING, calcCost, buildAnalysisPrompt } from "../gemini-helpers.js";
describe("PRICING table", () => {
test("includes all current production model slugs", () => {
assert.ok(PRICING["gemini-3.1-pro-preview"]);
assert.ok(PRICING["gemini-2.5-pro"]);
assert.ok(PRICING["gemini-3-flash-preview"]);
assert.ok(PRICING["gemini-2.5-flash"]);
assert.ok(PRICING["gemini-3.1-flash-lite"]);
});
test("has a 'default' fallback row", () => {
assert.ok(PRICING["default"]);
assert.equal(typeof PRICING["default"].input, "number");
});
test("each row has input/output/thinking rates", () => {
for (const [model, rates] of Object.entries(PRICING)) {
assert.equal(typeof rates.input, "number", `${model}.input`);
assert.equal(typeof rates.output, "number", `${model}.output`);
assert.equal(typeof rates.thinking, "number", `${model}.thinking`);
}
});
});
describe("calcCost", () => {
test("uses model-specific rates when known", () => {
const cost = calcCost("gemini-3-flash-preview", {
promptTokenCount: 1_000_000,
candidatesTokenCount: 1_000_000,
thoughtsTokenCount: 0,
});
assert.equal(cost.inputTokens, 1_000_000);
assert.equal(cost.outputTokens, 1_000_000);
// 1M tokens × $0.50 input + 1M × $3.00 output = $3.50
assert.equal(parseFloat(cost.totalCost).toFixed(2), "3.50");
});
test("falls back to 'default' rates for unknown model", () => {
const cost = calcCost("invented-model-x", {
promptTokenCount: 1_000_000,
candidatesTokenCount: 0,
thoughtsTokenCount: 0,
});
// default input rate = $1.00 per 1M
assert.equal(parseFloat(cost.totalCost).toFixed(2), "1.00");
});
test("treats missing usage fields as zero", () => {
const cost = calcCost("gemini-3-flash-preview", {});
assert.equal(cost.inputTokens, 0);
assert.equal(cost.outputTokens, 0);
assert.equal(cost.thinkingTokens, 0);
assert.equal(parseFloat(cost.totalCost), 0);
});
test("formats sub-cent totals as ¢", () => {
const cost = calcCost("gemini-2.5-flash", {
promptTokenCount: 1000,
candidatesTokenCount: 1000,
thoughtsTokenCount: 0,
});
// tiny — display in ¢
assert.match(cost.totalCostDisplay, /¢$/);
});
test("formats >$0.01 totals as $X.XXXX", () => {
const cost = calcCost("gemini-3.1-pro-preview", {
promptTokenCount: 1_000_000,
candidatesTokenCount: 0,
thoughtsTokenCount: 0,
});
// $2.00
assert.match(cost.totalCostDisplay, /^\$\d+\.\d{4}$/);
});
test("totalTokens prefers usage.totalTokenCount when provided", () => {
const cost = calcCost("gemini-3-flash-preview", {
promptTokenCount: 100,
candidatesTokenCount: 50,
thoughtsTokenCount: 25,
totalTokenCount: 200, // server-reported total may include hidden tokens
});
assert.equal(cost.totalTokens, 200);
});
test("totalTokens falls back to sum when totalTokenCount missing", () => {
const cost = calcCost("gemini-3-flash-preview", {
promptTokenCount: 100,
candidatesTokenCount: 50,
thoughtsTokenCount: 25,
});
assert.equal(cost.totalTokens, 175);
});
});
describe("buildAnalysisPrompt", () => {
test("numbers entries with timestamp + offset", () => {
const entries = [
{ offset: 0, text: "Welcome", duration: 5 },
{ offset: 30, text: "Topic two", duration: 60 },
];
const prompt = buildAnalysisPrompt(entries);
assert.match(prompt, /\[0\] \(0:00\) Welcome/);
assert.match(prompt, /\[1\] \(0:30\) Topic two/);
});
test("substitutes the last segment index in the constraint", () => {
const entries = [{ offset: 0, text: "x", duration: 1 }];
const prompt = buildAnalysisPrompt(entries);
assert.match(prompt, /from 0 to 0 must belong/);
});
test("handles long-running content (hours)", () => {
const entries = [
{ offset: 0, text: "start", duration: 1 },
{ offset: 3661, text: "later", duration: 1 },
];
const prompt = buildAnalysisPrompt(entries);
assert.match(prompt, /\(1:01:01\) later/);
});
test("emits a JSON-output instruction", () => {
const prompt = buildAnalysisPrompt([{ offset: 0, text: "x", duration: 1 }]);
assert.match(prompt, /Respond with ONLY valid JSON/);
assert.match(prompt, /"sections":/);
assert.match(prompt, /"startIndex":/);
assert.match(prompt, /"endIndex":/);
});
});