fix: check saved OpenRouter key and return empty list when cloud config exists (#2640)

collectMissingCredentials() was incorrectly reporting saved credentials as
missing in two ways:
1. It only checked process.env.OPENROUTER_API_KEY, ignoring keys saved via
   OAuth flow to ~/.config/spawn/openrouter.json
2. When hasCloudConfigCredentials() returned true, it filtered to keep
   OPENROUTER_API_KEY in the missing list instead of returning []

Fix: also call hasSavedOpenRouterKey() before marking OPENROUTER_API_KEY as
missing, and return [] (not a filtered list) when cloud config exists.

Fixes #2639

Agent: issue-fixer

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
A 2026-03-14 17:37:18 -07:00 committed by GitHub
parent 245a2a46f9
commit f03e5683c1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 58 additions and 3 deletions

View file

@ -1,6 +1,8 @@
import type { Manifest } from "../manifest";
import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test";
import * as fs from "node:fs";
import * as path from "node:path";
import { preflightCredentialCheck } from "../commands/index.js";
import { mockClackPrompts } from "./test-helpers";
@ -173,4 +175,56 @@ describe("preflightCredentialCheck", () => {
const warnMsg = String(mockLog.warn.mock.calls[0][0]);
expect(warnMsg).toContain("OPENROUTER_API_KEY");
});
it("should not warn when OPENROUTER_API_KEY is saved in config file", async () => {
clearEnv("OPENROUTER_API_KEY");
setEnv("HCLOUD_TOKEN", "test-token");
const spawnConfigDir = path.join(process.env.HOME ?? "", ".config", "spawn");
fs.mkdirSync(spawnConfigDir, {
recursive: true,
});
const keyPath = path.join(spawnConfigDir, "openrouter.json");
fs.writeFileSync(
keyPath,
JSON.stringify({
api_key: "sk-or-v1-" + "a".repeat(64),
}),
);
const manifest = makeManifest("HCLOUD_TOKEN");
await preflightCredentialCheck(manifest, "testcloud");
fs.rmSync(keyPath, {
force: true,
});
expect(mockLog.warn).not.toHaveBeenCalled();
});
it("should not warn when cloud config file has saved credentials", async () => {
clearEnv("OPENROUTER_API_KEY");
clearEnv("HCLOUD_TOKEN");
const spawnConfigDir = path.join(process.env.HOME ?? "", ".config", "spawn");
fs.mkdirSync(spawnConfigDir, {
recursive: true,
});
const cloudConfigPath = path.join(spawnConfigDir, "testcloud.json");
fs.writeFileSync(
cloudConfigPath,
JSON.stringify({
token: "saved-cloud-token",
}),
);
const manifest = makeManifest("HCLOUD_TOKEN");
await preflightCredentialCheck(manifest, "testcloud");
fs.rmSync(cloudConfigPath, {
force: true,
});
// Both OPENROUTER_API_KEY and HCLOUD_TOKEN should be considered satisfied
// because the cloud config file exists with credentials
expect(mockLog.warn).not.toHaveBeenCalled();
});
});

View file

@ -8,6 +8,7 @@ import pc from "picocolors";
import pkg from "../../package.json" with { type: "json" };
import { agentKeys, cloudKeys, isStaleCache, loadManifest, matrixStatus } from "../manifest.js";
import { validateIdentifier, validatePrompt } from "../security.js";
import { hasSavedOpenRouterKey } from "../shared/oauth.js";
import { PkgVersionSchema, parseJsonObj } from "../shared/parse.js";
import { getSpawnCloudConfigPath } from "../shared/paths.js";
import { asyncTryCatch, isFileError, tryCatch, tryCatchIf, unwrapOr } from "../shared/result.js";
@ -525,7 +526,7 @@ function hasCloudConfigCredentials(cloud: string): boolean {
export function collectMissingCredentials(authVars: string[], cloud?: string): string[] {
const missing: string[] = [];
if (!process.env.OPENROUTER_API_KEY) {
if (!process.env.OPENROUTER_API_KEY && !hasSavedOpenRouterKey()) {
missing.push("OPENROUTER_API_KEY");
}
for (const v of authVars) {
@ -534,9 +535,9 @@ export function collectMissingCredentials(authVars: string[], cloud?: string): s
}
}
// If there are missing credentials but the cloud has saved config, don't report them as missing
// If the cloud has saved config credentials, all vars (including cloud-specific ones) are covered
if (missing.length > 0 && cloud && hasCloudConfigCredentials(cloud)) {
return missing.filter((v) => v === "OPENROUTER_API_KEY");
return [];
}
return missing;