Merge public-gh/master into paperclip-company-import-export

This commit is contained in:
Dotta
2026-03-16 17:02:39 -05:00
125 changed files with 38085 additions and 683 deletions

View File

@@ -0,0 +1,250 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import express from "express";
import request from "supertest";
import { MAX_ATTACHMENT_BYTES } from "../attachment-types.js";
import { assetRoutes } from "../routes/assets.js";
import type { StorageService } from "../storage/types.js";
const { createAssetMock, getAssetByIdMock, logActivityMock } = vi.hoisted(() => ({
createAssetMock: vi.fn(),
getAssetByIdMock: vi.fn(),
logActivityMock: vi.fn(),
}));
vi.mock("../services/index.js", () => ({
assetService: vi.fn(() => ({
create: createAssetMock,
getById: getAssetByIdMock,
})),
logActivity: logActivityMock,
}));
function createAsset() {
const now = new Date("2026-01-01T00:00:00.000Z");
return {
id: "asset-1",
companyId: "company-1",
provider: "local",
objectKey: "assets/abc",
contentType: "image/png",
byteSize: 40,
sha256: "sha256-sample",
originalFilename: "logo.png",
createdByAgentId: null,
createdByUserId: "user-1",
createdAt: now,
updatedAt: now,
};
}
function createStorageService(contentType = "image/png"): StorageService {
const putFile: StorageService["putFile"] = vi.fn(async (input: {
companyId: string;
namespace: string;
originalFilename: string | null;
contentType: string;
body: Buffer;
}) => {
return {
provider: "local_disk" as const,
objectKey: `${input.namespace}/${input.originalFilename ?? "upload"}`,
contentType: contentType || input.contentType,
byteSize: input.body.length,
sha256: "sha256-sample",
originalFilename: input.originalFilename,
};
});
return {
provider: "local_disk" as const,
putFile,
getObject: vi.fn(),
headObject: vi.fn(),
deleteObject: vi.fn(),
};
}
function createApp(storage: ReturnType<typeof createStorageService>) {
const app = express();
app.use((req, _res, next) => {
req.actor = {
type: "board",
source: "local_implicit",
userId: "user-1",
};
next();
});
app.use("/api", assetRoutes({} as any, storage));
return app;
}
describe("POST /api/companies/:companyId/assets/images", () => {
afterEach(() => {
createAssetMock.mockReset();
getAssetByIdMock.mockReset();
logActivityMock.mockReset();
});
it("accepts PNG image uploads and returns an asset path", async () => {
const png = createStorageService("image/png");
const app = createApp(png);
createAssetMock.mockResolvedValue(createAsset());
const res = await request(app)
.post("/api/companies/company-1/assets/images")
.field("namespace", "goals")
.attach("file", Buffer.from("png"), "logo.png");
expect(res.status).toBe(201);
expect(res.body.contentPath).toBe("/api/assets/asset-1/content");
expect(createAssetMock).toHaveBeenCalledTimes(1);
expect(png.putFile).toHaveBeenCalledWith({
companyId: "company-1",
namespace: "assets/goals",
originalFilename: "logo.png",
contentType: "image/png",
body: expect.any(Buffer),
});
});
it("allows supported non-image attachments outside the company logo flow", async () => {
const text = createStorageService("text/plain");
const app = createApp(text);
createAssetMock.mockResolvedValue({
...createAsset(),
contentType: "text/plain",
originalFilename: "note.txt",
});
const res = await request(app)
.post("/api/companies/company-1/assets/images")
.field("namespace", "issues/drafts")
.attach("file", Buffer.from("hello"), { filename: "note.txt", contentType: "text/plain" });
expect(res.status).toBe(201);
expect(text.putFile).toHaveBeenCalledWith({
companyId: "company-1",
namespace: "assets/issues/drafts",
originalFilename: "note.txt",
contentType: "text/plain",
body: expect.any(Buffer),
});
});
});
describe("POST /api/companies/:companyId/logo", () => {
afterEach(() => {
createAssetMock.mockReset();
getAssetByIdMock.mockReset();
logActivityMock.mockReset();
});
it("accepts PNG logo uploads and returns an asset path", async () => {
const png = createStorageService("image/png");
const app = createApp(png);
createAssetMock.mockResolvedValue(createAsset());
const res = await request(app)
.post("/api/companies/company-1/logo")
.attach("file", Buffer.from("png"), "logo.png");
expect(res.status).toBe(201);
expect(res.body.contentPath).toBe("/api/assets/asset-1/content");
expect(createAssetMock).toHaveBeenCalledTimes(1);
expect(png.putFile).toHaveBeenCalledWith({
companyId: "company-1",
namespace: "assets/companies",
originalFilename: "logo.png",
contentType: "image/png",
body: expect.any(Buffer),
});
});
it("sanitizes SVG logo uploads before storing them", async () => {
const svg = createStorageService("image/svg+xml");
const app = createApp(svg);
createAssetMock.mockResolvedValue({
...createAsset(),
contentType: "image/svg+xml",
originalFilename: "logo.svg",
});
const res = await request(app)
.post("/api/companies/company-1/logo")
.attach(
"file",
Buffer.from(
"<svg xmlns='http://www.w3.org/2000/svg' onload='alert(1)'><script>alert(1)</script><a href='https://evil.example/'><circle cx='12' cy='12' r='10'/></a></svg>",
),
"logo.svg",
);
expect(res.status).toBe(201);
expect(svg.putFile).toHaveBeenCalledTimes(1);
const stored = (svg.putFile as ReturnType<typeof vi.fn>).mock.calls[0]?.[0];
expect(stored.contentType).toBe("image/svg+xml");
expect(stored.originalFilename).toBe("logo.svg");
const body = stored.body.toString("utf8");
expect(body).toContain("<svg");
expect(body).toContain("<circle");
expect(body).not.toContain("<script");
expect(body).not.toContain("onload=");
expect(body).not.toContain("https://evil.example/");
});
it("allows logo uploads within the general attachment limit", async () => {
const png = createStorageService("image/png");
const app = createApp(png);
createAssetMock.mockResolvedValue(createAsset());
const file = Buffer.alloc(150 * 1024, "a");
const res = await request(app)
.post("/api/companies/company-1/logo")
.attach("file", file, "within-limit.png");
expect(res.status).toBe(201);
});
it("rejects logo files larger than the general attachment limit", async () => {
const app = createApp(createStorageService());
createAssetMock.mockResolvedValue(createAsset());
const file = Buffer.alloc(MAX_ATTACHMENT_BYTES + 1, "a");
const res = await request(app)
.post("/api/companies/company-1/logo")
.attach("file", file, "too-large.png");
expect(res.status).toBe(422);
expect(res.body.error).toBe(`Image exceeds ${MAX_ATTACHMENT_BYTES} bytes`);
});
it("rejects unsupported image types", async () => {
const app = createApp(createStorageService("text/plain"));
createAssetMock.mockResolvedValue(createAsset());
const res = await request(app)
.post("/api/companies/company-1/logo")
.attach("file", Buffer.from("not an image"), "note.txt");
expect(res.status).toBe(422);
expect(res.body.error).toBe("Unsupported image type: text/plain");
expect(createAssetMock).not.toHaveBeenCalled();
});
it("rejects SVG image uploads that cannot be sanitized", async () => {
const app = createApp(createStorageService("image/svg+xml"));
createAssetMock.mockResolvedValue(createAsset());
const res = await request(app)
.post("/api/companies/company-1/logo")
.attach("file", Buffer.from("not actually svg"), "logo.svg");
expect(res.status).toBe(422);
expect(res.body.error).toBe("SVG could not be sanitized");
expect(createAssetMock).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,311 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { budgetService } from "../services/budgets.ts";
const mockLogActivity = vi.hoisted(() => vi.fn());
vi.mock("../services/activity-log.js", () => ({
logActivity: mockLogActivity,
}));
type SelectResult = unknown[];
function createDbStub(selectResults: SelectResult[]) {
const pendingSelects = [...selectResults];
const selectWhere = vi.fn(async () => pendingSelects.shift() ?? []);
const selectThen = vi.fn((resolve: (value: unknown[]) => unknown) => Promise.resolve(resolve(pendingSelects.shift() ?? [])));
const selectOrderBy = vi.fn(async () => pendingSelects.shift() ?? []);
const selectFrom = vi.fn(() => ({
where: selectWhere,
then: selectThen,
orderBy: selectOrderBy,
}));
const select = vi.fn(() => ({
from: selectFrom,
}));
const insertValues = vi.fn();
const insertReturning = vi.fn(async () => pendingInserts.shift() ?? []);
const insert = vi.fn(() => ({
values: insertValues.mockImplementation(() => ({
returning: insertReturning,
})),
}));
const updateSet = vi.fn();
const updateWhere = vi.fn(async () => pendingUpdates.shift() ?? []);
const update = vi.fn(() => ({
set: updateSet.mockImplementation(() => ({
where: updateWhere,
})),
}));
const pendingInserts: unknown[][] = [];
const pendingUpdates: unknown[][] = [];
return {
db: {
select,
insert,
update,
},
queueInsert: (rows: unknown[]) => {
pendingInserts.push(rows);
},
queueUpdate: (rows: unknown[] = []) => {
pendingUpdates.push(rows);
},
selectWhere,
insertValues,
updateSet,
};
}
describe("budgetService", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("creates a hard-stop incident and pauses an agent when spend exceeds a budget", async () => {
const policy = {
id: "policy-1",
companyId: "company-1",
scopeType: "agent",
scopeId: "agent-1",
metric: "billed_cents",
windowKind: "calendar_month_utc",
amount: 100,
warnPercent: 80,
hardStopEnabled: true,
notifyEnabled: false,
isActive: true,
};
const dbStub = createDbStub([
[policy],
[{ total: 150 }],
[],
[{
companyId: "company-1",
name: "Budget Agent",
status: "running",
pauseReason: null,
}],
]);
dbStub.queueInsert([{
id: "approval-1",
companyId: "company-1",
status: "pending",
}]);
dbStub.queueInsert([{
id: "incident-1",
companyId: "company-1",
policyId: "policy-1",
approvalId: "approval-1",
}]);
dbStub.queueUpdate([]);
const cancelWorkForScope = vi.fn().mockResolvedValue(undefined);
const service = budgetService(dbStub.db as any, { cancelWorkForScope });
await service.evaluateCostEvent({
companyId: "company-1",
agentId: "agent-1",
projectId: null,
} as any);
expect(dbStub.insertValues).toHaveBeenCalledWith(
expect.objectContaining({
companyId: "company-1",
type: "budget_override_required",
status: "pending",
}),
);
expect(dbStub.insertValues).toHaveBeenCalledWith(
expect.objectContaining({
companyId: "company-1",
policyId: "policy-1",
thresholdType: "hard",
amountLimit: 100,
amountObserved: 150,
approvalId: "approval-1",
}),
);
expect(dbStub.updateSet).toHaveBeenCalledWith(
expect.objectContaining({
status: "paused",
pauseReason: "budget",
pausedAt: expect.any(Date),
}),
);
expect(mockLogActivity).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
action: "budget.hard_threshold_crossed",
entityId: "incident-1",
}),
);
expect(cancelWorkForScope).toHaveBeenCalledWith({
companyId: "company-1",
scopeType: "agent",
scopeId: "agent-1",
});
});
it("blocks new work when an agent hard-stop remains exceeded even if the agent is not paused yet", async () => {
const agentPolicy = {
id: "policy-agent-1",
companyId: "company-1",
scopeType: "agent",
scopeId: "agent-1",
metric: "billed_cents",
windowKind: "calendar_month_utc",
amount: 100,
warnPercent: 80,
hardStopEnabled: true,
notifyEnabled: true,
isActive: true,
};
const dbStub = createDbStub([
[{
status: "running",
pauseReason: null,
companyId: "company-1",
name: "Budget Agent",
}],
[{
status: "active",
name: "Paperclip",
}],
[],
[agentPolicy],
[{ total: 120 }],
]);
const service = budgetService(dbStub.db as any);
const block = await service.getInvocationBlock("company-1", "agent-1");
expect(block).toEqual({
scopeType: "agent",
scopeId: "agent-1",
scopeName: "Budget Agent",
reason: "Agent cannot start because its budget hard-stop is still exceeded.",
});
});
it("surfaces a budget-owned company pause distinctly from a manual pause", async () => {
const dbStub = createDbStub([
[{
status: "idle",
pauseReason: null,
companyId: "company-1",
name: "Budget Agent",
}],
[{
status: "paused",
pauseReason: "budget",
name: "Paperclip",
}],
]);
const service = budgetService(dbStub.db as any);
const block = await service.getInvocationBlock("company-1", "agent-1");
expect(block).toEqual({
scopeType: "company",
scopeId: "company-1",
scopeName: "Paperclip",
reason: "Company is paused because its budget hard-stop was reached.",
});
});
it("uses live observed spend when raising a budget incident", async () => {
const dbStub = createDbStub([
[{
id: "incident-1",
companyId: "company-1",
policyId: "policy-1",
amountObserved: 120,
approvalId: "approval-1",
}],
[{
id: "policy-1",
companyId: "company-1",
scopeType: "company",
scopeId: "company-1",
metric: "billed_cents",
windowKind: "calendar_month_utc",
}],
[{ total: 150 }],
]);
const service = budgetService(dbStub.db as any);
await expect(
service.resolveIncident(
"company-1",
"incident-1",
{ action: "raise_budget_and_resume", amount: 140 },
"board-user",
),
).rejects.toThrow("New budget must exceed current observed spend");
});
it("syncs company monthly budget when raising and resuming a company incident", async () => {
const now = new Date();
const dbStub = createDbStub([
[{
id: "incident-1",
companyId: "company-1",
policyId: "policy-1",
scopeType: "company",
scopeId: "company-1",
metric: "billed_cents",
windowKind: "calendar_month_utc",
windowStart: now,
windowEnd: now,
thresholdType: "hard",
amountLimit: 100,
amountObserved: 120,
status: "open",
approvalId: "approval-1",
resolvedAt: null,
createdAt: now,
updatedAt: now,
}],
[{
id: "policy-1",
companyId: "company-1",
scopeType: "company",
scopeId: "company-1",
metric: "billed_cents",
windowKind: "calendar_month_utc",
amount: 100,
}],
[{ total: 120 }],
[{ id: "approval-1", status: "approved" }],
[{
companyId: "company-1",
name: "Paperclip",
status: "paused",
pauseReason: "budget",
pausedAt: now,
}],
]);
const service = budgetService(dbStub.db as any);
await service.resolveIncident(
"company-1",
"incident-1",
{ action: "raise_budget_and_resume", amount: 175 },
"board-user",
);
expect(dbStub.updateSet).toHaveBeenCalledWith(
expect.objectContaining({
budgetMonthlyCents: 175,
updatedAt: expect.any(Date),
}),
);
});
});

