File size: 5,151 Bytes
391c43e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 | 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);
});
});
|