import { beforeEach, describe, expect, jest, test } from '@jest/globals';
import { GateAccumulator } from '../../../../../src/engine/execution/pipeline/state/accumulators/gate-accumulator.js';
import { GATE_SOURCE_PRIORITY } from '../../../../../src/engine/execution/pipeline/state/types.js';
import type { GateSource } from '../../../../../src/engine/execution/pipeline/state/types.js';
const mockLogger = {
debug: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
};
describe('GateAccumulator', () => {
let accumulator: GateAccumulator;
beforeEach(() => {
jest.clearAllMocks();
accumulator = new GateAccumulator(mockLogger as any);
});
describe('basic operations', () => {
test('adds a gate successfully', () => {
const added = accumulator.add('research-quality', 'registry-auto');
expect(added).toBe(true);
expect(accumulator.has('research-quality')).toBe(true);
expect(accumulator.size).toBe(1);
});
test('returns false for empty or whitespace gate IDs', () => {
expect(accumulator.add('', 'registry-auto')).toBe(false);
expect(accumulator.add(' ', 'registry-auto')).toBe(false);
expect(accumulator.add(null as any, 'registry-auto')).toBe(false);
expect(accumulator.size).toBe(0);
});
test('trims whitespace from gate IDs', () => {
accumulator.add(' code-quality ', 'registry-auto');
expect(accumulator.has('code-quality')).toBe(true);
expect(accumulator.has(' code-quality ')).toBe(true);
});
test('getAll returns all gate IDs', () => {
accumulator.add('gate-1', 'registry-auto');
accumulator.add('gate-2', 'methodology');
accumulator.add('gate-3', 'inline-operator');
const allGates = accumulator.getAll();
expect(allGates).toHaveLength(3);
expect(allGates).toContain('gate-1');
expect(allGates).toContain('gate-2');
expect(allGates).toContain('gate-3');
});
test('addAll adds multiple gates from same source', () => {
const count = accumulator.addAll(['gate-1', 'gate-2', 'gate-3'], 'methodology');
expect(count).toBe(3);
expect(accumulator.size).toBe(3);
});
test('addAll handles undefined and non-array inputs', () => {
expect(accumulator.addAll(undefined, 'methodology')).toBe(0);
expect(accumulator.addAll(null as any, 'methodology')).toBe(0);
expect(accumulator.size).toBe(0);
});
});
describe('deduplication', () => {
test('deduplicates gates from same source', () => {
accumulator.add('research-quality', 'registry-auto');
accumulator.add('research-quality', 'registry-auto');
expect(accumulator.size).toBe(1);
});
test('deduplicates gates from different sources with same/lower priority', () => {
accumulator.add('code-quality', 'methodology'); // priority 40
const added = accumulator.add('code-quality', 'registry-auto'); // priority 20
expect(added).toBe(false);
expect(accumulator.size).toBe(1);
const entry = accumulator.getEntries().find((e) => e.id === 'code-quality');
expect(entry?.source).toBe('methodology');
});
test('logs when skipping duplicate gate', () => {
accumulator.add('code-quality', 'methodology');
accumulator.add('code-quality', 'registry-auto');
expect(mockLogger.debug).toHaveBeenCalledWith(
'[GateAccumulator] Skipped duplicate gate',
expect.objectContaining({
gateId: 'code-quality',
attemptedSource: 'registry-auto',
existingSource: 'methodology',
})
);
});
});
describe('priority override', () => {
test('higher priority source overrides lower priority', () => {
accumulator.add('code-quality', 'registry-auto'); // priority 20
const added = accumulator.add('code-quality', 'inline-operator'); // priority 100
expect(added).toBe(true);
const entry = accumulator.getEntries().find((e) => e.id === 'code-quality');
expect(entry?.source).toBe('inline-operator');
});
test('logs when overriding with higher priority source', () => {
accumulator.add('code-quality', 'registry-auto');
accumulator.add('code-quality', 'inline-operator');
expect(mockLogger.debug).toHaveBeenCalledWith(
'[GateAccumulator] Overriding gate with higher priority source',
expect.objectContaining({
gateId: 'code-quality',
oldSource: 'registry-auto',
newSource: 'inline-operator',
})
);
});
test('priority order matches GATE_SOURCE_PRIORITY', () => {
const sources: GateSource[] = [
'registry-auto', // 20
'methodology', // 40
'chain-level', // 50
'prompt-config', // 60
'temporary-request', // 80
'client-selection', // 90
'inline-operator', // 100
];
// Add same gate from each source in increasing priority order
sources.forEach((source) => {
accumulator.add('test-gate', source);
});
// Should have highest priority source
const entry = accumulator.getEntries().find((e) => e.id === 'test-gate');
expect(entry?.source).toBe('inline-operator');
expect(entry?.priority).toBe(GATE_SOURCE_PRIORITY['inline-operator']);
});
test('same priority uses first-in-wins', () => {
// Using registry-auto twice via addAll (same priority)
accumulator.add('gate-a', 'registry-auto', { first: true });
// Cannot test same source override since the key is the same
// Instead test that same priority from same source keeps first
const entry = accumulator.getEntries().find((e) => e.id === 'gate-a');
expect(entry?.metadata).toEqual({ first: true });
});
});
describe('freeze behavior', () => {
test('freeze prevents further additions', () => {
accumulator.add('gate-1', 'registry-auto');
accumulator.freeze();
const added = accumulator.add('gate-2', 'inline-operator');
expect(added).toBe(false);
expect(accumulator.has('gate-2')).toBe(false);
expect(accumulator.size).toBe(1);
});
test('isFrozen returns correct state', () => {
expect(accumulator.isFrozen()).toBe(false);
accumulator.freeze();
expect(accumulator.isFrozen()).toBe(true);
});
test('freeze logs summary', () => {
accumulator.add('gate-1', 'registry-auto');
accumulator.add('gate-2', 'methodology');
accumulator.freeze();
expect(mockLogger.debug).toHaveBeenCalledWith(
'[GateAccumulator] Frozen with gates',
expect.objectContaining({
count: 2,
})
);
});
test('warns when attempting to add after freeze', () => {
accumulator.freeze();
accumulator.add('new-gate', 'inline-operator');
expect(mockLogger.warn).toHaveBeenCalledWith(
'[GateAccumulator] Attempted to add gate after freeze',
expect.objectContaining({
gateId: 'new-gate',
source: 'inline-operator',
})
);
});
test('clear is blocked after freeze', () => {
accumulator.add('gate-1', 'registry-auto');
accumulator.freeze();
accumulator.clear();
expect(accumulator.size).toBe(1);
expect(mockLogger.warn).toHaveBeenCalledWith(
'[GateAccumulator] Attempted to clear after freeze'
);
});
});
describe('filtering and queries', () => {
beforeEach(() => {
accumulator.add('gate-1', 'registry-auto');
accumulator.add('gate-2', 'registry-auto');
accumulator.add('gate-3', 'methodology');
accumulator.add('gate-4', 'inline-operator');
});
test('getBySource filters correctly', () => {
const categoryGates = accumulator.getBySource('registry-auto');
expect(categoryGates).toHaveLength(2);
expect(categoryGates).toContain('gate-1');
expect(categoryGates).toContain('gate-2');
const methodologyGates = accumulator.getBySource('methodology');
expect(methodologyGates).toHaveLength(1);
expect(methodologyGates).toContain('gate-3');
});
test('getBySource returns empty array for unused source', () => {
const tempGates = accumulator.getBySource('temporary-request');
expect(tempGates).toHaveLength(0);
});
test('getSourceCounts returns correct counts', () => {
const counts = accumulator.getSourceCounts();
expect(counts['registry-auto']).toBe(2);
expect(counts['methodology']).toBe(1);
expect(counts['inline-operator']).toBe(1);
});
test('getEntries returns full entry details', () => {
const entries = accumulator.getEntries();
expect(entries).toHaveLength(4);
const gate1Entry = entries.find((e) => e.id === 'gate-1');
expect(gate1Entry).toBeDefined();
expect(gate1Entry?.source).toBe('registry-auto');
expect(gate1Entry?.priority).toBe(GATE_SOURCE_PRIORITY['registry-auto']);
expect(gate1Entry?.addedAt).toBeDefined();
});
});
describe('metadata support', () => {
test('stores metadata with gate entry', () => {
accumulator.add('gate-with-meta', 'inline-operator', {
reason: 'user requested',
stepNumber: 2,
});
const entry = accumulator.getEntries().find((e) => e.id === 'gate-with-meta');
expect(entry?.metadata).toEqual({
reason: 'user requested',
stepNumber: 2,
});
});
test('metadata is optional', () => {
accumulator.add('gate-no-meta', 'registry-auto');
const entry = accumulator.getEntries().find((e) => e.id === 'gate-no-meta');
expect(entry?.metadata).toBeUndefined();
});
});
describe('clear operation', () => {
test('clear removes all gates when not frozen', () => {
accumulator.add('gate-1', 'registry-auto');
accumulator.add('gate-2', 'methodology');
expect(accumulator.size).toBe(2);
accumulator.clear();
expect(accumulator.size).toBe(0);
expect(accumulator.getAll()).toHaveLength(0);
});
});
});