fix(coding-agent,ai): show env auth source in /login

This commit is contained in:
Armin Ronacher
2026-04-23 21:01:55 +02:00
parent 05e2e9d170
commit e97051313d
10 changed files with 352 additions and 48 deletions

View File

@@ -0,0 +1,64 @@
import { describe, expect, test } from "vitest";
import { getAuthSelectorIndicator } from "../src/modes/interactive/components/auth-selector-status.js";
describe("getAuthSelectorIndicator", () => {
test("shows subscription configured when oauth matches the selector", () => {
expect(
getAuthSelectorIndicator("oauth", {
type: "oauth",
access: "access-token",
refresh: "refresh-token",
expires: Date.now() + 60_000,
}),
).toEqual({ kind: "configured", label: "subscription configured" });
});
test("shows api key configured in the subscription selector when an api key is stored", () => {
expect(
getAuthSelectorIndicator("oauth", {
type: "api_key",
key: "sk-test",
}),
).toEqual({ kind: "configured-other", label: "api key configured" });
});
test("shows api key configured when api key auth matches the selector", () => {
expect(
getAuthSelectorIndicator("api_key", {
type: "api_key",
key: "sk-test",
}),
).toEqual({ kind: "configured", label: "api key configured" });
});
test("shows subscription configured in the api key selector when oauth is stored", () => {
expect(
getAuthSelectorIndicator("api_key", {
type: "oauth",
access: "access-token",
refresh: "refresh-token",
expires: Date.now() + 60_000,
}),
).toEqual({ kind: "configured-other", label: "subscription configured" });
});
test("keeps env api key hints in the api key selector", () => {
expect(
getAuthSelectorIndicator("api_key", undefined, {
configured: false,
source: "environment",
label: "ANTHROPIC_API_KEY",
}),
).toEqual({ kind: "environment", label: "ANTHROPIC_API_KEY" });
});
test("ignores api key env hints in the subscription selector", () => {
expect(
getAuthSelectorIndicator("oauth", undefined, {
configured: false,
source: "environment",
label: "ANTHROPIC_API_KEY",
}),
).toEqual({ kind: "unconfigured" });
});
});

View File

@@ -431,6 +431,26 @@ describe("AuthStorage", () => {
});
});
describe("auth status", () => {
test("does not expose stored API keys or OAuth tokens", () => {
authStorage = AuthStorage.inMemory({
anthropic: { type: "api_key", key: "secret-api-key" },
openai: {
type: "oauth",
access: "secret-access-token",
refresh: "secret-refresh-token",
expires: Date.now() + 1000,
},
});
expect(authStorage.getAuthStatus("anthropic")).toEqual({ configured: true, source: "stored" });
expect(authStorage.getAuthStatus("openai")).toEqual({ configured: true, source: "stored" });
expect(JSON.stringify(authStorage.getAuthStatus("anthropic"))).not.toContain("secret-api-key");
expect(JSON.stringify(authStorage.getAuthStatus("openai"))).not.toContain("secret-access-token");
expect(JSON.stringify(authStorage.getAuthStatus("openai"))).not.toContain("secret-refresh-token");
});
});
describe("runtime overrides", () => {
test("runtime override takes priority over auth.json", async () => {
writeAuthJson({

View File

@@ -0,0 +1,59 @@
import { setKeybindings } from "@mariozechner/pi-tui";
import stripAnsi from "strip-ansi";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { AuthStorage } from "../src/core/auth-storage.js";
import { KeybindingsManager } from "../src/core/keybindings.js";
import { OAuthSelectorComponent } from "../src/modes/interactive/components/oauth-selector.js";
import { getApiKeyProviderDisplayName, isApiKeyLoginProvider } from "../src/modes/interactive/interactive-mode.js";
import { initTheme } from "../src/modes/interactive/theme/theme.js";
const originalOpenAiApiKey = process.env.OPENAI_API_KEY;
describe("OAuthSelectorComponent", () => {
beforeAll(() => {
initTheme("dark");
});
beforeEach(() => {
setKeybindings(new KeybindingsManager());
});
afterEach(() => {
if (originalOpenAiApiKey === undefined) {
delete process.env.OPENAI_API_KEY;
} else {
process.env.OPENAI_API_KEY = originalOpenAiApiKey;
}
});
it("keeps built-in API key providers separate from OAuth-only providers", () => {
const oauthProviderIds = new Set(["anthropic", "github-copilot", "custom-oauth"]);
const builtInProviderIds = new Set(["anthropic", "github-copilot", "amazon-bedrock", "openai"]);
expect(isApiKeyLoginProvider("anthropic", oauthProviderIds, builtInProviderIds)).toBe(true);
expect(getApiKeyProviderDisplayName("anthropic")).toBe("Anthropic");
expect(isApiKeyLoginProvider("openai", oauthProviderIds, builtInProviderIds)).toBe(true);
expect(isApiKeyLoginProvider("github-copilot", oauthProviderIds, builtInProviderIds)).toBe(false);
expect(isApiKeyLoginProvider("amazon-bedrock", oauthProviderIds, builtInProviderIds)).toBe(false);
expect(isApiKeyLoginProvider("custom-oauth", oauthProviderIds, builtInProviderIds)).toBe(false);
expect(isApiKeyLoginProvider("custom-api", oauthProviderIds, builtInProviderIds)).toBe(true);
});
it("shows environment API key auth while keeping provider unconfigured", () => {
process.env.OPENAI_API_KEY = "test-openai-key";
const authStorage = AuthStorage.inMemory();
const selector = new OAuthSelectorComponent(
"login",
authStorage,
[{ id: "openai", name: "OpenAI", authType: "api_key" }],
() => {},
() => {},
);
const output = stripAnsi(selector.render(120).join("\n"));
expect(output).toContain("OpenAI");
expect(output).toContain("unconfigured");
expect(output).toContain("env: OPENAI_API_KEY");
});
});