View File

@@ -22,6 +22,9 @@ vi.mock("../services/index.js", () => ({
canUser: vi.fn(),
ensureMembership: vi.fn(),
}),
budgetService: () => ({
upsertPolicy: vi.fn(),
}),
logActivity: vi.fn(),
}));

View File

@@ -0,0 +1,226 @@
import express from "express";
import request from "supertest";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { costRoutes } from "../routes/costs.js";
import { errorHandler } from "../middleware/index.js";
function makeDb(overrides: Record<string, unknown> = {}) {
const selectChain = {
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
leftJoin: vi.fn().mockReturnThis(),
innerJoin: vi.fn().mockReturnThis(),
groupBy: vi.fn().mockReturnThis(),
orderBy: vi.fn().mockReturnThis(),
limit: vi.fn().mockReturnThis(),
then: vi.fn().mockResolvedValue([]),
};
const thenableChain = Object.assign(Promise.resolve([]), selectChain);
return {
select: vi.fn().mockReturnValue(thenableChain),
insert: vi.fn().mockReturnValue({
values: vi.fn().mockReturnValue({ returning: vi.fn().mockResolvedValue([]) }),
}),
update: vi.fn().mockReturnValue({
set: vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue([]) }),
}),
...overrides,
};
}
const mockCompanyService = vi.hoisted(() => ({
getById: vi.fn(),
update: vi.fn(),
}));
const mockAgentService = vi.hoisted(() => ({
getById: vi.fn(),
update: vi.fn(),
}));
const mockHeartbeatService = vi.hoisted(() => ({
cancelBudgetScopeWork: vi.fn().mockResolvedValue(undefined),
}));
const mockLogActivity = vi.hoisted(() => vi.fn());
const mockFetchAllQuotaWindows = vi.hoisted(() => vi.fn());
const mockCostService = vi.hoisted(() => ({
createEvent: vi.fn(),
summary: vi.fn().mockResolvedValue({ spendCents: 0 }),
byAgent: vi.fn().mockResolvedValue([]),
byAgentModel: vi.fn().mockResolvedValue([]),
byProvider: vi.fn().mockResolvedValue([]),
byBiller: vi.fn().mockResolvedValue([]),
windowSpend: vi.fn().mockResolvedValue([]),
byProject: vi.fn().mockResolvedValue([]),
}));
const mockFinanceService = vi.hoisted(() => ({
createEvent: vi.fn(),
summary: vi.fn().mockResolvedValue({ debitCents: 0, creditCents: 0, netCents: 0, estimatedDebitCents: 0, eventCount: 0 }),
byBiller: vi.fn().mockResolvedValue([]),
byKind: vi.fn().mockResolvedValue([]),
list: vi.fn().mockResolvedValue([]),
}));
const mockBudgetService = vi.hoisted(() => ({
overview: vi.fn().mockResolvedValue({
companyId: "company-1",
policies: [],
activeIncidents: [],
pausedAgentCount: 0,
pausedProjectCount: 0,
pendingApprovalCount: 0,
}),
upsertPolicy: vi.fn(),
resolveIncident: vi.fn(),
}));
vi.mock("../services/index.js", () => ({
budgetService: () => mockBudgetService,
costService: () => mockCostService,
financeService: () => mockFinanceService,
companyService: () => mockCompanyService,
agentService: () => mockAgentService,
heartbeatService: () => mockHeartbeatService,
logActivity: mockLogActivity,
}));
vi.mock("../services/quota-windows.js", () => ({
fetchAllQuotaWindows: mockFetchAllQuotaWindows,
}));
function createApp() {
const app = express();
app.use(express.json());
app.use((req, _res, next) => {
req.actor = { type: "board", userId: "board-user", source: "local_implicit" };
next();
});
app.use("/api", costRoutes(makeDb() as any));
app.use(errorHandler);
return app;
}
function createAppWithActor(actor: any) {
const app = express();
app.use(express.json());
app.use((req, _res, next) => {
req.actor = actor;
next();
});
app.use("/api", costRoutes(makeDb() as any));
app.use(errorHandler);
return app;
}
beforeEach(() => {
vi.clearAllMocks();
mockCompanyService.update.mockResolvedValue({
id: "company-1",
name: "Paperclip",
budgetMonthlyCents: 100,
spentMonthlyCents: 0,
});
mockAgentService.update.mockResolvedValue({
id: "agent-1",
companyId: "company-1",
name: "Budget Agent",
budgetMonthlyCents: 100,
spentMonthlyCents: 0,
});
mockBudgetService.upsertPolicy.mockResolvedValue(undefined);
});
describe("cost routes", () => {
it("accepts valid ISO date strings and passes them to cost summary routes", async () => {
const app = createApp();
const res = await request(app)
.get("/api/companies/company-1/costs/summary")
.query({ from: "2026-01-01T00:00:00.000Z", to: "2026-01-31T23:59:59.999Z" });
expect(res.status).toBe(200);
});
it("returns 400 for an invalid 'from' date string", async () => {
const app = createApp();
const res = await request(app)
.get("/api/companies/company-1/costs/summary")
.query({ from: "not-a-date" });
expect(res.status).toBe(400);
expect(res.body.error).toMatch(/invalid 'from' date/i);
});
it("returns 400 for an invalid 'to' date string", async () => {
const app = createApp();
const res = await request(app)
.get("/api/companies/company-1/costs/summary")
.query({ to: "banana" });
expect(res.status).toBe(400);
expect(res.body.error).toMatch(/invalid 'to' date/i);
});
it("returns finance summary rows for valid requests", async () => {
const app = createApp();
const res = await request(app)
.get("/api/companies/company-1/costs/finance-summary")
.query({ from: "2026-02-01T00:00:00.000Z", to: "2026-02-28T23:59:59.999Z" });
expect(res.status).toBe(200);
expect(mockFinanceService.summary).toHaveBeenCalled();
});
it("returns 400 for invalid finance event list limits", async () => {
const app = createApp();
const res = await request(app)
.get("/api/companies/company-1/costs/finance-events")
.query({ limit: "0" });
expect(res.status).toBe(400);
expect(res.body.error).toMatch(/invalid 'limit'/i);
});
it("accepts valid finance event list limits", async () => {
const app = createApp();
const res = await request(app)
.get("/api/companies/company-1/costs/finance-events")
.query({ limit: "25" });
expect(res.status).toBe(200);
expect(mockFinanceService.list).toHaveBeenCalledWith("company-1", undefined, 25);
});
it("rejects company budget updates for board users outside the company", async () => {
const app = createAppWithActor({
type: "board",
userId: "board-user",
source: "session",
isInstanceAdmin: false,
companyIds: ["company-2"],
});
const res = await request(app)
.patch("/api/companies/company-1/budgets")
.send({ budgetMonthlyCents: 2500 });
expect(res.status).toBe(403);
expect(mockCompanyService.update).not.toHaveBeenCalled();
});
it("rejects agent budget updates for board users outside the agent company", async () => {
mockAgentService.getById.mockResolvedValue({
id: "agent-1",
companyId: "company-1",
name: "Budget Agent",
budgetMonthlyCents: 100,
spentMonthlyCents: 0,
});
const app = createAppWithActor({
type: "board",
userId: "board-user",
source: "session",
isInstanceAdmin: false,
companyIds: ["company-2"],
});
const res = await request(app)
.patch("/api/agents/agent-1/budgets")
.send({ budgetMonthlyCents: 2500 });
expect(res.status).toBe(403);
expect(mockAgentService.update).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,90 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { companyService } from "../services/companies.ts";
import { agentService } from "../services/agents.ts";
function createSelectSequenceDb(results: unknown[]) {
const pending = [...results];
const chain = {
from: vi.fn(() => chain),
where: vi.fn(() => chain),
leftJoin: vi.fn(() => chain),
groupBy: vi.fn(() => chain),
then: vi.fn((resolve: (value: unknown[]) => unknown) => Promise.resolve(resolve(pending.shift() ?? []))),
};
return {
db: {
select: vi.fn(() => chain),
},
};
}
describe("monthly spend hydration", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("recomputes company spentMonthlyCents from the current utc month instead of returning stale stored values", async () => {
const dbStub = createSelectSequenceDb([
[{
id: "company-1",
name: "Paperclip",
description: null,
status: "active",
issuePrefix: "PAP",
issueCounter: 1,
budgetMonthlyCents: 5000,
spentMonthlyCents: 999999,
requireBoardApprovalForNewAgents: false,
brandColor: null,
logoAssetId: null,
createdAt: new Date(),
updatedAt: new Date(),
}],
[{
companyId: "company-1",
spentMonthlyCents: 420,
}],
]);
const companies = companyService(dbStub.db as any);
const [company] = await companies.list();
expect(company.spentMonthlyCents).toBe(420);
});
it("recomputes agent spentMonthlyCents from the current utc month instead of returning stale stored values", async () => {
const dbStub = createSelectSequenceDb([
[{
id: "agent-1",
companyId: "company-1",
name: "Budget Agent",
role: "general",
title: null,
reportsTo: null,
capabilities: null,
adapterType: "claude-local",
adapterConfig: {},
runtimeConfig: {},
budgetMonthlyCents: 5000,
spentMonthlyCents: 999999,
metadata: null,
permissions: null,
status: "idle",
pauseReason: null,
pausedAt: null,
createdAt: new Date(),
updatedAt: new Date(),
}],
[{
agentId: "agent-1",
spentMonthlyCents: 175,
}],
]);
const agents = agentService(dbStub.db as any);
const agent = await agents.getById("agent-1");
expect(agent?.spentMonthlyCents).toBe(175);
});
});

View File

@@ -0,0 +1,56 @@
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
vi.mock("../adapters/registry.js", () => ({
listServerAdapters: vi.fn(),
}));
import { listServerAdapters } from "../adapters/registry.js";
import { fetchAllQuotaWindows } from "../services/quota-windows.js";
describe("fetchAllQuotaWindows", () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
});
it("returns adapter results without waiting for a slower provider to finish forever", async () => {
vi.mocked(listServerAdapters).mockReturnValue([
{
type: "codex_local",
getQuotaWindows: vi.fn().mockResolvedValue({
provider: "openai",
source: "codex-rpc",
ok: true,
windows: [{ label: "5h limit", usedPercent: 2, resetsAt: null, valueLabel: null, detail: null }],
}),
},
{
type: "claude_local",
getQuotaWindows: vi.fn(() => new Promise(() => {})),
},
] as never);
const promise = fetchAllQuotaWindows();
await vi.advanceTimersByTimeAsync(20_001);
const results = await promise;
expect(results).toEqual([
{
provider: "openai",
source: "codex-rpc",
ok: true,
windows: [{ label: "5h limit", usedPercent: 2, resetsAt: null, valueLabel: null, detail: null }],
},
{
provider: "anthropic",
ok: false,
error: "quota polling timed out after 20s",
windows: [],
},
]);
});
});

