Felipe97/llama-cpp-compiled
01.2k
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 