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) <noreply@anthropic.com>
This commit is contained in:
Qichang Zheng
2026-10-18 14:57:17 +08:00
co-authored by Claude Opus 5.5
parent e8fc388867
commit abec60eb72
2 changed files with 32 additions and 5 deletions
@@ -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([]);
});
@@ -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<string, DiscoverOptions>();
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<string, KnownReport>();