MiniCPM5-2B-WebGPU-Pi / test /workspace.test.js
Xenova's picture
Xenova HF Staff
Upload 65 files
ea7122c verified
Raw History Blame Contribute Delete
9.07 kB
import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdtemp, readFile, writeFile, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import {
createWorkspace, createStore, DEFAULT_FILES, MAX_EDITOR_BYTES, WORKSPACE_DB_NAME,
} from '../src/workspace.js';
const tool = (workspace, name) => workspace.tools.find(tool => tool.name === name);
test('real shell combines the demo data with pipes, awk, jq, redirects, and subshells', async () => {
const workspace = createWorkspace();
const response = await tool(workspace, 'bash').execute({
command: `cat sales.csv | tail -n +2 | awk -F, '{total += $2} END {print total}' > units.txt
printf '%s\\n' "$(jq -r '.[0].product' products.json):$(cat units.txt)"`,
});
assert.equal(response.content, 'Notebook:50\n\nExit code: 0');
assert.deepEqual(response.details, { exitCode: 0 });
assert.equal(await workspace.read('units.txt'), '50\n');
});
test('each command resets cwd and variables while keeping file changes', async () => {
const workspace = createWorkspace();
const bash = tool(workspace, 'bash');
await bash.execute({ command: 'mkdir nested; cd nested; VALUE=private; echo saved > note.txt' });
const response = await bash.execute({ command: 'printf "%s:%s:%s" "$PWD" "${VALUE-unset}" "$(cat nested/note.txt)"' });
assert.equal(response.content, '/workspace:unset:saved\nExit code: 0');
});
test('file tools use plain JSON Schema and text results with exact replacement semantics', async () => {
const workspace = createWorkspace();
for (const { parameters } of workspace.tools) {
assert.equal(parameters.type, 'object');
assert.equal(parameters.additionalProperties, false);
assert.doesNotThrow(() => JSON.stringify(parameters));
}
await tool(workspace, 'write').execute({ path: 'notes/new.txt', content: 'hello 🌍\nhello again' });
const response = await tool(workspace, 'read').execute({ path: 'notes/new.txt' });
assert.equal(response.content, 'hello 🌍\nhello again');
assert.equal(response.details.path, '/workspace/notes/new.txt');
await assert.rejects(tool(workspace, 'edit').execute({ path: 'notes/new.txt', oldText: 'hello', newText: 'bad' }), /exactly once/);
await assert.rejects(tool(workspace, 'edit').execute({ path: 'notes/new.txt', oldText: '', newText: 'bad' }), /exactly once/);
await tool(workspace, 'edit').execute({ path: 'notes/new.txt', oldText: 'hello 🌍', newText: '$& is literal' });
assert.equal(await workspace.read('notes/new.txt'), '$& is literal\nhello again');
});
test('a failed command rejects with its exit code and useful output', async () => {
const workspace = createWorkspace();
await assert.rejects(tool(workspace, 'bash').execute({ command: 'echo problem >&2; exit 7' }), error => {
assert.equal(error.exitCode, 7);
assert.match(error.message, /STDERR:\nproblem/);
assert.match(error.message, /Exit code: 7/);
return true;
});
assert.equal((await tool(workspace, 'bash').execute({ command: 'echo recovered' })).details.exitCode, 0);
});
test('editor limit counts UTF-8 bytes and tool output is truncated without changing the file', async () => {
const workspace = createWorkspace();
await assert.rejects(workspace.write('large.txt', '🌍'.repeat(MAX_EDITOR_BYTES / 4 + 1)), /up to 1 MiB/);
const original = 'x'.repeat(7_000);
await workspace.write('large.txt', original);
const response = await tool(workspace, 'read').execute({ path: 'large.txt' });
assert.match(response.content, /Output truncated at 6,000 characters/);
assert.equal(await workspace.read('large.txt'), original);
});
test('snapshot and serialization preserve default files and exclude generated command/device entries', async () => {
const workspace = createWorkspace();
assert.deepEqual(await workspace.snapshot(), DEFAULT_FILES);
const entries = await workspace.serialize();
assert.ok(entries.some(([path]) => path === '/workspace'));
assert.ok(!entries.some(([path]) => /^\/(?:bin|usr\/bin|dev|proc)(?:\/|$)/.test(path)));
const restored = createWorkspace(undefined, entries);
assert.deepEqual(await restored.snapshot(), DEFAULT_FILES);
});
test('binary contents, executable modes, empty directories, and symbolic links survive serialization', async () => {
const workspace = createWorkspace({});
await workspace.ready;
const binary = new Uint8Array([0, 255, 128, 65, 10, 0, 254]);
await workspace.bash.fs.writeFile('/workspace/binary.dat', binary);
await workspace.bash.fs.chmod('/workspace/binary.dat', 0o640);
await workspace.bash.fs.mkdir('/workspace/empty');
await workspace.bash.fs.chmod('/workspace/empty', 0o750);
await workspace.write('hello.sh', '#!/bin/bash\necho hello\n');
await workspace.bash.fs.chmod('/workspace/hello.sh', 0o755);
await workspace.bash.fs.symlink('hello.sh', '/workspace/hello-link');
const restored = createWorkspace(undefined, await workspace.serialize());
await restored.ready;
assert.deepEqual(await restored.bash.fs.readFileBuffer('/workspace/binary.dat'), binary);
assert.equal((await restored.bash.fs.stat('/workspace/binary.dat')).mode & 0o777, 0o640);
assert.equal((await restored.bash.fs.stat('/workspace/empty')).mode & 0o777, 0o750);
assert.equal((await restored.bash.fs.stat('/workspace/hello.sh')).mode & 0o777, 0o755);
assert.equal(await restored.bash.fs.readlink('/workspace/hello-link'), 'hello.sh');
assert.equal(await restored.read('hello-link'), '#!/bin/bash\necho hello\n');
assert.equal((await tool(restored, 'bash').execute({ command: 'bash hello-link' })).content, 'hello\n\nExit code: 0');
});
test('aborting a live shell interrupts it and later commands remain usable', async () => {
const workspace = createWorkspace();
const controller = new AbortController();
const running = tool(workspace, 'bash').execute({ command: 'sleep 10; echo should-not-run' }, { signal: controller.signal });
const timer = setTimeout(() => controller.abort(new DOMException('Stopped by user', 'AbortError')), 20);
try {
await assert.rejects(running, /Stopped by user|abort/i);
} finally {
clearTimeout(timer);
}
assert.equal((await tool(workspace, 'bash').execute({ command: 'echo ready' })).content, 'ready\n\nExit code: 0');
});
test('pre-aborted file operations do not change files', async () => {
const workspace = createWorkspace();
const controller = new AbortController();
controller.abort(new DOMException('Stopped', 'AbortError'));
for (const [name, args] of [
['write', { path: 'sales.csv', content: 'lost' }],
['edit', { path: 'sales.csv', oldText: 'Notebook', newText: 'lost' }],
['read', { path: 'sales.csv' }],
['bash', { command: 'echo lost > sales.csv' }],
]) await assert.rejects(tool(workspace, name).execute(args, { signal: controller.signal }), /Stopped/);
assert.equal(await workspace.read('sales.csv'), DEFAULT_FILES['/workspace/sales.csv']);
});
test('shell cannot use host binaries or network commands and does not call fetch', async () => {
const workspace = createWorkspace();
const originalFetch = globalThis.fetch;
let calls = 0;
globalThis.fetch = async () => { calls++; throw new Error('Unexpected network call'); };
try {
for (const command of ['curl https://example.com', 'wget https://example.com', 'node -e "process.exit(0)"', 'python3 -c "print(1)"', 'npm --version', 'js "console.log(1)"']) {
await assert.rejects(tool(workspace, 'bash').execute({ command }), /command not (?:found|available)/);
}
assert.equal(calls, 0);
} finally {
globalThis.fetch = originalFetch;
}
});
test('shell absolute paths stay in the virtual filesystem', async () => {
const temporary = await mkdtemp(join(tmpdir(), 'minicpm-workspace-test-'));
const hostFile = join(temporary, 'private.txt');
try {
await writeFile(hostFile, 'host-only-secret');
const workspace = createWorkspace();
await assert.rejects(tool(workspace, 'bash').execute({ command: `cat '${hostFile}'` }), /No such file|ENOENT|not found/);
await tool(workspace, 'bash').execute({ command: `mkdir -p '${temporary}'; printf virtual > '${hostFile}'` });
assert.equal(await workspace.read(hostFile), 'virtual');
assert.equal(await readFile(hostFile, 'utf8'), 'host-only-secret');
} finally {
await rm(temporary, { recursive: true, force: true });
}
});
test('shell rejects oversized source and excessive virtual filesystem allocation', async () => {
const workspace = createWorkspace({});
await assert.rejects(tool(workspace, 'bash').execute({ command: '#'.repeat(131_073) }), /script input size limit/);
await workspace.ready;
await assert.rejects(workspace.bash.fs.writeFile('/workspace/excessive.bin', new Uint8Array(16 * 1024 * 1024 + 1)), /ENOSPC|byte limit/);
});
test('new storage name avoids legacy Pi message data and missing IndexedDB is explicit', async () => {
assert.equal(WORKSPACE_DB_NAME, 'minicpm5-gguf-workspace');
await assert.rejects(createStore(null), /Browser storage is unavailable/);
});