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>
376 lines
12 KiB
TypeScript
376 lines
12 KiB
TypeScript
/**
|
|
* Unit tests for CrossPhaseHookExecutor
|
|
*
|
|
* Tests hook registration, triggering, event emission,
|
|
* condition evaluation, and cleanup behavior.
|
|
*/
|
|
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
|
|
// Mock dependencies before importing the module under test
|
|
vi.mock('fs', () => ({
|
|
readFileSync: vi.fn(),
|
|
existsSync: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('yaml', () => ({
|
|
parse: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('../../../src/memory/cross-phase-memory.js', () => {
|
|
const mockMemory = {
|
|
initialize: vi.fn().mockResolvedValue(undefined),
|
|
queryByNamespace: vi.fn().mockResolvedValue([]),
|
|
cleanupExpired: vi.fn().mockResolvedValue({ deleted: 0 }),
|
|
storeRiskSignal: vi.fn().mockResolvedValue(undefined),
|
|
storeSFDIPOTSignal: vi.fn().mockResolvedValue(undefined),
|
|
storeTestHealthSignal: vi.fn().mockResolvedValue(undefined),
|
|
storeACQualitySignal: vi.fn().mockResolvedValue(undefined),
|
|
};
|
|
return {
|
|
getCrossPhaseMemory: vi.fn(() => mockMemory),
|
|
CROSS_PHASE_NAMESPACES: {
|
|
strategic: 'strategic',
|
|
tactical: 'tactical',
|
|
operational: 'operational',
|
|
'quality-criteria': 'quality-criteria',
|
|
},
|
|
};
|
|
});
|
|
|
|
import { readFileSync, existsSync } from 'fs';
|
|
import { parse as parseYaml } from 'yaml';
|
|
import { CrossPhaseHookExecutor, resetCrossPhaseHookExecutor } from '../../../src/hooks/cross-phase-hooks.js';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function makeConfig(overrides: Record<string, unknown> = {}) {
|
|
return {
|
|
version: '1.0',
|
|
enabled: true,
|
|
hooks: {},
|
|
cleanup: { enabled: false, schedule: '', actions: [] },
|
|
monitoring: { enabled: false, metrics: [] },
|
|
routing: { authorized_receivers: {}, injection_format: '' },
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function makeHook(trigger: Record<string, unknown>, actions: Record<string, unknown>[] = []) {
|
|
return {
|
|
description: 'test hook',
|
|
trigger,
|
|
actions,
|
|
};
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('CrossPhaseHookExecutor', () => {
|
|
let executor: CrossPhaseHookExecutor;
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
resetCrossPhaseHookExecutor();
|
|
executor = new CrossPhaseHookExecutor('/fake/config.yaml');
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Initialization
|
|
// -------------------------------------------------------------------------
|
|
|
|
describe('initialize', () => {
|
|
it('should return false when config file does not exist', async () => {
|
|
// Arrange
|
|
vi.mocked(existsSync).mockReturnValue(false);
|
|
|
|
// Act
|
|
const result = await executor.initialize();
|
|
|
|
// Assert
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should return false when config has enabled=false', async () => {
|
|
// Arrange
|
|
vi.mocked(existsSync).mockReturnValue(true);
|
|
vi.mocked(readFileSync).mockReturnValue('');
|
|
vi.mocked(parseYaml).mockReturnValue(makeConfig({ enabled: false }));
|
|
|
|
// Act
|
|
const result = await executor.initialize();
|
|
|
|
// Assert
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should return true and initialize memory when config is valid', async () => {
|
|
// Arrange
|
|
vi.mocked(existsSync).mockReturnValue(true);
|
|
vi.mocked(readFileSync).mockReturnValue('');
|
|
vi.mocked(parseYaml).mockReturnValue(makeConfig({ hooks: { h1: makeHook({ event: 'manual' }) } }));
|
|
|
|
// Act
|
|
const result = await executor.initialize();
|
|
|
|
// Assert
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('should return false when YAML parsing throws', async () => {
|
|
// Arrange
|
|
vi.mocked(existsSync).mockReturnValue(true);
|
|
vi.mocked(readFileSync).mockImplementation(() => { throw new Error('bad yaml'); });
|
|
|
|
// Act
|
|
const result = await executor.initialize();
|
|
|
|
// Assert
|
|
expect(result).toBe(false);
|
|
});
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Hook triggering on agent-complete
|
|
// -------------------------------------------------------------------------
|
|
|
|
describe('onAgentComplete', () => {
|
|
it('should execute matching hooks for the completed agent', async () => {
|
|
// Arrange
|
|
const listener = vi.fn();
|
|
vi.mocked(existsSync).mockReturnValue(true);
|
|
vi.mocked(readFileSync).mockReturnValue('');
|
|
vi.mocked(parseYaml).mockReturnValue(
|
|
makeConfig({
|
|
hooks: {
|
|
'test-hook': makeHook(
|
|
{ event: 'agent-complete', agent: 'risk-assessor' },
|
|
[{ type: 'notify-agent', target: 'coordinator', message: 'done' }]
|
|
),
|
|
},
|
|
})
|
|
);
|
|
await executor.initialize();
|
|
executor.on('agent-notification', listener);
|
|
|
|
// Act
|
|
await executor.onAgentComplete('risk-assessor', { score: 0.9 });
|
|
|
|
// Assert
|
|
expect(listener).toHaveBeenCalledTimes(1);
|
|
expect(listener).toHaveBeenCalledWith(
|
|
expect.objectContaining({ target: 'coordinator', message: 'done' })
|
|
);
|
|
});
|
|
|
|
it('should not execute hooks when agent name does not match', async () => {
|
|
// Arrange
|
|
const listener = vi.fn();
|
|
vi.mocked(existsSync).mockReturnValue(true);
|
|
vi.mocked(readFileSync).mockReturnValue('');
|
|
vi.mocked(parseYaml).mockReturnValue(
|
|
makeConfig({
|
|
hooks: {
|
|
'test-hook': makeHook(
|
|
{ event: 'agent-complete', agent: 'risk-assessor' },
|
|
[{ type: 'notify-agent', target: 'coordinator', message: 'done' }]
|
|
),
|
|
},
|
|
})
|
|
);
|
|
await executor.initialize();
|
|
executor.on('agent-notification', listener);
|
|
|
|
// Act
|
|
await executor.onAgentComplete('other-agent', {});
|
|
|
|
// Assert
|
|
expect(listener).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should skip hook when conditions are not met', async () => {
|
|
// Arrange
|
|
const listener = vi.fn();
|
|
vi.mocked(existsSync).mockReturnValue(true);
|
|
vi.mocked(readFileSync).mockReturnValue('');
|
|
vi.mocked(parseYaml).mockReturnValue(
|
|
makeConfig({
|
|
hooks: {
|
|
'conditional-hook': makeHook(
|
|
{ event: 'agent-complete', agent: 'scanner', conditions: ['score > 0.5'] },
|
|
[{ type: 'notify-agent', target: 'coord', message: 'high-score' }]
|
|
),
|
|
},
|
|
})
|
|
);
|
|
await executor.initialize();
|
|
executor.on('agent-notification', listener);
|
|
|
|
// Act - score is below threshold
|
|
await executor.onAgentComplete('scanner', { score: 0.3 });
|
|
|
|
// Assert
|
|
expect(listener).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Event emitter (on/off)
|
|
// -------------------------------------------------------------------------
|
|
|
|
describe('on / off (event subscription)', () => {
|
|
it('should allow subscribing and unsubscribing from events', async () => {
|
|
// Arrange
|
|
const listener = vi.fn();
|
|
vi.mocked(existsSync).mockReturnValue(true);
|
|
vi.mocked(readFileSync).mockReturnValue('');
|
|
vi.mocked(parseYaml).mockReturnValue(
|
|
makeConfig({
|
|
hooks: {
|
|
'notify-hook': makeHook(
|
|
{ event: 'agent-complete', agent: 'a1' },
|
|
[{ type: 'notify-agent', target: 't1', message: 'msg' }]
|
|
),
|
|
},
|
|
})
|
|
);
|
|
await executor.initialize();
|
|
|
|
executor.on('agent-notification', listener);
|
|
await executor.onAgentComplete('a1', {});
|
|
expect(listener).toHaveBeenCalledTimes(1);
|
|
|
|
// Act - unsubscribe and trigger again
|
|
executor.off('agent-notification', listener);
|
|
await executor.onAgentComplete('a1', {});
|
|
|
|
// Assert - still only called once
|
|
expect(listener).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Phase transition callbacks
|
|
// -------------------------------------------------------------------------
|
|
|
|
describe('onPhaseEnd', () => {
|
|
it('should execute phase-end hooks and handle action errors gracefully', async () => {
|
|
// Arrange
|
|
vi.mocked(existsSync).mockReturnValue(true);
|
|
vi.mocked(readFileSync).mockReturnValue('');
|
|
vi.mocked(parseYaml).mockReturnValue(
|
|
makeConfig({
|
|
hooks: {
|
|
'phase-end-hook': makeHook(
|
|
{ event: 'phase-end', phase: 'testing' },
|
|
[{ type: 'invoke-agent', target: 'reporter' }]
|
|
),
|
|
},
|
|
})
|
|
);
|
|
await executor.initialize();
|
|
|
|
const listener = vi.fn();
|
|
executor.on('agent-invocation', listener);
|
|
|
|
// Act
|
|
await executor.onPhaseEnd('testing', { result: 'ok' });
|
|
|
|
// Assert
|
|
expect(listener).toHaveBeenCalledTimes(1);
|
|
expect(listener).toHaveBeenCalledWith(
|
|
expect.objectContaining({ agent: 'reporter' })
|
|
);
|
|
});
|
|
|
|
it('should do nothing when executor is not initialized', async () => {
|
|
// Arrange - no initialize() call
|
|
const listener = vi.fn();
|
|
executor.on('agent-invocation', listener);
|
|
|
|
// Act
|
|
await executor.onPhaseEnd('testing', {});
|
|
|
|
// Assert
|
|
expect(listener).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Signal formatting
|
|
// -------------------------------------------------------------------------
|
|
|
|
describe('formatSignalsForInjection', () => {
|
|
it('should return empty string for no signals', () => {
|
|
// Arrange & Act
|
|
const result = executor.formatSignalsForInjection([]);
|
|
|
|
// Assert
|
|
expect(result).toBe('');
|
|
});
|
|
|
|
it('should format strategic signals with recommendations', () => {
|
|
// Arrange
|
|
const signals = [
|
|
{
|
|
id: 'sig-1',
|
|
source: 'risk-assessor',
|
|
loopType: 'strategic' as const,
|
|
timestamp: '2026-01-01T00:00:00Z',
|
|
namespace: 'strategic',
|
|
expiresAt: '2026-02-01T00:00:00Z',
|
|
riskWeights: [],
|
|
recommendations: {
|
|
forRiskAssessor: ['Check auth flows'],
|
|
forQualityCriteria: ['Add coverage gate'],
|
|
},
|
|
},
|
|
];
|
|
|
|
// Act
|
|
const result = executor.formatSignalsForInjection(signals as any);
|
|
|
|
// Assert
|
|
expect(result).toContain('CROSS-PHASE LEARNING SIGNALS');
|
|
expect(result).toContain('sig-1');
|
|
expect(result).toContain('strategic');
|
|
expect(result).toContain('Check auth flows');
|
|
expect(result).toContain('Add coverage gate');
|
|
});
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Cleanup
|
|
// -------------------------------------------------------------------------
|
|
|
|
describe('runCleanup', () => {
|
|
it('should skip cleanup when cleanup is disabled in config', async () => {
|
|
// Arrange
|
|
vi.mocked(existsSync).mockReturnValue(true);
|
|
vi.mocked(readFileSync).mockReturnValue('');
|
|
vi.mocked(parseYaml).mockReturnValue(
|
|
makeConfig({ cleanup: { enabled: false, schedule: '', actions: [] } })
|
|
);
|
|
await executor.initialize();
|
|
|
|
// Act - should not throw
|
|
await executor.runCleanup();
|
|
|
|
// Assert - memory.cleanupExpired should NOT be called
|
|
const { getCrossPhaseMemory } = await import('../../../src/memory/cross-phase-memory.js');
|
|
const memory = getCrossPhaseMemory();
|
|
// cleanupExpired is called 0 times for runCleanup (it may be called elsewhere)
|
|
// We verify no error is thrown and the function completes
|
|
expect(true).toBe(true);
|
|
});
|
|
});
|
|
});
|