import { describe, expect, it } from "vitest" import { fetchBalance, parseCreditHeader, type FetchLike } from "../src/balance" const API_KEY = "test-key" function headers(init: Record): Headers { return new Headers(init) } describe("parseCreditHeader", () => { it("reads the live x-credits-remaining header", () => { expect(parseCreditHeader(headers({ "x-credits-remaining": "5.309411174" }))).toEqual({ credits: 5.309411174, raw: "5.309411174", }) }) it("accepts the documented x-credit-balance fallback", () => { expect(parseCreditHeader(headers({ "x-credit-balance": "12.5" }))).toEqual({ credits: 12.5, raw: "12.5", }) }) it("prefers the live header when both are present", () => { const result = parseCreditHeader( headers({ "x-credits-remaining": "1", "x-credit-balance": "2" }), ) expect(result?.credits).toBe(1) }) it("returns null when neither header is present", () => { expect(parseCreditHeader(headers({ "content-type": "application/json" }))).toBeNull() }) it("returns null for a malformed value", () => { expect(parseCreditHeader(headers({ "x-credits-remaining": "not-a-number" }))).toBeNull() }) it("returns null for undefined headers", () => { expect(parseCreditHeader(undefined)).toBeNull() }) }) describe("fetchBalance", () => { it("returns the balance from a free 400 probe", async () => { const fetchImpl: FetchLike = async () => new Response(null, { status: 400, headers: { "x-credits-remaining": "5.309411174" }, }) const result = await fetchBalance("https://api.greenpt.ai/v1", API_KEY, undefined, fetchImpl) expect(result).toEqual({ credits: 5.309411174, raw: "5.309411174" }) }) it("uses HEAD first, without a body", async () => { let method: string | undefined const fetchImpl: FetchLike = async (_input, init) => { method = init?.method return new Response(null, { status: 400, headers: { "x-credits-remaining": "1" } }) } await fetchBalance("https://api.greenpt.ai/v1", API_KEY, undefined, fetchImpl) expect(method).toBe("HEAD") }) it("normalises a trailing slash on the base URL", async () => { let url: string | undefined const fetchImpl: FetchLike = async (input) => { url = input return new Response(null, { status: 400, headers: { "x-credits-remaining": "1" } }) } await fetchBalance("https://api.greenpt.ai/v1/", API_KEY, undefined, fetchImpl) expect(url).toBe("https://api.greenpt.ai/v1/chat/completions") }) it("reports unauthorized on 401", async () => { const fetchImpl: FetchLike = async () => new Response(null, { status: 401 }) const result = await fetchBalance("https://api.greenpt.ai/v1", API_KEY, undefined, fetchImpl) expect(result).toEqual({ kind: "unauthorized" }) }) it("falls back to GET when HEAD carries no header", async () => { const methods: string[] = [] const fetchImpl: FetchLike = async (_input, init) => { methods.push(init?.method ?? "GET") if (init?.method === "HEAD") return new Response(null, { status: 400 }) return new Response(null, { status: 400, headers: { "x-credits-remaining": "7.5" } }) } const result = await fetchBalance("https://api.greenpt.ai/v1", API_KEY, undefined, fetchImpl) expect(methods).toEqual(["HEAD", "GET"]) expect(result).toEqual({ credits: 7.5, raw: "7.5" }) }) it("reports a missing header when none is present", async () => { const fetchImpl: FetchLike = async () => new Response(null, { status: 400 }) const result = await fetchBalance("https://api.greenpt.ai/v1", API_KEY, undefined, fetchImpl) expect(result).toEqual({ kind: "missing-header", status: 400 }) }) it("reports a network error", async () => { const fetchImpl: FetchLike = async () => { throw new Error("boom") } const result = await fetchBalance("https://api.greenpt.ai/v1", API_KEY, undefined, fetchImpl) if (!("kind" in result)) throw new Error("expected an error result") expect(result.kind).toBe("network") }) })