Files
archived-gitea-ai-assistant/src/config/__tests__/config-manager.test.ts
jeffusion 1c0c9afd17 feat(review): remove legacy mode and harden agent/codex pipeline
Drop legacy runtime paths and role assignments across backend/frontend, and add upgrade-safe DB migration for existing installs. This aligns config, docs, tests, and UI to the agent-first architecture with codex as the only alternate engine.
2026-03-24 12:30:13 +08:00

228 lines
9.3 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
import { randomUUID } from 'node:crypto';
import { existsSync, mkdirSync, unlinkSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { initMasterKey } from '../../crypto/secrets';
import { closeDatabase, initDatabase } from '../../db/database';
import { settingsRepo } from '../../db/repositories/settings-repo';
import { configManager } from '../config-manager';
// ── Helpers ───────────────────────────────────────────────────────────────────
function makeTmpDb(): string {
const dir = join(tmpdir(), `cfg-test-${randomUUID()}`);
mkdirSync(dir, { recursive: true });
return join(dir, 'test.db');
}
// ── Tests ─────────────────────────────────────────────────────────────────────
describe('ConfigManager (DB backend)', () => {
let dbPath: string;
const savedDbPath = process.env.DATABASE_PATH;
const savedEncryptionKey = process.env.ENCRYPTION_KEY;
beforeEach(() => {
dbPath = makeTmpDb();
process.env.DATABASE_PATH = dbPath;
process.env.ENCRYPTION_KEY = Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString(
'hex'
);
initMasterKey();
initDatabase();
});
afterEach(() => {
closeDatabase();
if (savedDbPath === undefined) {
Reflect.deleteProperty(process.env, 'DATABASE_PATH');
} else {
process.env.DATABASE_PATH = savedDbPath;
}
if (savedEncryptionKey === undefined) {
Reflect.deleteProperty(process.env, 'ENCRYPTION_KEY');
} else {
process.env.ENCRYPTION_KEY = savedEncryptionKey;
}
try {
if (existsSync(dbPath)) unlinkSync(dbPath);
} catch {
/* ok */
}
try {
if (existsSync(`${dbPath}-wal`)) unlinkSync(`${dbPath}-wal`);
} catch {
/* ok */
}
try {
if (existsSync(`${dbPath}-shm`)) unlinkSync(`${dbPath}-shm`);
} catch {
/* ok */
}
});
// ─── 1. getCurrent() defaults ─────────────────────────────────────────────
describe('getCurrent() defaults', () => {
test('returns default engine when DB is empty', () => {
expect(configManager.getCurrent().review.engine).toBe('agent');
});
test('reads port from process.env.PORT, defaults to 5174', () => {
const orig = process.env.PORT;
Reflect.deleteProperty(process.env, 'PORT');
expect(configManager.getCurrent().app.port).toBe(5174);
if (orig !== undefined) process.env.PORT = orig;
});
test('returns default admin password', () => {
expect(configManager.getCurrent().admin.password).toBe('password');
});
test('optional fields with no default return undefined', () => {
const cfg = configManager.getCurrent();
expect(cfg.feishu.webhookUrl).toBeUndefined();
expect(cfg.feishu.webhookSecret).toBeUndefined();
expect(cfg.admin.giteaAdminToken).toBeUndefined();
expect(cfg.review.qdrantUrl).toBeUndefined();
});
test('returns review size thresholds and token budget defaults', () => {
const cfg = configManager.getCurrent();
expect(cfg.review.smallMaxFiles).toBe(3);
expect(cfg.review.smallMaxChangedLines).toBe(80);
expect(cfg.review.mediumMaxFiles).toBe(10);
expect(cfg.review.mediumMaxChangedLines).toBe(400);
expect(cfg.review.tokenBudgetSmall).toBe(12000);
expect(cfg.review.tokenBudgetMedium).toBe(45000);
expect(cfg.review.tokenBudgetLarge).toBe(120000);
});
});
// ─── 2. setOverrides() / getSource() ─────────────────────────────────────
describe('setOverrides() and getSource()', () => {
test('setOverrides writes to DB, getCurrent reflects the change', async () => {
await configManager.setOverrides({ REVIEW_ENGINE: 'agent' });
expect(configManager.getCurrent().review.engine).toBe('agent');
});
test('setOverrides with empty string deletes the key (resets to default)', async () => {
await configManager.setOverrides({ REVIEW_ENGINE: 'agent' });
await configManager.setOverrides({ REVIEW_ENGINE: '' });
expect(configManager.getCurrent().review.engine).toBe('agent');
});
test('getSource returns "db" when value is stored', async () => {
await configManager.setOverrides({ REVIEW_ENGINE: 'agent' });
expect(configManager.getSource('REVIEW_ENGINE')).toBe('db');
});
test('getSource returns "default" when key is absent from DB', () => {
expect(configManager.getSource('REVIEW_ENGINE')).toBe('default');
});
test('sensitive fields are stored encrypted and retrieved correctly', async () => {
await configManager.setOverrides({ GITEA_ACCESS_TOKEN: 'secret-token-123' });
expect(configManager.getCurrent().gitea.accessToken).toBe('secret-token-123');
});
test('unknown keys are silently ignored', async () => {
await configManager.setOverrides({ UNKNOWN_KEY_XYZ: 'value' });
expect(configManager.getCurrent().review.engine).toBe('agent');
});
});
// ─── 3. resetKeys() ──────────────────────────────────────────────────────
describe('resetKeys()', () => {
test('resetKeys deletes key from DB, value reverts to default', async () => {
await configManager.setOverrides({ REVIEW_ENGINE: 'agent' });
await configManager.resetKeys(['REVIEW_ENGINE']);
expect(configManager.getCurrent().review.engine).toBe('agent');
expect(configManager.getSource('REVIEW_ENGINE')).toBe('default');
});
test('resetKeys on non-existent key does not throw', async () => {
await configManager.resetKeys(['REVIEW_ENGINE']);
expect(configManager.getCurrent().review.engine).toBe('agent');
});
});
// ─── 4. seedDefaults() ───────────────────────────────────────────────────
describe('seedDefaults()', () => {
test('seeds all fields with defaults on empty DB', () => {
expect(settingsRepo.listAll().length).toBe(0);
configManager.seedDefaults();
expect(settingsRepo.listAll().length).toBeGreaterThan(0);
});
test('JWT_SECRET and WEBHOOK_SECRET are auto-generated (64 hex chars)', () => {
configManager.seedDefaults();
const jwtSecret = settingsRepo.get('JWT_SECRET');
const webhookSecret = settingsRepo.get('WEBHOOK_SECRET');
expect(jwtSecret).not.toBeNull();
expect(webhookSecret).not.toBeNull();
expect(/^[0-9a-f]{64}$/.test(jwtSecret!)).toBe(true);
expect(/^[0-9a-f]{64}$/.test(webhookSecret!)).toBe(true);
});
test('seedDefaults is idempotent — no-op when DB already has entries', async () => {
await configManager.setOverrides({ REVIEW_ENGINE: 'agent' });
configManager.seedDefaults();
expect(configManager.getCurrent().review.engine).toBe('agent');
});
test('ADMIN_PASSWORD defaults to "password"', () => {
configManager.seedDefaults();
expect(configManager.getCurrent().admin.password).toBe('password');
});
test('seeded JWT_SECRET is a 64-char hex string', () => {
configManager.seedDefaults();
expect(configManager.getCurrent().admin.jwtSecret).toMatch(/^[0-9a-f]{64}$/);
});
test('seeded WEBHOOK_SECRET is a 64-char hex string', () => {
configManager.seedDefaults();
expect(configManager.getCurrent().app.webhookSecret).toMatch(/^[0-9a-f]{64}$/);
});
});
// ─── 5. Type conversions ─────────────────────────────────────────────────
describe('type conversions in getCurrent()', () => {
test('boolean field "true" → true', async () => {
await configManager.setOverrides({ REVIEW_ENABLE_HUMAN_GATE: 'true' });
expect(configManager.getCurrent().review.enableHumanGate).toBe(true);
});
test('boolean field "false" → false', async () => {
await configManager.setOverrides({ REVIEW_ENABLE_HUMAN_GATE: 'false' });
expect(configManager.getCurrent().review.enableHumanGate).toBe(false);
});
test('number field is parsed correctly', async () => {
await configManager.setOverrides({ REVIEW_MAX_PARALLEL_RUNS: '4' });
expect(configManager.getCurrent().review.maxParallelRuns).toBe(4);
});
test('review budget fields are parsed correctly', async () => {
await configManager.setOverrides({
REVIEW_SMALL_MAX_FILES: '5',
REVIEW_TOKEN_BUDGET_SMALL: '22222',
});
expect(configManager.getCurrent().review.smallMaxFiles).toBe(5);
expect(configManager.getCurrent().review.tokenBudgetSmall).toBe(22222);
});
test('comma-separated REVIEW_ALLOWED_COMMANDS parsed to array', async () => {
await configManager.setOverrides({ REVIEW_ALLOWED_COMMANDS: 'git, rg, cat' });
expect(configManager.getCurrent().review.allowedCommands).toEqual(['git', 'rg', 'cat']);
});
});
});