mirror of
https://github.com/proffesor-for-testing/agentic-qe.git
synced 2026-09-19 08:45:47 +08:00
2253d05618
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>
546 lines
20 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|