mirror of
https://github.com/jeffusion/gitea-ai-assistant.git
synced 2026-03-27 10:05:50 +00:00
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.
228 lines
9.3 KiB
TypeScript
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']);
|
|
});
|
|
});
|
|
});
|