Batch 1 — tool-call-parser.ts: replaces xml-parser.ts with a port of
Unsloth's tool_call_parser.py. Adds balanced-brace JSON scanner,
single-param fast path, hasToolSignal/stripToolMarkup/parseToolCallsFromText
exports, and stream-finalization stripping at all three final-write sites
(error-handler, finalizeCompletion, executeToolPhase). Anthropic <invoke>
shape preserved. 75+12 tests.
Batch 2 — web/html-to-md.ts: parse5 tree-walking HTML-to-Markdown converter
ported from Unsloth's _html_to_md.py. Replaces web_fetch's regex stripHtml
with structured markdown output (headings, links, lists, tables, code blocks,
blockquotes, entity decoding). 29 tests.
Batch 3 — llama-args-validator.ts: port of llama_server_args.py deny-list
validator. Wired into AGENTS.md frontmatter parser — llama_extra_args field
validated at load time, rejects managed flags (model identity, networking,
auth/TLS, server UI). No runtime consumer yet (llama-swap boundary). 76 tests.
All three files carry SPDX-License-Identifier: AGPL-3.0-only headers.
LICENSE flipped to AGPL-3.0-only in prior commit (a938cf1).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
83 lines
2.6 KiB
TypeScript
83 lines
2.6 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import {
|
|
levenshtein,
|
|
suggestToolName,
|
|
formatUnknownToolError,
|
|
} from '../inference/tool-suggestions.js';
|
|
|
|
describe('levenshtein', () => {
|
|
it('returns 0 for identical strings', () => {
|
|
expect(levenshtein('view_file', 'view_file')).toBe(0);
|
|
});
|
|
|
|
it('returns the length when one string is empty', () => {
|
|
expect(levenshtein('', 'view_file')).toBe(9);
|
|
expect(levenshtein('view_file', '')).toBe(9);
|
|
});
|
|
|
|
it('computes a small distance for a single-character substitution', () => {
|
|
expect(levenshtein('cat', 'bat')).toBe(1);
|
|
});
|
|
|
|
it('computes a known case: read_file → view_file is 4', () => {
|
|
expect(levenshtein('read_file', 'view_file')).toBe(4);
|
|
});
|
|
});
|
|
|
|
describe('suggestToolName (v1.13.16)', () => {
|
|
const tools = [
|
|
'view_file',
|
|
'list_dir',
|
|
'grep',
|
|
'find_files',
|
|
'view_truncated_output',
|
|
'ask_user_input',
|
|
'web_search',
|
|
];
|
|
|
|
it('suggests the closest match when distance is small', () => {
|
|
expect(suggestToolName('view_files', tools)).toBe('view_file');
|
|
});
|
|
|
|
it('suggests via substring match when distance alone would miss', () => {
|
|
expect(suggestToolName('file', tools)).toBe('view_file');
|
|
});
|
|
|
|
it('returns null when nothing is close', () => {
|
|
expect(suggestToolName('xxxx_yyyy_zzzz', tools)).toBeNull();
|
|
});
|
|
|
|
it('is case-insensitive in the distance check', () => {
|
|
expect(suggestToolName('VIEW_FILE', tools)).toBe('view_file');
|
|
});
|
|
});
|
|
|
|
describe('formatUnknownToolError (v1.13.16)', () => {
|
|
const tools = ['view_file', 'list_dir', 'grep', 'find_files'];
|
|
|
|
it('includes the wrong name and the available tools list', () => {
|
|
const msg = formatUnknownToolError('read_file', tools);
|
|
expect(msg).toContain("Tool 'read_file' not found");
|
|
expect(msg).toContain('Available tools:');
|
|
expect(msg).toContain('view_file');
|
|
expect(msg).toContain('find_files');
|
|
});
|
|
|
|
it('includes a suggestion when the drifted name is within threshold', () => {
|
|
const msg = formatUnknownToolError('view_files', tools);
|
|
expect(msg).toContain('Did you mean: view_file?');
|
|
});
|
|
|
|
it('omits the suggestion clause when no tool is close enough', () => {
|
|
const msg = formatUnknownToolError('zzzzzzz', tools);
|
|
expect(msg).toContain("Tool 'zzzzzzz' not found");
|
|
expect(msg).toContain('Available tools:');
|
|
expect(msg).not.toContain('Did you mean');
|
|
});
|
|
|
|
it('does not suggest view_file for the read_file drift case (distance is 4, over threshold)', () => {
|
|
const msg = formatUnknownToolError('read_file', tools);
|
|
expect(msg).not.toContain('Did you mean');
|
|
});
|
|
});
|