mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-05 07:54:38 +00:00
Resolve relative database overrides against the OpenCode data directory so usage discovery follows the same store as the CLI. Co-authored-by: Ivan Pantic <panta@talentkit.io>
281 lines
11 KiB
TypeScript
281 lines
11 KiB
TypeScript
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { dirname, join } from "node:path";
|
|
import { afterEach, beforeEach, expect, test } from "vitest";
|
|
import { discover, fetchUsage } from "./usage.js";
|
|
import { defaultStorePaths } from "./stores.js";
|
|
|
|
import { inputSchema } from "../shared/input.js";
|
|
|
|
let fixtureDirectory: string;
|
|
let fixturePath: string;
|
|
function stores(path: string) {
|
|
return { file: path, database: join(dirname(path), "opencode.db") };
|
|
}
|
|
async function database() {
|
|
const { DatabaseSync } = await import("node:sqlite");
|
|
const db = new DatabaseSync(join(fixtureDirectory, "opencode.db"));
|
|
db.exec(
|
|
"CREATE TABLE credential (id TEXT PRIMARY KEY, integration_id TEXT, value TEXT, active INTEGER, time_updated INTEGER)",
|
|
);
|
|
return db;
|
|
}
|
|
beforeEach(async () => {
|
|
fixtureDirectory = await mkdtemp(join(tmpdir(), "go-default-"));
|
|
fixturePath = join(fixtureDirectory, "auth.json");
|
|
await writeFile(
|
|
fixturePath,
|
|
JSON.stringify({ "opencode-go": { type: "api", key: "fixture-secret" } }),
|
|
);
|
|
});
|
|
afterEach(async () => {
|
|
await rm(fixtureDirectory, { recursive: true, force: true });
|
|
});
|
|
|
|
test("discovery input rejects an explicit API key", () => {
|
|
expect(inputSchema.safeParse({ apiKey: "unused" }).success).toBe(false);
|
|
expect(
|
|
inputSchema.safeParse({ store: "database", path: "opencode.db", apiKey: "unused" }).success,
|
|
).toBe(false);
|
|
expect(() => inputSchema.parse({})).toThrow();
|
|
});
|
|
|
|
const upstreamResponse = {
|
|
usage: {
|
|
rolling: { status: "ok", percent: 21, resetsAt: "2026-09-26T20:00:00.000Z" },
|
|
weekly: { status: "ok", percent: 42, resetsAt: "2026-09-28T00:00:00.000Z" },
|
|
monthly: { status: "rate-limited", percent: 100, resetsAt: "2026-10-01T00:00:00.000Z" },
|
|
},
|
|
};
|
|
|
|
test("uses the active v2 Go key instead of a stale legacy key without writing either store", async () => {
|
|
const databasePath = join(fixtureDirectory, "opencode.db");
|
|
const db = await database();
|
|
const insert = db.prepare("INSERT INTO credential VALUES (?, ?, ?, ?, ?)");
|
|
insert.run("inactive", "opencode-go", JSON.stringify({ type: "key", key: "inactive-key" }), 0, 3);
|
|
insert.run("active", "opencode-go", JSON.stringify({ type: "key", key: "active-key" }), 1, 1);
|
|
insert.run("zen", "opencode", JSON.stringify({ type: "key", key: "zen-key" }), 1, 4);
|
|
db.close();
|
|
const beforeDatabase = await readFile(databasePath);
|
|
const beforeFile = await readFile(fixturePath);
|
|
const accounts = await discover(stores(fixturePath));
|
|
expect(accounts).toEqual([
|
|
{
|
|
key: expect.stringMatching(/^[a-f0-9]{64}$/),
|
|
input: { store: "database", path: databasePath },
|
|
},
|
|
]);
|
|
const input = inputSchema.parse(accounts[0]!.input);
|
|
const report = await fetchUsage(input, async (_url, init) => {
|
|
expect(new Headers(init?.headers).get("Authorization")).toBe("Bearer active-key");
|
|
return Response.json(upstreamResponse);
|
|
});
|
|
expect(report.status).toBe("available");
|
|
expect(report.windows.map((window) => window.usedPct)).toEqual([21, 42, 100]);
|
|
expect(await readFile(databasePath)).toEqual(beforeDatabase);
|
|
expect(await readFile(fixturePath)).toEqual(beforeFile);
|
|
});
|
|
|
|
test("discovers v2 Go usage without a legacy auth.json", async () => {
|
|
const db = await database();
|
|
db.prepare("INSERT INTO credential VALUES (?, ?, ?, ?, ?)").run(
|
|
"go",
|
|
"opencode-go",
|
|
JSON.stringify({ type: "key", key: "database-key" }),
|
|
1,
|
|
1,
|
|
);
|
|
db.close();
|
|
await rm(fixturePath);
|
|
const accounts = await discover(stores(fixturePath));
|
|
expect(accounts).toEqual([
|
|
{
|
|
key: expect.stringMatching(/^[a-f0-9]{64}$/),
|
|
input: { store: "database", path: join(fixtureDirectory, "opencode.db") },
|
|
},
|
|
]);
|
|
expect(JSON.stringify(accounts)).not.toContain("database-key");
|
|
});
|
|
|
|
test("refresh follows the active Go account and re-reads its rotated key", async () => {
|
|
const db = await database();
|
|
const insert = db.prepare("INSERT INTO credential VALUES (?, ?, ?, ?, ?)");
|
|
insert.run("first", "opencode-go", JSON.stringify({ type: "key", key: "first-key" }), 1, 1);
|
|
insert.run("second", "opencode-go", JSON.stringify({ type: "key", key: "second-key" }), 0, 2);
|
|
db.close();
|
|
const paths = stores(fixturePath);
|
|
const accounts = await discover(paths);
|
|
const input = inputSchema.parse(accounts[0]!.input);
|
|
const { DatabaseSync } = await import("node:sqlite");
|
|
const writer = new DatabaseSync(paths.database);
|
|
writer.exec("UPDATE credential SET active = CASE id WHEN 'second' THEN 1 ELSE 0 END");
|
|
writer
|
|
.prepare("UPDATE credential SET value = ? WHERE id = 'second'")
|
|
.run(JSON.stringify({ type: "key", key: "rotated-key" }));
|
|
writer.close();
|
|
expect(await discover(paths)).toEqual(accounts);
|
|
const before = await readFile(paths.database);
|
|
const report = await fetchUsage(input, async (_url, init) => {
|
|
expect(new Headers(init?.headers).get("Authorization")).toBe("Bearer rotated-key");
|
|
return Response.json(upstreamResponse);
|
|
});
|
|
expect(report.status).toBe("available");
|
|
expect(JSON.stringify(report)).not.toContain("rotated-key");
|
|
expect(await readFile(paths.database)).toEqual(before);
|
|
});
|
|
|
|
test.each([
|
|
["inactive Go key", "opencode-go", 0, JSON.stringify({ type: "key", key: "inactive-key" })],
|
|
["other provider", "opencode", 1, JSON.stringify({ type: "key", key: "zen-key" })],
|
|
["Console OAuth", "opencode-go", 1, JSON.stringify({ type: "oauth", access: "oauth-token" })],
|
|
["malformed credential", "opencode-go", 1, "invalid JSON"],
|
|
["empty key", "opencode-go", 1, JSON.stringify({ type: "key", key: "" })],
|
|
])(
|
|
"does not revive a legacy key for a v2 store with %s",
|
|
async (_name, provider, active, value) => {
|
|
const db = await database();
|
|
db.prepare("INSERT INTO credential VALUES (?, ?, ?, ?, ?)").run(
|
|
"credential",
|
|
provider,
|
|
value,
|
|
active,
|
|
1,
|
|
);
|
|
db.close();
|
|
expect(await discover(stores(fixturePath))).toEqual([]);
|
|
},
|
|
);
|
|
|
|
test("keeps legacy discovery when the v1 session database has no credential table", async () => {
|
|
const { DatabaseSync } = await import("node:sqlite");
|
|
const db = new DatabaseSync(join(fixtureDirectory, "opencode.db"));
|
|
db.exec("CREATE TABLE session (id TEXT PRIMARY KEY)");
|
|
db.close();
|
|
expect(await discover(stores(fixturePath))).toEqual([
|
|
{
|
|
key: expect.stringMatching(/^[a-f0-9]{64}$/),
|
|
input: { store: "file", path: fixturePath },
|
|
},
|
|
]);
|
|
});
|
|
|
|
test("fails before fetching when the selected database disappears", async () => {
|
|
await expect(
|
|
fetchUsage({ store: "database", path: join(fixtureDirectory, "missing.db") }, async () => {
|
|
throw new Error("must not fetch");
|
|
}),
|
|
).rejects.toThrow("login store no longer exists");
|
|
});
|
|
|
|
test("resolves default and overridden OpenCode store paths", () => {
|
|
expect(defaultStorePaths({ HOME: fixtureDirectory })).toEqual({
|
|
file: join(fixtureDirectory, ".local", "share", "opencode", "auth.json"),
|
|
database: join(fixtureDirectory, ".local", "share", "opencode", "opencode.db"),
|
|
});
|
|
const customDatabase = join(fixtureDirectory, "custom.db");
|
|
expect(
|
|
defaultStorePaths({ XDG_DATA_HOME: fixtureDirectory, OPENCODE_DB: customDatabase }),
|
|
).toEqual({
|
|
file: join(fixtureDirectory, "opencode", "auth.json"),
|
|
database: customDatabase,
|
|
});
|
|
});
|
|
|
|
test("discovers a relative OPENCODE_DB under the OpenCode data directory", async () => {
|
|
const paths = defaultStorePaths({
|
|
XDG_DATA_HOME: fixtureDirectory,
|
|
OPENCODE_DB: "go.db",
|
|
});
|
|
const databasePath = join(fixtureDirectory, "opencode", "go.db");
|
|
await mkdir(dirname(databasePath));
|
|
const { DatabaseSync } = await import("node:sqlite");
|
|
const db = new DatabaseSync(databasePath);
|
|
db.exec(
|
|
"CREATE TABLE credential (id TEXT PRIMARY KEY, integration_id TEXT, value TEXT, active INTEGER, time_updated INTEGER)",
|
|
);
|
|
db.prepare("INSERT INTO credential VALUES (?, ?, ?, ?, ?)").run(
|
|
"go",
|
|
"opencode-go",
|
|
JSON.stringify({ type: "key", key: "relative-key" }),
|
|
1,
|
|
1,
|
|
);
|
|
db.close();
|
|
expect(await discover(paths)).toEqual([
|
|
{
|
|
key: expect.stringMatching(/^[a-f0-9]{64}$/),
|
|
input: { store: "database", path: databasePath },
|
|
},
|
|
]);
|
|
});
|
|
|
|
test("discovers and fetches the default key from read-only auth.json", async () => {
|
|
const directory = await mkdtemp(join(tmpdir(), "paseo-go-usage-"));
|
|
const path = join(directory, "auth.json");
|
|
const content = JSON.stringify({ "opencode-go": { type: "api", key: "fixture-key" } });
|
|
try {
|
|
await writeFile(path, content);
|
|
expect(await discover(stores(path))).toEqual([
|
|
{ key: expect.stringMatching(/^[a-f0-9]{64}$/), input: { store: "file", path } },
|
|
]);
|
|
const requests: Array<{ url: string; authorization: string | null }> = [];
|
|
const fetchApi = async (url: string | URL | Request, init?: RequestInit) => {
|
|
requests.push({
|
|
url: String(url),
|
|
authorization: new Headers(init?.headers).get("Authorization"),
|
|
});
|
|
return Response.json(upstreamResponse);
|
|
};
|
|
const report = await fetchUsage({ store: "file", path }, fetchApi);
|
|
expect(requests).toEqual([
|
|
{ url: "https://opencode.ai/zen/go/v1/usage", authorization: "Bearer fixture-key" },
|
|
]);
|
|
expect(report.status).toBe("available");
|
|
expect(report.windows.map((window) => [window.id, window.usedPct, window.resetsAt])).toEqual([
|
|
["rolling", 21, "2026-09-26T20:00:00.000Z"],
|
|
["weekly", 42, "2026-09-28T00:00:00.000Z"],
|
|
["monthly", 100, "2026-10-01T00:00:00.000Z"],
|
|
]);
|
|
// The rolling window's length is not reported, so its percent stands without a name.
|
|
expect(report.windows.map((window) => window.shortLabel)).toEqual(["", "wk", "mo"]);
|
|
// The current upstream endpoint returns windows only; do not invent balances.
|
|
expect(report.balances).toBeUndefined();
|
|
expect(await readFile(path, "utf8")).toBe(content);
|
|
} finally {
|
|
await rm(directory, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("omits default discovery when auth.json lacks an OpenCode Go API key", async () => {
|
|
const directory = await mkdtemp(join(tmpdir(), "paseo-go-usage-"));
|
|
const path = join(directory, "auth.json");
|
|
try {
|
|
await writeFile(path, JSON.stringify({ openai: { type: "oauth", access: "other-token" } }));
|
|
expect(await discover(stores(path))).toEqual([]);
|
|
await expect(
|
|
fetchUsage({ store: "file", path }, async () => {
|
|
throw new Error("must not fetch");
|
|
}),
|
|
).rejects.toThrow("login store no longer exists");
|
|
} finally {
|
|
await rm(directory, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test.each([401, 403])("maps HTTP %i to unavailable", async (status) => {
|
|
const report = await fetchUsage(
|
|
{ store: "file", path: fixturePath },
|
|
async () => new Response(null, { status }),
|
|
);
|
|
expect(report).toEqual({
|
|
status: "unavailable",
|
|
problem: { kind: "rejected", status, refreshedBy: "opencode" },
|
|
});
|
|
});
|
|
|
|
test.each(["missing file", "unrelated file"])("discovers no accounts for %s", async (scenario) => {
|
|
const path = join(fixtureDirectory, "missing.json");
|
|
if (scenario === "unrelated file") await writeFile(path, "{}");
|
|
expect(await discover(stores(path))).toEqual([]);
|
|
});
|