import { describe, it, expect } from 'vitest'; import { rescueInlineToolCalls, startsWithDialectMarker, couldBecomeDialectMarker, containsDialectMarker, } from '../../lib/tool-call-rescue.js'; const TOOLS = new Set(['Read', 'Write', 'Bash', 'Grep', 'list_dir']); describe('inline tool-call dialect rescue', () => { describe('Kimi/DeepSeek token dialect', () => { it('parses a well-formed functions.NAME:IDX call', () => { const text = '<|tool_calls_section_begin|><|tool_call_begin|>functions.Read:0<|tool_call_argument_begin|>{"file_path":"/tmp/x.txt"}<|tool_call_end|><|tool_calls_section_end|>'; const r = rescueInlineToolCalls(text, TOOLS); expect(r.detected).toBe(true); expect(r.calls).toEqual([{ name: 'Read', arguments: '{"file_path":"/tmp/x.txt"}' }]); expect(r.cleanText).toBe(''); }); it('parses multiple calls and keeps surrounding prose as cleanText', () => { const text = 'Let me check.\n<|tool_call_begin|>functions.Read:0<|tool_call_argument_begin|>{"file_path":"/a"}<|tool_call_end|><|tool_call_begin|>functions.Grep:1<|tool_call_argument_begin|>{"pattern":"TODO"}<|tool_call_end|>'; const r = rescueInlineToolCalls(text, TOOLS); expect(r.calls).toHaveLength(2); expect(r.calls![1]).toEqual({ name: 'Grep', arguments: '{"pattern":"TODO"}' }); expect(r.cleanText).toBe('Let me check.'); }); it('reports detected-but-unparseable when the id token is degraded (no function name)', () => { // The live capture that motivated this module: an opaque id where // functions.NAME:IDX should be — there is no way to know the tool. const text = '<|tool_calls_section_begin|> <|tool_call_begin|> chatcmpl-tool-bde5fae954a11b1b <|tool_call_argument_begin|> {"file_path": "/private/tmp/demo-project/src"} <|tool_call_end|>'; const r = rescueInlineToolCalls(text, TOOLS); expect(r.detected).toBe(true); expect(r.calls).toBeNull(); }); it('rejects a call naming a tool the request never declared', () => { const text = '<|tool_call_begin|>functions.DropTables:0<|tool_call_argument_begin|>{}<|tool_call_end|>'; const r = rescueInlineToolCalls(text, TOOLS); expect(r.detected).toBe(true); expect(r.calls).toBeNull(); }); }); describe('llama/groq dialect', () => { it('parses (the failed_generation shape)', () => { const text = ''; const r = rescueInlineToolCalls(text, TOOLS); expect(r.detected).toBe(true); expect(r.calls).toEqual([{ name: 'Bash', arguments: '{"command": "npm run build"}' }]); }); it('parses the variant with a closing > after the name', () => { const text = '{"file_path": "/tmp/a"}'; const r = rescueInlineToolCalls(text, TOOLS); expect(r.calls).toEqual([{ name: 'Read', arguments: '{"file_path": "/tmp/a"}' }]); }); it('treats array-shaped arguments as unparseable (live Groq failure shape)', () => { const text = ''; const r = rescueInlineToolCalls(text, TOOLS); expect(r.detected).toBe(true); expect(r.calls).toBeNull(); }); }); describe('Qwen/Hermes XML dialect', () => { it('parses a single block with object arguments', () => { const text = '\n{"name": "Write", "arguments": {"file_path": "/tmp/n.txt", "content": "hi"}}\n'; const r = rescueInlineToolCalls(text, TOOLS); expect(r.calls).toHaveLength(1); expect(r.calls![0].name).toBe('Write'); expect(JSON.parse(r.calls![0].arguments)).toEqual({ file_path: '/tmp/n.txt', content: 'hi' }); }); it('accepts the "parameters" key and multiple blocks', () => { const text = '{"name": "Read", "parameters": {"file_path": "/a"}}{"name": "Grep", "arguments": {"pattern": "x"}}'; const r = rescueInlineToolCalls(text, TOOLS); expect(r.calls).toHaveLength(2); }); it('treats a truncated unclosed block as unparseable', () => { const text = '{"name": "Read", "argum'; const r = rescueInlineToolCalls(text, TOOLS); expect(r.detected).toBe(true); expect(r.calls).toBeNull(); }); }); describe('bare / fenced JSON dialect (schema-gated)', () => { it('rescues a bare JSON object naming a known tool', () => { const r = rescueInlineToolCalls('{"name": "list_dir", "arguments": {"path": "/tmp"}}', TOOLS); expect(r.detected).toBe(true); expect(r.calls).toEqual([{ name: 'list_dir', arguments: '{"path":"/tmp"}' }]); }); it('rescues a ```json fenced call', () => { const r = rescueInlineToolCalls('```json\n{"name": "Bash", "arguments": {"command": "ls"}}\n```', TOOLS); expect(r.calls).toHaveLength(1); }); it('does NOT touch ordinary JSON answers that do not name a requested tool', () => { const r = rescueInlineToolCalls('{"name": "Tashfeen", "age": 30}', TOOLS); expect(r.detected).toBe(false); expect(r.cleanText).toBe('{"name": "Tashfeen", "age": 30}'); }); it('does NOT touch plain prose', () => { const r = rescueInlineToolCalls('The port is 49152, configured in config.json.', TOOLS); expect(r.detected).toBe(false); }); }); describe('stream hold-window helpers', () => { it('startsWithDialectMarker matches all dialects, with leading whitespace', () => { expect(startsWithDialectMarker('<|tool_calls_section_begin|>x')).toBe(true); expect(startsWithDialectMarker(' <|tool_call_begin|>')).toBe(true); expect(startsWithDialectMarker('\n{}')).toBe(true); expect(startsWithDialectMarker('')).toBe(false); }); it('couldBecomeDialectMarker holds strict prefixes and releases divergent text', () => { expect(couldBecomeDialectMarker('<')).toBe(true); expect(couldBecomeDialectMarker('<|to')).toBe(true); expect(couldBecomeDialectMarker('')).toBe(false); }); it('containsDialectMarker finds markers mid-text', () => { expect(containsDialectMarker('prose then ')).toBe(true); expect(containsDialectMarker('plain text')).toBe(false); }); }); });