218 lines
7.9 KiB
TypeScript
218 lines
7.9 KiB
TypeScript
import fs from 'fs';
|
|
import path from 'path';
|
|
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
describe('config/env loading', () => {
|
|
let tempRoot: string;
|
|
let previousCwd: string;
|
|
let previousEnv: NodeJS.ProcessEnv;
|
|
|
|
beforeEach(() => {
|
|
tempRoot = fs.mkdtempSync(path.join('/tmp', 'ejclaw-config-'));
|
|
previousCwd = process.cwd();
|
|
previousEnv = { ...process.env };
|
|
process.chdir(tempRoot);
|
|
delete process.env.EJCLAW_STORE_DIR;
|
|
delete process.env.EJCLAW_GROUPS_DIR;
|
|
delete process.env.EJCLAW_DATA_DIR;
|
|
delete process.env.EJCLAW_CACHE_DIR;
|
|
delete process.env.AGENT_TIMEOUT;
|
|
delete process.env.LOG_LEVEL;
|
|
delete process.env.CLAUDE_MODEL;
|
|
delete process.env.CODEX_MODEL;
|
|
delete process.env.STATUS_CHANNEL_ID;
|
|
delete process.env.TZ;
|
|
delete process.env.DISCORD_BOT_TOKEN;
|
|
delete process.env.DISCORD_CLAUDE_BOT_TOKEN;
|
|
delete process.env.DISCORD_CODEX_BOT_TOKEN;
|
|
delete process.env.DISCORD_CODEX_MAIN_BOT_TOKEN;
|
|
delete process.env.DISCORD_REVIEW_BOT_TOKEN;
|
|
delete process.env.DISCORD_CODEX_REVIEW_BOT_TOKEN;
|
|
delete process.env.PAIRED_CARRY_FORWARD_LATEST_OWNER_FINAL;
|
|
delete process.env.PAIRED_FORCE_FRESH_CLAUDE_REVIEWER_SESSION;
|
|
delete process.env.CODEX_WARMUP_ENABLED;
|
|
delete process.env.CODEX_WARMUP_PROMPT;
|
|
delete process.env.CODEX_WARMUP_MODEL;
|
|
delete process.env.CODEX_WARMUP_INTERVAL_MS;
|
|
delete process.env.CODEX_WARMUP_MIN_INTERVAL_MS;
|
|
delete process.env.CODEX_WARMUP_STAGGER_MS;
|
|
delete process.env.CODEX_WARMUP_MAX_USAGE_PCT;
|
|
delete process.env.CODEX_WARMUP_MAX_D7_USAGE_PCT;
|
|
delete process.env.CODEX_WARMUP_COMMAND_TIMEOUT_MS;
|
|
delete process.env.CODEX_WARMUP_FAILURE_COOLDOWN_MS;
|
|
delete process.env.CODEX_WARMUP_MAX_CONSECUTIVE_FAILURES;
|
|
delete process.env.WEB_DASHBOARD_ENABLED;
|
|
delete process.env.WEB_DASHBOARD_HOST;
|
|
delete process.env.WEB_DASHBOARD_PORT;
|
|
delete process.env.WEB_DASHBOARD_STATIC_DIR;
|
|
delete process.env.WEB_DASHBOARD_TOKEN;
|
|
delete process.env.SESSION_COMMAND_USER_IDS;
|
|
vi.resetModules();
|
|
});
|
|
|
|
afterEach(() => {
|
|
process.chdir(previousCwd);
|
|
for (const key of Object.keys(process.env)) {
|
|
if (!(key in previousEnv)) delete process.env[key];
|
|
}
|
|
for (const [key, value] of Object.entries(previousEnv)) {
|
|
if (value === undefined) {
|
|
delete process.env[key];
|
|
} else {
|
|
process.env[key] = value;
|
|
}
|
|
}
|
|
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
});
|
|
|
|
it('preserves empty-string env values and still prefers process.env over .env', async () => {
|
|
fs.writeFileSync(
|
|
path.join(tempRoot, '.env'),
|
|
[
|
|
'FROM_FILE=file-value',
|
|
'EMPTY_IN_FILE=',
|
|
'PROCESS_EMPTY=file-fallback',
|
|
].join('\n'),
|
|
);
|
|
process.env.FROM_FILE = 'process-value';
|
|
process.env.PROCESS_EMPTY = '';
|
|
|
|
const env = await import('./env.js');
|
|
|
|
expect(env.getEnv('FROM_FILE')).toBe('process-value');
|
|
expect(env.getEnv('EMPTY_IN_FILE')).toBe('');
|
|
expect(env.getEnv('PROCESS_EMPTY')).toBe('');
|
|
expect(env.readEnvFile(['EMPTY_IN_FILE', 'PROCESS_EMPTY'])).toEqual({
|
|
EMPTY_IN_FILE: '',
|
|
PROCESS_EMPTY: 'file-fallback',
|
|
});
|
|
});
|
|
|
|
it('loads formerly process-only config values through the shared env path', async () => {
|
|
fs.writeFileSync(
|
|
path.join(tempRoot, '.env'),
|
|
[
|
|
'EJCLAW_STORE_DIR=./custom-store',
|
|
'EJCLAW_GROUPS_DIR=./custom-groups',
|
|
'EJCLAW_DATA_DIR=./custom-data',
|
|
'EJCLAW_CACHE_DIR=./custom-cache',
|
|
'AGENT_TIMEOUT=12345',
|
|
'LOG_LEVEL=trace',
|
|
'CLAUDE_MODEL=claude-opus-test',
|
|
'CODEX_MODEL=gpt-5.4-test',
|
|
'STATUS_CHANNEL_ID=status-room',
|
|
'TZ=UTC',
|
|
].join('\n'),
|
|
);
|
|
|
|
const config = await import('./config.js');
|
|
|
|
expect(config.STORE_DIR).toBe(path.resolve(tempRoot, 'custom-store'));
|
|
expect(config.GROUPS_DIR).toBe(path.resolve(tempRoot, 'custom-groups'));
|
|
expect(config.DATA_DIR).toBe(path.resolve(tempRoot, 'custom-data'));
|
|
expect(config.CACHE_DIR).toBe(path.resolve(tempRoot, 'custom-cache'));
|
|
expect(config.AGENT_TIMEOUT).toBe(12345);
|
|
expect(config.LOG_LEVEL).toBe('trace');
|
|
expect(config.DEFAULT_CLAUDE_MODEL).toBe('claude-opus-test');
|
|
expect(config.DEFAULT_CODEX_MODEL).toBe('gpt-5.4-test');
|
|
expect(config.STATUS_CHANNEL_ID).toBe('status-room');
|
|
expect(config.TIMEZONE).toBe('UTC');
|
|
});
|
|
|
|
it('defaults latest-owner-final carry-forward to disabled and honors explicit opt-in', async () => {
|
|
let config = await import('./config.js');
|
|
expect(config.PAIRED_CARRY_FORWARD_LATEST_OWNER_FINAL).toBe(false);
|
|
|
|
vi.resetModules();
|
|
process.env.PAIRED_CARRY_FORWARD_LATEST_OWNER_FINAL = 'true';
|
|
config = await import('./config.js');
|
|
expect(config.PAIRED_CARRY_FORWARD_LATEST_OWNER_FINAL).toBe(true);
|
|
});
|
|
|
|
it('defaults unsafe-host Claude reviewer fresh-session forcing to disabled and honors explicit opt-in', async () => {
|
|
let config = await import('./config.js');
|
|
expect(config.PAIRED_FORCE_FRESH_CLAUDE_REVIEWER_SESSION).toBe(false);
|
|
|
|
vi.resetModules();
|
|
process.env.PAIRED_FORCE_FRESH_CLAUDE_REVIEWER_SESSION = 'true';
|
|
config = await import('./config.js');
|
|
expect(config.PAIRED_FORCE_FRESH_CLAUDE_REVIEWER_SESSION).toBe(true);
|
|
});
|
|
|
|
it('keeps Codex warm-up disabled by default and exposes conservative opt-in env config', async () => {
|
|
let config = await import('./config.js');
|
|
expect(config.CODEX_WARMUP_CONFIG.enabled).toBe(false);
|
|
expect(config.CODEX_WARMUP_CONFIG.maxUsagePct).toBe(0);
|
|
expect(config.CODEX_WARMUP_CONFIG.maxD7UsagePct).toBe(0);
|
|
expect(
|
|
config.CODEX_WARMUP_CONFIG.maxConsecutiveFailures,
|
|
).toBeGreaterThanOrEqual(1);
|
|
|
|
vi.resetModules();
|
|
process.env.CODEX_MODEL = 'gpt-5.5';
|
|
process.env.CODEX_WARMUP_ENABLED = 'true';
|
|
process.env.CODEX_WARMUP_PROMPT = '.';
|
|
process.env.CODEX_WARMUP_STAGGER_MS = '600000';
|
|
process.env.CODEX_WARMUP_MAX_CONSECUTIVE_FAILURES = '1';
|
|
config = await import('./config.js');
|
|
|
|
expect(config.CODEX_WARMUP_CONFIG).toEqual(
|
|
expect.objectContaining({
|
|
enabled: true,
|
|
prompt: '.',
|
|
model: 'gpt-5.5',
|
|
staggerMs: 600000,
|
|
maxConsecutiveFailures: 1,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('keeps the web dashboard disabled by default and loads explicit bind/static settings', async () => {
|
|
let config = await import('./config.js');
|
|
expect(config.WEB_DASHBOARD.enabled).toBe(false);
|
|
expect(config.WEB_DASHBOARD.host).toBe('127.0.0.1');
|
|
expect(config.WEB_DASHBOARD.port).toBe(8734);
|
|
expect(config.WEB_DASHBOARD.staticDir).toBe(
|
|
path.resolve(tempRoot, 'apps', 'dashboard', 'dist'),
|
|
);
|
|
expect(config.WEB_DASHBOARD.token).toBe('');
|
|
|
|
vi.resetModules();
|
|
process.env.WEB_DASHBOARD_ENABLED = 'true';
|
|
process.env.WEB_DASHBOARD_HOST = '0.0.0.0';
|
|
process.env.WEB_DASHBOARD_PORT = '9001';
|
|
process.env.WEB_DASHBOARD_STATIC_DIR = './custom-dashboard-dist';
|
|
process.env.WEB_DASHBOARD_TOKEN = 'mobile-secret';
|
|
config = await import('./config.js');
|
|
|
|
expect(config.WEB_DASHBOARD).toEqual({
|
|
enabled: true,
|
|
host: '0.0.0.0',
|
|
port: 9001,
|
|
staticDir: path.resolve(tempRoot, 'custom-dashboard-dist'),
|
|
token: 'mobile-secret',
|
|
});
|
|
});
|
|
|
|
it('fails fast when a legacy Discord token alias is configured', async () => {
|
|
process.env.DISCORD_BOT_TOKEN = 'legacy...oken';
|
|
|
|
const { loadConfig } = await import('./config/load-config.js');
|
|
|
|
expect(() => loadConfig()).toThrow(
|
|
/Legacy env aliases are no longer supported; remove or rename \(DISCORD_BOT_TOKEN\) to the canonical keys/,
|
|
);
|
|
});
|
|
|
|
it('fails fast when SESSION_COMMAND_USER_IDS is configured', async () => {
|
|
process.env.SESSION_COMMAND_USER_IDS = 'alice,bob';
|
|
|
|
const { loadConfig } = await import('./config/load-config.js');
|
|
|
|
expect(() => loadConfig()).toThrow(
|
|
/Legacy env aliases are no longer supported; remove or rename \(SESSION_COMMAND_USER_IDS\) to the canonical keys/,
|
|
);
|
|
});
|
|
});
|