a09ad9c429
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.
132 lines
4.4 KiB
JavaScript
132 lines
4.4 KiB
JavaScript
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":/);
|
||
});
|
||
});
|