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'); }); });