/ tests / agents / agent-tools-supplement.test.js
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  });