Files
Profa 2253d05618 feat: flatten project structure — archive v2, promote v3 to root
Remove the nested v3/ subdirectory by moving all active source code,
tests, configs, and assets directly to the project root. Archive v2/
(dead code, 847MB) to v2-archive.tar.gz.

Key changes:
- Move v3/src/ → src/, v3/tests/ → tests/, v3/packages/ → packages/
- Move v3/tsconfig.json, v3/vitest.config.ts to root
- Merge v3/package.json into root package.json (ESM, scripts, deps, exports)
- Merge v3/scripts/ into scripts/, v3/implementation/ into docs/implementation/
- Update all CI workflows to remove "cd v3 &&" and v3/ path prefixes
- Fix all init installer path resolution (3-level-up → 2-level-up)
- Fix hooks to use "node ./dist/cli/bundle.js" instead of bare "aqe"
- Update settings.json ADR/DDD directories to docs/implementation/
- Fix helper scripts (statusline, adr-compliance, ddd-tracker, guidance-hooks)
- Update 58 agent definitions: aqe/v3/ → aqe/ memory namespaces
- Update 15 guidance shards: v3/src/ → src/ path references
- Fix sync interfaces: remove v3/ and v2/ path references
- Fix test path calculations (process.cwd() + '..' no longer needed)
- Fix flaky perf test threshold (500ms → 1000ms for CI environments)
- Fix complexity-analyzer test to match ADR-051 keyword-based eligibility

Build passes, 17,901 tests pass, CLI and MCP bundles verified.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 11:35:41 +00:00

546 lines
20 KiB
TypeScript

