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);
  });
});