From abec60eb72616e343fdb082fb3cd095a39fbf3c7 Mon Sep 17 00:00:00 2001 From: Qichang Zheng Date: Sun, 18 Oct 2026 14:57:17 +0800 Subject: [PATCH] fix(usage): key each host scope's kept accounts by launch environment only Renaming a provider no longer drops what its scope kept, providers that share an environment are discovered once under the first one's name, and unregistering a source is covered by a test. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../plugins/usage-sources/index.test.ts | 21 +++++++++++++++++++ .../src/server/plugins/usage-sources/index.ts | 16 +++++++++----- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/packages/server/src/server/plugins/usage-sources/index.test.ts b/packages/server/src/server/plugins/usage-sources/index.test.ts index 32c5e5804a..03d93f6cde 100644 --- a/packages/server/src/server/plugins/usage-sources/index.test.ts +++ b/packages/server/src/server/plugins/usage-sources/index.test.ts @@ -861,6 +861,9 @@ test("#3589: a provider's failed discovery keeps only that provider's last login expect(await listIds()).toEqual(["claude:default", "claude:work"]); failing.add("/work"); expect(await listIds()).toEqual(["claude:default", "claude:work"]); + // Renaming a provider keeps its launch environment. + providers = [{ provider: "claude", label: "Claude (Work)", env: { CLAUDE_CONFIG_DIR: "/work" } }]; + expect(await listIds()).toEqual(["claude:default", "claude:work"]); // A changed launch environment has no previous result to keep. providers = [ { provider: "claude", label: "Claude (work)", env: { CLAUDE_CONFIG_DIR: "/other" } }, @@ -875,3 +878,21 @@ test("#3589: a provider's failed discovery keeps only that provider's last login failing.add("/default"); expect(await listIds()).toEqual(["claude:default"]); }); + +test("unregistering a source drops the accounts each host scope kept for it", async () => { + let fails = false; + const registry = new UsageSourceRegistry(Date.now, 300_000, { warn: () => {} }); + const claude = source({ + id: "claude", + discover: async () => { + if (fails) throw new Error("Login store temporarily unavailable"); + return [{ key: "default", input: {} }]; + }, + }); + registry.register(claude); + expect((await registry.listReports()).map((entry) => entry.id)).toEqual(["claude:default"]); + registry.unregister("claude"); + registry.register(claude); + fails = true; + expect(await registry.listReports()).toEqual([]); +}); diff --git a/packages/server/src/server/plugins/usage-sources/index.ts b/packages/server/src/server/plugins/usage-sources/index.ts index bebce206f4..a2121fa334 100644 --- a/packages/server/src/server/plugins/usage-sources/index.ts +++ b/packages/server/src/server/plugins/usage-sources/index.ts @@ -128,14 +128,20 @@ export class UsageSourceRegistry { harness: label, })), ]; - // Each scope keeps its own last result, keyed by its launch environment. + // Each launch environment is discovered once, under the first provider that uses it, and + // keeps its own last result. + const unique = new Map(); + for (const entry of scopes) { + const key = canonicalJson(entry.scope); + if (!unique.has(key)) unique.set(key, entry); + } const previous = this.defaultsByScope; this.defaultsByScope = new Map( await Promise.all( - scopes.map(async (entry) => { - const key = canonicalJson(entry); - return [key, await this.discover({ ...entry, previous: previous.get(key) })] as const; - }), + [...unique].map( + async ([key, entry]) => + [key, await this.discover({ ...entry, previous: previous.get(key) })] as const, + ), ), ); const defaults = new Map();