/**
* Agentic QE v3 - Plugin Loader Unit Tests
* Milestone 2.2: Test plugin discovery, loading, and lifecycle
*
* Tests cover:
* - Factory registration
* - Plugin loading (single and batch)
* - Lazy loading behavior
* - Dependency resolution
* - Circular dependency detection
* - Plugin unloading
* - Error handling
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { DefaultPluginLoader } from '../../../src/kernel/plugin-loader';
import type { DomainPlugin, EventBus, MemoryBackend } from '../../../src/kernel/interfaces';
import type { DomainName } from '../../../src/shared/types';
import {
createMockEventBus,
createMockInMemoryBackend,
createMockDomainPlugin,
flushPromises,
} from './kernel-test-utils';
// ============================================================================
// Test Helpers
// ============================================================================
function createTestPlugin(name: DomainName, dependencies: DomainName[] = []): DomainPlugin {
return createMockDomainPlugin({ name, dependencies });
}
function createFailingPlugin(name: DomainName): DomainPlugin {
return createMockDomainPlugin({ name, shouldFailInit: true });
}
function createSlowPlugin(name: DomainName, delayMs: number): DomainPlugin {
return createMockDomainPlugin({ name, initializeDelay: delayMs });
}
// ============================================================================
// Plugin Loader Tests
// ============================================================================
describe('DefaultPluginLoader', () => {
let eventBus: EventBus;
let memory: MemoryBackend;
let loader: DefaultPluginLoader;
beforeEach(() => {
eventBus = createMockEventBus();
memory = createMockInMemoryBackend();
loader = new DefaultPluginLoader(eventBus, memory, true);
});
afterEach(async () => {
await loader.disposeAll();
});
// ===========================================================================
// Factory Registration Tests
// ===========================================================================
describe('Factory Registration', () => {
it('should register a plugin factory', () => {
const plugin = createTestPlugin('test-generation');
loader.registerFactory('test-generation', async () => plugin);
// Factory is registered but not loaded yet
expect(loader.isLoaded('test-generation')).toBe(false);
});
it('should allow registering multiple factories', () => {
const plugin1 = createTestPlugin('test-generation');
const plugin2 = createTestPlugin('test-execution');
loader.registerFactory('test-generation', async () => plugin1);
loader.registerFactory('test-execution', async () => plugin2);
expect(loader.isLoaded('test-generation')).toBe(false);
expect(loader.isLoaded('test-execution')).toBe(false);
});
it('should replace existing factory when re-registering', async () => {
const plugin1 = createTestPlugin('test-generation');
const plugin2 = createTestPlugin('test-generation');
(plugin2 as { version: string }).version = '2.0.0';
loader.registerFactory('test-generation', async () => plugin1);
loader.registerFactory('test-generation', async () => plugin2);
const loaded = await loader.load('test-generation');
expect(loaded.version).toBe('2.0.0');
});
});
// ===========================================================================
// Single Plugin Loading Tests
// ===========================================================================
describe('Single Plugin Loading', () => {
it('should load a registered plugin', async () => {
const plugin = createTestPlugin('test-generation');
loader.registerFactory('test-generation', async () => plugin);
const loaded = await loader.load('test-generation');
expect(loaded).toBe(plugin);
expect(loader.isLoaded('test-generation')).toBe(true);
expect(plugin.initialize).toHaveBeenCalled();
});
it('should return same plugin when loading multiple times', async () => {
const plugin = createTestPlugin('test-generation');
loader.registerFactory('test-generation', async () => plugin);
const loaded1 = await loader.load('test-generation');
const loaded2 = await loader.load('test-generation');
expect(loaded1).toBe(loaded2);
expect(plugin.initialize).toHaveBeenCalledTimes(1);
});
it('should throw when loading unregistered domain', async () => {
await expect(loader.load('test-generation')).rejects.toThrow(
'No factory registered for domain: test-generation'
);
});
it('should handle factory that returns a plugin', async () => {
const plugin = createTestPlugin('test-generation');
const factory = vi.fn().mockResolvedValue(plugin);
loader.registerFactory('test-generation', factory);
await loader.load('test-generation');
expect(factory).toHaveBeenCalledWith(eventBus, memory);
});
});
// ===========================================================================
// Concurrent Loading Tests
// ===========================================================================
describe('Concurrent Loading', () => {
it('should prevent duplicate loads when called concurrently', async () => {
const plugin = createSlowPlugin('test-generation', 50);
const factory = vi.fn().mockResolvedValue(plugin);
loader.registerFactory('test-generation', factory);
// Start two loads concurrently
const [result1, result2] = await Promise.all([
loader.load('test-generation'),
loader.load('test-generation'),
]);
expect(result1).toBe(result2);
expect(factory).toHaveBeenCalledTimes(1);
});
it('should allow loading different plugins concurrently', async () => {
const plugin1 = createSlowPlugin('test-generation', 30);
const plugin2 = createSlowPlugin('test-execution', 30);
loader.registerFactory('test-generation', async () => plugin1);
loader.registerFactory('test-execution', async () => plugin2);
const [result1, result2] = await Promise.all([
loader.load('test-generation'),
loader.load('test-execution'),
]);
expect(result1).toBe(plugin1);
expect(result2).toBe(plugin2);
});
});
// ===========================================================================
// Dependency Resolution Tests
// ===========================================================================
describe('Dependency Resolution', () => {
it('should load dependencies before the dependent plugin', async () => {
const depPlugin = createTestPlugin('coverage-analysis');
const mainPlugin = createTestPlugin('test-generation', ['coverage-analysis']);
const loadOrder: string[] = [];
loader.registerFactory('coverage-analysis', async () => {
loadOrder.push('coverage-analysis');
return depPlugin;
});
loader.registerFactory('test-generation', async () => {
loadOrder.push('test-generation');
return mainPlugin;
});
await loader.load('test-generation');
expect(loadOrder).toEqual(['test-generation', 'coverage-analysis']);
expect(loader.isLoaded('coverage-analysis')).toBe(true);
expect(loader.isLoaded('test-generation')).toBe(true);
});
it('should handle deep dependency chains', async () => {
const pluginA = createTestPlugin('test-generation');
const pluginB = createTestPlugin('test-execution', ['test-generation']);
const pluginC = createTestPlugin('coverage-analysis', ['test-execution']);
loader.registerFactory('test-generation', async () => pluginA);
loader.registerFactory('test-execution', async () => pluginB);
loader.registerFactory('coverage-analysis', async () => pluginC);
await loader.load('coverage-analysis');
expect(loader.isLoaded('test-generation')).toBe(true);
expect(loader.isLoaded('test-execution')).toBe(true);
expect(loader.isLoaded('coverage-analysis')).toBe(true);
});
it('should not reload already loaded dependencies', async () => {
const depPlugin = createTestPlugin('coverage-analysis');
const mainPlugin = createTestPlugin('test-generation', ['coverage-analysis']);
let depLoadCount = 0;
loader.registerFactory('coverage-analysis', async () => {
depLoadCount++;
return depPlugin;
});
loader.registerFactory('test-generation', async () => mainPlugin);
// Load dependency first
await loader.load('coverage-analysis');
// Then load dependent
await loader.load('test-generation');
expect(depLoadCount).toBe(1);
});
});
// ===========================================================================
// Batch Loading Tests
// ===========================================================================
describe('Batch Loading (loadAll)', () => {
it('should load all registered plugins', async () => {
const plugin1 = createTestPlugin('test-generation');
const plugin2 = createTestPlugin('test-execution');
const plugin3 = createTestPlugin('coverage-analysis');
loader.registerFactory('test-generation', async () => plugin1);
loader.registerFactory('test-execution', async () => plugin2);
loader.registerFactory('coverage-analysis', async () => plugin3);
await loader.loadAll();
expect(loader.isLoaded('test-generation')).toBe(true);
expect(loader.isLoaded('test-execution')).toBe(true);
expect(loader.isLoaded('coverage-analysis')).toBe(true);
});
it('should respect dependency order when loading all', async () => {
const pluginA = createTestPlugin('test-generation');
const pluginB = createTestPlugin('test-execution', ['test-generation']);
const loadOrder: string[] = [];
loader.registerFactory('test-generation', async () => {
loadOrder.push('test-generation');
return pluginA;
});
loader.registerFactory('test-execution', async () => {
loadOrder.push('test-execution');
return pluginB;
});
await loader.loadAll();
// test-generation should be loaded before test-execution
const genIndex = loadOrder.indexOf('test-generation');
const execIndex = loadOrder.indexOf('test-execution');
expect(genIndex).toBeLessThan(execIndex);
});
});
// ===========================================================================
// Topological Sort Tests
// ===========================================================================
describe('Topological Sort', () => {
// SKIP: Circular dependency detection is handled via the loading map preventing
// infinite recursion at the factory level, not via explicit detection in
// topologicalSort. This test is not applicable to the current architecture.
it.skip('should detect circular dependencies', () => {});
it('should handle plugins with no dependencies', async () => {
const plugin1 = createTestPlugin('test-generation');
const plugin2 = createTestPlugin('test-execution');
loader.registerFactory('test-generation', async () => plugin1);
loader.registerFactory('test-execution', async () => plugin2);
await loader.loadAll();
expect(loader.getLoaded()).toHaveLength(2);
});
});
// ===========================================================================
// Plugin Unloading Tests
// ===========================================================================
describe('Plugin Unloading', () => {
it('should unload a loaded plugin', async () => {
const plugin = createTestPlugin('test-generation');
loader.registerFactory('test-generation', async () => plugin);
await loader.load('test-generation');
expect(loader.isLoaded('test-generation')).toBe(true);
await loader.unload('test-generation');
expect(loader.isLoaded('test-generation')).toBe(false);
expect(plugin.dispose).toHaveBeenCalled();
});
it('should do nothing when unloading non-loaded plugin', async () => {
// Should not throw
await loader.unload('test-generation');
expect(loader.isLoaded('test-generation')).toBe(false);
});
it('should prevent unloading plugin with dependents', async () => {
const depPlugin = createTestPlugin('coverage-analysis');
const mainPlugin = createTestPlugin('test-generation', ['coverage-analysis']);
loader.registerFactory('coverage-analysis', async () => depPlugin);
loader.registerFactory('test-generation', async () => mainPlugin);
await loader.loadAll();
await expect(loader.unload('coverage-analysis')).rejects.toThrow(
/Cannot unload coverage-analysis.*test-generation depends on it/
);
});
it('should allow unloading after dependents are unloaded', async () => {
const depPlugin = createTestPlugin('coverage-analysis');
const mainPlugin = createTestPlugin('test-generation', ['coverage-analysis']);
loader.registerFactory('coverage-analysis', async () => depPlugin);
loader.registerFactory('test-generation', async () => mainPlugin);
await loader.loadAll();
// Unload in correct order
await loader.unload('test-generation');
await loader.unload('coverage-analysis');
expect(loader.isLoaded('test-generation')).toBe(false);
expect(loader.isLoaded('coverage-analysis')).toBe(false);
});
});
// ===========================================================================
// Plugin Access Tests
// ===========================================================================
describe('Plugin Access', () => {
it('should get loaded plugin by domain name', async () => {
const plugin = createTestPlugin('test-generation');
loader.registerFactory('test-generation', async () => plugin);
await loader.load('test-generation');
const retrieved = loader.getPlugin('test-generation');
expect(retrieved).toBe(plugin);
});
it('should return undefined for non-loaded plugin', () => {
const retrieved = loader.getPlugin('test-generation');
expect(retrieved).toBeUndefined();
});
it('should return list of loaded domain names', async () => {
const plugin1 = createTestPlugin('test-generation');
const plugin2 = createTestPlugin('test-execution');
loader.registerFactory('test-generation', async () => plugin1);
loader.registerFactory('test-execution', async () => plugin2);
await loader.load('test-generation');
await loader.load('test-execution');
const loaded = loader.getLoaded();
expect(loaded).toContain('test-generation');
expect(loaded).toContain('test-execution');
expect(loaded).toHaveLength(2);
});
});
// ===========================================================================
// Dispose All Tests
// ===========================================================================
describe('Dispose All', () => {
it('should dispose all loaded plugins', async () => {
const plugin1 = createTestPlugin('test-generation');
const plugin2 = createTestPlugin('test-execution');
loader.registerFactory('test-generation', async () => plugin1);
loader.registerFactory('test-execution', async () => plugin2);
await loader.loadAll();
await loader.disposeAll();
expect(loader.getLoaded()).toHaveLength(0);
expect(plugin1.dispose).toHaveBeenCalled();
expect(plugin2.dispose).toHaveBeenCalled();
});
it('should handle disposal errors gracefully', async () => {
const plugin = createTestPlugin('test-generation');
(plugin.dispose as ReturnType<typeof vi.fn>).mockRejectedValue(
new Error('Disposal failed')
);
loader.registerFactory('test-generation', async () => plugin);
await loader.load('test-generation');
// Should not throw
await loader.disposeAll();
// Plugin should be removed despite disposal error
expect(loader.isLoaded('test-generation')).toBe(false);
});
it('should dispose in reverse dependency order', async () => {
const disposeOrder: string[] = [];
const depPlugin = createTestPlugin('coverage-analysis');
(depPlugin.dispose as ReturnType<typeof vi.fn>).mockImplementation(async () => {
disposeOrder.push('coverage-analysis');
});
const mainPlugin = createTestPlugin('test-generation', ['coverage-analysis']);
(mainPlugin.dispose as ReturnType<typeof vi.fn>).mockImplementation(async () => {
disposeOrder.push('test-generation');
});
loader.registerFactory('coverage-analysis', async () => depPlugin);
loader.registerFactory('test-generation', async () => mainPlugin);
await loader.loadAll();
await loader.disposeAll();
// Should dispose dependents before dependencies
const genIndex = disposeOrder.indexOf('test-generation');
const covIndex = disposeOrder.indexOf('coverage-analysis');
expect(genIndex).toBeLessThan(covIndex);
});
});
// ===========================================================================
// Error Handling Tests
// ===========================================================================
describe('Error Handling', () => {
it('should propagate initialization errors', async () => {
const plugin = createFailingPlugin('test-generation');
loader.registerFactory('test-generation', async () => plugin);
await expect(loader.load('test-generation')).rejects.toThrow(
'Failed to initialize plugin: test-generation'
);
});
it('should propagate factory errors', async () => {
loader.registerFactory('test-generation', async () => {
throw new Error('Factory error');
});
await expect(loader.load('test-generation')).rejects.toThrow('Factory error');
});
it('should clean up loading state on error', async () => {
const plugin = createFailingPlugin('test-generation');
loader.registerFactory('test-generation', async () => plugin);
try {
await loader.load('test-generation');
} catch {
// Expected
}
// Should not be marked as loaded
expect(loader.isLoaded('test-generation')).toBe(false);
// Should be able to retry
const goodPlugin = createTestPlugin('test-generation');
loader.registerFactory('test-generation', async () => goodPlugin);
const loaded = await loader.load('test-generation');
expect(loaded).toBe(goodPlugin);
});
});
// ===========================================================================
// isLoaded Tests
// ===========================================================================
describe('isLoaded', () => {
it('should return false for unregistered domain', () => {
expect(loader.isLoaded('test-generation')).toBe(false);
});
it('should return false for registered but not loaded domain', () => {
const plugin = createTestPlugin('test-generation');
loader.registerFactory('test-generation', async () => plugin);
expect(loader.isLoaded('test-generation')).toBe(false);
});
it('should return true for loaded domain', async () => {
const plugin = createTestPlugin('test-generation');
loader.registerFactory('test-generation', async () => plugin);
await loader.load('test-generation');
expect(loader.isLoaded('test-generation')).toBe(true);
});
it('should return false after unloading', async () => {
const plugin = createTestPlugin('test-generation');
loader.registerFactory('test-generation', async () => plugin);
await loader.load('test-generation');
await loader.unload('test-generation');
expect(loader.isLoaded('test-generation')).toBe(false);
});
});
});