File size: 9,074 Bytes
ea7122c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
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/);
});