GPU_GUARD_MONOREPO/packages/backend/test/tool_operations.test.ts

105 lines
3.6 KiB
TypeScript
Raw Normal View History

2026-05-20 21:39:12 +08:00
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');
});
});