import { describe, it, expect } from 'vitest'; import { detectMalformedToolCalls, extractToolCallsFromText, getToolCallSignature, detectRepeatingPattern, } from '../core/agent-loop'; describe('detectMalformedToolCalls', () => { it('returns false for empty/null content', () => { expect(detectMalformedToolCalls('')).toBe(false); }); it('detects shell code blocks', () => { expect(detectMalformedToolCalls('```bash\nls -la\n```')).toBe(true); expect(detectMalformedToolCalls('```sh\necho hello\n```')).toBe(true); expect(detectMalformedToolCalls('```shell\ncat file.txt\n```')).toBe(true); }); it('detects bash/shell JSON invocation as text', () => { expect(detectMalformedToolCalls('bash {"command": "ls"}')).toBe(true); expect(detectMalformedToolCalls('shell {"cmd": "ls"}')).toBe(true); expect(detectMalformedToolCalls('bash ["ls -la"]')).toBe(true); }); it('does not flag normal prose', () => { expect(detectMalformedToolCalls('I will list the files for you.')).toBe(false); expect(detectMalformedToolCalls('Here is the result of the operation.')).toBe(false); }); it('does not flag long content without trailing tool pattern', () => { const longText = 'A'.repeat(300) + '\n```bash\nls\n```\n' + 'B'.repeat(300); expect(detectMalformedToolCalls(longText)).toBe(false); }); it('flags long content that ends with tool pattern', () => { const longText = 'A'.repeat(300) + '\n```bash\nls -la\n```'; expect(detectMalformedToolCalls(longText)).toBe(true); }); }); describe('extractToolCallsFromText', () => { it('returns undefined for empty content', () => { expect(extractToolCallsFromText('')).toBeUndefined(); }); it('extracts commands from bash code blocks', () => { const content = '```bash\nls -la\n```'; const result = extractToolCallsFromText(content)!; expect(result).toHaveLength(1); expect(result[0].function.name).toBe('bash'); expect(JSON.parse(result[0].function.arguments).command).toBe('ls -la'); }); it('extracts multiple code blocks', () => { const content = '```bash\nls\n```\nSome text\n```shell\ncat file.txt\n```'; const result = extractToolCallsFromText(content)!; expect(result).toHaveLength(2); expect(JSON.parse(result[0].function.arguments).command).toBe('ls'); expect(JSON.parse(result[1].function.arguments).command).toBe('cat file.txt'); }); it('extracts bash JSON format', () => { const content = 'bash{"command": "echo hello"}'; const result = extractToolCallsFromText(content)!; expect(result).toHaveLength(1); expect(JSON.parse(result[0].function.arguments).command).toBe('echo hello'); }); it('extracts tool_code blocks (Gemini-style)', () => { const content = '```tool_code\nbash.run_command("grep -r pattern src")\n```'; const result = extractToolCallsFromText(content)!; expect(result).toHaveLength(1); expect(JSON.parse(result[0].function.arguments).command).toBe('grep -r pattern src'); }); it('returns undefined when no commands found', () => { expect(extractToolCallsFromText('Just some regular text.')).toBeUndefined(); }); }); describe('getToolCallSignature', () => { it('normalizes bash commands', () => { const tc = { id: 'tc1', type: 'function' as const, function: { name: 'bash', arguments: '{"command":"ls -la"}' } }; expect(getToolCallSignature(tc)).toBe('bash:ls -la'); }); it('handles legacy cmd format', () => { const tc = { id: 'tc1', type: 'function' as const, function: { name: 'bash', arguments: '{"cmd":"ls -la"}' } }; expect(getToolCallSignature(tc)).toBe('bash:ls -la'); }); it('handles array command format', () => { const tc = { id: 'tc1', type: 'function' as const, function: { name: 'bash', arguments: '{"command":["echo","hello"]}' } }; expect(getToolCallSignature(tc)).toBe('bash:echo hello'); }); it('returns raw args for non-bash tools', () => { const tc = { id: 'tc1', type: 'function' as const, function: { name: 'other', arguments: '{"key":"val"}' } }; expect(getToolCallSignature(tc)).toBe('other:{"key":"val"}'); }); it('handles invalid JSON gracefully', () => { const tc = { id: 'tc1', type: 'function' as const, function: { name: 'bash', arguments: 'broken' } }; expect(getToolCallSignature(tc)).toBe('bash:broken'); }); }); describe('detectRepeatingPattern', () => { it('returns null for short signature lists', () => { expect(detectRepeatingPattern(['a', 'b'], 3)).toBeNull(); }); it('detects cycle of length 2', () => { const sigs = ['a', 'b', 'a', 'b']; expect(detectRepeatingPattern(sigs, 2)).toBe(2); }); it('detects cycle of length 3', () => { const sigs = ['a', 'b', 'c', 'a', 'b', 'c']; expect(detectRepeatingPattern(sigs, 2)).toBe(3); }); it('returns null when no cycle exists', () => { const sigs = ['a', 'b', 'c', 'd', 'e', 'f']; expect(detectRepeatingPattern(sigs, 2)).toBeNull(); }); it('only checks the tail of the array', () => { const sigs = ['x', 'y', 'z', 'a', 'b', 'a', 'b']; expect(detectRepeatingPattern(sigs, 2)).toBe(2); }); });