fix(core): harden env loading, OpenAI compatibility, and runtime recovery

This commit is contained in:
William Valentin
2026-02-22 15:56:21 -08:00
parent 387906ce4d
commit dafe9b4d3d
11 changed files with 450 additions and 21 deletions
+46
View File
@@ -484,6 +484,52 @@ models:
}
});
it('loads env vars from FLYNN_ENV_FILE before env-var checks', async () => {
const originalEnvFile = process.env.FLYNN_ENV_FILE;
const originalOpenAIKey = process.env.OPENAI_API_KEY;
delete process.env.OPENAI_API_KEY;
try {
mkdirSync(testDir, { recursive: true });
const envPath = join(testDir, 'cloud.env');
writeFileSync(envPath, 'OPENAI_API_KEY=sk-test-from-env-file\n');
process.env.FLYNN_ENV_FILE = envPath;
const configPath = join(testDir, 'openai-env-file.yaml');
writeFileSync(configPath, `
telegram:
bot_token: "test-token"
allowed_chat_ids: [123]
models:
default:
provider: openai
model: gpt-5.2
auth_mode: api_key
api_key: \${OPENAI_API_KEY}
`);
const ctx: DoctorContext = { configPath, dataDir: testDir };
const results = await runChecks(ctx);
const envCheck = results.find((r) => r.label.includes('Env vars resolved')) as CheckResult | undefined;
const modelCheck = results.find((r) => r.label.includes('Model connectivity')) as CheckResult | undefined;
expect(envCheck?.status).toBe('pass');
expect(modelCheck?.status).toBe('pass');
expect(modelCheck?.detail).toContain('api_key=config+env');
} finally {
if (originalEnvFile !== undefined) {
process.env.FLYNN_ENV_FILE = originalEnvFile;
} else {
delete process.env.FLYNN_ENV_FILE;
}
if (originalOpenAIKey !== undefined) {
process.env.OPENAI_API_KEY = originalOpenAIKey;
} else {
delete process.env.OPENAI_API_KEY;
}
}
});
it('reports WARN when Vercel AI Gateway has no available API key sources', async () => {
const originalKey = process.env.AI_GATEWAY_API_KEY;
delete process.env.AI_GATEWAY_API_KEY;
+3 -1
View File
@@ -1,6 +1,6 @@
import type { Command } from 'commander';
import type { Config } from '../config/index.js';
import { getConfigPath, getDataDir, formatStatus, resolveOverlayPath } from './shared.js';
import { getConfigPath, getDataDir, formatStatus, resolveOverlayPath, loadEnvFileIfPresent } from './shared.js';
import { existsSync, readFileSync, writeFileSync, unlinkSync } from 'fs';
import { homedir } from 'os';
import { resolve, join } from 'path';
@@ -643,6 +643,8 @@ const allChecks: Check[] = [
/** Run all doctor checks in order. Exported for testing. */
export async function runChecks(ctx: DoctorContext): Promise<CheckResult[]> {
// Keep doctor behavior aligned with other CLI commands that load cloud.env.
loadEnvFileIfPresent();
const results: CheckResult[] = [];
for (const check of allChecks) {
const result = await check(ctx);
+1 -1
View File
@@ -4,7 +4,7 @@ import { resolve, dirname, join } from 'path';
import { homedir } from 'os';
import { existsSync, readFileSync } from 'fs';
function loadEnvFileIfPresent(): void {
export function loadEnvFileIfPresent(): void {
const envFile = process.env.FLYNN_ENV_FILE ?? resolve(homedir(), '.config/flynn/cloud.env');
if (!existsSync(envFile)) {
return;