env-ensure.test.ts•3.81 kB
import { promises as fs } from 'node:fs';
import { join } from 'node:path';
import os from 'node:os';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { ensureEnv, homeConfigDir, PROVIDER_ENV_KEYS } from '../src/cli/env.js';
const ORIGINAL_ENV = { ...process.env };
describe('ensureEnv', () => {
beforeEach(() => {
process.exitCode = undefined;
process.env = { ...ORIGINAL_ENV };
for (const key of PROVIDER_ENV_KEYS) {
delete process.env[key];
}
});
afterEach(() => {
vi.restoreAllMocks();
process.env = { ...ORIGINAL_ENV };
});
it.each(PROVIDER_ENV_KEYS)(
'returns without writing when %s is present non-interactively',
async (key) => {
process.env[key] = 'present';
const result = await ensureEnv({ interactive: false });
expect(result.wrote).toBe(false);
expect(result.path).toBeUndefined();
expect(result.missing).toBeUndefined();
},
);
it('reports missing values when non-interactive', async () => {
delete process.env.ANTHROPIC_API_KEY;
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
const result = await ensureEnv({ interactive: false });
expect(result.wrote).toBe(false);
expect(result.missing).toEqual([...PROVIDER_ENV_KEYS]);
expect(logSpy).toHaveBeenCalledWith(
expect.stringContaining('No provider API keys detected'),
);
logSpy.mockRestore();
});
it('writes missing secrets to the home config when interactive', async () => {
const tmpHome = await fs.mkdtemp(join(os.tmpdir(), 'vibe-env-'));
vi.spyOn(os, 'homedir').mockReturnValue(tmpHome);
const prompt = vi.fn().mockImplementation(async (key: string) => {
if (key === 'ANTHROPIC_API_KEY') {
return 'interactive-secret';
}
return '';
});
const result = await ensureEnv({ interactive: true, prompt });
expect(prompt).toHaveBeenCalledWith('ANTHROPIC_API_KEY');
expect(result.wrote).toBe(true);
expect(result.path).toBe(join(homeConfigDir(), '.env'));
const stat = await fs.stat(result.path as string);
expect(stat.mode & 0o777).toBe(0o600);
const content = await fs.readFile(result.path as string, 'utf8');
expect(content).toContain('ANTHROPIC_API_KEY=interactive-secret');
expect(process.env.ANTHROPIC_API_KEY).toBe('interactive-secret');
});
it('loads missing secrets from existing env files', async () => {
const tmpHome = await fs.mkdtemp(join(os.tmpdir(), 'vibe-env-'));
vi.spyOn(os, 'homedir').mockReturnValue(tmpHome);
const homeDir = join(tmpHome, '.vibe-check');
await fs.mkdir(homeDir, { recursive: true });
await fs.writeFile(join(homeDir, '.env'), 'OPENAI_API_KEY=from-file\n', 'utf8');
const result = await ensureEnv({ interactive: false });
expect(result.wrote).toBe(false);
expect(process.env.OPENAI_API_KEY).toBe('from-file');
});
it('appends new secrets to the local project env file', async () => {
const tmpDir = await fs.mkdtemp(join(os.tmpdir(), 'vibe-env-local-'));
const originalCwd = process.cwd();
await fs.writeFile(join(tmpDir, '.env'), 'EXISTING=value\n', 'utf8');
try {
process.chdir(tmpDir);
const prompt = vi.fn().mockImplementation(async (key: string) => {
if (key === 'GEMINI_API_KEY') {
return 'value with spaces';
}
return '';
});
const result = await ensureEnv({ interactive: true, local: true, prompt });
expect(result.path).toBe(join(tmpDir, '.env'));
const content = await fs.readFile(result.path as string, 'utf8');
expect(content).toContain('EXISTING=value');
expect(content).toMatch(/GEMINI_API_KEY="value with spaces"/);
} finally {
process.chdir(originalCwd);
}
});
});