Add unit tests for util / gemini-helpers / license / history modules

69 tests across 16 suites, ~120 ms total. Uses node:test (built into
Node 20+) — no new dependency, no Docker rebuild churn. Run with:

  cd server && npm test

Coverage:
  • util.js               extractVideoId, formatTime,
                          parseTimestampedTranscript, safeText,
                          retryGemini (incl. 503 retry, network-error
                          retry, non-retryable passthrough), sendEvent
  • gemini-helpers.js     PRICING table integrity, calcCost (model-
                          specific rates, default fallback, missing
                          fields, sub-cent ¢ formatting, totalTokens
                          precedence), buildAnalysisPrompt
  • license.js            checkLicense (no key, malformed, fallback to
                          startos-config.json, license.txt priority),
                          activate (bad-format throw, file write),
                          deactivate (file removal, idempotent),
                          publicView (no raw key leak, sorted
                          entitlements, ISO dates), has()
  • history.js            initHistory + getHistoryDir, saveToHistory
                          (id shape, defaults, podcast guid encoding),
                          loadMeta + saveMeta round-trip, corrupt-file
                          tolerance

Tests that need module-private file paths (license, history) use a
mkdtemp'd tmp dir as DATA_DIR + dynamic import() so each suite starts
clean. No test mocks the filesystem — they read/write real files
inside the tmp dir, matching production behavior.

Deliberately not yet covered (need an Express app harness or external
binaries): license-middleware (gate behavior), config (live-reload
poll), audio (ffmpeg/ffprobe), ytdlp (yt-dlp + git), cookies (state
mutation routes), the /api/process pipeline. Worth a follow-up after
the current refactor settles.
This commit is contained in:
Keysat
2026-05-09 10:36:12 -05:00
parent fe07580a12
commit a09ad9c429
5 changed files with 673 additions and 1 deletions
+131
View File
@@ -0,0 +1,131 @@
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-flash-preview"]);
assert.ok(PRICING["gemini-3-pro-preview"]);
assert.ok(PRICING["gemini-3.1-pro-preview"]);
assert.ok(PRICING["gemini-2.5-flash"]);
});
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-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":/);
});
});