import Database from 'better-sqlite3'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { ConfigStore } from '../src/config/persistence.js'; import { loadConfig } from '../src/config/index.js'; import { runMigrations } from '../src/database/client.js'; import { getTestConfig } from './testUtils.js'; describe('ConfigStore', () => { let db: Database.Database; let store: ConfigStore; beforeEach(() => { db = new Database(':memory:'); runMigrations(db); store = new ConfigStore(db); }); afterEach(() => db.close()); it('round-trips application configuration through SQLite', () => { const config = getTestConfig({ rateLimit: { minDelayBetweenMessagesMs: 42, maxMessagesPerHour: 7, jitterMs: 3 }, }); store.save(config); const loaded = store.load({ OPENROUTER_API_KEY: 'environment-key', WEB_DASHBOARD_TOKEN: 'environment-dashboard-token', DATABASE_PATH: './data/from-environment.db', }); expect(loaded?.rateLimit).toEqual(config.rateLimit); expect(loaded?.openrouter.apiKey).toBe('environment-key'); expect(loaded?.web.authToken).toBe('environment-dashboard-token'); expect(loaded?.database.path).toBe('./data/from-environment.db'); }); it('never stores secrets in the database record', () => { const config = getTestConfig(); config.openrouter.apiKey = 'database-must-not-contain-this'; config.web.authToken = 'also-secret'; store.save(config); const row = db.prepare('SELECT config_json FROM app_config WHERE id = 1').get() as { config_json: string }; expect(row.config_json).not.toContain('database-must-not-contain-this'); expect(row.config_json).not.toContain('also-secret'); expect(JSON.parse(row.config_json)).not.toHaveProperty('database'); }); it('updates the singleton record instead of appending versions', () => { store.save(getTestConfig()); store.save(getTestConfig({ browser: { sessionDir: './data/browser-session', dryRun: true } })); expect(db.prepare('SELECT COUNT(*) AS count FROM app_config').get()).toEqual({ count: 1 }); expect(store.load({ OPENROUTER_API_KEY: 'key', WEB_DASHBOARD_TOKEN: 'dashboard-token', })?.browser.dryRun).toBe(true); }); it('imports legacy YAML once and then starts without the file', () => { const originalDirectory = process.cwd(); const tempDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'redditbot-config-import-')); const previousApiKey = process.env.OPENROUTER_API_KEY; const previousDashboardToken = process.env.WEB_DASHBOARD_TOKEN; try { process.chdir(tempDirectory); process.env.OPENROUTER_API_KEY = 'environment-key'; process.env.WEB_DASHBOARD_TOKEN = 'environment-dashboard-token'; fs.writeFileSync('config.yaml', ` reddit: subreddits: [example] openrouter: imageAnalysisPrompt: analyze messageGenerationPrompt: compose `); expect(loadConfig(store).reddit.subreddits[0].name).toBe('example'); fs.rmSync('config.yaml'); expect(loadConfig(store).reddit.subreddits[0].name).toBe('example'); } finally { process.chdir(originalDirectory); fs.rmSync(tempDirectory, { recursive: true, force: true }); if (previousApiKey === undefined) delete process.env.OPENROUTER_API_KEY; else process.env.OPENROUTER_API_KEY = previousApiKey; if (previousDashboardToken === undefined) delete process.env.WEB_DASHBOARD_TOKEN; else process.env.WEB_DASHBOARD_TOKEN = previousDashboardToken; } }); });