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":/); }); });