fix(coding-agent): show models.json auth in login selector

This commit is contained in:
Armin Ronacher
2026-04-24 17:27:53 +02:00
parent 3e0ee69b5e
commit 39db145495
8 changed files with 197 additions and 139 deletions

View File

@@ -1,64 +0,0 @@
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

@@ -1164,6 +1164,64 @@ describe("ModelRegistry", () => {
expect(count).toBe(2);
});
test("provider auth status reports apiKey environment variables from models.json", () => {
const envVarName = "TEST_API_KEY_STATUS_TEST_98765";
const originalEnv = process.env[envVarName];
try {
process.env[envVarName] = "status-test-key";
writeRawModelsJson({
"custom-provider": providerWithApiKey(envVarName),
});
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
expect(registry.getProviderAuthStatus("custom-provider")).toEqual({
configured: true,
source: "environment",
label: envVarName,
});
} finally {
if (originalEnv === undefined) {
delete process.env[envVarName];
} else {
process.env[envVarName] = originalEnv;
}
}
});
test("provider auth status reports non-env apiKey values from models.json as a config key", () => {
writeRawModelsJson({
"custom-provider": providerWithApiKey("literal_api_key_value"),
});
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
expect(registry.getProviderAuthStatus("custom-provider")).toEqual({
configured: true,
source: "models_json_key",
});
});
test("provider auth status reports command apiKey values from models.json without executing them", () => {
const counterFile = join(tempDir, "status-counter");
writeFileSync(counterFile, "0");
const counterPath = toShPath(counterFile);
const command = `!sh -c 'echo 1 > "${counterPath}"; echo key-value'`;
writeRawModelsJson({
"custom-provider": providerWithApiKey(command),
});
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
expect(registry.getProviderAuthStatus("custom-provider")).toEqual({
configured: true,
source: "models_json_command",
});
expect(readFileSync(counterFile, "utf-8")).toBe("0");
});
test("environment variables are not cached (changes are picked up)", async () => {
const envVarName = "TEST_API_KEY_CACHE_TEST_98765";
const originalEnv = process.env[envVarName];

View File

@@ -34,12 +34,35 @@ describe("OAuthSelectorComponent", () => {
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("amazon-bedrock", oauthProviderIds, builtInProviderIds)).toBe(true);
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", () => {
it("shows stored OAuth auth distinctly in the API key selector", () => {
const authStorage = AuthStorage.inMemory({
anthropic: {
type: "oauth",
access: "access-token",
refresh: "refresh-token",
expires: Date.now() + 60_000,
},
});
const selector = new OAuthSelectorComponent(
"login",
authStorage,
[{ id: "anthropic", name: "Anthropic", authType: "api_key" }],
() => {},
() => {},
);
const output = stripAnsi(selector.render(120).join("\n"));
expect(output).toContain("Anthropic");
expect(output).toContain("subscription configured");
});
it("shows environment API key auth as configured", () => {
process.env.OPENAI_API_KEY = "test-openai-key";
const authStorage = AuthStorage.inMemory();
const selector = new OAuthSelectorComponent(
@@ -53,7 +76,61 @@ describe("OAuthSelectorComponent", () => {
const output = stripAnsi(selector.render(120).join("\n"));
expect(output).toContain("OpenAI");
expect(output).toContain("unconfigured");
expect(output).toContain("env: OPENAI_API_KEY");
expect(output).toContain("✓ env: OPENAI_API_KEY");
expect(output).not.toContain("unconfigured");
});
it("shows custom provider environment API key auth from status resolver", () => {
const authStorage = AuthStorage.inMemory();
const selector = new OAuthSelectorComponent(
"login",
authStorage,
[{ id: "ollama", name: "ollama", authType: "api_key" }],
() => {},
() => {},
() => ({ configured: true, source: "environment", label: "OLLAMA_API_KEY" }),
);
const output = stripAnsi(selector.render(120).join("\n"));
expect(output).toContain("ollama");
expect(output).toContain("✓ env: OLLAMA_API_KEY");
expect(output).not.toContain("unconfigured");
});
it("shows models.json API key auth as configured", () => {
const authStorage = AuthStorage.inMemory();
const selector = new OAuthSelectorComponent(
"login",
authStorage,
[{ id: "local-proxy", name: "local-proxy", authType: "api_key" }],
() => {},
() => {},
() => ({ configured: true, source: "models_json_key" }),
);
const output = stripAnsi(selector.render(120).join("\n"));
expect(output).toContain("local-proxy");
expect(output).toContain("✓ key in models.json");
expect(output).not.toContain("unconfigured");
});
it("shows models.json command auth as configured", () => {
const authStorage = AuthStorage.inMemory();
const selector = new OAuthSelectorComponent(
"login",
authStorage,
[{ id: "op-proxy", name: "op-proxy", authType: "api_key" }],
() => {},
() => {},
() => ({ configured: true, source: "models_json_command" }),
);
const output = stripAnsi(selector.render(120).join("\n"));
expect(output).toContain("op-proxy");
expect(output).toContain("✓ command in models.json");
expect(output).not.toContain("unconfigured");
});
});