105 lines
3.6 KiB
TypeScript
105 lines
3.6 KiB
TypeScript
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');
|
|
});
|
|
});
|