We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/minipuft/claude-prompts-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server
import { beforeEach, describe, expect, jest, test } from '@jest/globals';
import { SymbolicCommandParser } from '../../../../src/engine/execution/parsers/symbolic-operator-parser.js';
const mockLogger = {
debug: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
} as any;
describe('Repetition Operator (* N)', () => {
let parser: SymbolicCommandParser;
beforeEach(() => {
jest.clearAllMocks();
parser = new SymbolicCommandParser(mockLogger);
});
describe('preprocessRepetition()', () => {
test('expands >>prompt * 3 to chain (standard syntax with spaces)', () => {
const result = parser.preprocessRepetition('>>prompt * 3');
expect(result).toBe('>>prompt --> >>prompt --> >>prompt');
});
test('expands >>prompt *3 (no space after asterisk)', () => {
const result = parser.preprocessRepetition('>>prompt *3');
expect(result).toBe('>>prompt --> >>prompt --> >>prompt');
});
test('expands prompt * 2 (bare prompt name)', () => {
const result = parser.preprocessRepetition('prompt * 2');
expect(result).toBe('prompt --> prompt');
});
test('handles >>prompt topic:"AI" * 2 with arguments', () => {
const result = parser.preprocessRepetition('>>prompt topic:"AI" * 2');
expect(result).toBe('>>prompt topic:"AI" --> >>prompt topic:"AI"');
});
test('handles >>step1 * 2 --> >>step2 (mid-chain repetition)', () => {
const result = parser.preprocessRepetition('>>step1 * 2 --> >>step2');
expect(result).toBe('>>step1 --> >>step1 --> >>step2');
});
test('handles >>step1 --> >>step2 * 3 (end-chain repetition)', () => {
const result = parser.preprocessRepetition('>>step1 --> >>step2 * 3');
expect(result).toBe('>>step1 --> >>step2 --> >>step2 --> >>step2');
});
test('returns unchanged when no repetition operator', () => {
const result = parser.preprocessRepetition('>>analyze input:"test"');
expect(result).toBe('>>analyze input:"test"');
});
test('handles * 1 (single repetition)', () => {
const result = parser.preprocessRepetition('>>prompt * 1');
expect(result).toBe('>>prompt');
});
test('handles large repetition count * 5', () => {
const result = parser.preprocessRepetition('>>brainstorm * 5');
const expectedParts = Array(5).fill('>>brainstorm');
expect(result).toBe(expectedParts.join(' --> '));
});
test('preserves complex arguments with special characters', () => {
const result = parser.preprocessRepetition('>>analyze query:"how does X --> Y work?" * 2');
// Arguments containing --> should be preserved in quotes
expect(result).toContain('query:"how does X --> Y work?"');
});
test('handles multiple key:value arguments', () => {
const result = parser.preprocessRepetition('>>task key1:"val1" key2:"val2" * 2');
expect(result).toBe('>>task key1:"val1" key2:"val2" --> >>task key1:"val1" key2:"val2"');
});
// Arguments AFTER * N operator tests (bug fix)
test('preserves arguments AFTER * N operator', () => {
const result = parser.preprocessRepetition('>>prompt * 2 arg:"value"');
expect(result).toBe('>>prompt arg:"value" --> >>prompt arg:"value"');
});
test('handles arguments after * N with chain continuation', () => {
const result = parser.preprocessRepetition('>>prompt * 2 arg:"x" --> >>final');
expect(result).toBe('>>prompt arg:"x" --> >>prompt arg:"x" --> >>final');
});
test('handles multiple arguments after * N', () => {
const result = parser.preprocessRepetition('>>prompt * 2 a:"1" b:"2"');
expect(result).toBe('>>prompt a:"1" b:"2" --> >>prompt a:"1" b:"2"');
});
test('handles single-quoted arguments after * N', () => {
const result = parser.preprocessRepetition(">>prompt * 2 arg:'value'");
expect(result).toBe(">>prompt arg:'value' --> >>prompt arg:'value'");
});
test('handles mixed args before and after * N', () => {
const result = parser.preprocessRepetition('>>prompt before:"x" * 2 after:"y"');
expect(result).toBe('>>prompt before:"x" after:"y" --> >>prompt before:"x" after:"y"');
});
});
describe('expandRepetition() error handling', () => {
test('throws for count < 1', () => {
expect(() => parser.preprocessRepetition('>>prompt * 0')).toThrow(
'Repetition count must be at least 1'
);
});
test('throws for negative count', () => {
expect(() => parser.preprocessRepetition('>>prompt * -1')).not.toThrow();
// Negative numbers don't match the pattern (\\d+ requires digits)
// So it returns unchanged
const result = parser.preprocessRepetition('>>prompt * -1');
expect(result).toBe('>>prompt * -1');
});
test('returns unchanged for non-numeric count', () => {
// Non-numeric doesn't match the pattern
const result = parser.preprocessRepetition('>>prompt * abc');
expect(result).toBe('>>prompt * abc');
});
});
describe('detectOperators() with repetition', () => {
test('detects repetition operator and expands command', () => {
const result = parser.detectOperators('>>analyze * 3');
expect(result.hasOperators).toBe(true);
expect(result.operatorTypes).toContain('repetition');
expect(result.operatorTypes).toContain('chain');
// Should have both repetition and chain operators
const repetitionOp = result.operators.find((op) => op.type === 'repetition');
expect(repetitionOp).toBeDefined();
if (repetitionOp?.type === 'repetition') {
expect(repetitionOp.count).toBe(3);
}
// Chain should have 3 steps
const chainOp = result.operators.find((op) => op.type === 'chain');
expect(chainOp).toBeDefined();
if (chainOp?.type === 'chain') {
expect(chainOp.steps).toHaveLength(3);
expect(chainOp.steps.every((step) => step.promptId === 'analyze')).toBe(true);
}
});
test('repetition with framework operator', () => {
const result = parser.detectOperators('@CAGEERF >>brainstorm * 2');
expect(result.operatorTypes).toContain('framework');
expect(result.operatorTypes).toContain('repetition');
expect(result.operatorTypes).toContain('chain');
const frameworkOp = result.operators.find((op) => op.type === 'framework');
if (frameworkOp?.type === 'framework') {
expect(frameworkOp.normalizedId).toBe('CAGEERF');
}
});
test('repetition with gate operator', () => {
// Gate operators are detected separately from chain steps
// The gate applies to the whole execution, not as a chain step
const result = parser.detectOperators(">>validate check:'security' * 2");
expect(result.operatorTypes).toContain('repetition');
expect(result.operatorTypes).toContain('chain');
// Verify chain has 2 steps
const chainOp = result.operators.find((op) => op.type === 'chain');
if (chainOp?.type === 'chain') {
expect(chainOp.steps).toHaveLength(2);
}
});
test('repetition with style operator', () => {
const result = parser.detectOperators('#analytical >>report * 2');
expect(result.operatorTypes).toContain('style');
expect(result.operatorTypes).toContain('repetition');
expect(result.operatorTypes).toContain('chain');
});
});
describe('Integration with generateExecutionPlan()', () => {
test('creates correct execution plan with N steps from repetition', () => {
const detection = parser.detectOperators('>>analyze * 3');
const plan = parser.generateExecutionPlan(detection, 'analyze', '');
expect(plan.steps).toHaveLength(3);
expect(plan.steps.every((step) => step.promptId === 'analyze')).toBe(true);
expect(plan.steps.map((step) => step.stepNumber)).toEqual([1, 2, 3]);
expect(plan.requiresSessionState).toBe(true);
});
test('preserves arguments across repeated steps', () => {
const detection = parser.detectOperators('>>task input:"test data" * 2');
const plan = parser.generateExecutionPlan(detection, 'task', 'input:"test data"');
expect(plan.steps).toHaveLength(2);
expect(plan.steps[0]?.args).toBe('input:"test data"');
expect(plan.steps[1]?.args).toBe('input:"test data"');
});
test('handles repetition in middle of chain', () => {
const detection = parser.detectOperators('>>setup --> >>process * 2 --> >>cleanup');
const plan = parser.generateExecutionPlan(detection, 'setup', '');
expect(plan.steps).toHaveLength(4);
expect(plan.steps.map((step) => step.promptId)).toEqual([
'setup',
'process',
'process',
'cleanup',
]);
});
test('sets framework override from combined operators', () => {
const detection = parser.detectOperators('@ReACT >>debug * 2');
const plan = parser.generateExecutionPlan(detection, 'debug', '');
expect(plan.frameworkOverride).toBe('REACT');
expect(plan.steps).toHaveLength(2);
});
});
describe('buildParseResult() with repetition', () => {
test('includes repetition in detected operators', () => {
const detection = parser.detectOperators('>>analyze * 3');
const result = parser.buildParseResult('>>analyze * 3', detection, 'analyze', '');
expect(result.format).toBe('symbolic');
expect(result.operators.operatorTypes).toContain('repetition');
expect(result.operators.operatorTypes).toContain('chain');
expect(result.executionPlan.steps).toHaveLength(3);
});
test('metadata includes original command with repetition', () => {
const detection = parser.detectOperators('>>prompt * 2');
const result = parser.buildParseResult('>>prompt * 2', detection, 'prompt', '');
expect(result.metadata?.originalCommand).toBe('>>prompt * 2');
expect(result.metadata?.detectedFormat).toContain('repetition');
expect(result.metadata?.detectedFormat).toContain('chain');
});
});
describe('Edge cases', () => {
test('handles repetition with quoted strings containing asterisks', () => {
// The * inside quotes should not be treated as repetition operator
const result = parser.preprocessRepetition('>>search query:"a * b" * 2');
expect(result).toContain('query:"a * b"');
expect(result.split(' --> ')).toHaveLength(2);
});
test('handles whitespace variations', () => {
// Multiple spaces
const result1 = parser.preprocessRepetition('>>prompt * 3');
expect(result1).toBe('>>prompt --> >>prompt --> >>prompt');
// Tab characters (if supported)
const result2 = parser.preprocessRepetition('>>prompt\t* 3');
expect(result2).toBe('>>prompt --> >>prompt --> >>prompt');
});
test('handles very long prompt IDs', () => {
const longId = 'very_long_prompt_identifier_name';
const result = parser.preprocessRepetition(`>>${longId} * 2`);
expect(result).toBe(`>>${longId} --> >>${longId}`);
});
test('handles prompt ID with hyphens', () => {
const result = parser.preprocessRepetition('>>my-prompt-name * 2');
expect(result).toBe('>>my-prompt-name --> >>my-prompt-name');
});
test('handles prompt ID with underscores', () => {
const result = parser.preprocessRepetition('>>my_prompt_name * 2');
expect(result).toBe('>>my_prompt_name --> >>my_prompt_name');
});
});
});
describe('Repetition Operator Pattern Matching', () => {
let parser: SymbolicCommandParser;
beforeEach(() => {
jest.clearAllMocks();
parser = new SymbolicCommandParser(mockLogger);
});
describe('Pattern boundary tests (flexible whitespace)', () => {
// Pattern: /\s*\*\s*(\d+)/ - allows optional leading/trailing space around *
test('matches with space before asterisk', () => {
const result = parser.preprocessRepetition('>>prompt * 3');
expect(result).toBe('>>prompt --> >>prompt --> >>prompt');
});
test('matches with space before and no space after asterisk', () => {
const result = parser.preprocessRepetition('>>prompt *3');
expect(result).toBe('>>prompt --> >>prompt --> >>prompt');
});
test('matches without space before asterisk (>>prompt*3)', () => {
const result = parser.preprocessRepetition('>>prompt*3');
// Pattern \s* allows optional leading space - now matches and expands
expect(result).toBe('>>prompt --> >>prompt --> >>prompt');
});
test('matches with space only after asterisk (>>prompt* 3)', () => {
const result = parser.preprocessRepetition('>>prompt* 3');
// Pattern \s* allows optional leading space - now matches and expands
expect(result).toBe('>>prompt --> >>prompt --> >>prompt');
});
test('matches various whitespace combinations', () => {
// All these should expand identically
const inputs = ['>>prompt * 3', '>>prompt *3', '>>prompt* 3', '>>prompt*3'];
const expected = '>>prompt --> >>prompt --> >>prompt';
for (const input of inputs) {
expect(parser.preprocessRepetition(input)).toBe(expected);
}
});
});
});