From bd5671fd490911ecf83bef7770214fc8f3533b60 Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Tue, 3 Nov 2026 08:29:14 +0000 Subject: [PATCH] fix(usage): discover migrated database logins and relative paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Match the source application’s migration and database-path rules so usage discovery does not keep reporting a stale file login after migration. Co-authored-by: Jiayang Wu <651475459@qq.com> --- .../server/plugins/usage-sources.e2e.test.ts | 27 +++++- .../codex-usage-source/server/stores.test.ts | 96 +++++++++++++++---- plugins/codex-usage-source/server/stores.ts | 6 +- 3 files changed, 107 insertions(+), 22 deletions(-) diff --git a/packages/server/src/server/plugins/usage-sources.e2e.test.ts b/packages/server/src/server/plugins/usage-sources.e2e.test.ts index 46dc134ff6..f00fddaa3d 100644 --- a/packages/server/src/server/plugins/usage-sources.e2e.test.ts +++ b/packages/server/src/server/plugins/usage-sources.e2e.test.ts @@ -224,7 +224,7 @@ async function fetchApi(url, init) { return new Response(JSON.stringify({ account: { uuid: second ? 'claude-two' : 'claude-one' }, organization: { uuid: second ? 'org-two' : 'org-one' } })); } if (token === 'Bearer fixture-revoked') return new Response(null, { status: 401 }); - const used = { 'Bearer fixture-codex': 11, 'Bearer fixture-opencode': 22, 'Bearer fixture-pi-codex': 33, 'Bearer fixture-omp-codex': 44, 'Bearer fixture-omp-claude': 66, 'Bearer fixture-pi-claude': 77 }[token] || 55; + const used = { 'Bearer fixture-codex': 11, 'Bearer fixture-opencode': 22, 'Bearer fixture-opencode-db': 23, 'Bearer fixture-pi-codex': 33, 'Bearer fixture-omp-codex': 44, 'Bearer fixture-omp-claude': 66, 'Bearer fixture-pi-claude': 77 }[token] || 55; return new Response(JSON.stringify({ rate_limit: { primary_window: { used_percent: used } }, five_hour: { utilization: used } })); } export default function contribute(server) { @@ -270,6 +270,31 @@ export default function contribute(server) { (await client.listUsageReports({ reportIds: ["codex:chatgpt-one"], forceRefresh: true })) .reports[0]; expect((await refresh())?.report.windows[0]?.usedPct).toBe(22); + const openCodeDatabase = path.join(path.dirname(openCodePath), "opencode.db"); + const openCodeDb = new DatabaseSync(openCodeDatabase); + openCodeDb.exec( + "CREATE TABLE credential (id TEXT PRIMARY KEY, integration_id TEXT, label TEXT, value TEXT, active INTEGER, time_created INTEGER, time_updated INTEGER)", + ); + // OpenCode's v1 migration leaves active NULL and does not update auth.json. + openCodeDb.prepare("INSERT INTO credential VALUES (?, ?, ?, ?, NULL, 1, 1)").run( + "migrated-login", + "openai", + "OpenAI", + JSON.stringify({ + type: "oauth", + access: "fixture-opencode-db", + expires: Date.now() + 60_000, + metadata: { accountID: "chatgpt-one" }, + }), + ); + openCodeDb.close(); + const rediscover = async () => + (await client.listUsageReports({ forceRefresh: true })).reports.find( + (entry) => entry.id === "codex:chatgpt-one", + ); + expect((await rediscover())!.report.windows[0]!.usedPct).toBe(23); + await rm(openCodeDatabase); + expect((await rediscover())!.report.windows[0]!.usedPct).toBe(22); await rm(openCodePath); expect((await refresh())?.report.windows[0]?.usedPct).toBe(33); await json(piPath, { diff --git a/plugins/codex-usage-source/server/stores.test.ts b/plugins/codex-usage-source/server/stores.test.ts index 9e3f2b0e75..9c97816e5c 100644 --- a/plugins/codex-usage-source/server/stores.test.ts +++ b/plugins/codex-usage-source/server/stores.test.ts @@ -470,22 +470,80 @@ test.each([ expect(await logins(lookup(env))).toEqual([{ route: { store: "opencode", path: file } }]); }); -test("OpenCode v2 database path honours OPENCODE_DB", async () => { - const databasePath = join(home, "custom-opencode.db"); - const db = await opencodeDatabase(databasePath); - db.prepare("INSERT INTO credential VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)").run( - "cred_1", - "openai", - "OpenAI", - opencodeOauth("fixture-db", 2000, "db-account"), - null, - "chatgpt-headless", - 1, - 1, - 1, - ); - db.close(); - expect(await logins(lookup({ OPENCODE_DB: databasePath }))).toEqual([ - { route: { store: "opencode-db", path: databasePath, credentialId: "cred_1" } }, - ]); -}); +test.each(["absolute", "relative"])( + "OpenCode v2 database path honours %s OPENCODE_DB", + async (kind) => { + const data = join(home, "data"); + const databasePath = join(data, "opencode", "custom-opencode.db"); + const db = await opencodeDatabase(databasePath); + db.prepare("INSERT INTO credential VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)").run( + "cred_1", + "openai", + "OpenAI", + opencodeOauth("fixture-db", 2000, "db-account"), + null, + "chatgpt-headless", + 1, + 1, + 1, + ); + db.close(); + expect( + await logins( + lookup({ + XDG_DATA_HOME: data, + OPENCODE_DB: kind === "absolute" ? databasePath : "custom-opencode.db", + }), + ), + ).toEqual([{ route: { store: "opencode-db", path: databasePath, credentialId: "cred_1" } }]); + }, +); + +test.each([false, true])( + "OpenCode v2 selects its current login after legacy migration (explicit selection: %s)", + async (selected) => { + const data = join(home, "data"); + const env = { XDG_DATA_HOME: data }; + const databasePath = join(data, "opencode", "opencode.db"); + const db = await opencodeDatabase(databasePath); + const insert = db.prepare("INSERT INTO credential VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"); + // The v1 migration leaves active NULL; OpenCode uses creation order until a login is selected. + insert.run( + "older", + "openai", + "OpenAI", + opencodeOauth("fixture-older", 2000, "older-account"), + null, + "chatgpt-headless", + selected ? 1 : null, + 1, + 3, + ); + insert.run( + "newer", + "openai", + "OpenAI", + opencodeOauth("fixture-newer", 2000, "newer-account"), + null, + "chatgpt-headless", + null, + 2, + 2, + ); + db.close(); + + const inputs = await logins(lookup(env)); + expect(inputs).toEqual([ + { + route: { + store: "opencode-db", + path: databasePath, + credentialId: selected ? "older" : "newer", + }, + }, + ]); + expect(await accountIdentity(inputs[0]!, env)).toEqual({ + key: selected ? "older-account" : "newer-account", + }); + }, +); diff --git a/plugins/codex-usage-source/server/stores.ts b/plugins/codex-usage-source/server/stores.ts index 9dc5861706..a524a94c1d 100644 --- a/plugins/codex-usage-source/server/stores.ts +++ b/plugins/codex-usage-source/server/stores.ts @@ -73,7 +73,7 @@ export function opencodeStorePaths(env: NodeJS.ProcessEnv, home: string): Openco const directory = join(dataHome, "opencode"); return { file: join(directory, "auth.json"), - database: env.OPENCODE_DB ? resolve(env.OPENCODE_DB) : join(directory, "opencode.db"), + database: resolve(directory, env.OPENCODE_DB || "opencode.db"), }; } @@ -176,7 +176,9 @@ export function readOpencodeOauth(path: string, lookup: StoreLookup = {}): Openc } const rows = db .prepare( - "SELECT id, value FROM credential WHERE integration_id = ? AND active = 1 ORDER BY time_updated DESC, id DESC", + // Migrated v1 rows have no active flag. OpenCode selects the newest + // created login until an explicit selection pins another one. + "SELECT id, value FROM credential WHERE integration_id = ? AND (active = 1 OR active IS NULL) ORDER BY active DESC, time_created DESC, id DESC LIMIT 1", ) .all("openai"); return rows.flatMap((value) => {