runtimeConfig.test.ts
4,860 bytes
| 1 | import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' |
|---|---|
| 2 | import { tmpdir } from 'node:os' |
| 3 | import path from 'node:path' |
| 4 | import { afterEach, beforeEach, describe, expect, it } from 'vitest' |
| 5 | import { applyConfig, configStatus, keyHint, mergeEnvFile } from './runtimeConfig' |
| 6 | |
| 7 | describe('configStatus', () => { |
| 8 | it('reports demo mode when mocks are forced', () => { |
| 9 | const status = configStatus(true, { MOCK_PROVIDERS: '1' }) |
| 10 | expect(status.mode).toBe('demo') |
| 11 | expect(status.demoLocked).toBe(true) |
| 12 | }) |
| 13 | |
| 14 | it('reports real mode only when both keys are present', () => { |
| 15 | expect(configStatus(false, { ANTHROPIC_API_KEY: 'sk-ant-1234abcd' }).mode).toBe('unconfigured') |
| 16 | expect( |
| 17 | configStatus(false, { ANTHROPIC_API_KEY: 'sk-ant-1234abcd', OPENAI_API_KEY: 'sk-1234wxyz' }).mode, |
| 18 | ).toBe('real') |
| 19 | }) |
| 20 | |
| 21 | it('treats .env.example placeholders as unset', () => { |
| 22 | const status = configStatus(false, { |
| 23 | ANTHROPIC_API_KEY: '@insert-anthropic-api-key@', |
| 24 | OPENAI_API_KEY: '@insert-openai-api-key@', |
| 25 | }) |
| 26 | expect(status.mode).toBe('unconfigured') |
| 27 | expect(status.anthropicKeySet).toBe(false) |
| 28 | expect(status.openaiKeySet).toBe(false) |
| 29 | }) |
| 30 | |
| 31 | it('never exposes more than the last four characters of a key', () => { |
| 32 | const status = configStatus(false, { ANTHROPIC_API_KEY: 'sk-ant-secret-value-abcd' }) |
| 33 | expect(status.anthropicKeyHint).toBe('...abcd') |
| 34 | expect(JSON.stringify(status)).not.toContain('secret') |
| 35 | expect(keyHint(null)).toBeNull() |
| 36 | }) |
| 37 | }) |
| 38 | |
| 39 | describe('mergeEnvFile', () => { |
| 40 | it('replaces existing assignments and keeps comments and other keys', () => { |
| 41 | const merged = mergeEnvFile('# note\nMOCK_PROVIDERS=1\nPORT=4000\n', { |
| 42 | MOCK_PROVIDERS: '', |
| 43 | ANTHROPIC_API_KEY: 'sk-ant-new', |
| 44 | }) |
| 45 | expect(merged).toBe('# note\nMOCK_PROVIDERS=\nPORT=4000\nANTHROPIC_API_KEY=sk-ant-new\n') |
| 46 | }) |
| 47 | |
| 48 | it('drops duplicate assignments of an updated key', () => { |
| 49 | const merged = mergeEnvFile('OPENAI_API_KEY=old-one\nPORT=3001\nOPENAI_API_KEY=old-two\n', { |
| 50 | OPENAI_API_KEY: 'sk-new', |
| 51 | }) |
| 52 | expect(merged).toBe('OPENAI_API_KEY=sk-new\nPORT=3001\n') |
| 53 | }) |
| 54 | |
| 55 | it('writes into an empty file', () => { |
| 56 | expect(mergeEnvFile('', { MOCK_PROVIDERS: '1' })).toBe('MOCK_PROVIDERS=1\n') |
| 57 | }) |
| 58 | }) |
| 59 | |
| 60 | describe('applyConfig', () => { |
| 61 | let dir: string |
| 62 | let envPath: string |
| 63 | |
| 64 | beforeEach(async () => { |
| 65 | dir = await mkdtemp(path.join(tmpdir(), 'voicetask-env-')) |
| 66 | envPath = path.join(dir, '.env') |
| 67 | }) |
| 68 | |
| 69 | afterEach(async () => { |
| 70 | await rm(dir, { recursive: true, force: true }) |
| 71 | }) |
| 72 | |
| 73 | it('stores keys and switches the running process to real providers', async () => { |
| 74 | const env: NodeJS.ProcessEnv = { MOCK_PROVIDERS: '1' } |
| 75 | const result = applyConfig( |
| 76 | { mode: 'real', anthropicKey: 'sk-ant-1234abcd', openaiKey: 'sk-1234wxyz' }, |
| 77 | { demoLocked: false, envPath, env }, |
| 78 | ) |
| 79 | |
| 80 | expect(result.restartRequired).toBe(false) |
| 81 | expect(result.status.mode).toBe('real') |
| 82 | expect(env.MOCK_PROVIDERS).toBeUndefined() |
| 83 | expect(env.ANTHROPIC_API_KEY).toBe('sk-ant-1234abcd') |
| 84 | |
| 85 | const written = await readFile(envPath, 'utf8') |
| 86 | expect(written).toContain('ANTHROPIC_API_KEY=sk-ant-1234abcd') |
| 87 | expect(written).toContain('OPENAI_API_KEY=sk-1234wxyz') |
| 88 | expect(written).toContain('MOCK_PROVIDERS=') |
| 89 | }) |
| 90 | |
| 91 | it('keeps an existing key when only the other one is sent', async () => { |
| 92 | await writeFile(envPath, 'ANTHROPIC_API_KEY=sk-ant-old\n', 'utf8') |
| 93 | const env: NodeJS.ProcessEnv = { ANTHROPIC_API_KEY: 'sk-ant-old' } |
| 94 | applyConfig({ mode: 'real', openaiKey: 'sk-1234wxyz' }, { demoLocked: false, envPath, env }) |
| 95 | |
| 96 | const written = await readFile(envPath, 'utf8') |
| 97 | expect(written).toContain('ANTHROPIC_API_KEY=sk-ant-old') |
| 98 | expect(env.ANTHROPIC_API_KEY).toBe('sk-ant-old') |
| 99 | }) |
| 100 | |
| 101 | it('saves keys but stays in mocks when the server was started in demo mode', async () => { |
| 102 | const env: NodeJS.ProcessEnv = { MOCK_PROVIDERS: '1' } |
| 103 | const result = applyConfig( |
| 104 | { mode: 'real', anthropicKey: 'sk-ant-1234abcd', openaiKey: 'sk-1234wxyz' }, |
| 105 | { demoLocked: true, envPath, env }, |
| 106 | ) |
| 107 | |
| 108 | expect(result.restartRequired).toBe(true) |
| 109 | expect(result.status.mode).toBe('demo') |
| 110 | expect(env.ANTHROPIC_API_KEY).toBeUndefined() |
| 111 | expect(await readFile(envPath, 'utf8')).toContain('ANTHROPIC_API_KEY=sk-ant-1234abcd') |
| 112 | }) |
| 113 | |
| 114 | it('switches to demo mode without touching stored keys', async () => { |
| 115 | await writeFile(envPath, 'ANTHROPIC_API_KEY=sk-ant-old\nOPENAI_API_KEY=sk-old\n', 'utf8') |
| 116 | const env: NodeJS.ProcessEnv = { ANTHROPIC_API_KEY: 'sk-ant-old', OPENAI_API_KEY: 'sk-old' } |
| 117 | const result = applyConfig({ mode: 'demo' }, { demoLocked: false, envPath, env }) |
| 118 | |
| 119 | expect(result.status.mode).toBe('demo') |
| 120 | expect(env.MOCK_PROVIDERS).toBe('1') |
| 121 | expect(await readFile(envPath, 'utf8')).toContain('ANTHROPIC_API_KEY=sk-ant-old') |
| 122 | }) |
| 123 | }) |
| 124 | |