Team Ai
Modelpublic

Felipe97/llama-cpp-compiled

sourceHugging Faceupdated 21d agoView on Hugging Face
0likes1.2kdownloads
tool-calls.test.ts651 linesDownload Raw Back to unit
1import { parseToolArgs } from '$lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/_shared';2import {3	parseEditFileMeta,4	parseEditFileTitleMeta5} from '$lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/edit-file';6import { parseExecShellCommandMeta } from '$lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/exec-shell-command';7import { parseFileGlobSearchMeta } from '$lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/file-glob-search';8import { parseGrepSearchMeta } from '$lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/grep-search';9import { parseReadFileMeta } from '$lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/read-file';10import { parseRunJavascriptMeta } from '$lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/run-javascript';11import {12	parseWriteFileMeta,13	parseWriteFileTitleMeta14} from '$lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/write-file';15import { AgenticSectionType, BuiltInTool } from '$lib/enums';16import type { AgenticSection, WriteFileMeta } from '$lib/types';17import { abbreviateHome, formatCwdMessage, lastPathSegment, parseCwdMessage } from '$lib/utils';18import { describe, expect, it } from 'vitest';19 20function makeSection(21	overrides: Partial<AgenticSection> = {},22	toolName = BuiltInTool.SERVER_READ_FILE23): AgenticSection {24	return {25		content: '',26		toolArgs: JSON.stringify({ path: '/foo.txt' }),27		toolName,28		toolResult: undefined,29		type: AgenticSectionType.TOOL_CALL,30		...overrides31	};32}33 34describe('lastPathSegment', () => {35	it('returns the last segment of an absolute path', () => {36		expect(lastPathSegment('/Users/me/code/my-project')).toBe('my-project');37	});38 39	it('returns the last segment of a tilde-relative path', () => {40		expect(lastPathSegment('~/git/llama.brand')).toBe('llama.brand');41	});42 43	it('strips trailing slashes', () => {44		expect(lastPathSegment('/foo/bar/')).toBe('bar');45	});46 47	it('strips multiple trailing slashes', () => {48		expect(lastPathSegment('/foo/bar///')).toBe('bar');49	});50 51	it('returns the input unchanged when there is no slash', () => {52		expect(lastPathSegment('project')).toBe('project');53	});54 55	it('returns tilde when only tilde is given', () => {56		expect(lastPathSegment('~/')).toBe('~');57	});58});59 60describe('abbreviateHome', () => {61	it('abbreviates paths under home with a tilde', () => {62		expect(abbreviateHome('/Users/al/Documents/x.txt', '/Users/al')).toBe('~/Documents/x.txt');63	});64 65	it('abbreviates home itself to a bare tilde', () => {66		expect(abbreviateHome('/Users/al', '/Users/al')).toBe('~');67	});68 69	it('returns paths outside home unchanged', () => {70		expect(abbreviateHome('/opt/project', '/Users/al')).toBe('/opt/project');71	});72 73	it('does not abbreviate a mere prefix match', () => {74		expect(abbreviateHome('/Users/alice/x', '/Users/al')).toBe('/Users/alice/x');75	});76 77	it('returns the path unchanged when home is unknown', () => {78		expect(abbreviateHome('/Users/al/Documents', null)).toBe('/Users/al/Documents');79	});80});81 82describe('formatCwdMessage / parseCwdMessage', () => {83	it('formats a cwd change matching the UI text, with a file link', () => {84		expect(formatCwdMessage('/Users/al/Documents', '/Users/al')).toBe(85			'Set working directory to [file:///Users/al/Documents](~/Documents).'86		);87	});88 89	it('falls back to the basename display when home is unknown', () => {90		expect(formatCwdMessage('/opt/project', null)).toBe(91			'Set working directory to [file:///opt/project](project).'92		);93	});94 95	it('round-trips through the parser', () => {96		const info = parseCwdMessage(formatCwdMessage('/Users/al/Documents', '/Users/al'));97 98		expect(info?.path).toBe('/Users/al/Documents');99		expect(info?.display).toBe('~/Documents');100	});101 102	it('parses a cwd message even when guidance follows the link', () => {103		expect(104			parseCwdMessage(105				'Set working directory to [file:///a/b](~/b). Tool calls run with this as their working directory.'106			)107		).toEqual({ display: '~/b', path: '/a/b' });108	});109 110	it('parses the cleared marker', () => {111		expect(parseCwdMessage('Working directory cleared')).toEqual({ display: '', path: null });112	});113 114	it('returns null for non-cwd content', () => {115		expect(parseCwdMessage('hello there')).toBeNull();116	});117});118 119describe('parseToolArgs (shared)', () => {120	it('returns null when the section has no toolArgs', () => {121		const result = parseToolArgs(122			BuiltInTool.SERVER_READ_FILE,123			makeSection({ toolArgs: undefined })124		);125 126		expect(result).toBeNull();127	});128 129	it('returns null when the tool name does not match', () => {130		const result = parseToolArgs(131			BuiltInTool.SERVER_READ_FILE,132			makeSection({ toolArgs: '{"path":"/x"}' }, BuiltInTool.SERVER_WRITE_FILE)133		);134 135		expect(result).toBeNull();136	});137 138	it('returns null when args are not valid final JSON (partial: false)', () => {139		const result = parseToolArgs(140			BuiltInTool.SERVER_READ_FILE,141			makeSection({ toolArgs: '{"path": "/foo.tx' })142		);143 144		expect(result).toBeNull();145	});146 147	it('returns parsed args when valid final JSON', () => {148		const result = parseToolArgs(149			BuiltInTool.SERVER_READ_FILE,150			makeSection({ toolArgs: '{"path":"/foo.txt"}' })151		);152 153		expect(result).toEqual({ path: '/foo.txt' });154	});155 156	it('accepts partial JSON when partial: true', () => {157		const result = parseToolArgs(158			BuiltInTool.SERVER_READ_FILE,159			makeSection({ toolArgs: '{"path": "/foo.tx' }),160			{ partial: true }161		);162 163		expect(result).toEqual({ path: '/foo.tx' });164	});165});166 167describe('parseWriteFileMeta', () => {168	it('returns null for sections with a different tool name', () => {169		expect(170			parseWriteFileMeta(171				makeSection({172					toolArgs: '{"path":"/x","content":"y"}',173					toolName: BuiltInTool.SERVER_READ_FILE174				})175			)176		).toBeNull();177	});178 179	it('returns null when args have no path-like field', () => {180		expect(181			parseWriteFileMeta(182				makeSection({ toolArgs: '{"content":"x"}', toolName: BuiltInTool.SERVER_WRITE_FILE })183			)184		).toBeNull();185	});186 187	it('accepts partial args (renders incrementally as content streams in)', () => {188		const meta = parseWriteFileMeta(189			makeSection({ toolArgs: '{"path":"/foo.t', toolName: BuiltInTool.SERVER_WRITE_FILE })190		);191 192		expect(meta?.filePath).toBe('/foo.t');193	});194 195	it('returns file path, language, content, bytes, resultMessage', () => {196		const meta = parseWriteFileMeta(197			makeSection(198				{199					toolArgs: '{"path":"/foo.ts","content":"x"}',200					toolName: BuiltInTool.SERVER_WRITE_FILE,201					toolResult: '{"result":"wrote","bytes":42}'202				},203				BuiltInTool.SERVER_WRITE_FILE204			)205		);206 207		expect(meta).toMatchObject<Partial<WriteFileMeta>>({208			bytesWritten: 42,209			content: 'x',210			filePath: '/foo.ts',211			language: expect.any(String),212			resultMessage: 'wrote'213		});214	});215 216	it('surfaces errorMessage from the result blob', () => {217		const meta = parseWriteFileMeta(218			makeSection({219				toolArgs: '{"path":"/foo","content":"x"}',220				toolName: BuiltInTool.SERVER_WRITE_FILE,221				toolResult: '{"error":"permission denied"}'222			})223		);224 225		expect(meta?.errorMessage).toBe('permission denied');226	});227});228 229describe('parseWriteFileTitleMeta', () => {230	it('matches the full meta for path, language and result fields', () => {231		const args = JSON.stringify({ content: 'x'.repeat(50_000), path: '/foo.ts' });232		const toolResult = '{"result":"wrote","bytes":42}';233		const section = makeSection(234			{ toolArgs: args, toolName: BuiltInTool.SERVER_WRITE_FILE, toolResult },235			BuiltInTool.SERVER_WRITE_FILE236		);237		const full = parseWriteFileMeta(section);238		const title = parseWriteFileTitleMeta(section);239 240		expect(title?.filePath).toBe(full?.filePath);241		expect(title?.fileName).toBe(full?.fileName);242		expect(title?.language).toBe(full?.language);243		expect(title?.bytesWritten).toBe(full?.bytesWritten);244		expect(title?.resultMessage).toBe(full?.resultMessage);245		expect(title?.errorMessage).toBe(full?.errorMessage);246	});247 248	it('extracts a path with escaped characters without parsing the content blob', () => {249		const section = makeSection(250			{251				toolArgs: '{"path":"/a\\nb\\"c/d.ts","content":"x"}',252				toolName: BuiltInTool.SERVER_WRITE_FILE253			},254			BuiltInTool.SERVER_WRITE_FILE255		);256 257		expect(parseWriteFileTitleMeta(section)?.filePath).toBe('/a\nb"c/d.ts');258	});259 260	it('falls back to the full parse for args the extractor can not see', () => {261		const section = makeSection(262			{263				// key written with an escaped unicode escape sequence in the name264				toolArgs: '{"\\u0070ath":"/foo.ts","content":"x"}',265				toolName: BuiltInTool.SERVER_WRITE_FILE266			},267			BuiltInTool.SERVER_WRITE_FILE268		);269 270		expect(parseWriteFileTitleMeta(section)?.filePath).toBe('/foo.ts');271	});272 273	it('accepts partial args like the full parser', () => {274		const section = makeSection(275			{ toolArgs: '{"path":"/foo.t', toolName: BuiltInTool.SERVER_WRITE_FILE },276			BuiltInTool.SERVER_WRITE_FILE277		);278 279		expect(parseWriteFileTitleMeta(section)?.filePath).toBe('/foo.t');280	});281 282	it('returns null for sections with a different tool name', () => {283		expect(284			parseWriteFileTitleMeta(285				makeSection({286					toolArgs: '{"path":"/x","content":"y"}',287					toolName: BuiltInTool.SERVER_READ_FILE288				})289			)290		).toBeNull();291	});292});293 294describe('parseEditFileTitleMeta', () => {295	it('matches the full meta for path and result fields', () => {296		const section = makeSection(297			{298				toolArgs: '{"path":"/foo.ts","edits":[{"old_text":"a","new_text":"b"}]}' + ' '.repeat(0),299				toolName: BuiltInTool.SERVER_EDIT_FILE,300				toolResult: '{"result":"ok","edits_applied":1}'301			},302			BuiltInTool.SERVER_EDIT_FILE303		);304		const full = parseEditFileMeta(section);305		const title = parseEditFileTitleMeta(section);306 307		expect(title?.filePath).toBe(full?.filePath);308		expect(title?.fileName).toBe(full?.fileName);309		expect(title?.editsApplied).toBe(full?.editsApplied);310		expect(title?.resultMessage).toBe(full?.resultMessage);311		expect(title?.errorMessage).toBe(full?.errorMessage);312	});313 314	it('surfaces errorMessage from the result blob without parsing args', () => {315		const section = makeSection(316			{317				toolArgs: '{"path":"/foo.ts","edits":[]}',318				toolName: BuiltInTool.SERVER_EDIT_FILE,319				toolResult: '{"error":"permission denied"}'320			},321			BuiltInTool.SERVER_EDIT_FILE322		);323 324		expect(parseEditFileTitleMeta(section)?.errorMessage).toBe('permission denied');325	});326 327	it('returns null when args have no path-like field', () => {328		expect(329			parseEditFileTitleMeta(330				makeSection({ toolArgs: '{"edits":[]}', toolName: BuiltInTool.SERVER_EDIT_FILE })331			)332		).toBeNull();333	});334});335 336describe('parseEditFileMeta', () => {337	it('parses edits array and applies editsApplied from the result', () => {338		const section = makeSection(339			{340				toolArgs:341					'{"path":"/foo.ts","edits":[{"old_text":"a","new_text":"b"},{"old_text":"c","new_text":"d"}]}',342				toolName: BuiltInTool.SERVER_EDIT_FILE,343				toolResult: '{"result":"ok","edits_applied":2}'344			},345			BuiltInTool.SERVER_EDIT_FILE346		);347		const meta = parseEditFileMeta(section);348 349		expect(meta?.edits).toEqual([350			{ newText: 'b', oldText: 'a' },351			{ newText: 'd', oldText: 'c' }352		]);353		expect(meta?.editsApplied).toBe(2);354		expect(meta?.resultMessage).toBe('ok');355	});356 357	it('drops edits with empty old_text', () => {358		const section = makeSection(359			{360				toolArgs: '{"path":"/foo","edits":[{"old_text":""},{"old_text":"a","new_text":""}]}',361				toolName: BuiltInTool.SERVER_EDIT_FILE362			},363			BuiltInTool.SERVER_EDIT_FILE364		);365		const meta = parseEditFileMeta(section);366 367		// First entry is dropped (empty old_text). Second is kept368		// (empty new_text is fine - it's the "delete" case).369		expect(meta?.edits).toEqual([{ newText: '', oldText: 'a' }]);370	});371 372	it('errorMessage wins over result message', () => {373		const section = makeSection(374			{375				toolArgs: '{"path":"/foo"}',376				toolName: BuiltInTool.SERVER_EDIT_FILE,377				toolResult: '{"error":"bad path","result":"ok"}'378			},379			BuiltInTool.SERVER_EDIT_FILE380		);381		const meta = parseEditFileMeta(section);382 383		expect(meta?.errorMessage).toBe('bad path');384		expect(meta?.resultMessage).toBeUndefined();385	});386});387 388describe('parseReadFileMeta', () => {389	it('parses file name alone (no range)', () => {390		const meta = parseReadFileMeta(391			makeSection({ toolArgs: '{"path":"/foo.txt"}' }, BuiltInTool.SERVER_READ_FILE)392		);393 394		expect(meta?.fileName).toBe('foo.txt');395		expect(meta?.lineRange).toBeNull();396	});397 398	it('parses start_line + end_line into a range', () => {399		const meta = parseReadFileMeta(400			makeSection(401				{ toolArgs: '{"path":"/foo.ts","start_line":10,"end_line":20}' },402				BuiltInTool.SERVER_READ_FILE403			)404		);405 406		expect(meta?.lineRange).toEqual({ end: 20, start: 10 });407	});408 409	it('parses start_line + line_count into a range', () => {410		const meta = parseReadFileMeta(411			makeSection(412				{ toolArgs: '{"path":"/foo.ts","start_line":10,"line_count":5}' },413				BuiltInTool.SERVER_READ_FILE414			)415		);416 417		expect(meta?.lineRange).toEqual({ end: 14, start: 10 });418	});419 420	it('returns null when args cannot be parsed', () => {421		expect(422			parseReadFileMeta(makeSection({ toolArgs: '{bad' }, BuiltInTool.SERVER_READ_FILE))423		).toBeNull();424	});425});426 427describe('parseGrepSearchMeta', () => {428	it('returns null when path or pattern is missing', () => {429		expect(430			parseGrepSearchMeta(431				makeSection({ toolArgs: '{"pattern":"foo"}', toolName: BuiltInTool.SERVER_GREP_SEARCH })432			)433		).toBeNull();434		expect(435			parseGrepSearchMeta(436				makeSection({ toolArgs: '{"path":"/x"}', toolName: BuiltInTool.SERVER_GREP_SEARCH })437			)438		).toBeNull();439	});440 441	it('parses structured plain_text_response into matches', () => {442		const meta = parseGrepSearchMeta(443			makeSection(444				{445					toolArgs: '{"path":"/x","pattern":"foo"}',446					toolName: BuiltInTool.SERVER_GREP_SEARCH,447					toolResult: JSON.stringify({ plain_text_response: 'a.ts:hello\nb.ts:world' })448				},449				BuiltInTool.SERVER_GREP_SEARCH450			)451		);452 453		expect(meta?.matches).toHaveLength(2);454		expect(meta?.matches[0]).toEqual({ content: 'hello', file: 'a.ts' });455	});456 457	it('falls back to raw-text parsing when result is not JSON', () => {458		const meta = parseGrepSearchMeta(459			makeSection(460				{461					toolArgs: '{"path":"/x","pattern":"foo"}',462					toolName: BuiltInTool.SERVER_GREP_SEARCH,463					toolResult: 'a.ts:hello\nb.ts:world'464				},465				BuiltInTool.SERVER_GREP_SEARCH466			)467		);468 469		expect(meta?.matches).toHaveLength(2);470	});471 472	it('parses line numbers when return_line_numbers is true', () => {473		const meta = parseGrepSearchMeta(474			makeSection(475				{476					toolArgs: '{"path":"/x","pattern":"foo","return_line_numbers":true}',477					toolName: BuiltInTool.SERVER_GREP_SEARCH,478					toolResult: 'a.ts:12:hello'479				},480				BuiltInTool.SERVER_GREP_SEARCH481			)482		);483 484		expect(meta?.matches[0]).toEqual({ content: 'hello', file: 'a.ts', line: 12 });485		expect(meta?.showLineNumbers).toBe(true);486	});487});488 489describe('parseFileGlobSearchMeta', () => {490	it('falls back to raw-text parsing when result is not JSON', () => {491		const meta = parseFileGlobSearchMeta(492			makeSection(493				{494					toolArgs: '{"path":"/x"}',495					toolName: BuiltInTool.SERVER_FILE_GLOB_SEARCH,496					toolResult: 'a.ts\nb.ts'497				},498				BuiltInTool.SERVER_FILE_GLOB_SEARCH499			)500		);501 502		expect(meta?.matches).toEqual(['a.ts', 'b.ts']);503	});504 505	it('parses plain_text_response from a JSON object', () => {506		const meta = parseFileGlobSearchMeta(507			makeSection(508				{509					toolArgs: '{"path":"/x"}',510					toolName: BuiltInTool.SERVER_FILE_GLOB_SEARCH,511					toolResult: JSON.stringify({ plain_text_response: 'a.ts\nb.ts' })512				},513				BuiltInTool.SERVER_FILE_GLOB_SEARCH514			)515		);516 517		expect(meta?.matches).toEqual(['a.ts', 'b.ts']);518	});519 520	it('surfaces errorMessage from the result blob', () => {521		const meta = parseFileGlobSearchMeta(522			makeSection(523				{524					toolArgs: '{"path":"/x"}',525					toolName: BuiltInTool.SERVER_FILE_GLOB_SEARCH,526					toolResult: JSON.stringify({ error: 'permission denied' })527				},528				BuiltInTool.SERVER_FILE_GLOB_SEARCH529			)530		);531 532		expect(meta?.errorMessage).toBe('permission denied');533	});534});535 536describe('parseRunJavascriptMeta', () => {537	it('returns null when code is missing', () => {538		expect(539			parseRunJavascriptMeta(540				makeSection({ toolArgs: '{}', toolName: BuiltInTool.BROWSER_RUN_JAVASCRIPT })541			)542		).toBeNull();543	});544 545	it('reads code and timeout', () => {546		const meta = parseRunJavascriptMeta(547			makeSection(548				{549					toolArgs: '{"code":"Math.PI","timeout_ms":5000}',550					toolName: BuiltInTool.BROWSER_RUN_JAVASCRIPT551				},552				BuiltInTool.BROWSER_RUN_JAVASCRIPT553			)554		);555 556		expect(meta?.code).toBe('Math.PI');557		expect(meta?.timeoutMs).toBe(5000);558	});559 560	it('reads error field from a JSON-object result', () => {561		const meta = parseRunJavascriptMeta(562			makeSection(563				{564					toolArgs: '{"code":"throw new Error()"}',565					toolName: BuiltInTool.BROWSER_RUN_JAVASCRIPT,566					toolResult: JSON.stringify({ error: 'undefined is not a function' })567				},568				BuiltInTool.BROWSER_RUN_JAVASCRIPT569			)570		);571 572		expect(meta?.errorMessage).toBe('undefined is not a function');573	});574 575	it('does NOT treat a JSON-array result as an error', () => {576		// SandboxService returns successful output as a JSON array;577		// only JSON objects carry `error`. Raw arrays must round-trip578		// through unchanged.579		const meta = parseRunJavascriptMeta(580			makeSection(581				{582					toolArgs: '{"code":"[1,2,3]"}',583					toolName: BuiltInTool.BROWSER_RUN_JAVASCRIPT,584					toolResult: '[1,2,3]'585				},586				BuiltInTool.BROWSER_RUN_JAVASCRIPT587			)588		);589 590		expect(meta?.errorMessage).toBeUndefined();591	});592 593	it('scans a non-JSON string result for an `Error:` line', () => {594		const meta = parseRunJavascriptMeta(595			makeSection(596				{597					toolArgs: '{"code":"foo"}',598					toolName: BuiltInTool.BROWSER_RUN_JAVASCRIPT,599					toolResult: 'Error: undefined is not a function\n  at <anonymous>:1:1'600				},601				BuiltInTool.BROWSER_RUN_JAVASCRIPT602			)603		);604 605		expect(meta?.errorMessage).toBe('undefined is not a function');606	});607});608 609describe('parseExecShellCommandMeta', () => {610	it('reads command from the args', () => {611		const meta = parseExecShellCommandMeta(612			makeSection(613				{ toolArgs: '{"command":"ls -la"}', toolName: BuiltInTool.SERVER_EXEC_SHELL_COMMAND },614				BuiltInTool.SERVER_EXEC_SHELL_COMMAND615			)616		);617 618		expect(meta?.command).toBe('ls -la');619	});620 621	it('accepts cmd / shell_command aliases', () => {622		expect(623			parseExecShellCommandMeta(624				makeSection(625					{ toolArgs: '{"cmd":"ls"}', toolName: BuiltInTool.SERVER_EXEC_SHELL_COMMAND },626					BuiltInTool.SERVER_EXEC_SHELL_COMMAND627				)628			)?.command629		).toBe('ls');630		expect(631			parseExecShellCommandMeta(632				makeSection(633					{ toolArgs: '{"shell_command":"ls"}', toolName: BuiltInTool.SERVER_EXEC_SHELL_COMMAND },634					BuiltInTool.SERVER_EXEC_SHELL_COMMAND635				)636			)?.command637		).toBe('ls');638	});639 640	it('returns null when no command alias is present', () => {641		expect(642			parseExecShellCommandMeta(643				makeSection(644					{ toolArgs: '{"cwd":"/x"}', toolName: BuiltInTool.SERVER_EXEC_SHELL_COMMAND },645					BuiltInTool.SERVER_EXEC_SHELL_COMMAND646				)647			)648		).toBeNull();649	});650});651