Files
opencode-greenpt-usage/test/balance.test.ts
Stéphan Sainléger 5c11754e2d new: [opencode-greenpt-usage] add GreenPT credit balance plugin
Show the remaining GreenPT API credit balance in the OpenCode TUI
sidebar. The key is resolved from the ``apiKey`` option, the
``GREENPT_API_KEY`` environment variable, or the active OpenCode
``auth.json`` (provider ``greenpt``); the section is hidden when no key
is present, so the plugin stays portable across machines and profiles.

The balance is read from the ``x-credits-remaining`` header returned by
a free ``HEAD /v1/chat/completions`` probe (HTTP 400 before any credit
is consumed), which avoids a billable call per refresh. Tests cover the
credential resolution, the header parsing and the live API.

Distribute via git clone; there is no npm publish.
2026-09-29 22:00:57 +02:00

109 lines
4.0 KiB
TypeScript

import { describe, expect, it } from "vitest"
import { fetchBalance, parseCreditHeader, type FetchLike } from "../src/balance"
const API_KEY = "test-key"
function headers(init: Record<string, string>): 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")
})
})