import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import { PassThrough } from 'stream'; import { createLocalToolOperations } from '../src/modules/netaclaw/tools/operations/local.js'; describe('LocalToolOperations', () => { const cleanupTasks: Array<() => void> = []; afterEach(() => { jest.resetModules(); jest.clearAllMocks(); cleanupTasks.splice(0).forEach(cleanup => cleanup()); }); const createTempDir = () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'neta-tool-ops-')); cleanupTasks.push(() => fs.rmSync(dir, { recursive: true, force: true })); return dir; }; it('supports file read/write/append/readDir/realpath operations', async () => { const ops = createLocalToolOperations(); const root = createTempDir(); const nested = path.join(root, 'nested'); const filePath = path.join(nested, 'note.txt'); await ops.file.mkdir(nested); await ops.file.writeFile(filePath, 'line 1'); await ops.file.appendFile(filePath, '\nline 2'); await ops.file.access(filePath, 'readwrite'); const buffer = await ops.file.readFile(filePath); expect(buffer.toString('utf-8')).toBe('line 1\nline 2'); expect(await ops.file.isDirectory(root)).toBe(true); expect(await ops.file.isDirectory(filePath)).toBe(false); const entries = await ops.file.readDir(root); expect(entries).toEqual([ expect.objectContaining({ name: 'nested', isDirectory: true }), ]); expect(await ops.file.realpath(filePath)).toBe(path.resolve(filePath)); }); it('streams process output and returns exitCode', async () => { const ops = createLocalToolOperations(); const root = createTempDir(); const chunks: Array<{ stream: 'stdout' | 'stderr'; text: string }> = []; const result = await ops.process.exec( `node -e "process.stdout.write('OUT'); process.stderr.write('ERR')"`, root, { timeout: 5000, onData: (chunk, stream) => chunks.push({ stream, text: chunk.toString('utf-8') }), }, ); expect(result.exitCode).toBe(0); expect(chunks).toEqual( expect.arrayContaining([ expect.objectContaining({ stream: 'stdout', text: expect.stringContaining('OUT') }), expect.objectContaining({ stream: 'stderr', text: expect.stringContaining('ERR') }), ]), ); }); it('collects search output through SearchOperations wrappers', async () => { const spawnMock = jest.fn().mockImplementation((_cmd, _args, _options) => { const child: any = new PassThrough(); child.stdout = new PassThrough(); child.stderr = new PassThrough(); child.pid = 123; child.once = child.on.bind(child); child.off = child.removeListener.bind(child); setImmediate(() => { child.stdout.write('/tmp/a.txt\n/tmp/b.txt\n'); child.stdout.end(); child.stderr.end(); child.emit('close', 0); }); return child; }); let createOps: typeof createLocalToolOperations; jest.isolateModules(() => { jest.doMock('../src/comm/child_process.js', () => ({ spawn: spawnMock, resolveShellConfig: () => ({ shell: 'bash', args: ['-lc'] }), })); ({ createLocalToolOperations: createOps } = require('../src/modules/netaclaw/tools/operations/local.js')); }); const ops = createOps!(); const result = await ops.search.fd(['--glob', '*.txt'], '/tmp'); expect(spawnMock).toHaveBeenCalled(); expect(result.exitCode).toBe(0); expect(result.stdout).toContain('/tmp/a.txt'); expect(result.stdout).toContain('/tmp/b.txt'); }); });