diff --git a/utils/apiKeys.test.ts b/utils/apiKeys.test.ts index 6f927a1..c679a89 100644 --- a/utils/apiKeys.test.ts +++ b/utils/apiKeys.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { validateAgentApiKey } from "./apiKeys.ts"; +import { formatApiKeyErrorSummary, isApiKeyAuthError, validateAgentApiKey } from "./apiKeys.ts"; const base = { agent: { name: "opencode" }, @@ -156,3 +156,48 @@ describe("validateAgentApiKey", () => { }); }); }); + +describe("isApiKeyAuthError", () => { + it("matches the missing-key marker thrown by validateAgentApiKey", () => { + expect(isApiKeyAuthError("no API key found. Pullfrog needs ...")).toBe(true); + }); + + it("matches Claude CLI 401 strings", () => { + expect(isApiKeyAuthError("Invalid API key · Fix external API key")).toBe(true); + }); + + it("matches OpenAI / OpenRouter 401 phrasings", () => { + expect(isApiKeyAuthError("ProviderAuthError: User not found")).toBe(true); + expect(isApiKeyAuthError("401 Invalid authentication")).toBe(true); + }); + + it("ignores unrelated errors", () => { + expect(isApiKeyAuthError("git fetch failed")).toBe(false); + expect(isApiKeyAuthError("")).toBe(false); + }); +}); + +describe("formatApiKeyErrorSummary", () => { + it("renders the missing-key body when the raw error contains the marker", () => { + const msg = formatApiKeyErrorSummary({ + owner: "acme", + name: "repo", + raw: "no API key found in this run", + }); + expect(msg).toContain("no API key found"); + expect(msg).toContain("https://github.com/acme/repo/settings/secrets/actions"); + expect(msg).toContain("/console/acme/repo"); + expect(msg).toContain("https://discord.gg/8y96raFg8e"); + }); + + it("renders the invalid-key body for any other auth error", () => { + const msg = formatApiKeyErrorSummary({ + owner: "acme", + name: "repo", + raw: "Invalid API key · Fix external API key", + }); + expect(msg).toContain("rejected (401)"); + expect(msg).toContain("https://github.com/acme/repo/settings/secrets/actions"); + expect(msg).toContain("https://discord.gg/8y96raFg8e"); + }); +}); diff --git a/utils/apiKeys.ts b/utils/apiKeys.ts index 2b41a08..f34e464 100644 --- a/utils/apiKeys.ts +++ b/utils/apiKeys.ts @@ -19,7 +19,7 @@ function buildMissingApiKeyError(params: { owner: string; name: string }): strin return [ `**${MISSING_KEY_MARKER}** — Pullfrog needs at least one LLM provider API key (e.g. \`ANTHROPIC_API_KEY\`, \`OPENAI_API_KEY\`, \`GEMINI_API_KEY\`) configured as a GitHub Actions secret.`, "", - `[Open repo secrets →](${githubSecretsUrl}) · [Configure model →](${settingsUrl}) · [Setup docs →](https://docs.pullfrog.com/keys)`, + `[Open repo secrets →](${githubSecretsUrl}) · [Configure model →](${settingsUrl}) · [Setup docs →](https://docs.pullfrog.com/keys) · [Ask in Discord →](https://discord.gg/8y96raFg8e)`, ].join("\n"); } @@ -153,6 +153,6 @@ export function formatApiKeyErrorSummary(params: { return [ `**Your LLM provider API key was rejected (401).** Rotate the key in your provider dashboard, then update the matching GitHub Actions secret.`, "", - `[Update repo secret →](${githubSecretsUrl}) · [Model settings →](${settingsUrl}) · [Setup docs →](https://docs.pullfrog.com/keys)`, + `[Update repo secret →](${githubSecretsUrl}) · [Model settings →](${settingsUrl}) · [Setup docs →](https://docs.pullfrog.com/keys) · [Ask in Discord →](https://discord.gg/8y96raFg8e)`, ].join("\n"); }