mirror of
https://github.com/proffesor-for-testing/agentic-qe.git
synced 2026-09-19 08:45:47 +08:00
e82a7240ec
11 improvements across 4 priority tiers, all integrated into production code paths with 416 new tests across 33 test files. Each wave passed a brutal honesty audit with all findings resolved. Wave 0-2 (P0/P1): Middleware chain, microcompact, tool concurrency, retry engine, session durability, prompt cache latch, startup fast paths, hook security hardening. Wave 3 (P2): 4-tier context compaction pipeline, plugin architecture with local/GitHub/npm sources. Wave 4 (P3): QE Quality Daemon — priority queue, git watcher (with Linux polling fallback), coverage delta analyzer, CI monitor, test suggester, nightly consolidation, notification service with SSRF guard, persistent SQLite-backed memory, CLI commands, MCP startup integration. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
152 lines
4.6 KiB
TypeScript
152 lines
4.6 KiB
TypeScript
/**
|
|
* Tests for Plugin Dependency Resolver (IMP-09)
|
|
*/
|
|
|
|
import { describe, it, expect } from 'vitest';
|
|
import { PluginResolver } from '../../src/plugins/resolver';
|
|
import type { QEPluginManifest } from '../../src/plugins/manifest';
|
|
|
|
function makeManifest(name: string, deps?: Record<string, string>): QEPluginManifest {
|
|
return {
|
|
name,
|
|
version: '1.0.0',
|
|
description: `Plugin ${name}`,
|
|
author: 'test',
|
|
domains: ['test-generation' as never],
|
|
entryPoint: 'index.js',
|
|
dependencies: deps,
|
|
};
|
|
}
|
|
|
|
describe('PluginResolver', () => {
|
|
const resolver = new PluginResolver();
|
|
|
|
describe('simple resolution', () => {
|
|
it('should resolve plugins with no dependencies', () => {
|
|
const result = resolver.resolve([
|
|
makeManifest('alpha'),
|
|
makeManifest('beta'),
|
|
]);
|
|
expect(result.ordered).toHaveLength(2);
|
|
expect(result.missing.size).toBe(0);
|
|
});
|
|
|
|
it('should resolve empty list', () => {
|
|
const result = resolver.resolve([]);
|
|
expect(result.ordered).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe('dependency ordering', () => {
|
|
it('should load dependencies before dependents', () => {
|
|
const result = resolver.resolve([
|
|
makeManifest('child', { parent: '^1.0.0' }),
|
|
makeManifest('parent'),
|
|
]);
|
|
|
|
const names = result.ordered.map(r => r.manifest.name);
|
|
expect(names.indexOf('parent')).toBeLessThan(names.indexOf('child'));
|
|
});
|
|
|
|
it('should handle deep dependency chains', () => {
|
|
const result = resolver.resolve([
|
|
makeManifest('c', { b: '^1.0.0' }),
|
|
makeManifest('b', { a: '^1.0.0' }),
|
|
makeManifest('a'),
|
|
]);
|
|
|
|
const names = result.ordered.map(r => r.manifest.name);
|
|
expect(names).toEqual(['a', 'b', 'c']);
|
|
});
|
|
|
|
it('should handle diamond dependencies', () => {
|
|
// A depends on B and C, both depend on D
|
|
const result = resolver.resolve([
|
|
makeManifest('a', { b: '^1.0.0', c: '^1.0.0' }),
|
|
makeManifest('b', { d: '^1.0.0' }),
|
|
makeManifest('c', { d: '^1.0.0' }),
|
|
makeManifest('d'),
|
|
]);
|
|
|
|
const names = result.ordered.map(r => r.manifest.name);
|
|
// D must come before B and C, which must come before A
|
|
expect(names.indexOf('d')).toBeLessThan(names.indexOf('b'));
|
|
expect(names.indexOf('d')).toBeLessThan(names.indexOf('c'));
|
|
expect(names.indexOf('b')).toBeLessThan(names.indexOf('a'));
|
|
expect(names.indexOf('c')).toBeLessThan(names.indexOf('a'));
|
|
});
|
|
});
|
|
|
|
describe('cycle detection', () => {
|
|
it('should detect direct cycles', () => {
|
|
expect(() =>
|
|
resolver.resolve([
|
|
makeManifest('a', { b: '^1.0.0' }),
|
|
makeManifest('b', { a: '^1.0.0' }),
|
|
]),
|
|
).toThrow(/cycle/i);
|
|
});
|
|
|
|
it('should detect indirect cycles', () => {
|
|
expect(() =>
|
|
resolver.resolve([
|
|
makeManifest('a', { b: '^1.0.0' }),
|
|
makeManifest('b', { c: '^1.0.0' }),
|
|
makeManifest('c', { a: '^1.0.0' }),
|
|
]),
|
|
).toThrow(/cycle/i);
|
|
});
|
|
|
|
it('should include cycle path in error message', () => {
|
|
try {
|
|
resolver.resolve([
|
|
makeManifest('x', { y: '^1.0.0' }),
|
|
makeManifest('y', { x: '^1.0.0' }),
|
|
]);
|
|
expect.unreachable('Should have thrown');
|
|
} catch (err) {
|
|
const msg = (err as Error).message;
|
|
expect(msg).toContain('x');
|
|
expect(msg).toContain('y');
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('missing dependencies', () => {
|
|
it('should record missing dependencies without failing', () => {
|
|
const result = resolver.resolve([
|
|
makeManifest('a', { 'not-installed': '^1.0.0' }),
|
|
]);
|
|
expect(result.ordered).toHaveLength(1);
|
|
expect(result.missing.get('a')).toContain('not-installed');
|
|
});
|
|
});
|
|
|
|
describe('canLoad', () => {
|
|
it('should return true when all deps are loaded', () => {
|
|
const loaded = new Set(['dep-a', 'dep-b']);
|
|
const result = resolver.canLoad(
|
|
makeManifest('plugin', { 'dep-a': '^1.0.0', 'dep-b': '^1.0.0' }),
|
|
loaded,
|
|
);
|
|
expect(result.canLoad).toBe(true);
|
|
expect(result.missingDeps).toHaveLength(0);
|
|
});
|
|
|
|
it('should return false when deps are missing', () => {
|
|
const loaded = new Set(['dep-a']);
|
|
const result = resolver.canLoad(
|
|
makeManifest('plugin', { 'dep-a': '^1.0.0', 'dep-b': '^1.0.0' }),
|
|
loaded,
|
|
);
|
|
expect(result.canLoad).toBe(false);
|
|
expect(result.missingDeps).toContain('dep-b');
|
|
});
|
|
|
|
it('should return true for plugins with no deps', () => {
|
|
const result = resolver.canLoad(makeManifest('plugin'), new Set());
|
|
expect(result.canLoad).toBe(true);
|
|
});
|
|
});
|
|
});
|