diff --git a/packages/cli/src/__tests__/cmd-fix-cov.test.ts b/packages/cli/src/__tests__/cmd-fix-cov.test.ts index 953cd8ead..00503f446 100644 --- a/packages/cli/src/__tests__/cmd-fix-cov.test.ts +++ b/packages/cli/src/__tests__/cmd-fix-cov.test.ts @@ -10,7 +10,7 @@ import type { SpawnRecord } from "../history"; -import { beforeEach, describe, expect, it, mock } from "bun:test"; +import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test"; import { tryCatch } from "@openrouter/spawn-shared"; import { createMockManifest, mockClackPrompts } from "./test-helpers"; @@ -51,13 +51,25 @@ function makeRecord(overrides: Partial = {}): SpawnRecord { // ── Tests: fixSpawn edge cases ────────────────────────────────────────────── describe("fixSpawn (additional coverage)", () => { + let savedApiKey: string | undefined; + beforeEach(() => { + savedApiKey = process.env.OPENROUTER_API_KEY; + process.env.OPENROUTER_API_KEY = "sk-or-test-fix-key"; clack.logError.mockReset(); clack.logInfo.mockReset(); clack.logSuccess.mockReset(); clack.logStep.mockReset(); }); + afterEach(() => { + if (savedApiKey === undefined) { + delete process.env.OPENROUTER_API_KEY; + } else { + process.env.OPENROUTER_API_KEY = savedApiKey; + } + }); + it("shows error for invalid server_name in connection", async () => { const record = makeRecord({ connection: { @@ -145,12 +157,24 @@ describe("fixSpawn (additional coverage)", () => { // (error paths are covered in cmd-fix.test.ts; this covers the exact success message) describe("fixSpawn connection edge cases", () => { + let savedApiKey: string | undefined; + beforeEach(() => { + savedApiKey = process.env.OPENROUTER_API_KEY; + process.env.OPENROUTER_API_KEY = "sk-or-test-fix-key"; clack.logError.mockReset(); clack.logSuccess.mockReset(); clack.logStep.mockReset(); }); + afterEach(() => { + if (savedApiKey === undefined) { + delete process.env.OPENROUTER_API_KEY; + } else { + process.env.OPENROUTER_API_KEY = savedApiKey; + } + }); + it("shows success when fix script succeeds", async () => { const mockRunner = mock(async () => true); const record = makeRecord(); diff --git a/packages/cli/src/__tests__/cmd-fix.test.ts b/packages/cli/src/__tests__/cmd-fix.test.ts index e5ee162f6..d1650a63d 100644 --- a/packages/cli/src/__tests__/cmd-fix.test.ts +++ b/packages/cli/src/__tests__/cmd-fix.test.ts @@ -194,13 +194,25 @@ describe("buildFixScript", () => { // ── Tests: fixSpawn (DI for SSH runner) ───────────────────────────────────── describe("fixSpawn", () => { + let savedApiKey: string | undefined; + beforeEach(() => { + savedApiKey = process.env.OPENROUTER_API_KEY; + process.env.OPENROUTER_API_KEY = "sk-or-test-fix-key"; clack.logError.mockReset(); clack.logSuccess.mockReset(); clack.logInfo.mockReset(); clack.logStep.mockReset(); }); + afterEach(() => { + if (savedApiKey === undefined) { + delete process.env.OPENROUTER_API_KEY; + } else { + process.env.OPENROUTER_API_KEY = savedApiKey; + } + }); + it("shows error for record without connection info", async () => { const record = makeRecord({ connection: undefined, @@ -309,6 +321,7 @@ describe("fixSpawn", () => { describe("cmdFix", () => { let testDir: string; let savedSpawnHome: string | undefined; + let savedApiKey: string | undefined; let processExitSpy: ReturnType; function writeHistory(records: SpawnRecord[]) { @@ -328,6 +341,8 @@ describe("cmdFix", () => { }); savedSpawnHome = process.env.SPAWN_HOME; process.env.SPAWN_HOME = testDir; + savedApiKey = process.env.OPENROUTER_API_KEY; + process.env.OPENROUTER_API_KEY = "sk-or-test-fix-key"; clack.logError.mockReset(); clack.logSuccess.mockReset(); clack.logInfo.mockReset(); @@ -338,6 +353,11 @@ describe("cmdFix", () => { afterEach(() => { process.env.SPAWN_HOME = savedSpawnHome; + if (savedApiKey === undefined) { + delete process.env.OPENROUTER_API_KEY; + } else { + process.env.OPENROUTER_API_KEY = savedApiKey; + } processExitSpy.mockRestore(); if (existsSync(testDir)) { rmSync(testDir, { diff --git a/packages/cli/src/commands/fix.ts b/packages/cli/src/commands/fix.ts index d24d1e3bc..d8f3b0a04 100644 --- a/packages/cli/src/commands/fix.ts +++ b/packages/cli/src/commands/fix.ts @@ -8,6 +8,7 @@ import pc from "picocolors"; import { getActiveServers } from "../history.js"; import { loadManifest } from "../manifest.js"; import { validateConnectionIP, validateIdentifier, validateServerIdentifier, validateUsername } from "../security.js"; +import { loadSavedOpenRouterKey } from "../shared/oauth.js"; import { getHistoryPath } from "../shared/paths.js"; import { asyncTryCatch, tryCatch } from "../shared/result.js"; import { SSH_INTERACTIVE_OPTS } from "../shared/ssh.js"; @@ -176,6 +177,21 @@ export async function fixSpawn(record: SpawnRecord, manifest: Manifest | null, o return; } + // Ensure OPENROUTER_API_KEY is available before building the fix script. + // The normal provisioning flow uses getOrPromptApiKey() which loads from + // ~/.config/spawn/openrouter.json. buildFixScript() resolves env templates + // from process.env, so we must populate it here to avoid injecting empty keys. + if (!process.env.OPENROUTER_API_KEY) { + const savedKey = loadSavedOpenRouterKey(); + if (savedKey) { + process.env.OPENROUTER_API_KEY = savedKey; + } else { + p.log.error("No OpenRouter API key found."); + p.log.info("Set OPENROUTER_API_KEY in your environment, or run a new spawn to authenticate via OAuth."); + return; + } + } + // Build the remote fix script const scriptResult = tryCatch(() => buildFixScript(man!, record.agent)); if (!scriptResult.ok) { diff --git a/packages/cli/src/shared/oauth.ts b/packages/cli/src/shared/oauth.ts index 3bdc45b59..11772f818 100644 --- a/packages/cli/src/shared/oauth.ts +++ b/packages/cli/src/shared/oauth.ts @@ -285,7 +285,7 @@ export function hasSavedOpenRouterKey(): boolean { } /** Load a previously saved OpenRouter API key from ~/.config/spawn/openrouter.json. */ -function loadSavedOpenRouterKey(): string | null { +export function loadSavedOpenRouterKey(): string | null { const result = tryCatch(() => { const configPath = getSpawnCloudConfigPath("openrouter"); const data = parseJsonObj(readFileSync(configPath, "utf-8"));