import { beforeEach, describe, expect, jest, test } from '@jest/globals';
import { createSymbolicCommandParser } from '../../../../src/engine/execution/parsers/symbolic-operator-parser.js';
import type { Logger } from '../../../../src/infra/logging/index.js';
describe('SymbolicCommandParser', () => {
let parser: ReturnType<typeof createSymbolicCommandParser>;
let logger: Logger;
beforeEach(() => {
logger = {
debug: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
} as unknown as Logger;
parser = createSymbolicCommandParser(logger);
jest.clearAllMocks();
});
test('detectOperators identifies chained steps with parsed metadata', () => {
const command = '>>first_task input="value" --> second_task';
const result = parser.detectOperators(command);
expect(result.hasOperators).toBe(true);
expect(result.operatorTypes).toContain('chain');
const chainOp = result.operators.find((op) => op.type === 'chain');
expect(chainOp?.type).toBe('chain');
if (chainOp?.type === 'chain') {
expect(chainOp.steps).toHaveLength(2);
expect(chainOp.steps[0].promptId).toBe('first_task');
expect(chainOp.steps[0].args).toBe('input="value"');
expect(chainOp.steps[1].promptId).toBe('second_task');
expect(chainOp.steps[1].args).toBe('');
}
});
test('detectOperators parses gate operators and emits deprecation warnings for "="', () => {
const result = parser.detectOperators('>>prompt = "legacy criteria"');
expect(result.operatorTypes).toContain('gate');
const gateOp = result.operators.find((op) => op.type === 'gate');
expect(gateOp?.type).toBe('gate');
if (gateOp?.type === 'gate') {
expect(gateOp.criteria).toBe('legacy criteria');
expect(gateOp.parsedCriteria).toEqual(['legacy criteria']);
expect(gateOp.scope).toBe('execution');
expect(gateOp.retryOnFailure).toBe(true);
expect(gateOp.maxRetries).toBe(1);
}
expect(logger.warn).toHaveBeenCalledWith(
expect.stringContaining('Gate operator "=" is deprecated')
);
});
test('detectOperators captures framework overrides and normalizes identifiers', () => {
const result = parser.detectOperators('@ReACT >>debug_issue');
expect(result.operatorTypes).toContain('framework');
const frameworkOp = result.operators.find((op) => op.type === 'framework');
expect(frameworkOp?.type).toBe('framework');
if (frameworkOp?.type === 'framework') {
expect(frameworkOp.frameworkId).toBe('ReACT');
expect(frameworkOp.normalizedId).toBe('REACT');
expect(frameworkOp.scopeType).toBe('execution');
}
});
test('buildParseResult emits execution plan metadata for symbolic chains', () => {
const operators = parser.detectOperators('>>plan_one arg=1 --> plan_two :: "quality"');
const result = parser.buildParseResult(
'>>plan_one arg=1 --> plan_two :: "quality"',
operators,
'plan_one',
'arg=1'
);
expect(result.format).toBe('symbolic');
expect(result.executionPlan.steps).toHaveLength(2);
expect(result.executionPlan.steps[0].promptId).toBe('plan_one');
expect(result.executionPlan.steps[1].promptId).toBe('plan_two');
const gateOp = operators.operators.find((op) => op.type === 'gate');
expect(gateOp?.type).toBe('gate');
if (gateOp?.type === 'gate') {
expect(gateOp.parsedCriteria).toEqual(['quality']);
}
});
test('detectOperators handles parallel commands when no chain operator is present', () => {
const command = '>>first + >>second arg="value"';
const result = parser.detectOperators(command);
expect(result.operatorTypes).toContain('parallel');
const parallelOp = result.operators.find((op) => op.type === 'parallel');
expect(parallelOp?.type).toBe('parallel');
if (parallelOp?.type === 'parallel') {
expect(parallelOp.prompts).toHaveLength(2);
expect(parallelOp.prompts[0].promptId).toBe('first');
expect(parallelOp.prompts[1].promptId).toBe('second');
}
});
test('detectOperators correctly parses gate operator followed by chain operator', () => {
const command = 'test_prompt input:"testing" :: "code-quality-gate" -->';
const result = parser.detectOperators(command);
expect(result.hasOperators).toBe(true);
expect(result.operatorTypes).toContain('gate');
expect(result.operatorTypes).toContain('chain');
const gateOp = result.operators.find((op) => op.type === 'gate');
expect(gateOp?.type).toBe('gate');
if (gateOp?.type === 'gate') {
expect(gateOp.criteria).toBe('code-quality-gate');
expect(gateOp.parsedCriteria).toEqual(['code-quality-gate']);
expect(gateOp.scope).toBe('execution');
}
});
test('detectOperators parses gate operator in middle of chain sequence', () => {
const command = 'prompt1 :: "gate-id" --> prompt2 --> prompt3';
const result = parser.detectOperators(command);
expect(result.hasOperators).toBe(true);
expect(result.operatorTypes).toContain('gate');
expect(result.operatorTypes).toContain('chain');
const gateOp = result.operators.find((op) => op.type === 'gate');
expect(gateOp?.type).toBe('gate');
if (gateOp?.type === 'gate') {
expect(gateOp.criteria).toBe('gate-id');
expect(gateOp.parsedCriteria).toEqual(['gate-id']);
}
const chainOp = result.operators.find((op) => op.type === 'chain');
expect(chainOp?.type).toBe('chain');
if (chainOp?.type === 'chain') {
expect(chainOp.steps).toHaveLength(3);
expect(chainOp.steps[0].promptId).toBe('prompt1');
expect(chainOp.steps[1].promptId).toBe('prompt2');
expect(chainOp.steps[2].promptId).toBe('prompt3');
}
});
test('generateExecutionPlan includes gate operator in finalValidation', () => {
const operators = parser.detectOperators('task :: "quality-check" -->');
const plan = parser.generateExecutionPlan(operators, 'task', '');
expect(plan.finalValidation).toBeDefined();
expect(plan.finalValidation?.type).toBe('gate');
if (plan.finalValidation?.type === 'gate') {
expect(plan.finalValidation.criteria).toBe('quality-check');
expect(plan.finalValidation.parsedCriteria).toEqual(['quality-check']);
}
});
describe('>> Prefix Normalization (Defense-in-Depth)', () => {
test('handles >> prefix in chain steps', () => {
const command = '>>p1 input="test" --> >>p2 --> >>p3';
const result = parser.detectOperators(command);
expect(result.hasOperators).toBe(true);
expect(result.operatorTypes).toContain('chain');
const chainOp = result.operators.find((op) => op.type === 'chain');
expect(chainOp?.type).toBe('chain');
if (chainOp?.type === 'chain') {
expect(chainOp.steps).toHaveLength(3);
expect(chainOp.steps[0].promptId).toBe('p1');
expect(chainOp.steps[1].promptId).toBe('p2');
expect(chainOp.steps[2].promptId).toBe('p3');
}
});
test('handles >> prefix in parallel prompts', () => {
const command = '>>p1 arg="val1" + >>p2 arg="val2"';
const result = parser.detectOperators(command);
expect(result.operatorTypes).toContain('parallel');
const parallelOp = result.operators.find((op) => op.type === 'parallel');
expect(parallelOp?.type).toBe('parallel');
if (parallelOp?.type === 'parallel') {
expect(parallelOp.prompts).toHaveLength(2);
expect(parallelOp.prompts[0].promptId).toBe('p1');
expect(parallelOp.prompts[0].args).toBe('arg="val1"');
expect(parallelOp.prompts[1].promptId).toBe('p2');
expect(parallelOp.prompts[1].args).toBe('arg="val2"');
}
});
test('handles >> prefix in conditional branches', () => {
const command = '>>p1 ? "condition" : >>trueBranch';
const result = parser.detectOperators(command);
expect(result.operatorTypes).toContain('conditional');
const conditionalOp = result.operators.find((op) => op.type === 'conditional');
expect(conditionalOp?.type).toBe('conditional');
if (conditionalOp?.type === 'conditional') {
expect(conditionalOp.condition).toBe('condition');
expect(conditionalOp.trueBranch).toBe('trueBranch'); // >> should be stripped
expect(conditionalOp.conditionType).toBe('presence');
}
});
test('handles mixed >> prefixes with framework operator', () => {
const command = '@react >>p1 --> >>p2';
const result = parser.detectOperators(command);
expect(result.operatorTypes).toContain('framework');
expect(result.operatorTypes).toContain('chain');
const frameworkOp = result.operators.find((op) => op.type === 'framework');
expect(frameworkOp?.type).toBe('framework');
if (frameworkOp?.type === 'framework') {
expect(frameworkOp.frameworkId).toBe('react');
}
const chainOp = result.operators.find((op) => op.type === 'chain');
expect(chainOp?.type).toBe('chain');
if (chainOp?.type === 'chain') {
expect(chainOp.steps).toHaveLength(2);
expect(chainOp.steps[0].promptId).toBe('p1');
expect(chainOp.steps[1].promptId).toBe('p2');
}
});
test('handles mixed operators with multiple >> prefixes and gate', () => {
const command = '@cageerf >>p1 + >>p2 :: "quality-gate"';
const result = parser.detectOperators(command);
expect(result.operatorTypes).toContain('framework');
expect(result.operatorTypes).toContain('parallel');
expect(result.operatorTypes).toContain('gate');
const parallelOp = result.operators.find((op) => op.type === 'parallel');
expect(parallelOp?.type).toBe('parallel');
if (parallelOp?.type === 'parallel') {
expect(parallelOp.prompts).toHaveLength(2);
expect(parallelOp.prompts[0].promptId).toBe('p1');
expect(parallelOp.prompts[1].promptId).toBe('p2');
}
const gateOp = result.operators.find((op) => op.type === 'gate');
expect(gateOp?.type).toBe('gate');
if (gateOp?.type === 'gate') {
expect(gateOp.criteria).toBe('quality-gate');
}
});
test('handles >> prefix with complex chain and arguments', () => {
const command = '>>prompt1 input:"test --> quoted" --> >>prompt2 data="value"';
const result = parser.detectOperators(command);
expect(result.hasOperators).toBe(true);
expect(result.operatorTypes).toContain('chain');
const chainOp = result.operators.find((op) => op.type === 'chain');
expect(chainOp?.type).toBe('chain');
if (chainOp?.type === 'chain') {
expect(chainOp.steps).toHaveLength(2);
expect(chainOp.steps[0].promptId).toBe('prompt1');
expect(chainOp.steps[0].args).toBe('input:"test --> quoted"');
expect(chainOp.steps[1].promptId).toBe('prompt2');
expect(chainOp.steps[1].args).toBe('data="value"');
}
});
});
describe('Chain operator with modifiers and framework operators', () => {
test('parses chain with framework operator at start', () => {
const command = '@CAGEERF >>step1 input:"test" --> >>step2';
const result = parser.detectOperators(command);
expect(result.hasOperators).toBe(true);
expect(result.operatorTypes).toContain('chain');
expect(result.operatorTypes).toContain('framework');
const chainOp = result.operators.find((op) => op.type === 'chain');
expect(chainOp?.type).toBe('chain');
if (chainOp?.type === 'chain') {
expect(chainOp.steps).toHaveLength(2);
expect(chainOp.steps[0].promptId).toBe('step1');
expect(chainOp.steps[0].args).toBe('input:"test"');
expect(chainOp.steps[1].promptId).toBe('step2');
}
const frameworkOp = result.operators.find((op) => op.type === 'framework');
expect(frameworkOp?.type).toBe('framework');
if (frameworkOp?.type === 'framework') {
expect(frameworkOp.frameworkId).toBe('CAGEERF');
expect(frameworkOp.normalizedId).toBe('CAGEERF');
}
});
test('parses chain with operators on later steps (syntax tolerance)', () => {
const command = '>>step1 --> %lean @ReACT >>step2 input:"value"';
const result = parser.detectOperators(command);
expect(result.hasOperators).toBe(true);
expect(result.operatorTypes).toContain('chain');
const chainOp = result.operators.find((op) => op.type === 'chain');
expect(chainOp?.type).toBe('chain');
if (chainOp?.type === 'chain') {
expect(chainOp.steps).toHaveLength(2);
expect(chainOp.steps[0].promptId).toBe('step1');
expect(chainOp.steps[1].promptId).toBe('step2');
expect(chainOp.steps[1].args).toBe('input:"value"');
}
});
test('parses chain with operators on each step (syntax tolerance)', () => {
const command = '@CAGEERF >>step1 input:"test" --> @ReACT >>step2 --> @5W1H >>step3';
const result = parser.detectOperators(command);
expect(result.hasOperators).toBe(true);
expect(result.operatorTypes).toContain('chain');
expect(result.operatorTypes).toContain('framework');
const chainOp = result.operators.find((op) => op.type === 'chain');
expect(chainOp?.type).toBe('chain');
if (chainOp?.type === 'chain') {
expect(chainOp.steps).toHaveLength(3);
expect(chainOp.steps[0].promptId).toBe('step1');
expect(chainOp.steps[0].args).toBe('input:"test"');
expect(chainOp.steps[1].promptId).toBe('step2');
expect(chainOp.steps[2].promptId).toBe('step3');
}
// Note: Only the first framework operator is captured (execution-level)
// Operators on later steps are cleaned for parsing but not detected as operators
const frameworkOp = result.operators.find((op) => op.type === 'framework');
expect(frameworkOp?.type).toBe('framework');
if (frameworkOp?.type === 'framework') {
expect(frameworkOp.frameworkId).toBe('CAGEERF');
}
});
test('parses chain with only framework operators on steps', () => {
const command = '@CAGEERF >>step1 --> @ReACT >>step2';
const result = parser.detectOperators(command);
expect(result.hasOperators).toBe(true);
expect(result.operatorTypes).toContain('chain');
expect(result.operatorTypes).toContain('framework');
const chainOp = result.operators.find((op) => op.type === 'chain');
expect(chainOp?.type).toBe('chain');
if (chainOp?.type === 'chain') {
expect(chainOp.steps).toHaveLength(2);
expect(chainOp.steps[0].promptId).toBe('step1');
expect(chainOp.steps[1].promptId).toBe('step2');
}
});
test('parses chain with modifiers on steps (syntax tolerance)', () => {
// Note: Modifiers are handled by UnifiedCommandParser, not SymbolicCommandParser
// This test verifies that modifiers on individual steps are cleaned during parsing
const command = '>>step1 input:"test" --> %lean >>step2';
const result = parser.detectOperators(command);
expect(result.hasOperators).toBe(true);
expect(result.operatorTypes).toContain('chain');
const chainOp = result.operators.find((op) => op.type === 'chain');
expect(chainOp?.type).toBe('chain');
if (chainOp?.type === 'chain') {
expect(chainOp.steps).toHaveLength(2);
expect(chainOp.steps[0].promptId).toBe('step1');
expect(chainOp.steps[0].args).toBe('input:"test"');
expect(chainOp.steps[1].promptId).toBe('step2');
}
});
});
});