View File

@@ -0,0 +1,812 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import os from "node:os";
import path from "node:path";
import type { QuotaWindow } from "@paperclipai/adapter-utils";
// Pure utility functions — import directly from adapter source
import {
toPercent,
fetchWithTimeout,
fetchClaudeQuota,
parseClaudeCliUsageText,
readClaudeToken,
claudeConfigDir,
} from "@paperclipai/adapter-claude-local/server";
import {
secondsToWindowLabel,
readCodexAuthInfo,
readCodexToken,
fetchCodexQuota,
mapCodexRpcQuota,
codexHomeDir,
} from "@paperclipai/adapter-codex-local/server";
// ---------------------------------------------------------------------------
// toPercent
// ---------------------------------------------------------------------------
describe("toPercent", () => {
it("returns null for null input", () => {
expect(toPercent(null)).toBe(null);
});
it("returns null for undefined input", () => {
expect(toPercent(undefined)).toBe(null);
});
it("converts 0 to 0", () => {
expect(toPercent(0)).toBe(0);
});
it("converts 0.5 to 50", () => {
expect(toPercent(0.5)).toBe(50);
});
it("converts 1.0 to 100", () => {
expect(toPercent(1.0)).toBe(100);
});
it("clamps overshoot to 100", () => {
// floating-point utilization can slightly exceed 1.0
expect(toPercent(1.001)).toBe(100);
expect(toPercent(1.01)).toBe(100);
});
it("rounds to nearest integer", () => {
expect(toPercent(0.333)).toBe(33);
expect(toPercent(0.666)).toBe(67);
});
});
// ---------------------------------------------------------------------------
// secondsToWindowLabel
// ---------------------------------------------------------------------------
describe("secondsToWindowLabel", () => {
it("returns fallback for null seconds", () => {
expect(secondsToWindowLabel(null, "Primary")).toBe("Primary");
});
it("returns fallback for undefined seconds", () => {
expect(secondsToWindowLabel(undefined, "Secondary")).toBe("Secondary");
});
it("labels windows under 6 hours as '5h'", () => {
expect(secondsToWindowLabel(3600, "fallback")).toBe("5h"); // 1h
expect(secondsToWindowLabel(18000, "fallback")).toBe("5h"); // 5h exactly
});
it("labels windows up to 24 hours as '24h'", () => {
expect(secondsToWindowLabel(21600, "fallback")).toBe("24h"); // 6h (≥6h boundary)
expect(secondsToWindowLabel(86400, "fallback")).toBe("24h"); // 24h exactly
});
it("labels windows up to 7 days as '7d'", () => {
expect(secondsToWindowLabel(86401, "fallback")).toBe("7d"); // just over 24h
expect(secondsToWindowLabel(604800, "fallback")).toBe("7d"); // 7d exactly
});
it("labels windows beyond 7 days with actual day count", () => {
expect(secondsToWindowLabel(1209600, "fallback")).toBe("14d"); // 14d
expect(secondsToWindowLabel(2592000, "fallback")).toBe("30d"); // 30d
});
});
// ---------------------------------------------------------------------------
// WHAM used_percent normalization (codex / openai)
// ---------------------------------------------------------------------------
describe("WHAM used_percent normalization via fetchCodexQuota", () => {
beforeEach(() => {
vi.stubGlobal("fetch", vi.fn());
});
afterEach(() => {
vi.unstubAllGlobals();
});
function mockFetch(body: unknown) {
(fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
ok: true,
json: async () => body,
} as Response);
}
it("treats values >= 1 as already-percentage (50 → 50%)", async () => {
mockFetch({
rate_limit: {
primary_window: {
used_percent: 50,
limit_window_seconds: 18000,
reset_at: null,
},
},
});
const windows = await fetchCodexQuota("token", null);
expect(windows[0]!.usedPercent).toBe(50);
});
it("treats values < 1 as fraction and multiplies by 100 (0.5 → 50%)", async () => {
mockFetch({
rate_limit: {
primary_window: {
used_percent: 0.5,
limit_window_seconds: 18000,
reset_at: null,
},
},
});
const windows = await fetchCodexQuota("token", null);
expect(windows[0]!.usedPercent).toBe(50);
});
it("treats value exactly 1.0 as 1% (not 100%) — the < 1 heuristic boundary", async () => {
// 1.0 is NOT < 1, so it is treated as already-percentage → 1%
mockFetch({
rate_limit: {
primary_window: {
used_percent: 1.0,
limit_window_seconds: 18000,
reset_at: null,
},
},
});
const windows = await fetchCodexQuota("token", null);
expect(windows[0]!.usedPercent).toBe(1);
});
it("treats value 0 as 0%", async () => {
mockFetch({
rate_limit: {
primary_window: {
used_percent: 0,
limit_window_seconds: 18000,
reset_at: null,
},
},
});
const windows = await fetchCodexQuota("token", null);
expect(windows[0]!.usedPercent).toBe(0);
});
it("clamps 100% to 100 (no overshoot)", async () => {
mockFetch({
rate_limit: {
primary_window: {
used_percent: 105,
limit_window_seconds: 18000,
reset_at: null,
},
},
});
const windows = await fetchCodexQuota("token", null);
expect(windows[0]!.usedPercent).toBe(100);
});
it("sets usedPercent to null when used_percent is absent", async () => {
mockFetch({
rate_limit: {
primary_window: {
limit_window_seconds: 18000,
reset_at: null,
},
},
});
const windows = await fetchCodexQuota("token", null);
expect(windows[0]!.usedPercent).toBe(null);
});
});
// ---------------------------------------------------------------------------
// readClaudeToken — filesystem paths
// ---------------------------------------------------------------------------
describe("readClaudeToken", () => {
const savedEnv = process.env.CLAUDE_CONFIG_DIR;
afterEach(() => {
if (savedEnv === undefined) {
delete process.env.CLAUDE_CONFIG_DIR;
} else {
process.env.CLAUDE_CONFIG_DIR = savedEnv;
}
vi.restoreAllMocks();
});
it("returns null when credentials.json does not exist", async () => {
// Point to a directory that does not have credentials.json
process.env.CLAUDE_CONFIG_DIR = "/tmp/__no_such_paperclip_dir__";
const token = await readClaudeToken();
expect(token).toBe(null);
});
it("returns null for malformed JSON", async () => {
const tmpDir = path.join(os.tmpdir(), `paperclip-test-claude-${Date.now()}`);
await import("node:fs/promises").then((fs) =>
fs.mkdir(tmpDir, { recursive: true }).then(() =>
fs.writeFile(path.join(tmpDir, "credentials.json"), "not-json"),
),
);
process.env.CLAUDE_CONFIG_DIR = tmpDir;
const token = await readClaudeToken();
expect(token).toBe(null);
await import("node:fs/promises").then((fs) => fs.rm(tmpDir, { recursive: true }));
});
it("returns null when claudeAiOauth key is missing", async () => {
const tmpDir = path.join(os.tmpdir(), `paperclip-test-claude-${Date.now()}`);
await import("node:fs/promises").then((fs) =>
fs.mkdir(tmpDir, { recursive: true }).then(() =>
fs.writeFile(path.join(tmpDir, "credentials.json"), JSON.stringify({ other: "data" })),
),
);
process.env.CLAUDE_CONFIG_DIR = tmpDir;
const token = await readClaudeToken();
expect(token).toBe(null);
await import("node:fs/promises").then((fs) => fs.rm(tmpDir, { recursive: true }));
});
it("returns null when accessToken is an empty string", async () => {
const tmpDir = path.join(os.tmpdir(), `paperclip-test-claude-${Date.now()}`);
const creds = { claudeAiOauth: { accessToken: "" } };
await import("node:fs/promises").then((fs) =>
fs.mkdir(tmpDir, { recursive: true }).then(() =>
fs.writeFile(path.join(tmpDir, "credentials.json"), JSON.stringify(creds)),
),
);
process.env.CLAUDE_CONFIG_DIR = tmpDir;
const token = await readClaudeToken();
expect(token).toBe(null);
await import("node:fs/promises").then((fs) => fs.rm(tmpDir, { recursive: true }));
});
it("returns the token when credentials file is well-formed", async () => {
const tmpDir = path.join(os.tmpdir(), `paperclip-test-claude-${Date.now()}`);
const creds = { claudeAiOauth: { accessToken: "my-test-token" } };
await import("node:fs/promises").then((fs) =>
fs.mkdir(tmpDir, { recursive: true }).then(() =>
fs.writeFile(path.join(tmpDir, "credentials.json"), JSON.stringify(creds)),
),
);
process.env.CLAUDE_CONFIG_DIR = tmpDir;
const token = await readClaudeToken();
expect(token).toBe("my-test-token");
await import("node:fs/promises").then((fs) => fs.rm(tmpDir, { recursive: true }));
});
it("reads the token from .credentials.json when that is the available Claude auth file", async () => {
const tmpDir = path.join(os.tmpdir(), `paperclip-test-claude-${Date.now()}`);
const creds = { claudeAiOauth: { accessToken: "dotfile-token" } };
await import("node:fs/promises").then((fs) =>
fs.mkdir(tmpDir, { recursive: true }).then(() =>
fs.writeFile(path.join(tmpDir, ".credentials.json"), JSON.stringify(creds)),
),
);
process.env.CLAUDE_CONFIG_DIR = tmpDir;
const token = await readClaudeToken();
expect(token).toBe("dotfile-token");
await import("node:fs/promises").then((fs) => fs.rm(tmpDir, { recursive: true }));
});
});
describe("parseClaudeCliUsageText", () => {
it("parses the Claude usage panel layout into quota windows", () => {
const raw = `
Settings: Status Config Usage
Current session
2% used
Resets 5pm (America/Chicago)
Current week (all models)
47% used
Resets Mar 18 at 7:59am (America/Chicago)
Current week (Sonnet only)
0% used
Resets Mar 18 at 8:59am (America/Chicago)
Extra usage
Extra usage not enabled • /extra-usage to enable
`;
expect(parseClaudeCliUsageText(raw)).toEqual([
{
label: "Current session",
usedPercent: 2,
resetsAt: null,
valueLabel: null,
detail: "Resets 5pm (America/Chicago)",
},
{
label: "Current week (all models)",
usedPercent: 47,
resetsAt: null,
valueLabel: null,
detail: "Resets Mar 18 at 7:59am (America/Chicago)",
},
{
label: "Current week (Sonnet only)",
usedPercent: 0,
resetsAt: null,
valueLabel: null,
detail: "Resets Mar 18 at 8:59am (America/Chicago)",
},
{
label: "Extra usage",
usedPercent: null,
resetsAt: null,
valueLabel: null,
detail: "Extra usage not enabled • /extra-usage to enable",
},
]);
});
it("throws a useful error when the Claude CLI panel reports a usage load failure", () => {
expect(() => parseClaudeCliUsageText("Failed to load usage data")).toThrow(
"Claude CLI could not load usage data. Open the CLI and retry `/usage`.",
);
});
});
// ---------------------------------------------------------------------------
// readCodexAuthInfo / readCodexToken — filesystem paths
// ---------------------------------------------------------------------------
describe("readCodexAuthInfo", () => {
const savedEnv = process.env.CODEX_HOME;
afterEach(() => {
if (savedEnv === undefined) {
delete process.env.CODEX_HOME;
} else {
process.env.CODEX_HOME = savedEnv;
}
});
it("returns null when auth.json does not exist", async () => {
process.env.CODEX_HOME = "/tmp/__no_such_paperclip_codex_dir__";
const result = await readCodexAuthInfo();
expect(result).toBe(null);
});
it("returns null for malformed JSON", async () => {
const tmpDir = path.join(os.tmpdir(), `paperclip-test-codex-${Date.now()}`);
await import("node:fs/promises").then((fs) =>
fs.mkdir(tmpDir, { recursive: true }).then(() =>
fs.writeFile(path.join(tmpDir, "auth.json"), "{bad json"),
),
);
process.env.CODEX_HOME = tmpDir;
const result = await readCodexAuthInfo();
expect(result).toBe(null);
await import("node:fs/promises").then((fs) => fs.rm(tmpDir, { recursive: true }));
});
it("returns null when accessToken is absent", async () => {
const tmpDir = path.join(os.tmpdir(), `paperclip-test-codex-${Date.now()}`);
await import("node:fs/promises").then((fs) =>
fs.mkdir(tmpDir, { recursive: true }).then(() =>
fs.writeFile(path.join(tmpDir, "auth.json"), JSON.stringify({ accountId: "acc-1" })),
),
);
process.env.CODEX_HOME = tmpDir;
const result = await readCodexAuthInfo();
expect(result).toBe(null);
await import("node:fs/promises").then((fs) => fs.rm(tmpDir, { recursive: true }));
});
it("reads the legacy flat auth shape", async () => {
const tmpDir = path.join(os.tmpdir(), `paperclip-test-codex-${Date.now()}`);
const auth = { accessToken: "codex-token", accountId: "acc-123" };
await import("node:fs/promises").then((fs) =>
fs.mkdir(tmpDir, { recursive: true }).then(() =>
fs.writeFile(path.join(tmpDir, "auth.json"), JSON.stringify(auth)),
),
);
process.env.CODEX_HOME = tmpDir;
const result = await readCodexAuthInfo();
expect(result).toMatchObject({
accessToken: "codex-token",
accountId: "acc-123",
email: null,
planType: null,
});
await import("node:fs/promises").then((fs) => fs.rm(tmpDir, { recursive: true }));
});
it("reads the modern nested auth shape", async () => {
const tmpDir = path.join(os.tmpdir(), `paperclip-test-codex-${Date.now()}`);
const jwtPayload = Buffer.from(
JSON.stringify({
email: "codex@example.com",
"https://api.openai.com/auth": {
chatgpt_plan_type: "pro",
chatgpt_user_email: "codex@example.com",
},
}),
).toString("base64url");
const auth = {
tokens: {
access_token: `header.${jwtPayload}.sig`,
account_id: "acc-modern",
refresh_token: "refresh-me",
id_token: `header.${jwtPayload}.sig`,
},
last_refresh: "2026-03-14T12:00:00Z",
};
await import("node:fs/promises").then((fs) =>
fs.mkdir(tmpDir, { recursive: true }).then(() =>
fs.writeFile(path.join(tmpDir, "auth.json"), JSON.stringify(auth)),
),
);
process.env.CODEX_HOME = tmpDir;
const result = await readCodexAuthInfo();
expect(result).toMatchObject({
accessToken: `header.${jwtPayload}.sig`,
accountId: "acc-modern",
refreshToken: "refresh-me",
email: "codex@example.com",
planType: "pro",
lastRefresh: "2026-03-14T12:00:00Z",
});
await import("node:fs/promises").then((fs) => fs.rm(tmpDir, { recursive: true }));
});
});
describe("readCodexToken", () => {
const savedEnv = process.env.CODEX_HOME;
afterEach(() => {
if (savedEnv === undefined) {
delete process.env.CODEX_HOME;
} else {
process.env.CODEX_HOME = savedEnv;
}
});
it("returns token and accountId from the nested auth shape", async () => {
const tmpDir = path.join(os.tmpdir(), `paperclip-test-codex-${Date.now()}`);
await import("node:fs/promises").then((fs) =>
fs.mkdir(tmpDir, { recursive: true }).then(() =>
fs.writeFile(path.join(tmpDir, "auth.json"), JSON.stringify({
tokens: {
access_token: "nested-token",
account_id: "acc-nested",
},
})),
),
);
process.env.CODEX_HOME = tmpDir;
const result = await readCodexToken();
expect(result).toEqual({ token: "nested-token", accountId: "acc-nested" });
await import("node:fs/promises").then((fs) => fs.rm(tmpDir, { recursive: true }));
});
});
// ---------------------------------------------------------------------------
// fetchClaudeQuota — response parsing
// ---------------------------------------------------------------------------
describe("fetchClaudeQuota", () => {
beforeEach(() => {
vi.stubGlobal("fetch", vi.fn());
});
afterEach(() => {
vi.unstubAllGlobals();
});
function mockFetch(body: unknown, ok = true, status = 200) {
(fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
ok,
status,
json: async () => body,
} as Response);
}
it("throws when the API returns a non-200 status", async () => {
mockFetch({}, false, 401);
await expect(fetchClaudeQuota("token")).rejects.toThrow("anthropic usage api returned 401");
});
it("returns an empty array when all window fields are absent", async () => {
mockFetch({});
const windows = await fetchClaudeQuota("token");
expect(windows).toEqual([]);
});
it("parses five_hour window", async () => {
mockFetch({ five_hour: { utilization: 0.4, resets_at: "2026-01-01T00:00:00Z" } });
const windows = await fetchClaudeQuota("token");
expect(windows).toHaveLength(1);
expect(windows[0]).toMatchObject({
label: "Current session",
usedPercent: 40,
resetsAt: "2026-01-01T00:00:00Z",
});
});
it("parses seven_day window", async () => {
mockFetch({ seven_day: { utilization: 0.75, resets_at: null } });
const windows = await fetchClaudeQuota("token");
expect(windows).toHaveLength(1);
expect(windows[0]).toMatchObject({
label: "Current week (all models)",
usedPercent: 75,
resetsAt: null,
});
});
it("parses seven_day_sonnet and seven_day_opus windows", async () => {
mockFetch({
seven_day_sonnet: { utilization: 0.2, resets_at: null },
seven_day_opus: { utilization: 0.9, resets_at: null },
});
const windows = await fetchClaudeQuota("token");
expect(windows).toHaveLength(2);
expect(windows[0]!.label).toBe("Current week (Sonnet only)");
expect(windows[1]!.label).toBe("Current week (Opus only)");
});
it("sets usedPercent to null when utilization is absent", async () => {
mockFetch({ five_hour: { resets_at: null } });
const windows = await fetchClaudeQuota("token");
expect(windows[0]!.usedPercent).toBe(null);
});
it("includes all four windows when all are present", async () => {
mockFetch({
five_hour: { utilization: 0.1, resets_at: null },
seven_day: { utilization: 0.2, resets_at: null },
seven_day_sonnet: { utilization: 0.3, resets_at: null },
seven_day_opus: { utilization: 0.4, resets_at: null },
});
const windows = await fetchClaudeQuota("token");
expect(windows).toHaveLength(4);
const labels = windows.map((w: QuotaWindow) => w.label);
expect(labels).toEqual([
"Current session",
"Current week (all models)",
"Current week (Sonnet only)",
"Current week (Opus only)",
]);
});
it("parses extra usage when the OAuth response includes it", async () => {
mockFetch({
extra_usage: {
is_enabled: false,
utilization: null,
},
});
const windows = await fetchClaudeQuota("token");
expect(windows).toEqual([
{
label: "Extra usage",
usedPercent: null,
resetsAt: null,
valueLabel: "Not enabled",
detail: "Extra usage not enabled",
},
]);
});
});
// ---------------------------------------------------------------------------
// fetchCodexQuota — response parsing (credits, windows)
// ---------------------------------------------------------------------------
describe("fetchCodexQuota", () => {
beforeEach(() => {
vi.stubGlobal("fetch", vi.fn());
});
afterEach(() => {
vi.unstubAllGlobals();
});
function mockFetch(body: unknown, ok = true, status = 200) {
(fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
ok,
status,
json: async () => body,
} as Response);
}
it("throws when the WHAM API returns a non-200 status", async () => {
mockFetch({}, false, 403);
await expect(fetchCodexQuota("token", null)).rejects.toThrow("chatgpt wham api returned 403");
});
it("passes ChatGPT-Account-Id header when accountId is provided", async () => {
mockFetch({});
await fetchCodexQuota("token", "acc-xyz");
const callInit = (fetch as ReturnType<typeof vi.fn>).mock.calls[0][1] as RequestInit;
expect((callInit.headers as Record<string, string>)["ChatGPT-Account-Id"]).toBe("acc-xyz");
});
it("omits ChatGPT-Account-Id header when accountId is null", async () => {
mockFetch({});
await fetchCodexQuota("token", null);
const callInit = (fetch as ReturnType<typeof vi.fn>).mock.calls[0][1] as RequestInit;
expect((callInit.headers as Record<string, string>)["ChatGPT-Account-Id"]).toBeUndefined();
});
it("returns empty array when response body is empty", async () => {
mockFetch({});
const windows = await fetchCodexQuota("token", null);
expect(windows).toEqual([]);
});
it("normalizes numeric reset timestamps from WHAM", async () => {
mockFetch({
rate_limit: {
primary_window: { used_percent: 30, limit_window_seconds: 86400, reset_at: 1_767_312_000 },
},
});
const windows = await fetchCodexQuota("token", null);
expect(windows).toHaveLength(1);
expect(windows[0]).toMatchObject({ label: "5h limit", usedPercent: 30, resetsAt: "2026-01-02T00:00:00.000Z" });
});
it("parses secondary_window alongside primary_window", async () => {
mockFetch({
rate_limit: {
primary_window: { used_percent: 10, limit_window_seconds: 18000 },
secondary_window: { used_percent: 60, limit_window_seconds: 604800 },
},
});
const windows = await fetchCodexQuota("token", null);
expect(windows).toHaveLength(2);
expect(windows[0]!.label).toBe("5h limit");
expect(windows[1]!.label).toBe("Weekly limit");
});
it("includes Credits window when credits present and not unlimited", async () => {
mockFetch({
credits: { balance: 420, unlimited: false },
});
const windows = await fetchCodexQuota("token", null);
expect(windows).toHaveLength(1);
expect(windows[0]).toMatchObject({ label: "Credits", valueLabel: "$4.20 remaining", usedPercent: null });
});
it("omits Credits window when unlimited is true", async () => {
mockFetch({
credits: { balance: 9999, unlimited: true },
});
const windows = await fetchCodexQuota("token", null);
expect(windows).toEqual([]);
});
it("shows 'N/A' valueLabel when credits balance is null", async () => {
mockFetch({
credits: { balance: null, unlimited: false },
});
const windows = await fetchCodexQuota("token", null);
expect(windows[0]!.valueLabel).toBe("N/A");
});
});
describe("mapCodexRpcQuota", () => {
it("maps account and model-specific Codex limits into quota windows", () => {
const snapshot = mapCodexRpcQuota(
{
rateLimits: {
limitId: "codex",
primary: { usedPercent: 1, windowDurationMins: 300, resetsAt: 1_763_500_000 },
secondary: { usedPercent: 27, windowDurationMins: 10_080 },
planType: "pro",
},
rateLimitsByLimitId: {
codex_bengalfox: {
limitId: "codex_bengalfox",
limitName: "GPT-5.3-Codex-Spark",
primary: { usedPercent: 8, windowDurationMins: 300 },
secondary: { usedPercent: 20, windowDurationMins: 10_080 },
},
},
},
{
account: {
email: "codex@example.com",
planType: "pro",
},
},
);
expect(snapshot.email).toBe("codex@example.com");
expect(snapshot.planType).toBe("pro");
expect(snapshot.windows).toEqual([
{
label: "5h limit",
usedPercent: 1,
resetsAt: "2025-11-18T21:06:40.000Z",
valueLabel: null,
detail: null,
},
{
label: "Weekly limit",
usedPercent: 27,
resetsAt: null,
valueLabel: null,
detail: null,
},
{
label: "GPT-5.3-Codex-Spark · 5h limit",
usedPercent: 8,
resetsAt: null,
valueLabel: null,
detail: null,
},
{
label: "GPT-5.3-Codex-Spark · Weekly limit",
usedPercent: 20,
resetsAt: null,
valueLabel: null,
detail: null,
},
]);
});
it("includes a credits row when the root Codex limit reports finite credits", () => {
const snapshot = mapCodexRpcQuota({
rateLimits: {
limitId: "codex",
credits: {
unlimited: false,
balance: "12.34",
},
},
});
expect(snapshot.windows).toEqual([
{
label: "Credits",
usedPercent: null,
resetsAt: null,
valueLabel: "$12.34 remaining",
detail: null,
},
]);
});
});
// ---------------------------------------------------------------------------
// fetchWithTimeout — abort on timeout
// ---------------------------------------------------------------------------
describe("fetchWithTimeout", () => {
afterEach(() => {
vi.unstubAllGlobals();
vi.useRealTimers();
});
it("resolves normally when fetch completes before timeout", async () => {
const mockResponse = { ok: true, status: 200, json: async () => ({}) } as Response;
vi.stubGlobal("fetch", vi.fn().mockResolvedValue(mockResponse));
const result = await fetchWithTimeout("https://example.com", {}, 5000);
expect(result.ok).toBe(true);
});
it("rejects with abort error when fetch takes too long", async () => {
vi.useFakeTimers();
vi.stubGlobal(
"fetch",
vi.fn().mockImplementation(
(_url: string, init: RequestInit) =>
new Promise((_resolve, reject) => {
init.signal?.addEventListener("abort", () => {
reject(new DOMException("The operation was aborted.", "AbortError"));
});
}),
),
);
const promise = fetchWithTimeout("https://example.com", {}, 1000);
vi.advanceTimersByTime(1001);
await expect(promise).rejects.toThrow("aborted");
});
});