// @lifecycle canonical - Unit tests for HierarchyResolver
import { beforeEach, describe, expect, it, jest } from '@jest/globals';
import { DEFAULT_INJECTION_CONFIG } from '../../../../src/engine/execution/pipeline/decisions/injection/constants.js';
import { HierarchyResolver } from '../../../../src/engine/execution/pipeline/decisions/injection/index.js';
import type {
InjectionConfig,
InjectionDecisionInput,
} from '../../../../src/engine/execution/pipeline/decisions/injection/types.js';
describe('HierarchyResolver', () => {
let mockLogger: any;
beforeEach(() => {
mockLogger = {
debug: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
};
});
describe('resolve', () => {
it('should return system-default when no specific config exists', () => {
const minimalConfig: InjectionConfig = {
defaults: {
'system-prompt': true,
'gate-guidance': true,
'style-guidance': true,
},
};
const resolver = new HierarchyResolver(minimalConfig, mockLogger);
const input: InjectionDecisionInput = {
injectionType: 'system-prompt',
currentStep: 1,
};
const result = resolver.resolve('system-prompt', input);
expect(result.config.enabled).toBe(true);
expect(result.source).toBe('system-default');
});
it('should use global config when specified', () => {
const config: InjectionConfig = {
...DEFAULT_INJECTION_CONFIG,
'system-prompt': {
enabled: false,
frequency: { mode: 'never' },
},
};
const resolver = new HierarchyResolver(config, mockLogger);
const input: InjectionDecisionInput = {
injectionType: 'system-prompt',
currentStep: 1,
};
const result = resolver.resolve('system-prompt', input);
expect(result.config.enabled).toBe(false);
expect(result.source).toBe('global-config');
});
it('should override with category config', () => {
const config: InjectionConfig = {
...DEFAULT_INJECTION_CONFIG,
categories: [
{
categoryId: 'analysis',
'system-prompt': { enabled: false },
},
],
};
const resolver = new HierarchyResolver(config, mockLogger);
const input: InjectionDecisionInput = {
injectionType: 'system-prompt',
currentStep: 1,
categoryId: 'analysis',
};
const result = resolver.resolve('system-prompt', input);
expect(result.config.enabled).toBe(false);
expect(result.source).toBe('category-config');
});
it('should override with chain config', () => {
const config: InjectionConfig = {
...DEFAULT_INJECTION_CONFIG,
chains: [
{
chainPattern: 'deploy-*',
'system-prompt': { enabled: false },
'gate-guidance': { enabled: true },
},
],
};
const resolver = new HierarchyResolver(config, mockLogger);
const input: InjectionDecisionInput = {
injectionType: 'system-prompt',
currentStep: 1,
chainId: 'deploy-prod',
};
const result = resolver.resolve('system-prompt', input);
expect(result.config.enabled).toBe(false);
expect(result.source).toBe('chain-config');
});
it('should handle step config for specific step numbers', () => {
const config: InjectionConfig = {
...DEFAULT_INJECTION_CONFIG,
steps: [
{
stepTarget: 2,
'system-prompt': { enabled: false },
},
],
};
const resolver = new HierarchyResolver(config, mockLogger);
const resultStep1 = resolver.resolve('system-prompt', {
injectionType: 'system-prompt',
currentStep: 1,
});
const resultStep2 = resolver.resolve('system-prompt', {
injectionType: 'system-prompt',
currentStep: 2,
});
// Step 1 should use global config (DEFAULT_INJECTION_CONFIG has global settings)
expect(resultStep1.source).toBe('global-config');
// Step 2 should use step config
expect(resultStep2.config.enabled).toBe(false);
expect(resultStep2.source).toBe('step-config');
});
it('should handle step config for first step', () => {
const config: InjectionConfig = {
...DEFAULT_INJECTION_CONFIG,
steps: [
{
stepTarget: 'first',
'gate-guidance': { enabled: false },
},
],
};
const resolver = new HierarchyResolver(config, mockLogger);
const result = resolver.resolve('gate-guidance', {
injectionType: 'gate-guidance',
currentStep: 1,
});
expect(result.config.enabled).toBe(false);
expect(result.source).toBe('step-config');
});
it('should handle step config for last step', () => {
const config: InjectionConfig = {
...DEFAULT_INJECTION_CONFIG,
steps: [
{
stepTarget: 'last',
'style-guidance': { enabled: false },
},
],
};
const resolver = new HierarchyResolver(config, mockLogger);
const result = resolver.resolve('style-guidance', {
injectionType: 'style-guidance',
currentStep: 5,
totalSteps: 5,
});
expect(result.config.enabled).toBe(false);
expect(result.source).toBe('step-config');
});
it('should handle step config for odd steps', () => {
const config: InjectionConfig = {
...DEFAULT_INJECTION_CONFIG,
steps: [
{
stepTarget: 'odd',
'system-prompt': { enabled: false },
},
],
};
const resolver = new HierarchyResolver(config, mockLogger);
const oddResult = resolver.resolve('system-prompt', {
injectionType: 'system-prompt',
currentStep: 3,
});
const evenResult = resolver.resolve('system-prompt', {
injectionType: 'system-prompt',
currentStep: 4,
});
expect(oddResult.config.enabled).toBe(false);
expect(oddResult.source).toBe('step-config');
expect(evenResult.source).toBe('global-config'); // Falls back to global
});
it('should handle step config for even steps', () => {
const config: InjectionConfig = {
...DEFAULT_INJECTION_CONFIG,
steps: [
{
stepTarget: 'even',
'gate-guidance': { enabled: false },
},
],
};
const resolver = new HierarchyResolver(config, mockLogger);
const result = resolver.resolve('gate-guidance', {
injectionType: 'gate-guidance',
currentStep: 4,
});
expect(result.config.enabled).toBe(false);
expect(result.source).toBe('step-config');
});
});
describe('pattern matching', () => {
it('should match exact chain patterns', () => {
const config: InjectionConfig = {
...DEFAULT_INJECTION_CONFIG,
chains: [
{
chainPattern: 'exact-match',
'system-prompt': { enabled: false },
},
],
};
const resolver = new HierarchyResolver(config, mockLogger);
const exactResult = resolver.resolve('system-prompt', {
injectionType: 'system-prompt',
currentStep: 1,
chainId: 'exact-match',
});
const noMatch = resolver.resolve('system-prompt', {
injectionType: 'system-prompt',
currentStep: 1,
chainId: 'exact-match-extended',
});
expect(exactResult.config.enabled).toBe(false);
expect(exactResult.source).toBe('chain-config');
expect(noMatch.source).toBe('global-config'); // Falls back to global config
});
it('should match suffix wildcards (prefix-*)', () => {
const config: InjectionConfig = {
...DEFAULT_INJECTION_CONFIG,
chains: [
{
chainPattern: 'test-*',
'gate-guidance': { enabled: false },
},
],
};
const resolver = new HierarchyResolver(config, mockLogger);
const result = resolver.resolve('gate-guidance', {
injectionType: 'gate-guidance',
currentStep: 1,
chainId: 'test-integration',
});
expect(result.config.enabled).toBe(false);
expect(result.source).toBe('chain-config');
});
it('should match prefix wildcards (*-suffix)', () => {
const config: InjectionConfig = {
...DEFAULT_INJECTION_CONFIG,
chains: [
{
chainPattern: '*-workflow',
'style-guidance': { enabled: false },
},
],
};
const resolver = new HierarchyResolver(config, mockLogger);
const result = resolver.resolve('style-guidance', {
injectionType: 'style-guidance',
currentStep: 1,
chainId: 'deploy-workflow',
});
expect(result.config.enabled).toBe(false);
expect(result.source).toBe('chain-config');
});
it('should match contains wildcards (*contains*)', () => {
const config: InjectionConfig = {
...DEFAULT_INJECTION_CONFIG,
chains: [
{
chainPattern: '*research*',
'system-prompt': { enabled: false },
},
],
};
const resolver = new HierarchyResolver(config, mockLogger);
const result = resolver.resolve('system-prompt', {
injectionType: 'system-prompt',
currentStep: 1,
chainId: 'deep-research-v2',
});
expect(result.config.enabled).toBe(false);
expect(result.source).toBe('chain-config');
});
});
describe('runtime override', () => {
it('should use runtime override when applicable', () => {
const resolver = new HierarchyResolver(DEFAULT_INJECTION_CONFIG, mockLogger);
const input: InjectionDecisionInput = {
injectionType: 'system-prompt',
currentStep: 1,
};
const runtimeOverride = {
type: 'system-prompt' as const,
enabled: false,
scope: 'session' as const,
setAt: Date.now(),
};
const result = resolver.resolve('system-prompt', input, runtimeOverride);
expect(result.config.enabled).toBe(false);
expect(result.source).toBe('runtime-override');
});
it('should ignore expired runtime override', () => {
const resolver = new HierarchyResolver(DEFAULT_INJECTION_CONFIG, mockLogger);
const input: InjectionDecisionInput = {
injectionType: 'system-prompt',
currentStep: 1,
};
const runtimeOverride = {
type: 'system-prompt' as const,
enabled: false,
scope: 'session' as const,
setAt: Date.now() - 10000,
expiresAt: Date.now() - 1000, // Already expired
};
const result = resolver.resolve('system-prompt', input, runtimeOverride);
// Should fall back to global config, not use expired override
expect(result.source).toBe('global-config');
});
it('should use chain-scoped override only for matching chain', () => {
const resolver = new HierarchyResolver(DEFAULT_INJECTION_CONFIG, mockLogger);
const runtimeOverride = {
type: 'gate-guidance' as const,
enabled: false,
scope: 'chain' as const,
scopeId: 'target-chain',
setAt: Date.now(),
};
const matchResult = resolver.resolve(
'gate-guidance',
{ injectionType: 'gate-guidance', currentStep: 1, chainId: 'target-chain' },
runtimeOverride
);
const noMatchResult = resolver.resolve(
'gate-guidance',
{ injectionType: 'gate-guidance', currentStep: 1, chainId: 'other-chain' },
runtimeOverride
);
expect(matchResult.source).toBe('runtime-override');
expect(noMatchResult.source).toBe('global-config');
});
});
describe('frequency resolution', () => {
it('should resolve frequency from global config', () => {
const config: InjectionConfig = {
...DEFAULT_INJECTION_CONFIG,
'system-prompt': {
enabled: true,
frequency: { mode: 'every', interval: 5 },
},
};
const resolver = new HierarchyResolver(config, mockLogger);
const result = resolver.resolve('system-prompt', {
injectionType: 'system-prompt',
currentStep: 1,
});
expect(result.config.frequency).toEqual({ mode: 'every', interval: 5 });
});
});
describe('getResolutionPriority', () => {
it('should return the resolution priority order', () => {
const resolver = new HierarchyResolver(DEFAULT_INJECTION_CONFIG, mockLogger);
const priority = resolver.getResolutionPriority();
expect(priority).toContain('runtime-override');
expect(priority).toContain('step-config');
expect(priority).toContain('chain-config');
expect(priority).toContain('category-config');
expect(priority).toContain('global-config');
expect(priority).toContain('system-default');
});
});
describe('injection types', () => {
it('should handle all injection types', () => {
const resolver = new HierarchyResolver(DEFAULT_INJECTION_CONFIG, mockLogger);
const types: Array<'system-prompt' | 'gate-guidance' | 'style-guidance'> = [
'system-prompt',
'gate-guidance',
'style-guidance',
];
for (const type of types) {
const result = resolver.resolve(type, {
injectionType: type,
currentStep: 1,
});
expect(result).toHaveProperty('config');
expect(result).toHaveProperty('source');
}
});
});
});