/** * Tests for module configuration service */ import { afterEach, beforeEach, describe, expect, it } from 'bun:test'; import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { closeDb, createDbClient } from '../db/client'; import { runMigrations } from '../db/migrate'; import { modules } from '../db/schema'; import { resetTestDbPath } from '../test-utils/db-path'; import { formatConfigValue, getAllModuleConfigValues, getModuleConfigValue, isComplexValue, parseConfigValue, setModuleConfigValue, } from './module-config'; describe('module-config service', () => { let testDbPath: string; let testDir: string; beforeEach(async () => { // Create temp directory for test database testDir = mkdtempSync(join(tmpdir(), 'celilo-test-')); testDbPath = join(testDir, 'test.db'); // Set environment variable for database path process.env.CELILO_DB_PATH = testDbPath; // Initialize database and run migrations await runMigrations(testDbPath); // Insert test module const db = createDbClient({ path: testDbPath }); db.insert(modules) .values({ id: 'test-module', name: 'Test Module', version: '1.0.0', manifestData: { variables: { owns: [ { name: 'string_var', type: 'string' }, { name: 'number_var', type: 'number' }, { name: 'bool_var', type: 'boolean' }, { name: 'array_var', type: 'array' }, { name: 'object_var', type: 'object' }, ], }, }, sourcePath: '/test/path', }) .run(); }); afterEach(() => { closeDb(); // Clean up environment variable resetTestDbPath(); // Clean up test database rmSync(testDir, { recursive: true, force: true }); }); describe('isComplexValue', () => { it('should return false for primitives', () => { expect(isComplexValue('string')).toBe(false); expect(isComplexValue(123)).toBe(false); expect(isComplexValue(true)).toBe(false); expect(isComplexValue(null)).toBe(false); expect(isComplexValue(undefined)).toBe(false); }); it('should return true for arrays', () => { expect(isComplexValue([])).toBe(true); expect(isComplexValue([1, 2, 3])).toBe(true); expect(isComplexValue(['a', 'b'])).toBe(true); }); it('should return true for objects', () => { expect(isComplexValue({})).toBe(false); // Empty object expect(isComplexValue({ key: 'value' })).toBe(true); expect(isComplexValue({ a: 1, b: 2 })).toBe(true); }); }); describe('parseConfigValue', () => { it('should parse primitives from strings', async () => { expect(await parseConfigValue('hello')).toBe('hello'); expect(await parseConfigValue('123')).toBe(123); expect(await parseConfigValue('true')).toBe(true); expect(await parseConfigValue('false')).toBe(false); }); it('should parse JSON strings', async () => { expect(await parseConfigValue('{"key":"value"}')).toEqual({ key: 'value' }); expect(await parseConfigValue('[1,2,3]')).toEqual([1, 2, 3]); }); it('should read from @file syntax', async () => { const filePath = join(testDir, 'config.json'); writeFileSync(filePath, JSON.stringify({ key: 'value' })); const result = await parseConfigValue(`@${filePath}`); expect(result).toEqual({ key: 'value' }); }); }); describe('setModuleConfigValue and getModuleConfigValue', () => { it('should store and retrieve primitive string', async () => { await setModuleConfigValue('test-module', 'string_var', 'hello'); const result = getModuleConfigValue('test-module', 'string_var'); expect(result).not.toBeNull(); expect(result?.value).toBe('hello'); expect(result?.isPrimitive).toBe(true); }); it('should store and retrieve primitive number', async () => { await setModuleConfigValue('test-module', 'number_var', '42'); const result = getModuleConfigValue('test-module', 'number_var'); expect(result).not.toBeNull(); expect(result?.value).toBe(42); expect(result?.isPrimitive).toBe(true); }); it('should store and retrieve primitive boolean', async () => { await setModuleConfigValue('test-module', 'bool_var', 'true'); const result = getModuleConfigValue('test-module', 'bool_var'); expect(result).not.toBeNull(); expect(result?.value).toBe(true); expect(result?.isPrimitive).toBe(true); }); it('should store and retrieve complex array', async () => { const arrayValue = JSON.stringify([1, 2, 3]); await setModuleConfigValue('test-module', 'array_var', arrayValue); const result = getModuleConfigValue('test-module', 'array_var'); expect(result).not.toBeNull(); expect(result?.value).toEqual([1, 2, 3]); expect(result?.isPrimitive).toBe(false); }); it('should store and retrieve complex object', async () => { const objectValue = JSON.stringify({ key: 'value', nested: { foo: 'bar' } }); await setModuleConfigValue('test-module', 'object_var', objectValue); const result = getModuleConfigValue('test-module', 'object_var'); expect(result).not.toBeNull(); expect(result?.value).toEqual({ key: 'value', nested: { foo: 'bar' } }); expect(result?.isPrimitive).toBe(false); }); it('should update existing config', async () => { await setModuleConfigValue('test-module', 'string_var', 'first'); await setModuleConfigValue('test-module', 'string_var', 'second'); const result = getModuleConfigValue('test-module', 'string_var'); expect(result?.value).toBe('second'); }); it('should reject type mismatch based on manifest (primitive to complex)', async () => { // array_var is declared as type: array in manifest // Setting it to a string should fail manifest validation await expect( setModuleConfigValue('test-module', 'array_var', 'simple string'), ).rejects.toThrow('Expected array'); }); it('should reject type mismatch based on manifest (complex to primitive)', async () => { // string_var is declared as type: string in manifest // Setting it to an object (via JSON) should fail manifest validation await expect( setModuleConfigValue('test-module', 'string_var', '{"key":"value"}'), ).rejects.toThrow('Expected string'); }); }); describe('getAllModuleConfigValues', () => { it('should return empty array when no config exists', () => { const result = getAllModuleConfigValues('test-module'); expect(result).toEqual([]); }); it('should return all config values', async () => { await setModuleConfigValue('test-module', 'string_var', 'hello'); await setModuleConfigValue('test-module', 'number_var', '42'); await setModuleConfigValue('test-module', 'array_var', '[1,2,3]'); const result = getAllModuleConfigValues('test-module'); expect(result).toHaveLength(3); const stringVar = result.find((c) => c.key === 'string_var'); expect(stringVar?.value).toBe('hello'); expect(stringVar?.isPrimitive).toBe(true); const numberVar = result.find((c) => c.key === 'number_var'); expect(numberVar?.value).toBe(42); expect(numberVar?.isPrimitive).toBe(true); const arrayVar = result.find((c) => c.key === 'array_var'); expect(arrayVar?.value).toEqual([1, 2, 3]); expect(arrayVar?.isPrimitive).toBe(false); }); }); describe('formatConfigValue', () => { it('should format primitive values', () => { expect(formatConfigValue({ key: 'test', value: 'hello', isPrimitive: true })).toBe('hello'); expect(formatConfigValue({ key: 'test', value: 42, isPrimitive: true })).toBe('42'); expect(formatConfigValue({ key: 'test', value: true, isPrimitive: true })).toBe('true'); }); it('should format complex values as JSON', () => { const result = formatConfigValue({ key: 'test', value: [1, 2, 3], isPrimitive: false, }); expect(result).toBe('[\n 1,\n 2,\n 3\n]'); }); it('should format object values as JSON', () => { const result = formatConfigValue({ key: 'test', value: { key: 'value' }, isPrimitive: false, }); expect(result).toBe('{\n "key": "value"\n}'); }); }); });