MiniCPM5-2B-WebGPU-Pi-HTTP / app /tests /local-folder.test.mjs
Mike0021's picture
Explain binary and oversized files in the editor preview
b74c172 verified
Raw History Blame Contribute Delete
9.72 kB
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { LocalFolderFs, displayText } from '../src/local-folder.mjs';
import { createWorkspace } from '../src/workspace.mjs';
// OS-backed handle fixture for adapter semantics. Actual browser handle cloning,
// permissions, picker UX, and IndexedDB are covered by the browser suite.
async function fixture(t, initial = { 'README.md': '# Local project\n', 'src/app.js': 'export const answer = 1;\n' }) {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pi-folder-test-'));
t.after(() => fs.rm(root, { recursive: true, force: true }));
for (const [name, data] of Object.entries(initial)) { await fs.mkdir(path.dirname(path.join(root, name)), { recursive: true }); await fs.writeFile(path.join(root, name), data); }
let reads = 0, failWrite = false;
const handle = (p, kind) => ({ kind, name: path.basename(p),
async *entries() { for (const item of await fs.readdir(p, { withFileTypes: true })) yield [item.name, handle(path.join(p, item.name), item.isDirectory() ? 'directory' : 'file')]; },
async getFile() { const stat = await fs.stat(p); return { size: stat.size, lastModified: stat.mtimeMs, async arrayBuffer() { reads++; return new Uint8Array(await fs.readFile(p)).buffer; } }; },
fixturePath: p,
async isSameEntry(other) { return other.fixturePath === p; },
async createWritable() {
if (failWrite) throw new DOMException('Write denied', 'NotAllowedError');
let bytes;
return { async write(data) { bytes = data; }, async close() { await fs.writeFile(p, bytes); }, async abort() {} };
},
async removeEntry(name) { await fs.rm(path.join(p, name)); },
});
// Install traversal separately so each handle closes over its own path.
function make(p, kind) {
const h = handle(p, kind);
const child = async (name, wanted, create) => {
assert(!name.includes('/') && name !== '..'); const target = path.join(p, name);
let stat;
try { stat = await fs.stat(target); }
catch (e) { if (e.code !== 'ENOENT') throw e; if (!create) throw new DOMException('Missing', 'NotFoundError'); if (wanted === 'directory') await fs.mkdir(target); else await fs.writeFile(target, ''); stat = await fs.stat(target); }
if (stat.isDirectory() !== (wanted === 'directory')) throw new DOMException('Wrong kind', 'TypeMismatchError');
return make(target, wanted);
};
h.getDirectoryHandle = (name, { create = false } = {}) => child(name, 'directory', create);
h.getFileHandle = (name, { create = false } = {}) => child(name, 'file', create);
h.entries = async function* () { for (const item of await fs.readdir(p, { withFileTypes: true })) yield [item.name, make(path.join(p, item.name), item.isDirectory() ? 'directory' : 'file')]; };
return h;
}
const directory = make(root, 'directory');
return { root, directory, get reads() { return reads; }, denyWrites() { failWrite = true; }, read: name => fs.readFile(path.join(root, name), 'utf8'), write: (name, text) => fs.writeFile(path.join(root, name), text) };
}
test('Local folder: indexing/listing/stat do not read file bytes; workspace snapshots stay metadata-only', async t => {
const disk = await fixture(t, { 'a.txt': 'hello', 'large.bin': Buffer.alloc(2 * 1024 ** 2), 'node_modules/ignore.js': 'ignored' });
const local = await LocalFolderFs.open(disk.directory), workspace = createWorkspace({}, undefined, local);
await workspace.ready;
assert.equal(disk.reads, 0);
assert.deepEqual(Object.keys(await workspace.snapshot()), ['/workspace/a.txt', '/workspace/large.bin']);
assert.equal(await workspace.serialize(), null);
const listing = await workspace.bash.exec('ls -l /workspace'); assert.equal(listing.exitCode, 0, listing.stderr);
assert.equal(disk.reads, 0); assert.equal(local.skipped, 1);
assert.equal(await workspace.read('a.txt'), 'hello'); assert.equal(disk.reads, 1);
await assert.rejects(workspace.read('large.bin'), { code: 'FILE_TOO_LARGE' });
});
test('Local folder: real Bash and agent tools stage edits, mkdir, copy, rename, delete; Apply writes only staged paths', async t => {
const disk = await fixture(t);
const local = await LocalFolderFs.open(disk.directory), workspace = createWorkspace({}, undefined, local);
await workspace.ready;
await workspace.tools.find(t => t.name === 'edit').execute('', { path: 'src/app.js', oldText: '= 1', newText: '= 2' });
const result = await workspace.bash.exec("mkdir -p reports/nested; printf 'ok\\n' > reports/nested/result.txt; cp README.md copy.md; mv copy.md moved.md; rm README.md");
assert.equal(result.exitCode, 0, result.stderr);
assert.equal(await disk.read('src/app.js'), 'export const answer = 1;\n');
assert.equal(await disk.read('README.md'), '# Local project\n');
const patches = await local.patches(); assert.equal(patches.length, 6);
assert.equal(await local.apply(), 6);
assert.equal(await disk.read('src/app.js'), 'export const answer = 2;\n');
assert.equal(await disk.read('moved.md'), '# Local project\n');
assert.equal(await disk.read('reports/nested/result.txt'), 'ok\n');
await assert.rejects(disk.read('README.md'), { code: 'ENOENT' });
assert.equal((await local.patches()).length, 0);
});
test('Local folder: conflicts preflight all writes, including same-size edits, creations and deletions', async t => {
const disk = await fixture(t);
const local = await LocalFolderFs.open(disk.directory);
await local.writeFile('/README.md', 'changed'); await local.writeFile('/src/app.js', 'staged');
await disk.write('src/app.js', 'export const answer = 9;\n');
await assert.rejects(local.apply(), /Conflict/);
assert.equal(await disk.read('README.md'), '# Local project\n');
assert.equal(await disk.read('src/app.js'), 'export const answer = 9;\n');
const fresh = await LocalFolderFs.open(disk.directory);
await fresh.writeFile('/new.txt', 'staged'); await disk.write('new.txt', 'external');
await assert.rejects(fresh.apply(), /Conflict/); assert.equal(await disk.read('new.txt'), 'external');
const deleted = await LocalFolderFs.open(disk.directory);
await deleted.rm('/README.md'); await disk.write('README.md', 'other editor');
await assert.rejects(deleted.apply(), /Conflict/); assert.equal(await disk.read('README.md'), 'other editor');
});
test('Local folder: staged patches restore with the original baseline, not a newly changed disk file', async t => {
const disk = await fixture(t), local = await LocalFolderFs.open(disk.directory);
await local.writeFile('/src/app.js', 'staged'); await local.mkdir('/new'); await local.writeFile('/new/file', 'new');
const patches = await local.patches();
await disk.write('src/app.js', 'external');
const restored = await LocalFolderFs.open(disk.directory, { patches });
assert.equal(await restored.readFile('/src/app.js'), 'staged');
assert.equal(await restored.readFile('/new/file'), 'new');
await assert.rejects(restored.apply(), /Conflict/);
});
test('Local folder: failed or cancelled application retains pending edits', async t => {
const disk = await fixture(t), local = await LocalFolderFs.open(disk.directory);
await local.writeFile('/README.md', 'staged');
await assert.rejects(local.apply(AbortSignal.abort()), { name: 'AbortError' });
disk.denyWrites(); await assert.rejects(local.apply(), /0 of 1 changes applied/);
assert.equal((await local.patches()).length, 1);
assert.equal(await disk.read('README.md'), '# Local project\n');
const newFile = await LocalFolderFs.open(disk.directory);
await newFile.writeFile('/new.txt', 'staged');
await assert.rejects(newFile.apply(), /0 of 1/);
await assert.rejects(disk.read('new.txt'), { code: 'ENOENT' });
assert.equal((await newFile.patches()).length, 1);
});
test('Local folder: unsupported operations, excludes, binary editor and traversal do not touch disk', async t => {
const disk = await fixture(t), local = await LocalFolderFs.open(disk.directory), workspace = createWorkspace({}, undefined, local);
await workspace.ready;
for (const method of ['chmod', 'symlink', 'link', 'utimes']) assert.throws(() => local[method]('/README.md', 0o777), /ENOTSUP/);
await assert.rejects(local.rm('/src', { recursive: true }), /not supported/);
await assert.rejects(local.writeFile('/.git/config', 'oops'), /excluded/);
assert.throws(() => displayText(new Uint8Array([0, 1])), { code: 'BINARY_FILE' });
assert.throws(() => displayText(new Uint8Array([0xff, 0xfe])), { code: 'BINARY_FILE' });
assert.equal(displayText(new TextEncoder().encode('Café 🐾')), 'Café 🐾');
await workspace.write('../outside.txt', 'virtual only');
assert.deepEqual(await local.patches(), []);
await assert.rejects(disk.read('outside.txt'), { code: 'ENOENT' });
});
test('Local folder: an in-flight editor preview cannot overwrite a staged tool edit', async t => {
const disk = await fixture(t), local = await LocalFolderFs.open(disk.directory);
let unblock, started;
const gate = new Promise(resolve => { unblock = resolve; });
const loading = new Promise(resolve => { started = resolve; });
const load = local.loadOriginal.bind(local);
local.loadOriginal = async path => { started(); await gate; return load(path); };
const preview = local.readFile('/README.md'); await loading;
const edit = local.writeFile('/README.md', 'new staged version');
unblock(); await Promise.all([preview, edit]);
assert.equal(await local.readFile('/README.md'), 'new staged version');
assert.equal(await disk.read('README.md'), '# Local project\n');
assert.equal((await local.patches()).length, 1);
});