144 lines
4.7 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 sharp = require('sharp');
import { listDirTool, readFileTool, writeFileTool } from '../src/modules/netaclaw/tools/builtin/file.js';
describe('file tools', () => {
const tempDirs: string[] = [];
afterEach(() => {
tempDirs.splice(0).forEach(dir => fs.rmSync(dir, { recursive: true, force: true }));
});
const createTempDir = () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'neta-file-tool-'));
tempDirs.push(root);
return root;
};
it('reads a whole small file by default', async () => {
const root = createTempDir();
const filePath = path.join(root, 'note.txt');
fs.writeFileSync(filePath, 'alpha\nbeta', 'utf8');
const result = await readFileTool.execute('read-1', { path: filePath });
expect((result as any).text).toBe('alpha\nbeta');
});
it('reads paths with Pi-compatible Unicode space normalization', async () => {
const root = createTempDir();
fs.writeFileSync(path.join(root, 'my file.txt'), 'normalized', 'utf8');
const result = await readFileTool.execute('read-1', {
path: 'my\u00A0file.txt',
_netaRuntime: { sessionCwd: root },
} as any);
expect((result as any).text).toBe('normalized');
});
it('returns structured image results for image files', async () => {
const root = createTempDir();
const filePath = path.join(root, 'pixel.png');
fs.writeFileSync(
filePath,
Buffer.from(
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Wn0lVQAAAAASUVORK5CYII=',
'base64'
)
);
const result = await readFileTool.execute('read-image', { path: filePath });
expect(result).toEqual(expect.objectContaining({
type: 'image',
mimeType: 'image/png',
width: 1,
height: 1,
}));
expect((result as any).url).toContain('data:image/png;base64,');
expect((result as any).text).toContain('Dimensions: 1x1');
expect((result as any).text).toContain('Size:');
});
it('resizes oversized image files before returning structured image data', async () => {
const root = createTempDir();
const filePath = path.join(root, 'large.png');
const largeImage = await sharp({
create: {
width: 2400,
height: 1200,
channels: 3,
background: { r: 12, g: 34, b: 56 },
},
}).png().toBuffer();
fs.writeFileSync(filePath, largeImage);
const result = await readFileTool.execute('read-large-image', { path: filePath });
expect(result).toEqual(expect.objectContaining({
type: 'image',
resized: true,
originalWidth: 2400,
originalHeight: 1200,
}));
expect((result as any).width).toBeLessThanOrEqual(2000);
expect((result as any).height).toBeLessThanOrEqual(2000);
expect((result as any).url).toMatch(/^data:image\/(png|jpeg);base64,/);
expect((result as any).text).toContain('original 2400x1200');
});
it('reads a line window with offset and limit', async () => {
const root = createTempDir();
const filePath = path.join(root, 'note.txt');
fs.writeFileSync(filePath, 'line 1\nline 2\nline 3\nline 4', 'utf8');
const result = await readFileTool.execute('read-2', {
path: filePath,
offset: 2,
limit: 2,
});
expect((result as any).text).toContain('line 2\nline 3');
expect((result as any).text).toContain('offset=4');
});
it('truncates large reads with a continuation hint', async () => {
const root = createTempDir();
const filePath = path.join(root, 'large.txt');
const content = Array.from({ length: 2001 }, (_, index) => `line ${index + 1}`).join('\n');
fs.writeFileSync(filePath, content, 'utf8');
const result = await readFileTool.execute('read-3', { path: filePath });
const text = (result as any).text as string;
expect(text).toContain('line 1');
expect(text).toContain('line 2000');
expect(text).not.toContain('line 2001');
expect(text).toContain('已达到 2000 行限制');
expect(text).toContain('offset=2001');
});
it('serializes write_file mutations through the file mutation queue', async () => {
const root = createTempDir();
const filePath = path.join(root, 'nested', 'note.txt');
await writeFileTool.execute('write-1', { path: filePath, content: 'created' });
expect(fs.readFileSync(filePath, 'utf8')).toBe('created');
});
it('lists runtime session cwd when path is omitted', async () => {
const root = createTempDir();
fs.writeFileSync(path.join(root, 'note.txt'), 'created', 'utf8');
const result = await listDirTool.execute('list-1', {
_netaRuntime: { sessionCwd: root },
} as any);
expect((result as any).text).toContain('note.txt');
});
});