merge: pull mhiqrambg/9router-mibp-version into master
Merge the MIBP fork (v1.0.14, synced to decolua v0.5.81) into our master
(0.5.86) at merge-base a8c9d380. Keep HEAD's infra policy (untracked
lockfile, mirror-configurable Dockerfile, decolua GHCR/DockerHub, README)
while absorbing the fork's engine features:
- feat(providers): freebuff provider + executor + OAuth + usage tracking
- feat(providers): cline free-tier models, Freebuff catalog sync
- feat(proxy-pools): pool egress geo probe, proxy-pool fitness + retry
- fix(usage): hide noAuth providers (devin-cli, mimo-free) from usage list
- test(harness): DATA_DIR isolation so tests never write the real DB
- fix(codebuddy-intl): probe token in connection test, OAuth by identity
- chore(guards): durable markers so fixes aren't silently dropped
Resolutions:
- registry/index.js regenerated deterministically (122 providers, alpha
order). trae/windsurf/devin-cli stay hidden per HEAD security posture
(no tool-calling / local-agent shell access) — not re-enabled.
- nonStreamingHandler: drop the generic unconditional unwrapDataEnvelope
call; envelope unwrap stays scoped to clineEnvelope-quirk providers
(unwrapClineEnvelope), fixing a latent mibp bug where non-opted-in
providers ({success,data} bodies) were stripped.
- Drop fork-local Docker lockfile policy (package-lock.json, AGENTS.md,
.npmrc verify scripts): this repo keeps package-lock untracked (nix
build deploy). .npmrc (audit=false/fund=false) kept.
- Keep gitbook-pages workflow enabled (ours); mibp disabled it.
- Restore 13 upstream tests mibp deleted (they cover features we keep).
Verified: 2841 tests, 2687 pass, fail set byte-identical to HEAD (zero
new regressions); providers/alias/oauth baselines regenerated to merged
code and all green.
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("@/lib/usageDb.js", () => ({
|
||||
appendRequestLog: vi.fn(async () => {}),
|
||||
saveRequestDetail: vi.fn(async () => {}),
|
||||
saveRequestUsage: vi.fn(async () => {})
|
||||
}));
|
||||
|
||||
const { unwrapDataEnvelope } = await import("../../open-sse/handlers/chatCore/nonStreamingHandler.js");
|
||||
const { buildClineHeaders } = await import("../../open-sse/shared/clineAuth.js");
|
||||
|
||||
// Cline gateway wraps non-streaming Chat Completions in {data, success}.
|
||||
// handleNonStreamingResponse calls unwrapDataEnvelope unconditionally (it runs
|
||||
// before the needsTranslation gate, which openai→openai never passes).
|
||||
const ENVELOPED = {
|
||||
data: {
|
||||
id: "gen-123",
|
||||
object: "chat.completion",
|
||||
created: 1789056634,
|
||||
model: "deepseek/deepseek-v4-flash-0731",
|
||||
choices: [{ index: 0, message: { role: "assistant", content: "Hi there!" }, finish_reason: "stop" }],
|
||||
usage: { prompt_tokens: 9, completion_tokens: 29, total_tokens: 38 },
|
||||
},
|
||||
success: true,
|
||||
};
|
||||
|
||||
describe("cline {data} envelope unwrap", () => {
|
||||
it("unwraps choices/usage to top level", () => {
|
||||
const out = unwrapDataEnvelope(structuredClone(ENVELOPED));
|
||||
expect(out.choices?.[0]?.message?.content).toBe("Hi there!");
|
||||
expect(out.usage?.prompt_tokens).toBe(9);
|
||||
});
|
||||
|
||||
it("leaves plain OpenAI bodies untouched", () => {
|
||||
const out = unwrapDataEnvelope(structuredClone(ENVELOPED.data));
|
||||
expect(out.choices?.[0]?.message?.content).toBe("Hi there!");
|
||||
});
|
||||
|
||||
it("prefers top-level choices when both exist", () => {
|
||||
const body = { ...structuredClone(ENVELOPED), choices: [{ index: 0, message: { role: "assistant", content: "top" }, finish_reason: "stop" }] };
|
||||
const out = unwrapDataEnvelope(body);
|
||||
expect(out.choices?.[0]?.message?.content).toBe("top");
|
||||
});
|
||||
|
||||
it("ignores non-envelope bodies", () => {
|
||||
expect(unwrapDataEnvelope(null)).toBe(null);
|
||||
expect(unwrapDataEnvelope({ error: "x" })).toEqual({ error: "x" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("cline auth header shape", () => {
|
||||
it("sends API keys as plain Bearer", () => {
|
||||
expect(buildClineHeaders("sk_abc", {}, { isApiKey: true }).Authorization).toBe("Bearer sk_abc");
|
||||
});
|
||||
|
||||
it("prefixes WorkOS JWT OAuth tokens with workos:", () => {
|
||||
const jwt = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.sig";
|
||||
expect(buildClineHeaders(jwt).Authorization).toBe(`Bearer workos:${jwt}`);
|
||||
expect(buildClineHeaders(`workos:${jwt}`).Authorization).toBe(`Bearer workos:${jwt}`);
|
||||
});
|
||||
|
||||
it("does not workos:-prefix opaque tokens (API keys, clp_…)", () => {
|
||||
expect(buildClineHeaders("tok123").Authorization).toBe("Bearer tok123");
|
||||
expect(buildClineHeaders("clp_abc123").Authorization).toBe("Bearer clp_abc123");
|
||||
});
|
||||
|
||||
it("sends Cline product identity headers (free-model gate)", () => {
|
||||
const h = buildClineHeaders("tok123");
|
||||
expect(h["X-CLIENT-TYPE"]).toBe("cline-cli");
|
||||
expect(h["User-Agent"]).toMatch(/^Cline\//);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,119 @@
|
||||
// Existing CodeBuddy Intl OAuth connections created before mapTokens surfaced
|
||||
// identity show up as "Account N" with no email. The self-healing backfill must
|
||||
// fill email + displayName from the access-token JWT so the dashboard shows the
|
||||
// real identity without forcing a re-login.
|
||||
//
|
||||
// backfillCodeBuddyIntlIdentity has a module-level run-once guard, so each test
|
||||
// re-imports a fresh module instance via vi.resetModules() + dynamic import.
|
||||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
getProviderConnections: vi.fn(),
|
||||
updateProviderConnection: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/localDb", () => ({
|
||||
getProviderConnections: mocks.getProviderConnections,
|
||||
updateProviderConnection: mocks.updateProviderConnection,
|
||||
}));
|
||||
|
||||
// The providers index imports open-sse/index.js for proxy-aware fetch; stub it.
|
||||
vi.mock("open-sse/index.js", () => ({}));
|
||||
|
||||
function makeJwt(payload) {
|
||||
const b64 = (obj) => Buffer.from(JSON.stringify(obj)).toString("base64url");
|
||||
return `${b64({ alg: "RS256", typ: "JWT" })}.${b64(payload)}.sig`;
|
||||
}
|
||||
|
||||
const JWT = makeJwt({
|
||||
iss: "https://www.codebuddy.ai/auth/realms/copilot",
|
||||
email: "aghiyaramadh@gmail.com",
|
||||
name: "aghiya ramadh",
|
||||
});
|
||||
|
||||
async function loadBackfill() {
|
||||
vi.resetModules();
|
||||
const mod = await import("../../src/lib/oauth/providers/index.js");
|
||||
return mod.backfillCodeBuddyIntlIdentity;
|
||||
}
|
||||
|
||||
describe("backfillCodeBuddyIntlIdentity", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mocks.updateProviderConnection.mockResolvedValue({});
|
||||
});
|
||||
|
||||
it("fills email + displayName for a legacy 'Account N' connection", async () => {
|
||||
mocks.getProviderConnections.mockResolvedValue([
|
||||
{
|
||||
id: "conn-legacy",
|
||||
provider: "codebuddy-intl",
|
||||
authType: "oauth",
|
||||
name: "Account 1",
|
||||
email: null,
|
||||
displayName: null,
|
||||
accessToken: JWT,
|
||||
},
|
||||
]);
|
||||
|
||||
const backfill = await loadBackfill();
|
||||
await backfill();
|
||||
|
||||
expect(mocks.updateProviderConnection).toHaveBeenCalledTimes(1);
|
||||
const [id, patch] = mocks.updateProviderConnection.mock.calls[0];
|
||||
expect(id).toBe("conn-legacy");
|
||||
expect(patch.email).toBe("aghiyaramadh@gmail.com");
|
||||
expect(patch.displayName).toBe("aghiya ramadh");
|
||||
// Generic placeholder name is replaced by the identity.
|
||||
expect(patch.name).toBe("aghiyaramadh@gmail.com");
|
||||
});
|
||||
|
||||
it("keeps a user-customized name (does not overwrite non-generic names)", async () => {
|
||||
mocks.getProviderConnections.mockResolvedValue([
|
||||
{
|
||||
id: "conn-custom",
|
||||
provider: "codebuddy-intl",
|
||||
authType: "oauth",
|
||||
name: "My Work Account",
|
||||
email: null,
|
||||
displayName: null,
|
||||
accessToken: JWT,
|
||||
},
|
||||
]);
|
||||
|
||||
const backfill = await loadBackfill();
|
||||
await backfill();
|
||||
|
||||
expect(mocks.updateProviderConnection).toHaveBeenCalledTimes(1);
|
||||
const [, patch] = mocks.updateProviderConnection.mock.calls[0];
|
||||
expect(patch.email).toBe("aghiyaramadh@gmail.com");
|
||||
expect(patch.name).toBeUndefined();
|
||||
});
|
||||
|
||||
it("leaves connections that already have identity untouched", async () => {
|
||||
mocks.getProviderConnections.mockResolvedValue([
|
||||
{
|
||||
id: "conn-ok",
|
||||
provider: "codebuddy-intl",
|
||||
authType: "oauth",
|
||||
name: "aghiya ramadh",
|
||||
email: "aghiyaramadh@gmail.com",
|
||||
displayName: "aghiya ramadh",
|
||||
accessToken: JWT,
|
||||
},
|
||||
]);
|
||||
|
||||
const backfill = await loadBackfill();
|
||||
await backfill();
|
||||
expect(mocks.updateProviderConnection).not.toHaveBeenCalled();
|
||||
});
|
||||
it("ignores other providers", async () => {
|
||||
mocks.getProviderConnections.mockResolvedValue([
|
||||
{ id: "c1", provider: "codex", authType: "oauth", email: null, accessToken: JWT },
|
||||
]);
|
||||
|
||||
const backfill = await loadBackfill();
|
||||
await backfill();
|
||||
expect(mocks.updateProviderConnection).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,120 @@
|
||||
// CodeBuddy Intl (.ai) OAuth connections:
|
||||
// 1. The connection test must actually probe the token (was "Provider test not supported"
|
||||
// because codebuddy-intl was missing from OAUTH_TEST_CONFIG).
|
||||
// 2. mapTokens must surface email/displayName from the access token JWT so a fresh OAuth
|
||||
// login is named by identity instead of falling back to "Account N".
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
getProviderConnectionById: vi.fn(),
|
||||
updateProviderConnection: vi.fn(),
|
||||
resolveConnectionProxyConfig: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/localDb", () => ({
|
||||
getProviderConnectionById: mocks.getProviderConnectionById,
|
||||
updateProviderConnection: mocks.updateProviderConnection,
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/network/connectionProxy", () => ({
|
||||
resolveConnectionProxyConfig: mocks.resolveConnectionProxyConfig,
|
||||
}));
|
||||
|
||||
import codebuddyIntl from "../../src/lib/oauth/providers/codebuddy-intl.js";
|
||||
import { testSingleConnection } from "../../src/app/api/providers/[id]/test/testUtils.js";
|
||||
|
||||
// Minimal unsigned JWT (header.payload.sig) — mapTokens only decodes the payload.
|
||||
function makeJwt(payload) {
|
||||
const b64 = (obj) => Buffer.from(JSON.stringify(obj)).toString("base64url");
|
||||
return `${b64({ alg: "RS256", typ: "JWT" })}.${b64(payload)}.sig`;
|
||||
}
|
||||
|
||||
const originalFetch = global.fetch;
|
||||
|
||||
describe("codebuddy-intl mapTokens identity", () => {
|
||||
it("extracts email and display name from the access token JWT", () => {
|
||||
const token = makeJwt({
|
||||
iss: "https://www.codebuddy.ai/auth/realms/copilot",
|
||||
email: "aghiyaramadh@gmail.com",
|
||||
name: "aghiya ramadh",
|
||||
preferred_username: "aghiyaramadh@gmail.com",
|
||||
});
|
||||
|
||||
const out = codebuddyIntl.mapTokens({
|
||||
access_token: token,
|
||||
refresh_token: "rt",
|
||||
expires_in: 3600,
|
||||
});
|
||||
|
||||
expect(out.accessToken).toBe(token);
|
||||
expect(out.refreshToken).toBe("rt");
|
||||
expect(out.email).toBe("aghiyaramadh@gmail.com");
|
||||
expect(out.displayName).toBe("aghiya ramadh");
|
||||
});
|
||||
|
||||
it("falls back to given/family name when name is absent", () => {
|
||||
const token = makeJwt({ email: "a@b.com", given_name: "Aghiya", family_name: "Ramadh" });
|
||||
const out = codebuddyIntl.mapTokens({ access_token: token, expires_in: 3600 });
|
||||
expect(out.email).toBe("a@b.com");
|
||||
expect(out.displayName).toBe("Aghiya Ramadh");
|
||||
});
|
||||
|
||||
it("does not throw and leaves identity null for an opaque (non-JWT) token", () => {
|
||||
const out = codebuddyIntl.mapTokens({ access_token: "opaque-token", expires_in: 3600 });
|
||||
expect(out.accessToken).toBe("opaque-token");
|
||||
expect(out.email).toBeNull();
|
||||
expect(out.displayName).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("codebuddy-intl connection test", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mocks.resolveConnectionProxyConfig.mockResolvedValue({});
|
||||
mocks.updateProviderConnection.mockResolvedValue({});
|
||||
mocks.getProviderConnectionById.mockResolvedValue({
|
||||
id: "conn-cb-intl",
|
||||
provider: "codebuddy-intl",
|
||||
authType: "oauth",
|
||||
accessToken: makeJwt({ email: "aghiyaramadh@gmail.com", name: "aghiya ramadh" }),
|
||||
refreshToken: "rt",
|
||||
expiresAt: new Date(Date.now() + 3600_000).toISOString(),
|
||||
providerSpecificData: {},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
global.fetch = originalFetch;
|
||||
});
|
||||
|
||||
it("probes the token instead of returning 'Provider test not supported'", async () => {
|
||||
const calls = [];
|
||||
global.fetch = vi.fn((url) => {
|
||||
calls.push(String(url));
|
||||
return Promise.resolve(
|
||||
new Response(JSON.stringify({ email: "aghiyaramadh@gmail.com" }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
const result = await testSingleConnection("conn-cb-intl");
|
||||
|
||||
expect(result.error).not.toBe("Provider test not supported");
|
||||
expect(result.valid).toBe(true);
|
||||
expect(calls.length).toBeGreaterThan(0);
|
||||
// Must hit a real identity/usage endpoint on the codebuddy.ai domain.
|
||||
expect(calls.some((u) => u.includes("codebuddy.ai"))).toBe(true);
|
||||
});
|
||||
|
||||
it("marks the connection invalid on 401", async () => {
|
||||
global.fetch = vi.fn(() =>
|
||||
Promise.resolve(new Response("unauthorized", { status: 401 })),
|
||||
);
|
||||
|
||||
const result = await testSingleConnection("conn-cb-intl");
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.error).toMatch(/invalid|revoked/i);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { filterConnectionsForModel } from "../../src/sse/services/auth.js";
|
||||
|
||||
const connections = [
|
||||
{ id: "flash-1", providerSpecificData: { freebuffModel: "deepseek/deepseek-v4-flash" } },
|
||||
{ id: "mimo-1", providerSpecificData: { freebuffModel: "mimo/mimo-v2.5" } },
|
||||
{ id: "unassigned", providerSpecificData: {} },
|
||||
];
|
||||
|
||||
describe("Freebuff strict model assignment", () => {
|
||||
it("keeps only accounts assigned to the requested model", () => {
|
||||
const result = filterConnectionsForModel("freebuff", connections, "mimo/mimo-v2.5", {
|
||||
providerStrategies: { freebuff: { strictModelAssignment: true } },
|
||||
});
|
||||
|
||||
expect(result.map((connection) => connection.id)).toEqual(["mimo-1"]);
|
||||
});
|
||||
|
||||
it("excludes unassigned accounts when strict mode is enabled", () => {
|
||||
const result = filterConnectionsForModel("freebuff", connections, "deepseek/deepseek-v4-flash", {
|
||||
providerStrategies: { freebuff: { strictModelAssignment: true } },
|
||||
});
|
||||
|
||||
expect(result.map((connection) => connection.id)).toEqual(["flash-1"]);
|
||||
});
|
||||
|
||||
it("preserves the existing pool when strict mode is disabled", () => {
|
||||
expect(filterConnectionsForModel("freebuff", connections, "mimo/mimo-v2.5", {})).toBe(connections);
|
||||
});
|
||||
|
||||
it("does not affect other providers when their toggle is off", () => {
|
||||
expect(filterConnectionsForModel("codex", connections, "mimo/mimo-v2.5", {
|
||||
providerStrategies: {},
|
||||
})).toBe(connections);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,834 @@
|
||||
import { describe, it, expect, vi, afterEach, beforeEach } from "vitest";
|
||||
import freebuff from "../../src/lib/oauth/providers/freebuff.js";
|
||||
|
||||
// Mock proxyAwareFetch so session/run/chat flows never hit the network.
|
||||
const fetchMock = vi.fn();
|
||||
vi.mock("../../open-sse/utils/proxyFetch.js", () => ({
|
||||
proxyAwareFetch: (...args) => fetchMock(...args),
|
||||
}));
|
||||
|
||||
import { FreebuffExecutor, __test__ } from "../../open-sse/executors/freebuff.js";
|
||||
|
||||
const {
|
||||
ensureSession,
|
||||
requestSession,
|
||||
startRun,
|
||||
resetSessionCache,
|
||||
rootAgentIdForModel,
|
||||
injectFreebuffMarker,
|
||||
fetchSessionOffers,
|
||||
guardOfferClaim,
|
||||
FREEBUFF_SYSTEM_MARKER,
|
||||
} = __test__;
|
||||
|
||||
const CONFIG = {
|
||||
baseUrl: "https://freebuff.com",
|
||||
loginCodePath: "/api/auth/cli/code",
|
||||
loginStatusPath: "/api/auth/cli/status",
|
||||
};
|
||||
|
||||
function jsonResponse(data, { ok = true, status = 200 } = {}) {
|
||||
return {
|
||||
ok,
|
||||
status,
|
||||
json: async () => data,
|
||||
text: async () => JSON.stringify(data),
|
||||
};
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fetchMock.mockReset();
|
||||
resetSessionCache();
|
||||
});
|
||||
|
||||
describe("freebuff oauth flow", () => {
|
||||
it("requestDeviceCode posts a fingerprint to the freebuff.com login host and surfaces the login URL", async () => {
|
||||
const loginUrl = "https://freebuff.com/login?auth_code=AbCd-123";
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({
|
||||
fingerprintId: "fp-1",
|
||||
fingerprintHash: "hash-1",
|
||||
loginUrl,
|
||||
expiresAt: Date.now() + 60000,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
const out = await freebuff.requestDeviceCode(CONFIG);
|
||||
expect(out.verification_uri_complete).toBe(loginUrl);
|
||||
expect(out.user_code).toBe("AbCd-123");
|
||||
expect(out.interval).toBe(5);
|
||||
expect(out.expires_in).toBe(60);
|
||||
// The server echoes the request host into loginUrl — it must be freebuff.com,
|
||||
// not www.codebuff.com, to match the official CLI's login link.
|
||||
const [url] = global.fetch.mock.calls[0];
|
||||
expect(url.startsWith("https://freebuff.com/api/auth/cli/code")).toBe(true);
|
||||
const payload = JSON.parse(out.device_code);
|
||||
expect(payload.fingerprintId).toBe("fp-1");
|
||||
expect(payload.fingerprintHash).toBe("hash-1");
|
||||
});
|
||||
|
||||
it("requestDeviceCode falls back to oauthTimeoutMs when the server omits expiresAt", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({
|
||||
fingerprintId: "fp-1",
|
||||
fingerprintHash: "hash-1",
|
||||
loginUrl: "https://freebuff.com/login?auth_code=Ab",
|
||||
// no expiresAt — must NOT collapse to the 60s floor
|
||||
}),
|
||||
}),
|
||||
);
|
||||
const out = await freebuff.requestDeviceCode(CONFIG);
|
||||
expect(out.expires_in).toBe(300);
|
||||
});
|
||||
|
||||
it("requestDeviceCode leaves user_code empty when loginUrl has no auth_code", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({
|
||||
fingerprintId: "fp-1",
|
||||
fingerprintHash: "hash-1",
|
||||
loginUrl: "https://freebuff.com/login",
|
||||
expiresAt: Date.now() + 60000,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
const out = await freebuff.requestDeviceCode(CONFIG);
|
||||
expect(out.user_code).toBe("");
|
||||
});
|
||||
|
||||
it("requestDeviceCode clamps expires_in to oauthTimeoutMs", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({
|
||||
fingerprintId: "fp-1",
|
||||
fingerprintHash: "hash-1",
|
||||
loginUrl: "https://freebuff.com/login?auth_code=Ab",
|
||||
// server-side codes live ~1h; the modal deadline must stay at 5 min
|
||||
expiresAt: Date.now() + 3600000,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
const out = await freebuff.requestDeviceCode(CONFIG);
|
||||
expect(out.expires_in).toBe(300);
|
||||
});
|
||||
|
||||
it("pollToken keeps polling on 401 pending (GET with query params)", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
status: 401,
|
||||
json: async () => ({ error: "Authentication failed" }),
|
||||
}),
|
||||
);
|
||||
const res = await freebuff.pollToken(
|
||||
CONFIG,
|
||||
JSON.stringify({ fingerprintId: "fp-1", fingerprintHash: "h", expiresAt: 123 }),
|
||||
);
|
||||
expect(res.ok).toBe(true);
|
||||
expect(res.data.error).toBe("authorization_pending");
|
||||
const [url, opts] = global.fetch.mock.calls[0];
|
||||
expect(url).toContain("https://freebuff.com/api/auth/cli/status?");
|
||||
expect(url).toContain("fingerprintId=fp-1");
|
||||
expect(opts.method).toBe("GET");
|
||||
});
|
||||
|
||||
it("pollToken returns the authToken on success", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({
|
||||
user: { id: "u1", email: "a@b.c", name: "A", authToken: "tok-123", fingerprintId: "fp-1" },
|
||||
}),
|
||||
}),
|
||||
);
|
||||
const res = await freebuff.pollToken(
|
||||
CONFIG,
|
||||
JSON.stringify({ fingerprintId: "fp-1", fingerprintHash: "h", expiresAt: 123 }),
|
||||
);
|
||||
expect(res.data.access_token).toBe("tok-123");
|
||||
});
|
||||
|
||||
it("mapTokens stores accessToken + identity", () => {
|
||||
const t = freebuff.mapTokens({
|
||||
access_token: "tok",
|
||||
email: "a@b.c",
|
||||
name: "A",
|
||||
id: "u1",
|
||||
fingerprintId: "fp",
|
||||
});
|
||||
expect(t.accessToken).toBe("tok");
|
||||
expect(t.email).toBe("a@b.c");
|
||||
expect(t.displayName).toBe("A");
|
||||
expect(t.refreshToken).toBeNull();
|
||||
expect(t.providerSpecificData.fingerprintId).toBe("fp");
|
||||
expect(t.providerSpecificData.authMethod).toBe("device_code");
|
||||
});
|
||||
});
|
||||
|
||||
describe("freebuff executor wire shape", () => {
|
||||
it("injects codebuff_metadata at TOP LEVEL (mirrors the CLI, not nested under codebuff)", () => {
|
||||
const ex = new FreebuffExecutor();
|
||||
const body = { model: "deepseek/deepseek-v4-flash", messages: [{ role: "user", content: "hi" }] };
|
||||
const out = ex.transformRequest(body.model, body, true, {
|
||||
providerSpecificData: { fingerprintId: "fp-1" },
|
||||
});
|
||||
// Top-level keys — the backend rejects the nested shape with
|
||||
// "No runId found in request body".
|
||||
expect(out.codebuff_metadata.cost_mode).toBe("free");
|
||||
expect(out.codebuff_metadata.client_id).toBe("fp-1");
|
||||
// run_id is the registered runId and is attached by execute(), not here.
|
||||
expect(out.codebuff_metadata.run_id).toBeUndefined();
|
||||
expect(out.codebuff).toBeUndefined();
|
||||
expect(out.provider.allow_fallbacks).toBe(false);
|
||||
// Free-tier marker is prepended so the first message opens with the CLI root prompt.
|
||||
expect(out.messages[0].content).toBe(FREEBUFF_SYSTEM_MARKER);
|
||||
});
|
||||
|
||||
it("buildUrl targets the Codebuff chat completions endpoint (www.codebuff.com)", () => {
|
||||
const ex = new FreebuffExecutor();
|
||||
expect(ex.buildUrl()).toBe("https://www.codebuff.com/api/v1/chat/completions");
|
||||
});
|
||||
|
||||
it("injects the end_turn tool into any tool-calling request (backend foreign_toolset gate)", () => {
|
||||
const ex = new FreebuffExecutor();
|
||||
const body = {
|
||||
model: "deepseek/deepseek-v4-flash",
|
||||
messages: [{ role: "user", content: "hi" }],
|
||||
tools: [{ type: "function", function: { name: "read_file", description: "read" } }],
|
||||
};
|
||||
const out = ex.transformRequest(body.model, body, true, { providerSpecificData: { fingerprintId: "fp-1" } });
|
||||
const names = out.tools.map((t) => t.function.name);
|
||||
expect(names).toContain("read_file");
|
||||
expect(names).toContain("end_turn");
|
||||
expect(out.tools[out.tools.length - 1].function).toMatchObject({
|
||||
name: "end_turn",
|
||||
description: "Signal the end of the current task.",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not inject end_turn when the request has no tools", () => {
|
||||
const ex = new FreebuffExecutor();
|
||||
const body = { model: "deepseek/deepseek-v4-flash", messages: [{ role: "user", content: "hi" }] };
|
||||
const out = ex.transformRequest(body.model, body, true, { providerSpecificData: { fingerprintId: "fp-1" } });
|
||||
expect(out.tools).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not duplicate end_turn when the caller already declared it", () => {
|
||||
const ex = new FreebuffExecutor();
|
||||
const endTurn = { type: "function", function: { name: "end_turn", description: "Signal the end of the current task.", parameters: { type: "object", properties: {} } } };
|
||||
const body = {
|
||||
model: "deepseek/deepseek-v4-flash",
|
||||
messages: [{ role: "user", content: "hi" }],
|
||||
tools: [endTurn],
|
||||
};
|
||||
const out = ex.transformRequest(body.model, body, true, { providerSpecificData: { fingerprintId: "fp-1" } });
|
||||
expect(out.tools).toHaveLength(1);
|
||||
expect(out.tools[0].function.name).toBe("end_turn");
|
||||
});
|
||||
});
|
||||
|
||||
describe("freebuff session pre-flight", () => {
|
||||
it("claims a session via POST /session with x-freebuff-model and caches it per token+model", async () => {
|
||||
fetchMock.mockResolvedValue(
|
||||
jsonResponse({
|
||||
status: "active",
|
||||
instanceId: "inst-1",
|
||||
model: "deepseek/deepseek-v4-flash",
|
||||
expiresAt: new Date(Date.now() + 3600000).toISOString(),
|
||||
}),
|
||||
);
|
||||
const first = await ensureSession("tok-1", "deepseek/deepseek-v4-flash", null);
|
||||
expect(first).toEqual({ instanceId: "inst-1", status: "active" });
|
||||
|
||||
const [url, opts] = fetchMock.mock.calls[0];
|
||||
expect(url).toBe("https://www.codebuff.com/api/v1/freebuff/session");
|
||||
expect(opts.method).toBe("POST");
|
||||
expect(opts.headers["x-freebuff-model"]).toBe("deepseek/deepseek-v4-flash");
|
||||
expect(opts.headers.Authorization).toBe("Bearer tok-1");
|
||||
|
||||
// Second call for the same token+model hits the cache — no new claim.
|
||||
await ensureSession("tok-1", "deepseek/deepseek-v4-flash", null);
|
||||
expect(fetchMock.mock.calls.length).toBe(1);
|
||||
|
||||
// Different model → separate claim.
|
||||
fetchMock.mockResolvedValue(
|
||||
jsonResponse({ status: "active", instanceId: "inst-2", expiresAt: new Date(Date.now() + 3600000).toISOString() }),
|
||||
);
|
||||
await ensureSession("tok-1", "z-ai/glm-5.3-flash", null);
|
||||
expect(fetchMock.mock.calls.length).toBe(2);
|
||||
});
|
||||
|
||||
it("treats status none as no-session-needed (instanceId null)", async () => {
|
||||
fetchMock.mockResolvedValue(jsonResponse({ status: "none", accessTier: "full" }));
|
||||
const res = await ensureSession("tok-1", "deepseek/deepseek-v4-flash", null);
|
||||
expect(res).toEqual({ instanceId: null, status: "none" });
|
||||
});
|
||||
|
||||
it("throws a friendly error on rate_limited", async () => {
|
||||
fetchMock.mockResolvedValue(
|
||||
jsonResponse({ status: "rate_limited", message: "4 of 6 sessions used today" }),
|
||||
);
|
||||
await expect(ensureSession("tok-1", "deepseek/deepseek-v4-flash", null)).rejects.toThrow(
|
||||
/session limit reached/i,
|
||||
);
|
||||
});
|
||||
|
||||
it("rate_limited with a resetAt locks the account until the daily Pacific reset (skip the day, not retry loops)", async () => {
|
||||
const resetAt = "2099-01-01T00:00:00.000Z";
|
||||
fetchMock.mockResolvedValue(
|
||||
jsonResponse({
|
||||
status: "rate_limited",
|
||||
resetAt,
|
||||
retryAfterMs: 1234,
|
||||
freebucksShortfall: { price: 15, balance: 0 },
|
||||
message: "Freebucks exhausted",
|
||||
}),
|
||||
);
|
||||
await expect(requestSession("tok-1", "deepseek/deepseek-v4-flash", null)).rejects.toMatchObject({
|
||||
status: 429,
|
||||
resetsAtMs: Date.parse(resetAt),
|
||||
});
|
||||
});
|
||||
|
||||
it("spend_limited falls back to retryAfterMs when resetAt is absent", async () => {
|
||||
const retryAfterMs = 90 * 60 * 1000;
|
||||
fetchMock.mockResolvedValue(
|
||||
jsonResponse({ status: "spend_limited", retryAfterMs, message: "daily spend cap" }),
|
||||
);
|
||||
const before = Date.now();
|
||||
try {
|
||||
await requestSession("tok-1", "deepseek/deepseek-v4-flash", null);
|
||||
throw new Error("should have rejected");
|
||||
} catch (error) {
|
||||
expect(error.status).toBe(429);
|
||||
expect(error.resetsAtMs).toBeGreaterThanOrEqual(before + retryAfterMs - 1000);
|
||||
expect(error.resetsAtMs).toBeLessThanOrEqual(before + retryAfterMs + 1000);
|
||||
}
|
||||
});
|
||||
|
||||
it("handles spend_limited arriving as HTTP 429 (the actual wire shape) — still skips until reset", async () => {
|
||||
const resetAt = "2099-01-01T00:00:00.000Z";
|
||||
fetchMock.mockResolvedValue(
|
||||
jsonResponse(
|
||||
{
|
||||
status: "spend_limited",
|
||||
accessTier: "full",
|
||||
upgrade: { url: "https://freebuff.com/plans", message: "Get 150 Freebucks a day from $8/mo." },
|
||||
message: "This account hit today's hard usage cap.",
|
||||
resetAt,
|
||||
},
|
||||
{ status: 429, ok: false },
|
||||
),
|
||||
);
|
||||
await expect(requestSession("tok-1", "deepseek/deepseek-v4-flash", null)).rejects.toMatchObject({
|
||||
status: 429,
|
||||
resetsAtMs: Date.parse(resetAt),
|
||||
});
|
||||
});
|
||||
|
||||
it("handles rate_limited arriving as HTTP 429 with only retryAfterMs", async () => {
|
||||
const retryAfterMs = 15 * 60 * 1000;
|
||||
fetchMock.mockResolvedValue(
|
||||
jsonResponse({ status: "rate_limited", retryAfterMs, message: "limit" }, { status: 429, ok: false }),
|
||||
);
|
||||
const before = Date.now();
|
||||
try {
|
||||
await requestSession("tok-1", "deepseek/deepseek-v4-flash", null);
|
||||
throw new Error("should have rejected");
|
||||
} catch (error) {
|
||||
expect(error.status).toBe(429);
|
||||
expect(error.resetsAtMs).toBeGreaterThanOrEqual(before + retryAfterMs - 1000);
|
||||
expect(error.resetsAtMs).toBeLessThanOrEqual(before + retryAfterMs + 1000);
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps an unknown HTTP 429 as a generic failure (no gate status → no resetsAtMs)", async () => {
|
||||
fetchMock.mockResolvedValue(jsonResponse({ error: "nope" }, { status: 429, ok: false }));
|
||||
try {
|
||||
await requestSession("tok-1", "deepseek/deepseek-v4-flash", null);
|
||||
throw new Error("should have rejected");
|
||||
} catch (error) {
|
||||
expect(error.status).toBe(429);
|
||||
expect(error.resetsAtMs).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it("rate_limited without any reset hint stays a plain error (transient cooldown path)", async () => {
|
||||
fetchMock.mockResolvedValue(jsonResponse({ status: "rate_limited", message: "busy" }));
|
||||
try {
|
||||
await requestSession("tok-1", "deepseek/deepseek-v4-flash", null);
|
||||
throw new Error("should have rejected");
|
||||
} catch (error) {
|
||||
expect(error.status).toBeUndefined();
|
||||
expect(error.resetsAtMs).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it("throws a friendly error on country_blocked", async () => {
|
||||
fetchMock.mockResolvedValue(jsonResponse({ status: "country_blocked" }));
|
||||
await expect(ensureSession("tok-1", "deepseek/deepseek-v4-flash", null)).rejects.toThrow(
|
||||
/not available in your region/i,
|
||||
);
|
||||
});
|
||||
|
||||
it("throws a 401 re-login error when the session endpoint rejects the token", async () => {
|
||||
fetchMock.mockResolvedValue(jsonResponse({ error: "unauthorized" }, { status: 401, ok: false }));
|
||||
await expect(requestSession("tok-expired", "deepseek/deepseek-v4-flash", null)).rejects.toThrow(/re-login/i);
|
||||
});
|
||||
});
|
||||
|
||||
describe("freebuff limited-offer (Claude Fable 5) claims", () => {
|
||||
const FABLE = "anthropic/claude-fable-5";
|
||||
const offerRow = (over = {}) => ({
|
||||
model: FABLE,
|
||||
remaining: 3,
|
||||
total: 10,
|
||||
userRemaining: 1,
|
||||
userResetAt: new Date(Date.now() + 3600000).toISOString(),
|
||||
...over,
|
||||
});
|
||||
|
||||
it("GETs limitedModelOffers (never claims) and caches them per token", async () => {
|
||||
fetchMock.mockResolvedValue(jsonResponse({ status: "none", limitedModelOffers: [offerRow()] }));
|
||||
const offers = await fetchSessionOffers("tok-1", null);
|
||||
expect(offers.map((o) => o.model)).toEqual([FABLE]);
|
||||
|
||||
const [url, opts] = fetchMock.mock.calls[0];
|
||||
expect(url).toBe("https://www.codebuff.com/api/v1/freebuff/session");
|
||||
expect(opts.method).toBe("GET");
|
||||
expect(opts.headers.Authorization).toBe("Bearer tok-1");
|
||||
expect(opts.headers.Accept).toBe("application/json");
|
||||
|
||||
// Second read within the cache TTL does not refetch.
|
||||
await fetchSessionOffers("tok-1", null);
|
||||
expect(fetchMock.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it("allows the claim while the offer is open", async () => {
|
||||
fetchMock.mockResolvedValue(jsonResponse({ status: "none", limitedModelOffers: [offerRow()] }));
|
||||
expect(await guardOfferClaim("tok-1", FABLE, null)).toMatchObject({ model: FABLE, remaining: 3 });
|
||||
expect(fetchMock.mock.calls.length).toBe(1);
|
||||
expect(fetchMock.mock.calls[0][1].method).toBe("GET");
|
||||
});
|
||||
|
||||
it("lets non-offer models claim without any offer GET", async () => {
|
||||
expect(await guardOfferClaim("tok-1", "deepseek/deepseek-v4-flash", null)).toBeNull();
|
||||
expect(fetchMock.mock.calls.length).toBe(0);
|
||||
});
|
||||
|
||||
it("refuses the claim when the wave pool is closed (no offer row)", async () => {
|
||||
fetchMock.mockResolvedValue(jsonResponse({ status: "none", limitedModelOffers: [] }));
|
||||
await expect(guardOfferClaim("tok-1", FABLE, null)).rejects.toThrow(/not being offered right now/i);
|
||||
});
|
||||
|
||||
it("refuses the claim when the account's daily Fable sessions are used up", async () => {
|
||||
fetchMock.mockResolvedValue(
|
||||
jsonResponse({ status: "none", limitedModelOffers: [offerRow({ userRemaining: 0 })] }),
|
||||
);
|
||||
await expect(guardOfferClaim("tok-1", FABLE, null)).rejects.toThrow(/has used its Claude Fable 5 sessions/i);
|
||||
});
|
||||
|
||||
it("claims a Fable session only after the offer passes: GET offers, then POST claim", async () => {
|
||||
fetchMock.mockImplementation(async (url, opts = {}) => {
|
||||
if (url.includes("/freebuff/session") && opts.method === "GET") {
|
||||
return jsonResponse({ status: "none", limitedModelOffers: [offerRow()] });
|
||||
}
|
||||
if (url.includes("/freebuff/session")) {
|
||||
return jsonResponse({ status: "active", instanceId: "inst-fable", expiresAt: new Date(Date.now() + 3600000).toISOString() });
|
||||
}
|
||||
return jsonResponse({ ok: false }, { status: 500, ok: false });
|
||||
});
|
||||
|
||||
const res = await ensureSession("tok-1", FABLE, null);
|
||||
expect(res).toEqual({ instanceId: "inst-fable", status: "active" });
|
||||
|
||||
const methods = fetchMock.mock.calls.map(([, o]) => o.method);
|
||||
expect(methods).toEqual(["GET", "POST"]);
|
||||
const [, postOpts] = fetchMock.mock.calls[1];
|
||||
expect(postOpts.headers["x-freebuff-model"]).toBe(FABLE);
|
||||
|
||||
// Cached claim → no further requests.
|
||||
await ensureSession("tok-1", FABLE, null);
|
||||
expect(fetchMock.mock.calls.length).toBe(2);
|
||||
});
|
||||
|
||||
it("never POSTs a claim when the Fable wave is closed", async () => {
|
||||
fetchMock.mockResolvedValue(jsonResponse({ status: "none", limitedModelOffers: [] }));
|
||||
await expect(ensureSession("tok-1", FABLE, null)).rejects.toThrow(/not being offered right now/i);
|
||||
const methods = fetchMock.mock.calls.map(([, o]) => o.method);
|
||||
expect(methods).toEqual(["GET"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("freebuff free-tier system marker", () => {
|
||||
it("prepends the canonical marker when the first message is a system prompt", () => {
|
||||
const out = injectFreebuffMarker({
|
||||
messages: [{ role: "system", content: "You are a helpful assistant." }, { role: "user", content: "hi" }],
|
||||
});
|
||||
expect(out.messages[0].content).toBe(`${FREEBUFF_SYSTEM_MARKER}\n\nYou are a helpful assistant.`);
|
||||
expect(out.messages[1].role).toBe("user");
|
||||
});
|
||||
|
||||
it("inserts a marker system message when the first message is not a system prompt", () => {
|
||||
const out = injectFreebuffMarker({ messages: [{ role: "user", content: "hi" }] });
|
||||
expect(out.messages[0]).toEqual({ role: "system", content: FREEBUFF_SYSTEM_MARKER });
|
||||
expect(out.messages[1]).toEqual({ role: "user", content: "hi" });
|
||||
});
|
||||
|
||||
it("is idempotent when the first system message already opens with the marker", () => {
|
||||
const messages = [{ role: "system", content: FREEBUFF_SYSTEM_MARKER }];
|
||||
const out = injectFreebuffMarker({ messages });
|
||||
expect(out.messages).toBe(messages);
|
||||
});
|
||||
|
||||
it("inserts a marker system message when the first system content is a block array", () => {
|
||||
const out = injectFreebuffMarker({
|
||||
messages: [{ role: "system", content: [{ type: "text", text: "hi" }] }, { role: "user", content: "x" }],
|
||||
});
|
||||
expect(out.messages[0]).toEqual({ role: "system", content: FREEBUFF_SYSTEM_MARKER });
|
||||
expect(out.messages[1].content).toEqual([{ type: "text", text: "hi" }]);
|
||||
expect(out.messages[2].content).toBe("x");
|
||||
});
|
||||
});
|
||||
|
||||
describe("freebuff run registration", () => {
|
||||
it("maps freebuff models to their root free agent ids", () => {
|
||||
expect(rootAgentIdForModel("deepseek/deepseek-v4-flash")).toBe("base3-free-deepseek-flash");
|
||||
expect(rootAgentIdForModel("z-ai/glm-5.3-flash")).toBe("base3-free-glm-5-3-flash");
|
||||
expect(rootAgentIdForModel("z-ai/glm-5.2")).toBe("base3-free-glm");
|
||||
expect(rootAgentIdForModel("mimo/mimo-v2.5")).toBe("base3-free-mimo");
|
||||
expect(rootAgentIdForModel("openai/gpt-5.6-luna")).toBe("base3-free-luna");
|
||||
expect(rootAgentIdForModel("upstage/solar-pro4")).toBe("base3-free-solar-pro4");
|
||||
expect(rootAgentIdForModel("meta/muse-spark-1.2-contributor")).toBe("base3-free-muse-spark");
|
||||
expect(rootAgentIdForModel("anthropic/claude-fable-5")).toBe("base3-free-fable");
|
||||
// Withdrawn upstream models are unmapped — they fall back, and the backend
|
||||
// refuses their sessions anyway.
|
||||
expect(rootAgentIdForModel("meta/muse-spark-1.3-contributor")).toBe("base2-free");
|
||||
expect(rootAgentIdForModel("deepseek/deepseek-v4-pro")).toBe("base2-free");
|
||||
expect(rootAgentIdForModel("minimax/minimax-m3")).toBe("base2-free");
|
||||
expect(rootAgentIdForModel("some/unknown-model")).toBe("base2-free");
|
||||
});
|
||||
|
||||
it("registers a run via POST /agent-runs and returns the runId", async () => {
|
||||
fetchMock.mockResolvedValue(jsonResponse({ runId: "run-abc" }));
|
||||
const runId = await startRun("tok-1", "deepseek/deepseek-v4-flash", null);
|
||||
expect(runId).toBe("run-abc");
|
||||
|
||||
const [url, opts] = fetchMock.mock.calls[0];
|
||||
expect(url).toBe("https://www.codebuff.com/api/v1/agent-runs");
|
||||
expect(opts.method).toBe("POST");
|
||||
expect(opts.headers.Authorization).toBe("Bearer tok-1");
|
||||
const payload = JSON.parse(opts.body);
|
||||
expect(payload.action).toBe("START");
|
||||
expect(payload.agentId).toBe("base3-free-deepseek-flash");
|
||||
expect(payload.ancestorRunIds).toEqual([]);
|
||||
});
|
||||
|
||||
it("throws when the run start fails", async () => {
|
||||
fetchMock.mockResolvedValue(jsonResponse({ error: "bad" }, { status: 500, ok: false }));
|
||||
await expect(startRun("tok-1", "deepseek/deepseek-v4-flash", null)).rejects.toThrow(/run start failed/i);
|
||||
});
|
||||
|
||||
it("retries transient network errors on run registration", async () => {
|
||||
fetchMock.mockImplementation(async (url) => {
|
||||
if (url.includes("/agent-runs")) {
|
||||
const calls = fetchMock.mock.calls.filter(([u]) => u.includes("/agent-runs")).length;
|
||||
if (calls === 1) throw new Error("fetch failed (cause: ECONNRESET)");
|
||||
return jsonResponse({ runId: "run-retried" });
|
||||
}
|
||||
throw new Error("unexpected url");
|
||||
});
|
||||
const runId = await startRun("tok-1", "deepseek/deepseek-v4-flash", null);
|
||||
expect(runId).toBe("run-retried");
|
||||
});
|
||||
});
|
||||
|
||||
describe("freebuff executor execute", () => {
|
||||
const CHAT_URL = "https://www.codebuff.com/api/v1/chat/completions";
|
||||
const SESSION_URL = "https://www.codebuff.com/api/v1/freebuff/session";
|
||||
const RUN_URL = "https://www.codebuff.com/api/v1/agent-runs";
|
||||
const MODEL = "deepseek/deepseek-v4-flash";
|
||||
const credentials = { accessToken: "tok-1", providerSpecificData: { fingerprintId: "fp-1" } };
|
||||
|
||||
// Default happy-path backend: session active, run registered, chat 200.
|
||||
const happyPath = () => {
|
||||
fetchMock.mockImplementation(async (url) => {
|
||||
if (url === SESSION_URL) {
|
||||
return jsonResponse({ status: "active", instanceId: "inst-1", expiresAt: new Date(Date.now() + 3600000).toISOString() });
|
||||
}
|
||||
if (url === RUN_URL) {
|
||||
return jsonResponse({ runId: "run-1" });
|
||||
}
|
||||
return jsonResponse({ choices: [{ message: { content: "hi" } }] });
|
||||
});
|
||||
};
|
||||
|
||||
it("sends the registered runId + session instance id on the chat request", async () => {
|
||||
happyPath();
|
||||
|
||||
const ex = new FreebuffExecutor();
|
||||
const body = { model: MODEL, messages: [{ role: "user", content: "hi" }] };
|
||||
const { response } = await ex.execute({ model: MODEL, body, stream: false, credentials, log: null });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
const chatCall = fetchMock.mock.calls.find(([u]) => u === CHAT_URL);
|
||||
expect(chatCall).toBeTruthy();
|
||||
const sent = JSON.parse(chatCall[1].body);
|
||||
expect(sent.codebuff_metadata.run_id).toBe("run-1");
|
||||
expect(sent.codebuff_metadata.freebuff_instance_id).toBe("inst-1");
|
||||
expect(sent.codebuff_metadata.trace_session_id).toMatch(
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/,
|
||||
);
|
||||
expect(sent.codebuff_metadata.cost_mode).toBe("free");
|
||||
expect(sent.codebuff_metadata.client_id).toBe("fp-1");
|
||||
expect(sent.codebuff).toBeUndefined();
|
||||
// Free-tier marker present at position 0 of the request body.
|
||||
expect(sent.messages[0].content.startsWith("You are Buffy,")).toBe(true);
|
||||
});
|
||||
|
||||
it("retries exactly once on 428 with a fresh session AND a fresh run", async () => {
|
||||
let chatHits = 0;
|
||||
fetchMock.mockImplementation(async (url) => {
|
||||
if (url === SESSION_URL) {
|
||||
return jsonResponse({ status: "active", instanceId: "inst-2", expiresAt: new Date(Date.now() + 3600000).toISOString() });
|
||||
}
|
||||
if (url === RUN_URL) {
|
||||
return jsonResponse({ runId: "run-2" });
|
||||
}
|
||||
chatHits += 1;
|
||||
if (chatHits === 1) return jsonResponse({ error: "waiting_room_required" }, { status: 428, ok: false });
|
||||
return jsonResponse({ choices: [{ message: { content: "hi" } }] });
|
||||
});
|
||||
|
||||
const ex = new FreebuffExecutor();
|
||||
const body = { model: MODEL, messages: [{ role: "user", content: "hi" }] };
|
||||
const { response } = await ex.execute({ model: MODEL, body, stream: false, credentials, log: null });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(chatHits).toBe(2);
|
||||
// Session was claimed twice (initial + forced re-claim).
|
||||
expect(fetchMock.mock.calls.filter(([u]) => u === SESSION_URL).length).toBe(2);
|
||||
// Runs: START #1, FINISH(cancelled) #1 (abandoned on 428), START #2,
|
||||
// FINISH(completed) #2.
|
||||
const runCalls = fetchMock.mock.calls.filter(([u]) => u === RUN_URL);
|
||||
expect(runCalls.length).toBe(4);
|
||||
const runActions = runCalls.map((c) => JSON.parse(c[1].body).action);
|
||||
expect(runActions.filter((a) => a === "START").length).toBe(2);
|
||||
expect(runActions.filter((a) => a === "FINISH").length).toBe(2);
|
||||
const finishPayload = JSON.parse(runCalls[runCalls.length - 1][1].body);
|
||||
expect(finishPayload.status).toBe("completed");
|
||||
// The retried chat request carried the re-claimed session + fresh run.
|
||||
const lastChat = fetchMock.mock.calls.filter(([u]) => u === CHAT_URL).pop();
|
||||
const sent = JSON.parse(lastChat[1].body);
|
||||
expect(sent.codebuff_metadata.run_id).toBe("run-2");
|
||||
expect(sent.codebuff_metadata.freebuff_instance_id).toBe("inst-2");
|
||||
});
|
||||
|
||||
it("re-claims the session on 409 session_superseded and retries once", async () => {
|
||||
let chatHits = 0;
|
||||
fetchMock.mockImplementation(async (url) => {
|
||||
if (url === SESSION_URL) return jsonResponse({ status: "active", instanceId: "inst-2", expiresAt: new Date(Date.now() + 3600000).toISOString() });
|
||||
if (url === RUN_URL) return jsonResponse({ runId: "run-2" });
|
||||
chatHits += 1;
|
||||
if (chatHits === 1) {
|
||||
return jsonResponse({ error: "session_superseded", message: "Another instance took over" }, { status: 409, ok: false });
|
||||
}
|
||||
return jsonResponse({ choices: [{ message: { content: "hi" } }] });
|
||||
});
|
||||
|
||||
const ex = new FreebuffExecutor();
|
||||
const body = { model: MODEL, messages: [{ role: "user", content: "hi" }] };
|
||||
const { response } = await ex.execute({ model: MODEL, body, stream: false, credentials, log: null });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(chatHits).toBe(2);
|
||||
// Session re-claimed (initial + forced) and runs restarted.
|
||||
expect(fetchMock.mock.calls.filter(([u]) => u === SESSION_URL).length).toBe(2);
|
||||
const runCalls = fetchMock.mock.calls.filter(([u]) => u === RUN_URL);
|
||||
expect(runCalls.filter((c) => JSON.parse(c[1].body).action === "START").length).toBe(2);
|
||||
});
|
||||
|
||||
it("re-claims the session on 410 session_expired and retries once", async () => {
|
||||
let chatHits = 0;
|
||||
fetchMock.mockImplementation(async (url) => {
|
||||
if (url === SESSION_URL) return jsonResponse({ status: "active", instanceId: "inst-2", expiresAt: new Date(Date.now() + 3600000).toISOString() });
|
||||
if (url === RUN_URL) return jsonResponse({ runId: "run-2" });
|
||||
chatHits += 1;
|
||||
if (chatHits === 1) return jsonResponse({ error: "session_expired" }, { status: 410, ok: false });
|
||||
return jsonResponse({ choices: [{ message: { content: "hi" } }] });
|
||||
});
|
||||
|
||||
const ex = new FreebuffExecutor();
|
||||
const body = { model: MODEL, messages: [{ role: "user", content: "hi" }] };
|
||||
const { response } = await ex.execute({ model: MODEL, body, stream: false, credentials, log: null });
|
||||
expect(response.status).toBe(200);
|
||||
expect(chatHits).toBe(2);
|
||||
});
|
||||
|
||||
it("throws a 401 re-login error when the chat endpoint rejects the token", async () => {
|
||||
fetchMock.mockImplementation(async (url) => {
|
||||
if (url === SESSION_URL) return jsonResponse({ status: "active", instanceId: "inst-1", expiresAt: new Date(Date.now() + 3600000).toISOString() });
|
||||
if (url === RUN_URL) return jsonResponse({ runId: "run-1" });
|
||||
return jsonResponse({ error: "unauthorized" }, { status: 401, ok: false });
|
||||
});
|
||||
|
||||
const ex = new FreebuffExecutor();
|
||||
const body = { model: MODEL, messages: [{ role: "user", content: "hi" }] };
|
||||
await expect(
|
||||
ex.execute({ model: MODEL, body, stream: false, credentials, log: null }),
|
||||
).rejects.toThrow(/re-login/i);
|
||||
});
|
||||
|
||||
it("throws when no access token is present", async () => {
|
||||
const ex = new FreebuffExecutor();
|
||||
await expect(
|
||||
ex.execute({ model: MODEL, body: { messages: [] }, stream: false, credentials: {}, log: null }),
|
||||
).rejects.toThrow(/no access token/i);
|
||||
});
|
||||
|
||||
it("finishes the run as failed when the chat upstream errors", async () => {
|
||||
// 400 (not in the 429/502/503 retry set) so the test stays fast and mirrors
|
||||
// the real upstream rejection.
|
||||
fetchMock.mockImplementation(async (url) => {
|
||||
if (url === SESSION_URL) return jsonResponse({ status: "active", instanceId: "inst-1", expiresAt: new Date(Date.now() + 3600000).toISOString() });
|
||||
if (url === RUN_URL) return jsonResponse({ runId: "run-1" });
|
||||
return jsonResponse({ error: "upstream boom" }, { status: 400, ok: false });
|
||||
});
|
||||
|
||||
const ex = new FreebuffExecutor();
|
||||
const body = { model: MODEL, messages: [{ role: "user", content: "hi" }] };
|
||||
const { response } = await ex.execute({ model: MODEL, body, stream: false, credentials, log: null });
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
const runCalls = fetchMock.mock.calls.filter(([u]) => u === RUN_URL);
|
||||
expect(runCalls.length).toBe(2); // START + FINISH
|
||||
const finishPayload = JSON.parse(runCalls[1][1].body);
|
||||
expect(finishPayload.action).toBe("FINISH");
|
||||
expect(finishPayload.status).toBe("failed");
|
||||
});
|
||||
|
||||
it("finishes the run as failed when execute throws mid-flight", async () => {
|
||||
// AbortError (caller/stream abort) is never retried, keeping this test fast.
|
||||
fetchMock.mockImplementation(async (url) => {
|
||||
if (url === SESSION_URL) return jsonResponse({ status: "active", instanceId: "inst-1", expiresAt: new Date(Date.now() + 3600000).toISOString() });
|
||||
if (url === RUN_URL) return jsonResponse({ runId: "run-1" });
|
||||
throw Object.assign(new Error("aborted"), { name: "AbortError" });
|
||||
});
|
||||
|
||||
const ex = new FreebuffExecutor();
|
||||
const body = { model: MODEL, messages: [{ role: "user", content: "hi" }] };
|
||||
await expect(
|
||||
ex.execute({ model: MODEL, body, stream: false, credentials, log: null }),
|
||||
).rejects.toThrow(/aborted/);
|
||||
|
||||
const runCalls = fetchMock.mock.calls.filter(([u]) => u === RUN_URL);
|
||||
expect(runCalls.length).toBe(2); // START + FINISH(failed) from the finally block
|
||||
const finishPayload = JSON.parse(runCalls[1][1].body);
|
||||
expect(finishPayload.action).toBe("FINISH");
|
||||
expect(finishPayload.status).toBe("failed");
|
||||
});
|
||||
|
||||
it("retries the chat POST on a transient fetch-level network error", async () => {
|
||||
let chatHits = 0;
|
||||
fetchMock.mockImplementation(async (url) => {
|
||||
if (url === SESSION_URL) return jsonResponse({ status: "active", instanceId: "inst-1", expiresAt: new Date(Date.now() + 3600000).toISOString() });
|
||||
if (url === RUN_URL) return jsonResponse({ runId: "run-1" });
|
||||
chatHits += 1;
|
||||
if (chatHits === 1) throw new Error("fetch failed (cause: ECONNRESET)");
|
||||
return jsonResponse({ choices: [{ message: { content: "hi" } }] });
|
||||
});
|
||||
|
||||
const ex = new FreebuffExecutor();
|
||||
const body = { model: MODEL, messages: [{ role: "user", content: "hi" }] };
|
||||
const { response } = await ex.execute({ model: MODEL, body, stream: false, credentials, log: null });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(chatHits).toBe(2);
|
||||
// Run FINISHed exactly once (completed) — no double-FINISH from the retry.
|
||||
const runCalls = fetchMock.mock.calls.filter(([u]) => u === RUN_URL);
|
||||
expect(runCalls.length).toBe(2); // START + FINISH(completed)
|
||||
expect(JSON.parse(runCalls[1][1].body).status).toBe("completed");
|
||||
});
|
||||
|
||||
it("does not double-FINISH the abandoned run when the re-claim fails", async () => {
|
||||
let chatHits = 0;
|
||||
let runStartCount = 0;
|
||||
fetchMock.mockImplementation(async (url) => {
|
||||
if (url === SESSION_URL) return jsonResponse({ status: "active", instanceId: "inst-2", expiresAt: new Date(Date.now() + 3600000).toISOString() });
|
||||
if (url === RUN_URL) {
|
||||
// First START succeeds (run-1). Every later call fails with the
|
||||
// transient ECONNRESET: the fire-and-forget FINISH swallows it, and
|
||||
// the re-claim START propagates after its 3 network-retry attempts.
|
||||
if (runStartCount === 0) {
|
||||
runStartCount += 1;
|
||||
return jsonResponse({ runId: "run-1" });
|
||||
}
|
||||
throw new Error("fetch failed (cause: ECONNRESET)");
|
||||
}
|
||||
chatHits += 1;
|
||||
if (chatHits === 1) return jsonResponse({ error: "session_superseded" }, { status: 409, ok: false });
|
||||
return jsonResponse({ choices: [{ message: { content: "hi" } }] });
|
||||
});
|
||||
|
||||
const ex = new FreebuffExecutor();
|
||||
const body = { model: MODEL, messages: [{ role: "user", content: "hi" }] };
|
||||
// The re-claim failure rethrows the raw upstream error (the log line above
|
||||
// it carries the "session re-claim failed" context).
|
||||
await expect(
|
||||
ex.execute({ model: MODEL, body, stream: false, credentials, log: null }),
|
||||
).rejects.toThrow(/fetch failed \(cause: ECONNRESET\)/);
|
||||
|
||||
// run-1 was FINISH'd exactly once, as "cancelled" — the failed re-claim
|
||||
// must NOT trigger a second (rejected by the server) FINISH.
|
||||
const runCalls = fetchMock.mock.calls.filter(([u]) => u === RUN_URL);
|
||||
const finishes = runCalls.filter(([, o]) => JSON.parse(o.body).action === "FINISH");
|
||||
expect(finishes.length).toBe(1);
|
||||
expect(JSON.parse(finishes[0][1].body).status).toBe("cancelled");
|
||||
});
|
||||
});
|
||||
|
||||
describe("freebuff executor parseError", () => {
|
||||
it("explains a 404 'No endpoints found' as the toolset gate, not a credential problem", async () => {
|
||||
const ex = new FreebuffExecutor();
|
||||
const res = jsonResponse(
|
||||
{ error: { message: "No endpoints found for deepseek/deepseek-v4-flash.", code: 404, type: null, param: null } },
|
||||
{ status: 404, ok: false },
|
||||
);
|
||||
const parsed = await ex.parseError(res, JSON.stringify({ error: { message: "No endpoints found for deepseek/deepseek-v4-flash.", code: 404 } }));
|
||||
|
||||
expect(parsed.status).toBe(404);
|
||||
expect(parsed.message).toMatch(/end_turn/i);
|
||||
expect(parsed.message).not.toMatch(/credential/i);
|
||||
expect(parsed.resetsAtMs).toBeGreaterThan(Date.now());
|
||||
});
|
||||
|
||||
it("passes other statuses through untouched", async () => {
|
||||
const ex = new FreebuffExecutor();
|
||||
const res = jsonResponse({ error: "bad" }, { status: 500, ok: false });
|
||||
const parsed = await ex.parseError(res, JSON.stringify({ error: "bad" }));
|
||||
expect(parsed.status).toBe(500);
|
||||
expect(parsed.message).toContain("bad");
|
||||
expect(parsed.resetsAtMs).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,313 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
vi.mock("../../open-sse/utils/proxyFetch.js", () => ({
|
||||
proxyAwareFetch: vi.fn(),
|
||||
}));
|
||||
|
||||
import { proxyAwareFetch } from "../../open-sse/utils/proxyFetch.js";
|
||||
import { getUsageForProvider } from "../../open-sse/services/usage.js";
|
||||
import { PROVIDERS } from "../../open-sse/providers/index.js";
|
||||
import { USAGE_SUPPORTED_PROVIDERS } from "../../src/shared/constants/providers.js";
|
||||
import { parseQuotaData } from "../../src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js";
|
||||
|
||||
const SESSION_URL = "https://www.codebuff.com/api/v1/freebuff/session";
|
||||
|
||||
function jsonResponse(body, status = 200) {
|
||||
return new Response(JSON.stringify(body), {
|
||||
status,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
const PRE_JOIN = {
|
||||
status: "none",
|
||||
accessTier: "limited",
|
||||
rateLimitsByModel: {
|
||||
"deepseek/deepseek-v4-flash": {
|
||||
limit: 6,
|
||||
recentCount: 4.1,
|
||||
period: "pacific_day",
|
||||
resetTimeZone: "America/Los_Angeles",
|
||||
resetAt: "2026-08-06T07:00:00.000Z",
|
||||
entitlementBreakdown: { base: 6, referral: 0, streak: 0 },
|
||||
},
|
||||
"openai/gpt-5.6-luna": {
|
||||
limit: 6,
|
||||
recentCount: 1,
|
||||
period: "pacific_day",
|
||||
resetTimeZone: "America/Los_Angeles",
|
||||
resetAt: "2026-08-06T07:00:00.000Z",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe("freebuff registry usage flag", () => {
|
||||
it("exposes the session endpoint as transport.usage url", () => {
|
||||
const cfg = PROVIDERS["freebuff"];
|
||||
expect(cfg.usage?.url).toBe(SESSION_URL);
|
||||
});
|
||||
|
||||
it("is listed in USAGE_SUPPORTED_PROVIDERS (features.usage)", () => {
|
||||
expect(USAGE_SUPPORTED_PROVIDERS).toContain("freebuff");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getUsageForProvider(freebuff)", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("GETs the session endpoint and normalizes per-model session quotas", async () => {
|
||||
proxyAwareFetch.mockResolvedValueOnce(jsonResponse(PRE_JOIN));
|
||||
|
||||
const usage = await getUsageForProvider({
|
||||
provider: "freebuff",
|
||||
accessToken: "tok-1",
|
||||
});
|
||||
|
||||
expect(usage.message).toBeUndefined();
|
||||
expect(usage.plan).toBe("Freebuff (Limited)");
|
||||
expect(usage.quotas["deepseek/deepseek-v4-flash"]).toMatchObject({
|
||||
used: 4.1,
|
||||
total: 6,
|
||||
resetAt: "2026-08-06T07:00:00.000Z",
|
||||
recurring: true,
|
||||
unlimited: false,
|
||||
displayName: "DeepSeek V4.1 Flash",
|
||||
});
|
||||
expect(usage.quotas["openai/gpt-5.6-luna"]).toMatchObject({
|
||||
used: 1,
|
||||
total: 6,
|
||||
displayName: "GPT-5.6 Luna",
|
||||
});
|
||||
|
||||
// Quota reads MUST be GET (a POST would claim a session and burn quota).
|
||||
const [url, opts] = proxyAwareFetch.mock.calls[0];
|
||||
expect(url).toBe(SESSION_URL);
|
||||
expect(opts.method).toBe("GET");
|
||||
expect(opts.headers.Authorization).toBe("Bearer tok-1");
|
||||
});
|
||||
|
||||
it("folds the active session's own rateLimit into the shared map", async () => {
|
||||
proxyAwareFetch.mockResolvedValueOnce(
|
||||
jsonResponse({
|
||||
status: "active",
|
||||
accessTier: "full",
|
||||
instanceId: "inst-1",
|
||||
model: "meta/muse-spark-1.2-contributor",
|
||||
expiresAt: new Date(Date.now() + 3600000).toISOString(),
|
||||
rateLimit: {
|
||||
limit: 6,
|
||||
recentCount: 2.4,
|
||||
period: "pacific_day",
|
||||
resetAt: "2026-08-06T07:00:00.000Z",
|
||||
},
|
||||
rateLimitsByModel: {},
|
||||
}),
|
||||
);
|
||||
|
||||
const usage = await getUsageForProvider({
|
||||
provider: "freebuff",
|
||||
accessToken: "tok-1",
|
||||
});
|
||||
|
||||
expect(usage.plan).toBe("Freebuff");
|
||||
expect(usage.quotas["meta/muse-spark-1.2-contributor"]).toMatchObject({
|
||||
used: 2.4,
|
||||
total: 6,
|
||||
displayName: "Muse Spark 1.2",
|
||||
});
|
||||
});
|
||||
|
||||
it("reports the Freebucks daily pool under each priced model for metered accounts (no rateLimitsByModel)", async () => {
|
||||
proxyAwareFetch.mockResolvedValueOnce(
|
||||
jsonResponse({
|
||||
status: "none",
|
||||
accessTier: "limited",
|
||||
rateLimitsByModel: {},
|
||||
freebucks: {
|
||||
balance: 10,
|
||||
daily: { limit: 25, spent: 15, remaining: 10, resetAt: "2026-09-08T07:00:00.000Z" },
|
||||
wallet: { balance: 0, monthlyBonus: 0 },
|
||||
spend: { limitUsd: 4, resetAt: "2026-09-08T07:00:00.000Z" },
|
||||
monthly: { limitUsd: 10, spentUsd: 3.5, remainingUsd: 6.5, resetAt: "2026-10-01T07:00:00.000Z" },
|
||||
planId: null,
|
||||
prices: { "deepseek/deepseek-v4-flash": 15, "z-ai/glm-5.3-flash": 5 },
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const usage = await getUsageForProvider({
|
||||
provider: "freebuff",
|
||||
accessToken: "tok-1",
|
||||
});
|
||||
|
||||
expect(usage.message).toBeUndefined();
|
||||
expect(usage.quotas["deepseek/deepseek-v4-flash"]).toMatchObject({
|
||||
used: 15,
|
||||
total: 25,
|
||||
resetAt: "2026-09-08T07:00:00.000Z",
|
||||
recurring: true,
|
||||
unlimited: false,
|
||||
price: 15,
|
||||
displayName: "DeepSeek V4.1 Flash",
|
||||
});
|
||||
expect(usage.quotas["z-ai/glm-5.3-flash"]).toMatchObject({
|
||||
used: 15,
|
||||
total: 25,
|
||||
price: 5,
|
||||
displayName: "GLM 5.3 Flash",
|
||||
});
|
||||
// No session pools → only the freebucks-derived rows exist.
|
||||
expect(Object.keys(usage.quotas)).toEqual([
|
||||
"deepseek/deepseek-v4-flash",
|
||||
"z-ai/glm-5.3-flash",
|
||||
]);
|
||||
// Account summary rides the response for the card header (server data,
|
||||
// nothing hardcoded client-side).
|
||||
expect(usage.freebucks).toEqual({
|
||||
balance: 10,
|
||||
daily: {
|
||||
limit: 25,
|
||||
spent: 15,
|
||||
remaining: 10,
|
||||
resetAt: "2026-09-08T07:00:00.000Z",
|
||||
},
|
||||
wallet: { balance: 0 },
|
||||
monthly: { remainingUsd: 6.5, limitUsd: 10, resetAt: "2026-10-01T07:00:00.000Z" },
|
||||
});
|
||||
});
|
||||
|
||||
it("folds the server's announced priceChanges into the live price (promos expire without a client release)", async () => {
|
||||
proxyAwareFetch.mockResolvedValueOnce(
|
||||
jsonResponse({
|
||||
status: "none",
|
||||
accessTier: "full",
|
||||
rateLimitsByModel: {},
|
||||
freebucks: {
|
||||
balance: 20,
|
||||
daily: { limit: 25, spent: 5, remaining: 20, resetAt: "2026-09-08T07:00:00.000Z" },
|
||||
wallet: { balance: 0, monthlyBonus: 0 },
|
||||
planId: null,
|
||||
prices: { "upstage/solar-pro4": 0 },
|
||||
priceNotices: { "upstage/solar-pro4": "0 Freebucks · Labor Day weekend (through Sep 7 PT)" },
|
||||
priceChanges: [
|
||||
{
|
||||
at: "2026-01-01T00:00:00.000Z", // already due
|
||||
modelId: "upstage/solar-pro4",
|
||||
price: 5,
|
||||
tagline: "Limited-time trial",
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const usage = await getUsageForProvider({
|
||||
provider: "freebuff",
|
||||
accessToken: "tok-1",
|
||||
});
|
||||
|
||||
// The due change is folded in: price 5 + new tagline, schedule emptied.
|
||||
expect(usage.quotas["upstage/solar-pro4"]).toMatchObject({
|
||||
price: 5,
|
||||
priceNote: "Limited-time trial",
|
||||
displayName: "Solar Pro 4",
|
||||
});
|
||||
});
|
||||
|
||||
it("attaches no price to legacy session-quota rows", async () => {
|
||||
proxyAwareFetch.mockResolvedValueOnce(jsonResponse(PRE_JOIN));
|
||||
|
||||
const usage = await getUsageForProvider({
|
||||
provider: "freebuff",
|
||||
accessToken: "tok-1",
|
||||
});
|
||||
|
||||
expect(usage.quotas["deepseek/deepseek-v4-flash"].price).toBeUndefined();
|
||||
expect(usage.freebucks).toBeUndefined();
|
||||
});
|
||||
|
||||
it("surfaces a re-login message on 401", async () => {
|
||||
proxyAwareFetch.mockResolvedValueOnce(jsonResponse({ error: "unauthorized" }, 401));
|
||||
|
||||
const usage = await getUsageForProvider({
|
||||
provider: "freebuff",
|
||||
accessToken: "expired",
|
||||
});
|
||||
|
||||
expect(usage.message).toMatch(/expired|re-login/i);
|
||||
expect(usage.quotas).toBeUndefined();
|
||||
});
|
||||
|
||||
it("surfaces a region message on 403 country_blocked (not a re-login hint)", async () => {
|
||||
proxyAwareFetch.mockResolvedValueOnce(
|
||||
jsonResponse({ status: "country_blocked", countryCode: "XX" }, 403),
|
||||
);
|
||||
|
||||
const usage = await getUsageForProvider({
|
||||
provider: "freebuff",
|
||||
accessToken: "tok-1",
|
||||
});
|
||||
|
||||
expect(usage.message).toMatch(/not available in your region/i);
|
||||
});
|
||||
|
||||
it("treats 404 (no session row) as pre-join with no quota", async () => {
|
||||
proxyAwareFetch.mockResolvedValueOnce(jsonResponse({}, 404));
|
||||
|
||||
const usage = await getUsageForProvider({
|
||||
provider: "freebuff",
|
||||
accessToken: "tok-1",
|
||||
});
|
||||
|
||||
expect(usage.message).toMatch(/no session quota/i);
|
||||
});
|
||||
|
||||
it("returns a message when the session response carries no quota map", async () => {
|
||||
proxyAwareFetch.mockResolvedValueOnce(jsonResponse({ status: "none", accessTier: "full" }));
|
||||
|
||||
const usage = await getUsageForProvider({
|
||||
provider: "freebuff",
|
||||
accessToken: "tok-1",
|
||||
});
|
||||
|
||||
expect(usage.plan).toBe("Freebuff");
|
||||
expect(usage.message).toMatch(/no session quota/i);
|
||||
});
|
||||
|
||||
it("does not throw when the session fetch fails", async () => {
|
||||
proxyAwareFetch.mockRejectedValueOnce(new Error("network down"));
|
||||
|
||||
const usage = await getUsageForProvider({
|
||||
provider: "freebuff",
|
||||
accessToken: "tok-1",
|
||||
});
|
||||
|
||||
expect(usage.message).toMatch(/usage error/i);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseQuotaData(freebuff)", () => {
|
||||
it("uses displayName for the row label and keeps modelKey for ordering", () => {
|
||||
const rows = parseQuotaData("freebuff", {
|
||||
plan: "Freebuff (Limited)",
|
||||
quotas: {
|
||||
"deepseek/deepseek-v4-flash": {
|
||||
used: 4.1,
|
||||
total: 6,
|
||||
resetAt: "2026-08-06T07:00:00.000Z",
|
||||
displayName: "DeepSeek V4.1 Flash",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0]).toMatchObject({
|
||||
name: "DeepSeek V4.1 Flash",
|
||||
modelKey: "deepseek/deepseek-v4-flash",
|
||||
used: 4.1,
|
||||
total: 6,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -48,3 +48,31 @@ describe("classifyOAuthProbeResult (grok-cli)", () => {
|
||||
expect(r).toEqual({ valid: true, error: null, soft: false });
|
||||
});
|
||||
});
|
||||
|
||||
describe("classifyOAuthProbeResult (freebuff)", () => {
|
||||
const FREEBUFF_PROBE = {
|
||||
acceptStatuses: [403, 404],
|
||||
softFailMessage: { 403: "gated" },
|
||||
};
|
||||
|
||||
it("treats 404 (no session row) as silent success", () => {
|
||||
const r = classifyOAuthProbeResult({ ok: false, status: 404 }, FREEBUFF_PROBE, "");
|
||||
expect(r).toEqual({ valid: true, error: null, soft: false });
|
||||
});
|
||||
|
||||
it("treats 403 (region/account gate) as soft success", () => {
|
||||
const r = classifyOAuthProbeResult(
|
||||
{ ok: false, status: 403 },
|
||||
FREEBUFF_PROBE,
|
||||
JSON.stringify({ status: "country_blocked", countryCode: "XX" }),
|
||||
);
|
||||
expect(r.valid).toBe(true);
|
||||
expect(r.soft).toBe(true);
|
||||
expect(r.error).toMatch(/gated/);
|
||||
});
|
||||
|
||||
it("treats 401 as hard auth failure", () => {
|
||||
const r = classifyOAuthProbeResult({ ok: false, status: 401 }, FREEBUFF_PROBE, "unauthorized");
|
||||
expect(r).toEqual({ valid: false, error: "Token invalid or revoked", soft: false });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { describe, it, expect, beforeEach } from "vitest";
|
||||
import { setPoolGeo, getPoolGeo, poolGeoSnapshot, pruneStaleGeo, resetPoolGeo } from "open-sse/services/poolGeo.js";
|
||||
|
||||
describe("pool egress geo cache", () => {
|
||||
beforeEach(() => resetPoolGeo());
|
||||
|
||||
it("stores geo and classifies stability from egress changes", () => {
|
||||
setPoolGeo("p1", { ip: "1.1.1.1", country: "US" });
|
||||
setPoolGeo("p1", { ip: "1.1.1.1", country: "US" }); // same IP twice
|
||||
expect(getPoolGeo("p1").isUnstable).toBe(false);
|
||||
expect(getPoolGeo("p1").ipCount).toBe(1);
|
||||
|
||||
setPoolGeo("p1", { ip: "2.2.2.2", country: "US" }); // changed
|
||||
expect(getPoolGeo("p1").isUnstable).toBe(true);
|
||||
expect(getPoolGeo("p1").ipCount).toBe(2);
|
||||
});
|
||||
|
||||
it("prunes TTL-stale entries (ipHistory rides along)", () => {
|
||||
setPoolGeo("p1", { ip: "1.1.1.1", country: "US" });
|
||||
setPoolGeo("p1", { ip: "2.2.2.2", country: "US" });
|
||||
const cache = globalThis["__9routerPoolGeo__"];
|
||||
cache.get("p1").ts = Date.now() - 2 * 60 * 60 * 1000;
|
||||
expect(pruneStaleGeo()).toBe(1);
|
||||
expect(poolGeoSnapshot()).toEqual({});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
import { describe, it, expect, beforeEach } from "vitest";
|
||||
import {
|
||||
markPoolUnfit,
|
||||
clearPoolUnfit,
|
||||
clearAllPoolUnfit,
|
||||
isPoolFit,
|
||||
fitPoolIds,
|
||||
poolFitnessSnapshot,
|
||||
pruneExpired,
|
||||
resetPoolFitness,
|
||||
} from "open-sse/services/proxyPoolFitness.js";
|
||||
import { pickProxyPoolId } from "../../src/lib/network/connectionProxy.js";
|
||||
|
||||
describe("proxy pool fitness registry", () => {
|
||||
beforeEach(() => resetPoolFitness());
|
||||
|
||||
it("marks a pool unfit for a scope and prunes on expiry", () => {
|
||||
markPoolUnfit("p1", "freebuff::gpt-5.6-luna", Date.now() + 60_000, "limited_ip");
|
||||
expect(isPoolFit("p1", "freebuff::gpt-5.6-luna")).toBe(false);
|
||||
expect(isPoolFit("p1", "freebuff::other-model")).toBe(true);
|
||||
expect(isPoolFit("p2", "freebuff::gpt-5.6-luna")).toBe(true);
|
||||
|
||||
markPoolUnfit("p1", "freebuff::gpt-5.6-luna", Date.now() - 1000); // expired
|
||||
expect(isPoolFit("p1", "freebuff::gpt-5.6-luna")).toBe(true); // pruned on read
|
||||
});
|
||||
|
||||
it("provider-wide mark (provider::*) covers any model lookup", () => {
|
||||
markPoolUnfit("p1", "opencode::*", Date.now() + 60_000, "manual");
|
||||
expect(isPoolFit("p1", "opencode::sonnet-4.6")).toBe(false);
|
||||
expect(isPoolFit("p1", "freebuff::gpt-5.6-luna")).toBe(true);
|
||||
});
|
||||
|
||||
it("fitPoolIds filters unfit pools; snapshot drops expired marks", () => {
|
||||
markPoolUnfit("p1", "fb::m1", Date.now() + 60_000);
|
||||
markPoolUnfit("p1", "fb::m2", Date.now() - 1000); // expired
|
||||
expect(fitPoolIds(["p1", "p2"], "fb::m1")).toEqual(["p2"]);
|
||||
|
||||
const snap = poolFitnessSnapshot();
|
||||
expect(snap.p1["fb::m1"]).toBeDefined();
|
||||
expect(snap.p1["fb::m2"]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not reuse a pool when every smart candidate is unfit", () => {
|
||||
markPoolUnfit("p1", "freebuff::openai/gpt-5.6-luna", Date.now() + 60_000, "limited_ip");
|
||||
markPoolUnfit("p2", "freebuff::openai/gpt-5.6-luna", Date.now() + 60_000, "limited_ip");
|
||||
|
||||
expect(pickProxyPoolId(
|
||||
["p1", "p2"],
|
||||
"smart",
|
||||
"freebuff",
|
||||
{ scope: "freebuff::openai/gpt-5.6-luna" },
|
||||
)).toBeNull();
|
||||
});
|
||||
|
||||
it("preserves fail-open smart fallback for non-Freebuff providers", () => {
|
||||
markPoolUnfit("p1", "opencode::sonnet-4.6", Date.now() + 60_000, "ip-limit");
|
||||
markPoolUnfit("p2", "opencode::sonnet-4.6", Date.now() + 60_000, "ip-limit");
|
||||
|
||||
expect(pickProxyPoolId(
|
||||
["p1", "p2"],
|
||||
"smart",
|
||||
"opencode",
|
||||
{ scope: "opencode::sonnet-4.6" },
|
||||
)).toBe("p1");
|
||||
});
|
||||
|
||||
it("clear per scope, clear-all per provider, clear-all global, pruneExpired", () => {
|
||||
markPoolUnfit("p1", "freebuff::m1", Date.now() + 60_000);
|
||||
markPoolUnfit("p1", "kiro::m3", Date.now() + 60_000);
|
||||
|
||||
clearPoolUnfit("p1", "freebuff::m1");
|
||||
expect(isPoolFit("p1", "freebuff::m1")).toBe(true);
|
||||
|
||||
clearAllPoolUnfit("kiro");
|
||||
expect(poolFitnessSnapshot().p1).toBeUndefined();
|
||||
|
||||
markPoolUnfit("p2", "x::y", Date.now() - 1000);
|
||||
expect(pruneExpired()).toBe(1);
|
||||
expect(poolFitnessSnapshot()).toEqual({});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
// Guard: the test harness must never write into the user's real DB.
|
||||
//
|
||||
// Root cause this locks down: route-level tests (zed-live-models,
|
||||
// zed-native-auth) call createProviderConnection, which persists to
|
||||
// $DATA_DIR/db/data.sqlite. With no DATA_DIR set, that resolved to ~/.9router —
|
||||
// polluting the live DB with "zed-live-*@example.com", "guard-*@example.com"
|
||||
// and "Account N" rows on every `npx vitest run`.
|
||||
//
|
||||
// tests/setup/isolateDataDir.js redirects DATA_DIR to a temp dir unless the
|
||||
// caller opts out via RUN_REAL=1 or an explicit DATA_DIR.
|
||||
import { describe, it, expect } from "vitest";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
// When the caller opts into the real DB (RUN_REAL=1) or supplies DATA_DIR,
|
||||
// isolation is intentionally disabled — this guard only applies to the default.
|
||||
const ISOLATED = !process.env.RUN_REAL && !process.env.EXPECT_REAL_DATA_DIR;
|
||||
|
||||
describe.skipIf(!ISOLATED)("test DATA_DIR isolation", () => {
|
||||
it("points DATA_DIR at a temp dir, not ~/.9router", () => {
|
||||
const dir = process.env.DATA_DIR;
|
||||
expect(dir).toBeTruthy();
|
||||
const home = path.join(os.homedir(), ".9router");
|
||||
expect(path.resolve(dir)).not.toBe(path.resolve(home));
|
||||
expect(dir.startsWith(os.tmpdir())).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
// The Usage page lists providers from two sources:
|
||||
// 1. active LLM connections (deduped by provider id), and
|
||||
// 2. noAuth free providers that need no connection (e.g. opencode).
|
||||
//
|
||||
// Hidden providers (devin-cli, mimo-free) must NOT be auto-added — they are
|
||||
// excluded from the Providers page, so surfacing them in Usage (with zero
|
||||
// connections and zero traffic) is a leak. Regression: devin-cli showed up in
|
||||
// Usage but nowhere in Providers.
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { buildUsageProviderList } from "../../src/shared/utils/usageProviders.js";
|
||||
|
||||
const isLLM = () => true;
|
||||
|
||||
describe("buildUsageProviderList", () => {
|
||||
it("does not auto-add hidden noAuth free providers", () => {
|
||||
const freeProviders = {
|
||||
opencode: { id: "opencode", name: "OpenCode", noAuth: true },
|
||||
"devin-cli": { id: "devin-cli", name: "Devin CLI", noAuth: true, hidden: true },
|
||||
"mimo-free": { id: "mimo-free", name: "MiMo Free", noAuth: true, hidden: true },
|
||||
};
|
||||
|
||||
const list = buildUsageProviderList({
|
||||
connections: [],
|
||||
freeProviders,
|
||||
isLLMProvider: isLLM,
|
||||
});
|
||||
|
||||
const ids = list.map((p) => p.provider);
|
||||
expect(ids).toContain("opencode");
|
||||
expect(ids).not.toContain("devin-cli");
|
||||
expect(ids).not.toContain("mimo-free");
|
||||
});
|
||||
|
||||
it("includes active LLM connections, deduped by provider", () => {
|
||||
const list = buildUsageProviderList({
|
||||
connections: [
|
||||
{ provider: "codex", isActive: true },
|
||||
{ provider: "codex", isActive: true },
|
||||
{ provider: "zed", isActive: true },
|
||||
],
|
||||
freeProviders: {},
|
||||
isLLMProvider: isLLM,
|
||||
});
|
||||
expect(list.map((p) => p.provider)).toEqual(["codex", "zed"]);
|
||||
});
|
||||
|
||||
it("skips inactive connections and non-LLM providers", () => {
|
||||
const list = buildUsageProviderList({
|
||||
connections: [
|
||||
{ provider: "codex", isActive: false },
|
||||
{ provider: "whisper", isActive: true },
|
||||
],
|
||||
freeProviders: {},
|
||||
isLLMProvider: (id) => id !== "whisper",
|
||||
});
|
||||
expect(list).toEqual([]);
|
||||
});
|
||||
|
||||
it("does not duplicate a free provider that already has a connection", () => {
|
||||
const freeProviders = {
|
||||
opencode: { id: "opencode", name: "OpenCode", noAuth: true },
|
||||
};
|
||||
const list = buildUsageProviderList({
|
||||
connections: [{ provider: "opencode", isActive: true }],
|
||||
freeProviders,
|
||||
isLLMProvider: isLLM,
|
||||
});
|
||||
expect(list.map((p) => p.provider)).toEqual(["opencode"]);
|
||||
});
|
||||
|
||||
it("attaches nodeName from the lookup when present", () => {
|
||||
const list = buildUsageProviderList({
|
||||
connections: [{ provider: "node-1", isActive: true }],
|
||||
freeProviders: {},
|
||||
nodeNameMap: { "node-1": "My Node" },
|
||||
isLLMProvider: isLLM,
|
||||
});
|
||||
expect(list[0].nodeName).toBe("My Node");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user