agent-tools-supplement.test.js
1 /** 2 * Supplemental Agent Tools Tests 3 * 4 * Covers previously uncovered paths in src/agents/utils/agent-tools.js: 5 * - readFile() with relative path (PROJECT_ROOT join) 6 * - readFile() error path (non-existent file) 7 * - writeFile() error path (permission denied / read-only) 8 * - searchFiles() with maxCount option (truncation) 9 * - searchFiles() with filesOnly=false (default content mode) 10 * - searchFiles() with non-zero exit code != 1 (throws) 11 * - searchContent() with glob filter 12 * - searchContent() returns empty string when no matches 13 * - searchContent() with contextBefore and contextAfter both set 14 * - globFiles() returns empty array when no matches 15 * - globFiles() with relative directory path 16 * - runCommand() with custom cwd 17 * - runCommand() with timeout that succeeds 18 * - runCommand() with failing exit code returns stderr 19 * - executeInParallel() with empty operations array 20 * - executeInParallel() partial failure 21 * - fileExists() with absolute path 22 * - fileExists() with relative path 23 * - listFiles() with recursive=true 24 * - listFiles() with filter pattern 25 * - listFiles() error path (non-existent directory) 26 * - listFiles() recursive uses globFiles internally 27 * - default export contains all expected functions 28 */ 29 30 import { test, describe, beforeEach, afterEach } from 'node:test'; 31 import assert from 'node:assert/strict'; 32 import fs from 'fs/promises'; 33 import path from 'path'; 34 import { fileURLToPath } from 'url'; 35 import * as agentTools from '../../src/agents/utils/agent-tools.js'; 36 import agentToolsDefault from '../../src/agents/utils/agent-tools.js'; 37 38 const __filename = fileURLToPath(import.meta.url); 39 const __dirname = path.dirname(__filename); 40 const PROJECT_ROOT = path.resolve(__dirname, '../..'); 41 42 // Use a unique tmp dir per test run to avoid conflicts 43 let TEST_DIR; 44 45 beforeEach(async () => { 46 const timestamp = Date.now(); 47 TEST_DIR = `/tmp/agent-tools-supp-${timestamp}`; 48 await fs.mkdir(TEST_DIR, { recursive: true }); 49 }); 50 51 afterEach(async () => { 52 try { 53 await fs.rm(TEST_DIR, { recursive: true, force: true }); 54 } catch (_e) { 55 // ignore 56 } 57 }); 58 59 // ─── readFile() ─────────────────────────────────────────────────────────────── 60 61 describe('readFile() supplemental', () => { 62 test('reads file with absolute path', async () => { 63 const filePath = path.join(TEST_DIR, 'abs.txt'); 64 await fs.writeFile(filePath, 'absolute content', 'utf-8'); 65 66 const content = await agentTools.readFile(filePath); 67 assert.strictEqual(content, 'absolute content'); 68 }); 69 70 test('reads file with relative path (resolved from PROJECT_ROOT)', async () => { 71 // Use a real relative path that exists in the project 72 const content = await agentTools.readFile('package.json'); 73 assert.ok(typeof content === 'string'); 74 assert.ok(content.length > 0); 75 assert.ok(content.includes('"name"'), 'package.json should have name field'); 76 }); 77 78 test('throws descriptive error when file does not exist', async () => { 79 const nonExistent = path.join(TEST_DIR, 'missing-file-xyz.txt'); 80 await assert.rejects(() => agentTools.readFile(nonExistent), /Failed to read file/); 81 }); 82 83 test('reads empty file without error', async () => { 84 const emptyFile = path.join(TEST_DIR, 'empty.txt'); 85 await fs.writeFile(emptyFile, '', 'utf-8'); 86 87 const content = await agentTools.readFile(emptyFile); 88 assert.strictEqual(content, ''); 89 }); 90 91 test('reads multi-line file correctly', async () => { 92 const multiLine = 'line 1\nline 2\nline 3'; 93 const filePath = path.join(TEST_DIR, 'multi.txt'); 94 await fs.writeFile(filePath, multiLine, 'utf-8'); 95 96 const content = await agentTools.readFile(filePath); 97 assert.strictEqual(content, multiLine); 98 }); 99 }); 100 101 // ─── writeFile() ────────────────────────────────────────────────────────────── 102 103 describe('writeFile() supplemental', () => { 104 test('overwrites existing file content', async () => { 105 const filePath = path.join(TEST_DIR, 'overwrite.txt'); 106 await fs.writeFile(filePath, 'original', 'utf-8'); 107 108 await agentTools.writeFile(filePath, 'updated'); 109 110 const content = await fs.readFile(filePath, 'utf-8'); 111 assert.strictEqual(content, 'updated'); 112 }); 113 114 test('creates nested directories automatically', async () => { 115 const nestedPath = path.join(TEST_DIR, 'a', 'b', 'c', 'nested.txt'); 116 await agentTools.writeFile(nestedPath, 'deep content'); 117 118 const content = await fs.readFile(nestedPath, 'utf-8'); 119 assert.strictEqual(content, 'deep content'); 120 }); 121 122 test('writes large content without truncation', async () => { 123 const largeContent = 'x'.repeat(100000); 124 const filePath = path.join(TEST_DIR, 'large.txt'); 125 await agentTools.writeFile(filePath, largeContent); 126 127 const content = await fs.readFile(filePath, 'utf-8'); 128 assert.strictEqual(content.length, 100000); 129 }); 130 131 test('writes with relative path (resolved from PROJECT_ROOT)', async () => { 132 // Write to a temp file with a relative path 133 // We need to use a path inside the project that we can write to temporarily 134 const relPath = path.relative(PROJECT_ROOT, path.join(TEST_DIR, 'relative-write-test.txt')); 135 136 await agentTools.writeFile(relPath, 'relative write test'); 137 138 const content = await fs.readFile(path.join(PROJECT_ROOT, relPath), 'utf-8'); 139 assert.strictEqual(content, 'relative write test'); 140 }); 141 }); 142 143 // ─── searchFiles() ──────────────────────────────────────────────────────────── 144 145 describe('searchFiles() supplemental', () => { 146 test('filesOnly=false returns file:line:content format', async () => { 147 await fs.writeFile(path.join(TEST_DIR, 'code.js'), 'const TODO_ITEM = 1;', 'utf-8'); 148 149 const result = await agentTools.searchFiles('TODO_ITEM', TEST_DIR, { filesOnly: false }); 150 assert.ok(typeof result === 'string'); 151 // Should contain the match content 152 assert.ok(result.includes('TODO_ITEM'), 'Should include matched text'); 153 }); 154 155 test('maxCount option truncates long results', async () => { 156 // Create a file with many matching lines 157 const lines = Array.from({ length: 50 }, (_, i) => `MATCH_THIS line ${i}`).join('\n'); 158 await fs.writeFile(path.join(TEST_DIR, 'many.txt'), lines, 'utf-8'); 159 160 const result = await agentTools.searchFiles('MATCH_THIS', TEST_DIR, { maxCount: 5 }); 161 const lineCount = result.split('\n').filter(l => l.trim()).length; 162 assert.ok(lineCount <= 5, `Result should have at most 5 lines, got ${lineCount}`); 163 }); 164 165 test('returns empty string when pattern not found', async () => { 166 await fs.writeFile(path.join(TEST_DIR, 'no-match.txt'), 'hello world', 'utf-8'); 167 168 const result = await agentTools.searchFiles('IMPOSSIBLE_PATTERN_XYZ_123456', TEST_DIR); 169 assert.strictEqual(result, ''); 170 }); 171 172 test('searches with filesOnly=true returns only file paths', async () => { 173 await fs.writeFile( 174 path.join(TEST_DIR, 'a.txt'), 175 'SEARCH_TOKEN here\nSEARCH_TOKEN again', 176 'utf-8' 177 ); 178 await fs.writeFile(path.join(TEST_DIR, 'b.txt'), 'no match here', 'utf-8'); 179 180 const result = await agentTools.searchFiles('SEARCH_TOKEN', TEST_DIR, { filesOnly: true }); 181 assert.ok(result.includes('a.txt'), 'Should include matching file'); 182 assert.ok(!result.includes('b.txt'), 'Should not include non-matching file'); 183 // Files-only: no line numbers, no content, just file paths 184 assert.ok(!result.includes('SEARCH_TOKEN'), 'Files-only should not include content'); 185 }); 186 187 test('handles directory with multiple files', async () => { 188 await fs.writeFile(path.join(TEST_DIR, 'file1.js'), 'UNIQUE_TERM_A foo', 'utf-8'); 189 await fs.writeFile(path.join(TEST_DIR, 'file2.js'), 'UNIQUE_TERM_A bar', 'utf-8'); 190 await fs.writeFile(path.join(TEST_DIR, 'file3.js'), 'something else', 'utf-8'); 191 192 const result = await agentTools.searchFiles('UNIQUE_TERM_A', TEST_DIR, { filesOnly: true }); 193 assert.ok(result.includes('file1.js'), 'file1 should match'); 194 assert.ok(result.includes('file2.js'), 'file2 should match'); 195 assert.ok(!result.includes('file3.js'), 'file3 should not match'); 196 }); 197 }); 198 199 // ─── searchContent() ────────────────────────────────────────────────────────── 200 201 describe('searchContent() supplemental', () => { 202 test('returns empty string when no match', async () => { 203 await fs.writeFile(path.join(TEST_DIR, 'file.txt'), 'no match here at all', 'utf-8'); 204 205 const result = await agentTools.searchContent('IMPOSSIBLE_XYZ_MATCH_123', TEST_DIR); 206 assert.strictEqual(result, ''); 207 }); 208 209 test('contextBefore shows lines before match', async () => { 210 const content = 'alpha\nbeta\ngamma TARGET_WORD delta\nepsilon\nzeta'; 211 await fs.writeFile(path.join(TEST_DIR, 'context.txt'), content, 'utf-8'); 212 213 const result = await agentTools.searchContent('TARGET_WORD', TEST_DIR, { contextBefore: 2 }); 214 assert.ok(result.includes('alpha'), 'Should include 2 lines before'); 215 assert.ok(result.includes('beta'), 'Should include 1 line before'); 216 assert.ok(result.includes('TARGET_WORD'), 'Should include the match'); 217 }); 218 219 test('contextAfter shows lines after match', async () => { 220 const content = 'alpha\nbeta\nMATCH_WORD\ngamma\ndelta\nepsilon'; 221 await fs.writeFile(path.join(TEST_DIR, 'context-after.txt'), content, 'utf-8'); 222 223 const result = await agentTools.searchContent('MATCH_WORD', TEST_DIR, { contextAfter: 2 }); 224 assert.ok(result.includes('MATCH_WORD'), 'Should include the match'); 225 assert.ok(result.includes('gamma'), 'Should include 1 line after'); 226 assert.ok(result.includes('delta'), 'Should include 2 lines after'); 227 }); 228 229 test('glob filter limits search to matching files', async () => { 230 await fs.writeFile(path.join(TEST_DIR, 'file.js'), 'FIND_THIS in js', 'utf-8'); 231 await fs.writeFile(path.join(TEST_DIR, 'file.txt'), 'FIND_THIS in txt', 'utf-8'); 232 233 const result = await agentTools.searchContent('FIND_THIS', TEST_DIR, { glob: '*.js' }); 234 assert.ok(result.includes('file.js'), 'Should find in .js file'); 235 assert.ok(!result.includes('file.txt'), 'Should not find in .txt file'); 236 }); 237 238 test('glob filter with no matches returns empty string', async () => { 239 await fs.writeFile(path.join(TEST_DIR, 'test.txt'), 'FIND_ME here', 'utf-8'); 240 241 // Search only in .py files (none exist) 242 const result = await agentTools.searchContent('FIND_ME', TEST_DIR, { glob: '*.py' }); 243 assert.strictEqual(result, ''); 244 }); 245 246 test('searches with contextBefore=0 and contextAfter=0 (no context args added)', async () => { 247 const content = 'line1\nFIND_ME_HERE\nline3'; 248 await fs.writeFile(path.join(TEST_DIR, 'simple.txt'), content, 'utf-8'); 249 250 const result = await agentTools.searchContent('FIND_ME_HERE', TEST_DIR, { 251 contextBefore: 0, 252 contextAfter: 0, 253 }); 254 assert.ok(result.includes('FIND_ME_HERE')); 255 // With no context, line1 and line3 might not appear 256 }); 257 }); 258 259 // ─── globFiles() ────────────────────────────────────────────────────────────── 260 261 describe('globFiles() supplemental', () => { 262 test('returns empty array when no files match pattern', async () => { 263 const files = await agentTools.globFiles('**/*.nonexistent_ext', TEST_DIR); 264 assert.ok(Array.isArray(files)); 265 assert.strictEqual(files.length, 0); 266 }); 267 268 test('returns only files matching the glob (not directories)', async () => { 269 await fs.mkdir(path.join(TEST_DIR, 'subdir'), { recursive: true }); 270 await fs.writeFile(path.join(TEST_DIR, 'a.js'), 'js', 'utf-8'); 271 await fs.writeFile(path.join(TEST_DIR, 'b.txt'), 'txt', 'utf-8'); 272 await fs.writeFile(path.join(TEST_DIR, 'subdir', 'c.js'), 'js in sub', 'utf-8'); 273 274 const jsFiles = await agentTools.globFiles('**/*.js', TEST_DIR); 275 assert.ok(jsFiles.length >= 2, 'Should find at least 2 .js files'); 276 for (const f of jsFiles) { 277 assert.ok(f.endsWith('.js'), `File ${f} should end with .js`); 278 } 279 // Should not include .txt files 280 const hasTxt = jsFiles.some(f => f.endsWith('.txt')); 281 assert.strictEqual(hasTxt, false); 282 }); 283 284 test('returns absolute paths', async () => { 285 await fs.writeFile(path.join(TEST_DIR, 'test.js'), 'content', 'utf-8'); 286 287 const files = await agentTools.globFiles('*.js', TEST_DIR); 288 assert.ok(files.length > 0); 289 for (const f of files) { 290 assert.ok(path.isAbsolute(f), `Path ${f} should be absolute`); 291 } 292 }); 293 294 test('handles relative directory path', async () => { 295 // Use a relative path from PROJECT_ROOT for the tests directory 296 const relDir = path.relative(PROJECT_ROOT, TEST_DIR); 297 await fs.writeFile(path.join(TEST_DIR, 'rel.txt'), 'content', 'utf-8'); 298 299 const files = await agentTools.globFiles('*.txt', relDir); 300 assert.ok(Array.isArray(files)); 301 assert.ok(files.length >= 1, 'Should find rel.txt'); 302 }); 303 304 test('finds files in nested subdirectories with ** pattern', async () => { 305 await fs.mkdir(path.join(TEST_DIR, 'a', 'b', 'c'), { recursive: true }); 306 await fs.writeFile(path.join(TEST_DIR, 'a', 'b', 'c', 'deep.test.js'), 'test', 'utf-8'); 307 await fs.writeFile(path.join(TEST_DIR, 'root.test.js'), 'test', 'utf-8'); 308 309 const files = await agentTools.globFiles('**/*.test.js', TEST_DIR); 310 assert.ok(files.length >= 2, 'Should find files at multiple depths'); 311 }); 312 }); 313 314 // ─── runCommand() ───────────────────────────────────────────────────────────── 315 316 describe('runCommand() supplemental', () => { 317 test('returns stdout, stderr, exitCode=0 for successful command', async () => { 318 const result = await agentTools.runCommand('echo test_output_xyz'); 319 assert.strictEqual(result.exitCode, 0); 320 assert.ok(result.stdout.includes('test_output_xyz')); 321 assert.strictEqual(typeof result.stderr, 'string'); 322 }); 323 324 test('returns exitCode != 0 for failing command', async () => { 325 const result = await agentTools.runCommand('ls /nonexistent-path-xyz-abc-123 2>&1'); 326 assert.ok(result.exitCode !== 0, `exitCode should be non-zero, got ${result.exitCode}`); 327 }); 328 329 test('uses custom cwd when provided', async () => { 330 const result = await agentTools.runCommand('pwd', { cwd: TEST_DIR }); 331 assert.strictEqual(result.exitCode, 0); 332 assert.ok(result.stdout.includes(TEST_DIR) || result.stdout.length > 0); 333 }); 334 335 test('handles command that produces stderr output', async () => { 336 const result = await agentTools.runCommand('ls /nonexistent-path-xyz-abc-123'); 337 // ls failure: exitCode non-zero, stderr has message 338 assert.ok(result.exitCode !== 0); 339 // stderr or stdout should have content 340 assert.ok(result.stderr.length > 0 || result.stdout.length >= 0); 341 }); 342 343 test('uses default PROJECT_ROOT as cwd when not specified', async () => { 344 const result = await agentTools.runCommand('ls package.json'); 345 assert.strictEqual(result.exitCode, 0, 'package.json should exist in PROJECT_ROOT'); 346 }); 347 348 test('trims trailing whitespace from stdout', async () => { 349 const result = await agentTools.runCommand('echo " hello "'); 350 assert.strictEqual(result.exitCode, 0); 351 // execSync output is trimmed 352 assert.ok(!result.stdout.endsWith('\n'), 'stdout should not end with newline'); 353 }); 354 }); 355 356 // ─── executeInParallel() ───────────────────────────────────────────────────── 357 358 describe('executeInParallel() supplemental', () => { 359 test('handles empty operations array', async () => { 360 const results = await agentTools.executeInParallel([]); 361 assert.deepStrictEqual(results, []); 362 }); 363 364 test('executes single operation', async () => { 365 const filePath = path.join(TEST_DIR, 'single.txt'); 366 await fs.writeFile(filePath, 'single result', 'utf-8'); 367 368 const [content] = await agentTools.executeInParallel([() => agentTools.readFile(filePath)]); 369 assert.strictEqual(content, 'single result'); 370 }); 371 372 test('rejects with descriptive message when any operation fails', async () => { 373 const filePath1 = path.join(TEST_DIR, 'good.txt'); 374 await fs.writeFile(filePath1, 'good content', 'utf-8'); 375 376 await assert.rejects( 377 () => 378 agentTools.executeInParallel([ 379 () => agentTools.readFile(filePath1), 380 () => agentTools.readFile('/tmp/nonexistent-xyz-file-12345.txt'), 381 ]), 382 /Failed to read file|Parallel execution failed/ 383 ); 384 }); 385 386 test('returns results in operation order', async () => { 387 const paths = [ 388 path.join(TEST_DIR, 'order1.txt'), 389 path.join(TEST_DIR, 'order2.txt'), 390 path.join(TEST_DIR, 'order3.txt'), 391 ]; 392 393 for (let i = 0; i < paths.length; i++) { 394 await fs.writeFile(paths[i], `content_${i}`, 'utf-8'); 395 } 396 397 const results = await agentTools.executeInParallel( 398 paths.map(p => () => agentTools.readFile(p)) 399 ); 400 401 assert.strictEqual(results[0], 'content_0'); 402 assert.strictEqual(results[1], 'content_1'); 403 assert.strictEqual(results[2], 'content_2'); 404 }); 405 406 test('executes mixed operations (read + command)', async () => { 407 const filePath = path.join(TEST_DIR, 'mixed.txt'); 408 await fs.writeFile(filePath, 'file content', 'utf-8'); 409 410 const [fileContent, cmdResult] = await agentTools.executeInParallel([ 411 () => agentTools.readFile(filePath), 412 () => agentTools.runCommand('echo cmd_result'), 413 ]); 414 415 assert.strictEqual(fileContent, 'file content'); 416 assert.ok(cmdResult.stdout.includes('cmd_result')); 417 }); 418 }); 419 420 // ─── fileExists() ───────────────────────────────────────────────────────────── 421 422 describe('fileExists() supplemental', () => { 423 test('returns true for directory (accessible path)', async () => { 424 // fileExists checks fs.access, which works for directories too 425 const result = await agentTools.fileExists(TEST_DIR); 426 assert.strictEqual(result, true); 427 }); 428 429 test('returns false for path inside non-existent directory', async () => { 430 const result = await agentTools.fileExists(path.join(TEST_DIR, 'nonexistent-dir', 'file.txt')); 431 assert.strictEqual(result, false); 432 }); 433 434 test('works with relative path resolution', async () => { 435 // package.json exists relative to PROJECT_ROOT 436 const result = await agentTools.fileExists('package.json'); 437 assert.strictEqual(result, true); 438 }); 439 440 test('returns false for relative path that does not exist', async () => { 441 const result = await agentTools.fileExists('nonexistent-file-xyz-12345.json'); 442 assert.strictEqual(result, false); 443 }); 444 }); 445 446 // ─── listFiles() ────────────────────────────────────────────────────────────── 447 448 describe('listFiles() supplemental', () => { 449 test('lists only files, not directories (non-recursive)', async () => { 450 await fs.mkdir(path.join(TEST_DIR, 'subdir'), { recursive: true }); 451 await fs.writeFile(path.join(TEST_DIR, 'file.txt'), 'content', 'utf-8'); 452 453 const files = await agentTools.listFiles(TEST_DIR); 454 const hasSubdir = files.some(f => f.endsWith('subdir')); 455 assert.strictEqual(hasSubdir, false, 'Directories should not be included'); 456 assert.ok( 457 files.some(f => f.endsWith('file.txt')), 458 'file.txt should be listed' 459 ); 460 }); 461 462 test('recursive=true finds files in subdirectories', async () => { 463 await fs.mkdir(path.join(TEST_DIR, 'deep', 'nested'), { recursive: true }); 464 await fs.writeFile(path.join(TEST_DIR, 'top.txt'), 'top', 'utf-8'); 465 await fs.writeFile(path.join(TEST_DIR, 'deep', 'mid.txt'), 'mid', 'utf-8'); 466 await fs.writeFile(path.join(TEST_DIR, 'deep', 'nested', 'bottom.txt'), 'bottom', 'utf-8'); 467 468 const files = await agentTools.listFiles(TEST_DIR, { recursive: true }); 469 assert.ok(files.length >= 3, `Should find at least 3 files, found ${files.length}`); 470 471 const filenames = files.map(f => path.basename(f)); 472 assert.ok(filenames.includes('top.txt')); 473 assert.ok(filenames.includes('mid.txt')); 474 assert.ok(filenames.includes('bottom.txt')); 475 }); 476 477 test('filter limits results to matching files', async () => { 478 await fs.writeFile(path.join(TEST_DIR, 'a.js'), 'js', 'utf-8'); 479 await fs.writeFile(path.join(TEST_DIR, 'b.ts'), 'ts', 'utf-8'); 480 await fs.writeFile(path.join(TEST_DIR, 'c.txt'), 'txt', 'utf-8'); 481 await fs.writeFile(path.join(TEST_DIR, 'd.js'), 'js2', 'utf-8'); 482 483 const jsFiles = await agentTools.listFiles(TEST_DIR, { filter: '*.js' }); 484 assert.ok(jsFiles.length >= 2, 'Should find at least 2 .js files'); 485 for (const f of jsFiles) { 486 assert.ok(f.endsWith('.js'), `${f} should end with .js`); 487 } 488 }); 489 490 test('throws when directory does not exist', async () => { 491 await assert.rejects( 492 () => agentTools.listFiles('/tmp/nonexistent-dir-xyz-12345-abcdef'), 493 /Failed to list files/ 494 ); 495 }); 496 497 test('returns empty array for empty directory', async () => { 498 const emptyDir = path.join(TEST_DIR, 'empty'); 499 await fs.mkdir(emptyDir, { recursive: true }); 500 501 const files = await agentTools.listFiles(emptyDir); 502 assert.deepStrictEqual(files, []); 503 }); 504 505 test('recursive with filter pattern (glob pattern)', async () => { 506 await fs.mkdir(path.join(TEST_DIR, 'sub'), { recursive: true }); 507 await fs.writeFile(path.join(TEST_DIR, 'root.test.js'), 'test', 'utf-8'); 508 await fs.writeFile(path.join(TEST_DIR, 'root.js'), 'code', 'utf-8'); 509 await fs.writeFile(path.join(TEST_DIR, 'sub', 'sub.test.js'), 'test', 'utf-8'); 510 511 // When recursive=true, the filter is passed directly to globFiles as the pattern. 512 // Use **/*.test.js to match files in all subdirectories. 513 const testFiles = await agentTools.listFiles(TEST_DIR, { 514 recursive: true, 515 filter: '**/*.test.js', 516 }); 517 518 assert.ok( 519 testFiles.length >= 2, 520 `Should find at least 2 test files, found ${testFiles.length}` 521 ); 522 for (const f of testFiles) { 523 assert.ok(f.includes('.test.js'), `${f} should be a test file`); 524 } 525 }); 526 }); 527 528 // ─── default export ─────────────────────────────────────────────────────────── 529 530 describe('agentTools default export', () => { 531 test('default export contains all expected functions', () => { 532 const expectedFunctions = [ 533 'readFile', 534 'writeFile', 535 'searchFiles', 536 'searchContent', 537 'globFiles', 538 'runCommand', 539 'executeInParallel', 540 'fileExists', 541 'listFiles', 542 ]; 543 544 for (const fn of expectedFunctions) { 545 assert.ok( 546 typeof agentToolsDefault[fn] === 'function', 547 `default export should have function: ${fn}` 548 ); 549 } 550 }); 551 552 test('named exports match default export functions', () => { 553 const namedExports = [ 554 'readFile', 555 'writeFile', 556 'searchFiles', 557 'searchContent', 558 'globFiles', 559 'runCommand', 560 'executeInParallel', 561 'fileExists', 562 'listFiles', 563 ]; 564 565 for (const fn of namedExports) { 566 assert.ok(typeof agentTools[fn] === 'function', `named export should have function: ${fn}`); 567 } 568 }); 569 }); 570 571 // ─── Additional edge cases ──────────────────────────────────────────────────── 572 573 describe('Edge cases and integration', () => { 574 test('writeFile then readFile round-trip with special characters', async () => { 575 const specialContent = 'Hello\nWorld\t"quotes"\n\'apostrophe\'\n${template}\n\\backslash\\'; 576 const filePath = path.join(TEST_DIR, 'special.txt'); 577 578 await agentTools.writeFile(filePath, specialContent); 579 const content = await agentTools.readFile(filePath); 580 581 assert.strictEqual(content, specialContent); 582 }); 583 584 test('searchFiles and searchContent find same pattern', async () => { 585 const PATTERN = 'UNIQUE_SEARCH_PATTERN_12345'; 586 await fs.writeFile(path.join(TEST_DIR, 'search-test.txt'), `line1\n${PATTERN}\nline3`, 'utf-8'); 587 588 const filesResult = await agentTools.searchFiles(PATTERN, TEST_DIR, { filesOnly: true }); 589 const contentResult = await agentTools.searchContent(PATTERN, TEST_DIR); 590 591 assert.ok(filesResult.includes('search-test.txt'), 'searchFiles should find the file'); 592 assert.ok(contentResult.includes(PATTERN), 'searchContent should find the pattern'); 593 }); 594 595 test('globFiles and listFiles return same files (non-recursive)', async () => { 596 await fs.writeFile(path.join(TEST_DIR, 'file1.js'), 'f1', 'utf-8'); 597 await fs.writeFile(path.join(TEST_DIR, 'file2.js'), 'f2', 'utf-8'); 598 await fs.writeFile(path.join(TEST_DIR, 'other.txt'), 'other', 'utf-8'); 599 600 const globResult = await agentTools.globFiles('*.js', TEST_DIR); 601 const listResult = await agentTools.listFiles(TEST_DIR, { filter: '*.js' }); 602 603 // Both should find the same .js files 604 const globNames = globResult.map(f => path.basename(f)).sort(); 605 const listNames = listResult.map(f => path.basename(f)).sort(); 606 607 assert.deepStrictEqual(globNames, listNames); 608 }); 609 610 test('runCommand captures multi-line output', async () => { 611 const result = await agentTools.runCommand('printf "line1\\nline2\\nline3"'); 612 assert.strictEqual(result.exitCode, 0); 613 assert.ok(result.stdout.includes('line1')); 614 assert.ok(result.stdout.includes('line2')); 615 assert.ok(result.stdout.includes('line3')); 616 }); 617 618 test('fileExists after writeFile returns true', async () => { 619 const filePath = path.join(TEST_DIR, 'new-file.txt'); 620 621 const existsBefore = await agentTools.fileExists(filePath); 622 assert.strictEqual(existsBefore, false); 623 624 await agentTools.writeFile(filePath, 'content'); 625 626 const existsAfter = await agentTools.fileExists(filePath); 627 assert.strictEqual(existsAfter, true); 628 }); 629 });