Files
paseo/plugins/claude-usage-source/server/keychain.test.ts
T
Mohamed Boudra 3e2d77723e fix(usage): scope Muse and agent logins, show every failed host login (#6156)
* fix(muse): show usage only for the agent account

Usage discovery ignored session scope and exposed all remembered accounts to unrelated agents. Resolve session accounts from their own launch environment while preserving global discovery.

* test(muse): own the usage scope regression in the plugin suite

* fix(usage): scope agent reports to their own logins

Account grouping also merged fetch inputs and cache entries across agent and host queries, hiding the agent login failure behind another harness. Preserve scoped login sets, collect every host failure with source-owned harness labels, and keep refresh results within their requesting view.

* test(usage): expect harness labels in discovered accounts
2026-10-05 18:41:54 +01:00

193 lines
6.7 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from "vitest";
import { claudeKeychainAccount, readClaudeKeychainCredentials } from "./usage.js";
const SERVICE = "Claude Code-credentials";
afterEach(() => {
vi.unstubAllEnvs();
});
describe("claudeKeychainAccount", () => {
it("uses a supported username verbatim", () => {
expect(claudeKeychainAccount("thomas.benoit")).toBe("thomas.benoit");
});
it("falls back to claude-code-user when the username carries a rejected character", () => {
expect(claudeKeychainAccount("first.last@example.com")).toBe("claude-code-user");
});
it("reads the username from $USER when no account is given", () => {
vi.stubEnv("USER", "ci-runner");
expect(claudeKeychainAccount()).toBe("ci-runner");
vi.stubEnv("USER", "first.last@example.com");
expect(claudeKeychainAccount()).toBe("claude-code-user");
});
});
describe("readClaudeKeychainCredentials", () => {
it("looks the item up by account and stops there when it exists", async () => {
const run = vi.fn(async () => JSON.stringify({ claudeAiOauth: { accessToken: "at_fresh" } }));
const credentials = await readClaudeKeychainCredentials(run, "claude-code-user");
expect(credentials).toEqual({ claudeAiOauth: { accessToken: "at_fresh" } });
expect(run).toHaveBeenCalledTimes(1);
expect(run).toHaveBeenCalledWith([
"find-generic-password",
"-a",
"claude-code-user",
"-w",
"-s",
SERVICE,
]);
});
it("falls back to the account-less lookup when no item matches the account", async () => {
const run = vi.fn(async (args: string[]) =>
args.includes("-a") ? null : JSON.stringify({ claudeAiOauth: { accessToken: "at_legacy" } }),
);
const credentials = await readClaudeKeychainCredentials(run, "claude-code-user");
expect(credentials).toEqual({ claudeAiOauth: { accessToken: "at_legacy" } });
expect(run).toHaveBeenNthCalledWith(2, ["find-generic-password", "-w", "-s", SERVICE]);
});
it("falls through when the account item carries no access token", async () => {
const run = vi.fn(async (args: string[]) =>
args.includes("-a")
? JSON.stringify({ claudeAiOauth: { subscriptionType: "team" } })
: JSON.stringify({ claudeAiOauth: { accessToken: "at_legacy" } }),
);
const credentials = await readClaudeKeychainCredentials(run, "claude-code-user");
expect(credentials).toEqual({ claudeAiOauth: { accessToken: "at_legacy" } });
expect(run).toHaveBeenCalledTimes(2);
});
it("is null when the Keychain holds no item at all", async () => {
const run = vi.fn(async () => null);
expect(await readClaudeKeychainCredentials(run, "claude-code-user")).toBeNull();
});
it("tries the next lookup when the item does not parse", async () => {
const run = vi.fn(async (args: string[]) =>
args.includes("-a")
? "not-json"
: JSON.stringify({ claudeAiOauth: { accessToken: "at_legacy" } }),
);
expect(await readClaudeKeychainCredentials(run, "claude-code-user")).toEqual({
claudeAiOauth: { accessToken: "at_legacy" },
});
});
});
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { discover, fetchUsage } from "./usage.js";
import { inputSchema } from "../shared/input.js";
function fixtureFetch(expectedToken: string): typeof fetch {
return vi.fn(async (_url: RequestInfo | URL, init?: RequestInit) => {
expect(init?.headers).toMatchObject({ Authorization: `Bearer ${expectedToken}` });
return new Response(
JSON.stringify({
account: { uuid: "fixture-account" },
organization: { uuid: "fixture-org" },
five_hour: { utilization: 12 },
}),
{ status: 200 },
);
}) as unknown as typeof fetch;
}
describe("Claude credential routes", () => {
it("reads the explicit Claude credential file", async () => {
const dir = mkdtempSync(join(tmpdir(), "claude-usage-input-"));
try {
writeFileSync(
join(dir, ".credentials.json"),
JSON.stringify({ claudeAiOauth: { accessToken: "fixture-file" } }),
);
expect(
(
await fetchUsage(
{ route: { store: "claude", path: join(dir, ".credentials.json") } },
fixtureFetch("fixture-file"),
)
).status,
).toBe("available");
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
it("discovery reads the configured credential file", async () => {
const dir = mkdtempSync(join(tmpdir(), "claude-usage-default-"));
try {
writeFileSync(
join(dir, ".credentials.json"),
JSON.stringify({ claudeAiOauth: { accessToken: "fixture-default" } }),
);
const accounts = await discover(
{ kind: "global" },
{ home: dir, env: { CLAUDE_CONFIG_DIR: dir }, platform: "linux" },
fixtureFetch("fixture-default"),
);
expect(accounts).toEqual([
{
key: "fixture-account.fixture-org",
harness: "Claude",
input: { route: { store: "claude", path: join(dir, ".credentials.json") } },
},
]);
expect(
(await fetchUsage(inputSchema.parse(accounts[0]!.input), fixtureFetch("fixture-default")))
.status,
).toBe("available");
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
it("throws without fetching when the routed file has no credentials", async () => {
const dir = mkdtempSync(join(tmpdir(), "claude-empty-config-"));
try {
const fetchApi = vi.fn() as unknown as typeof fetch;
await expect(
fetchUsage({ route: { store: "claude", path: join(dir, ".credentials.json") } }, fetchApi, {
platform: "linux",
}),
).rejects.toThrow("Claude login store no longer exists");
expect(fetchApi).not.toHaveBeenCalled();
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
});
describe("Claude Keychain token usage", () => {
it("returns unavailable on 401 without attempting refresh", async () => {
const run = vi.fn(async () =>
JSON.stringify({
claudeAiOauth: { accessToken: "fixture-expired", refreshToken: "fixture-refresh" },
}),
);
const credentials = await readClaudeKeychainCredentials(run, "fixture-account");
const fetchApi = vi.fn(
async () => new Response(null, { status: 401 }),
) as unknown as typeof fetch;
const report = await fetchUsage({ route: { store: "keychain" } }, fetchApi, {
platform: "darwin",
claudeHome: "/nonexistent-paseo-test-home",
readKeychainCredentials: async () => credentials,
});
expect(report.status).toBe("unavailable");
expect(fetchApi).toHaveBeenCalledTimes(1);
expect(run).toHaveBeenCalledTimes(1);
});
});