mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-08 09:24:41 +00:00
fix(usage): preserve saved usage when a provider is renamed
Polling identity belongs to the ordered login inputs. Apply current display labels when returning cached or pending reports so renaming a configured provider keeps cached values and successful history.
This commit is contained in:
@@ -175,7 +175,7 @@ test("concurrent requests for the same ID share one vendor fetch", async () => {
|
||||
const second = registry.listReports();
|
||||
finish({ status: "available", windows: [] });
|
||||
const [one, two] = await Promise.all([first, second]);
|
||||
expect(one[0]).toBe(two[0]);
|
||||
expect(one[0]).toEqual(two[0]);
|
||||
expect(fetches).toBe(1);
|
||||
});
|
||||
|
||||
@@ -963,3 +963,42 @@ test("unregistering a source drops the accounts each host scope kept for it", as
|
||||
fails = true;
|
||||
expect(await registry.listReports()).toEqual([]);
|
||||
});
|
||||
|
||||
test("#3589: renaming a provider preserves cached usage and recovery history while updating error labels", async () => {
|
||||
let label = "Before";
|
||||
let available = true;
|
||||
let polls = 0;
|
||||
const registry = new UsageSourceRegistry(Date.now, 300_000, undefined, {
|
||||
hasAgent: () => false,
|
||||
usageSession: () => null,
|
||||
providerEnvironments: () => [
|
||||
{ provider: "claude", label, env: { CLAUDE_CONFIG_DIR: "/work" } },
|
||||
],
|
||||
});
|
||||
registry.register({
|
||||
id: "claude",
|
||||
label: "Claude",
|
||||
discover: async (scope) =>
|
||||
scope.kind === "global" ? [] : [{ key: "work", input: { path: "/work" } }],
|
||||
fetch: async () => {
|
||||
polls++;
|
||||
return available
|
||||
? { status: "available", windows: [{ id: "weekly", label: "Weekly", usedPct: 42 }] }
|
||||
: { status: "error", error: "offline" };
|
||||
},
|
||||
});
|
||||
const [success] = await registry.listReports();
|
||||
available = false;
|
||||
label = "After";
|
||||
const [cached] = await registry.listReports();
|
||||
expect(cached?.report).toEqual(success?.report);
|
||||
expect(polls).toBe(1);
|
||||
const [failed] = await registry.listReports({ forceRefresh: true });
|
||||
expect(failed?.lastSuccessful?.report).toEqual(success?.report);
|
||||
expect(failed?.loginErrors?.map((login) => login.harness)).toEqual(["After"]);
|
||||
label = "Renamed again";
|
||||
const [relabeled] = await registry.listReports();
|
||||
expect(polls).toBe(2);
|
||||
expect(relabeled?.lastSuccessful).toEqual(failed?.lastSuccessful);
|
||||
expect(relabeled?.loginErrors?.map((login) => login.harness)).toEqual(["Renamed again"]);
|
||||
});
|
||||
|
||||
@@ -45,6 +45,11 @@ interface DiscoverOptions {
|
||||
previous?: ReadonlyMap<string, KnownReport>;
|
||||
}
|
||||
|
||||
interface PresentReportOptions {
|
||||
entry: UsageReportEntry;
|
||||
known: KnownReport;
|
||||
}
|
||||
|
||||
interface MergeReportsOptions {
|
||||
into: Map<string, KnownReport>;
|
||||
reports: Iterable<[string, KnownReport]>;
|
||||
@@ -157,8 +162,9 @@ export class UsageSourceRegistry {
|
||||
if (!known) return [];
|
||||
return [
|
||||
this.fetchId(id, known, options.forceRefresh).then((entry) => {
|
||||
options.onReport?.(entry);
|
||||
return entry;
|
||||
const report = presentReport({ entry, known });
|
||||
options.onReport?.(report);
|
||||
return report;
|
||||
}),
|
||||
];
|
||||
}),
|
||||
@@ -452,10 +458,6 @@ function sameLogin(a: Login, b: Login): boolean {
|
||||
return canonicalJson(a.input) === canonicalJson(b.input);
|
||||
}
|
||||
|
||||
function loginKey(login: Login): string {
|
||||
return canonicalJson(login);
|
||||
}
|
||||
|
||||
function canonicalJson(value: unknown): string {
|
||||
return JSON.stringify(value, (_key, nested) =>
|
||||
nested !== null && typeof nested === "object" && !Array.isArray(nested)
|
||||
@@ -466,6 +468,25 @@ function canonicalJson(value: unknown): string {
|
||||
|
||||
function cacheIdentity(id: string, known: KnownReport): string {
|
||||
return `${id}:${createHash("sha256")
|
||||
.update(JSON.stringify([known.label, known.logins.map(loginKey)]))
|
||||
.update(canonicalJson(known.logins.map((login) => login.input)))
|
||||
.digest("hex")}`;
|
||||
}
|
||||
|
||||
// Labels describe the current discovery, while cached polls and history belong to login inputs.
|
||||
function presentReport({ entry, known }: PresentReportOptions): UsageReportEntry {
|
||||
return {
|
||||
...entry,
|
||||
sourceLabel: known.source.label,
|
||||
icon: known.source.icon,
|
||||
account: { label: known.label },
|
||||
...(entry.loginErrors
|
||||
? {
|
||||
// Errors are emitted only when every ordered login failed, so positions match inputs.
|
||||
loginErrors: entry.loginErrors.map((error, index) => ({
|
||||
...error,
|
||||
harness: known.logins[index]!.harness,
|
||||
})),
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user