import { readFileSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
import {
antigravityUsageFeed,
createUsageState,
type FetchLike,
httpRequest,
hyperUsageFeed,
runUsageFeed,
type UsageCredential,
} from "../src/index.js";
import { parseAntigravityUsage } from "../src/utils/usage/providers/antigravity.js";
import { parseHyperBilling } from "../src/utils/usage/providers/hyper.js";
const fixturesDir = resolve(
dirname(fileURLToPath(import.meta.url)),
"..",
"..",
"..",
"native",
"usage",
"tests",
"fixtures",
);
const fixture = (name: string): unknown => JSON.parse(readFileSync(join(fixturesDir, name), "utf8"));
const antigravityOauthCredential: UsageCredential = {
kind: "oauth",
access: "ya29.test-access-token",
accountId: "aicode-consumers",
email: "dev@example.com",
extra: {
projectId: "aicode-consumers",
},
};
const hyperOauthCredential: UsageCredential = {
kind: "oauth",
access: "hyper-oauth-access-token",
email: "user@example.com",
};
const hyperApiKeyCredential: UsageCredential = {
kind: "apiKey",
apiKey: "sk-hyper-test1234567890abcdef",
accountId: "team_20439d8c54af",
};
function scriptedFetch(responses: { status: number; body: string }[]): FetchLike {
const queue = [...responses];
return async () => {
const next = queue.shift();
if (!next) throw new Error("scripted fetch ran out of responses");
return new Response(next.body, { status: next.status });
};
}
describe("Google Antigravity usage feed", () => {
it("requires an OAuth credential and rejects apiKey or session", () => {
const stepApiKey = antigravityUsageFeed.advance(createUsageState({ kind: "apiKey", apiKey: "secret" }, 0), []);
if (!("report" in stepApiKey)) throw new Error("expected a report");
expect(stepApiKey.report.error).toMatch(/needs an OAuth account/);
const stepSession = antigravityUsageFeed.advance(createUsageState({ kind: "session", session: {} }, 0), []);
if (!("report" in stepSession)) throw new Error("expected a report");
expect(stepSession.report.error).toMatch(/needs an OAuth account/);
});
it("sends a POST request with required antigravity User-Agent and empty body", () => {
const state = createUsageState(antigravityOauthCredential, 1_789_514_000_000);
const step = antigravityUsageFeed.advance(state, []);
if (!("requests" in step)) throw new Error("expected requests");
expect(step.requests).toHaveLength(1);
const req = httpRequest(step.requests[0]!);
expect(req.method).toBe("POST");
expect(req.url).toBe("https://daily-cloudcode-pa.googleapis.com/v1internal:retrieveUserQuota");
expect(req.headers?.Authorization).toBe("Bearer ya29.test-access-token");
expect(req.headers?.["Content-Type"]).toBe("application/json");
expect(req.headers?.["User-Agent"]).toMatch(/^antigravity\//);
expect(req.body).toBe("{}");
});
it("pools 33 buckets into Gemini, Claude, and GPT-OSS families while dropping chat/tab models", () => {
const raw = fixture("antigravity-quota.json");
const { limits } = parseAntigravityUsage(raw, 1_789_514_000_000);
// Expect 3 families in order
expect(limits.map((l) => l.label)).toEqual(["Gemini", "Claude", "GPT-OSS"]);
// Gemini: 0.16592255 remaining -> 83.4% used, 17% free
const gemini = limits[0]!;
expect(gemini.usedPercent).toBe(83.4);
expect(gemini.detail).toBe("17% free");
expect(gemini.resetsAt).toBe(Date.parse("2026-09-15T23:46:00Z"));
expect(gemini.status).toBe("ok");
// Claude: 1 remaining -> 0.0% used, 100% free
const claude = limits[1]!;
expect(claude.usedPercent).toBe(0);
expect(claude.detail).toBe("100% free");
expect(claude.resetsAt).toBe(Date.parse("2026-09-16T04:40:57Z"));
expect(claude.status).toBe("ok");
// GPT-OSS: 1 remaining -> 0.0% used, 100% free
const gpt = limits[2]!;
expect(gpt.usedPercent).toBe(0);
expect(gpt.detail).toBe("100% free");
expect(gpt.resetsAt).toBe(Date.parse("2026-09-16T04:40:57Z"));
expect(gpt.status).toBe("ok");
});
it("reports account, planType, notes and limits from recorded response", () => {
const state = createUsageState(antigravityOauthCredential, 1_789_514_000_000);
const step = antigravityUsageFeed.advance(state, [
{ status: 200, body: JSON.stringify(fixture("antigravity-quota.json")) },
]);
if (!("report" in step)) throw new Error("expected a report");
expect(step.report.planType).toBe("Cloud Code");
expect(step.report.accountId).toBe("aicode-consumers");
expect(step.report.email).toBe("dev@example.com");
expect(step.report.notes).toEqual(["Project: aicode-consumers"]);
expect(step.report.error).toBeNull();
expect(step.report.limits).toHaveLength(3);
});
it("reports unauthenticated error as data without throwing", async () => {
const unauthBody = JSON.stringify({
error: {
code: 401,
message: "Request had invalid authentication credentials.",
status: "UNAUTHENTICATED",
},
});
const report = await runUsageFeed(antigravityUsageFeed, antigravityOauthCredential, {
fetchImpl: scriptedFetch([{ status: 401, body: unauthBody }]),
});
expect(report.error).toMatch(/HTTP 401: Request had invalid authentication credentials/);
expect(report.limits).toEqual([]);
});
it("runs full host loop with runUsageFeed", async () => {
const report = await runUsageFeed(antigravityUsageFeed, antigravityOauthCredential, {
nowMs: 1_789_514_000_000,
fetchImpl: scriptedFetch([{ status: 200, body: JSON.stringify(fixture("antigravity-quota.json")) }]),
});
expect(report.provider).toBe("google-antigravity");
expect(report.providerName).toBe("Google Antigravity");
expect(report.limits).toHaveLength(3);
expect(report.error).toBeNull();
});
});
describe("Charm Hyper usage feed", () => {
it("accepts both oauth and apiKey credentials, rejecting session", () => {
const stepOAuth = hyperUsageFeed.advance(createUsageState(hyperOauthCredential, 0), []);
if (!("requests" in stepOAuth)) throw new Error("expected requests");
expect(httpRequest(stepOAuth.requests[0]!).headers?.Authorization).toBe("Bearer hyper-oauth-access-token");
const stepApiKey = hyperUsageFeed.advance(createUsageState(hyperApiKeyCredential, 0), []);
if (!("requests" in stepApiKey)) throw new Error("expected requests");
expect(httpRequest(stepApiKey.requests[0]!).headers?.Authorization).toBe("Bearer sk-hyper-test1234567890abcdef");
const stepSession = hyperUsageFeed.advance(createUsageState({ kind: "session", session: {} }, 0), []);
if (!("report" in stepSession)) throw new Error("expected a report");
expect(stepSession.report.error).toMatch(/needs an OAuth or apiKey credential/);
});
it("executes the two-step flow: teams discovery then team billing + members", () => {
const state = createUsageState(hyperOauthCredential, 1_789_514_000_000);
// Round 0 -> asks for /v1/teams
const step0 = hyperUsageFeed.advance(state, []);
if (!("requests" in step0)) throw new Error("expected requests");
expect(step0.requests).toHaveLength(1);
expect(httpRequest(step0.requests[0]!).url).toBe("https://hyper.charm.land/v1/teams");
// Round 1 -> receives teams, asks for billing and members
const teamsResponse = { status: 200, body: JSON.stringify(fixture("hyper-teams.json")) };
const step1 = hyperUsageFeed.advance(state, [teamsResponse]);
if (!("requests" in step1)) throw new Error("expected requests");
expect(step1.requests).toHaveLength(2);
expect(httpRequest(step1.requests[0]!).url).toBe("https://hyper.charm.land/v1/teams/team_20439d8c54af/billing");
expect(httpRequest(step1.requests[1]!).url).toBe("https://hyper.charm.land/v1/teams/team_20439d8c54af/members");
// Round 2 -> receives billing and members, reports
const billingResponse = { status: 200, body: JSON.stringify(fixture("hyper-billing.json")) };
const membersResponse = { status: 200, body: JSON.stringify(fixture("hyper-members.json")) };
const step2 = hyperUsageFeed.advance(state, [billingResponse, membersResponse]);
if (!("report" in step2)) throw new Error("expected a report");
expect(step2.report.planType).toBe("shred");
expect(step2.report.accountId).toBe("team_20439d8c54af");
expect(step2.report.email).toBe("user@example.com");
expect(step2.report.balance).toBe("11.83");
expect(step2.report.limits).toHaveLength(1);
const limit = step2.report.limits[0]!;
expect(limit.label).toBe("Primary");
expect(limit.usedPercent).toBe(5.2);
expect(limit.detail).toBe("237 of 250 credits");
expect(limit.windowLabel).toBe("24h window");
expect(limit.resetsAt).toBe(Date.parse("2026-09-16T22:26:09.702198Z"));
expect(limit.status).toBe("ok");
});
it("converts unit fractions, timestamps, and balance correctly", () => {
const raw = fixture("hyper-billing.json");
const { planType, limits, balance } = parseHyperBilling(raw, 1_789_514_000_000);
expect(planType).toBe("shred");
expect(balance).toBe("11.83");
expect(limits).toHaveLength(1);
expect(limits[0]).toMatchObject({
label: "Primary",
usedPercent: 5.2,
windowLabel: "24h window",
status: "ok",
detail: "237 of 250 credits",
});
expect(limits[0]!.resetsAt).toBe(1789597569702);
});
it("handles direct 1-step billing response for testing or single-step hosts", () => {
const state = createUsageState(hyperApiKeyCredential, 1_789_514_000_000);
const billingResponse = { status: 200, body: JSON.stringify(fixture("hyper-billing.json")) };
const step = hyperUsageFeed.advance(state, [billingResponse]);
if (!("report" in step)) throw new Error("expected a report");
expect(step.report.accountId).toBe("team_20439d8c54af");
expect(step.report.planType).toBe("shred");
expect(step.report.balance).toBe("11.83");
expect(step.report.limits[0]!.usedPercent).toBe(5.2);
});
it("reports authentication failure on teams endpoint as data without throwing", async () => {
const report = await runUsageFeed(hyperUsageFeed, hyperOauthCredential, {
fetchImpl: scriptedFetch([{ status: 401, body: JSON.stringify({ error: "authentication failed" }) }]),
});
expect(report.error).toMatch(/HTTP 401: authentication failed/);
expect(report.limits).toEqual([]);
});
it("runs full two-round host loop with runUsageFeed", async () => {
const report = await runUsageFeed(hyperUsageFeed, hyperOauthCredential, {
nowMs: 1_789_514_000_000,
fetchImpl: scriptedFetch([
{ status: 200, body: JSON.stringify(fixture("hyper-teams.json")) },
{ status: 200, body: JSON.stringify(fixture("hyper-billing.json")) },
{ status: 200, body: JSON.stringify(fixture("hyper-members.json")) },
]),
});
expect(report.provider).toBe("hyper");
expect(report.providerName).toBe("Charm Hyper");
expect(report.accountId).toBe("team_20439d8c54af");
expect(report.email).toBe("user@example.com");
expect(report.planType).toBe("shred");
expect(report.balance).toBe("11.83");
expect(report.limits).toHaveLength(1);
expect(report.limits[0]!.usedPercent).toBe(5.2);
expect(report.error).toBeNull();
});
});