MiniCPM5-2B-WebGPU-Pi-HTTP / app /tests /protocol.test.mjs
Mike0021's picture
Allow curl requests to any HTTPS host; remove experimental domain allowlist
9c380c2 verified
Raw History Blame Contribute Delete
9.1 kB
import test from 'node:test';
import assert from 'node:assert/strict';
import { parseCompletion, splitThinking, toMiniMessages, fitContext } from '../src/protocol.mjs';
import { createWorkspace } from '../src/workspace.mjs';
import { renderMarkdown } from '../src/markdown.mjs';
const { tools } = createWorkspace();
test('read results identify the real file through relative paths and symlinks', async () => {
const workspace = createWorkspace();
await workspace.write('nested/name%20#é.txt', '');
await workspace.bash.exec("ln -s 'nested/name%20#é.txt' link.txt");
const read = workspace.tools.find(tool => tool.name === 'read');
const result = await read.execute('read-path', { path: './link.txt' });
assert.equal(result.details.path, '/workspace/nested/name%20#é.txt');
assert.equal(result.content[0].text, '');
await assert.rejects(read.execute('missing', { path: 'missing.txt' }));
});
test('model Markdown cannot execute HTML or request remote images', () => {
const html = renderMarkdown('<script>alert(1)</script>\n![secret](https://example.com/track)\n[x](javascript:alert(1))');
assert(!html.includes('<script>')); assert(!html.includes('<img')); assert(!html.includes('href="javascript:'));
assert.match(renderMarkdown('```bash\necho ok\n```'), /<pre><code class="language-bash">/);
});
test('MiniCPM XML retains shell characters, CDATA, split CDATA, and multiple calls', () => {
const text = 'Inspecting.\n<function name="bash"><param name="command"><![CDATA[printf "<x>&</function>"\necho ]]]]><![CDATA[>]]></param></function>\n<function name="read"><param name="path">a&amp;b.txt</param></function><|im_end|>';
const blocks = parseCompletion(text, tools);
assert.equal(blocks[0].text, 'Inspecting.');
assert.equal(blocks[1].arguments.command, 'printf "<x>&</function>"\necho ]]>');
assert.equal(blocks[2].arguments.path, 'a&b.txt');
});
test('XML examples in fenced or inline code remain text', () => {
const call = '<function name="bash"><param name="command">ls</param></function>';
for (const example of ['```xml\n' + call + '\n```', '`' + call + '`', '~~~xml\n' + call + '\n~~~']) {
const blocks = parseCompletion(example, tools);
assert.deepEqual(blocks, [{ type: 'text', text: example }]);
}
const blocks = parseCompletion('```xml\n' + call + '\n```\n' + call, tools);
assert.equal(blocks[1].type, 'toolCall');
});
test('incomplete, unknown, nested, duplicate and malformed calls cannot execute', () => {
for (const text of [
'<function name="bash"><param name="command">ls',
'<function name="missing"></function>',
'<function name="bash"><param name="command"><x/></param></function>',
'<function name="bash"><param name="command">ls</param><param name="command">pwd</param></function>',
'<function name="bash"><param name="__proto__">x</param></function>',
'<function name="bash"><param name="command">ls</function>',
]) assert.throws(() => parseCompletion(text, tools), text);
});
test('thinking never becomes executable and tool history is serialized with names', () => {
const blocks = parseCompletion('<think><function name="bash"><param name="command">rm -r /</param></function></think>Done<|im_end|>', tools);
assert.equal(blocks[0].type, 'thinking');
assert.deepEqual(blocks.slice(1), [{ type: 'text', text: 'Done' }]);
const messages = toMiniMessages({ systemPrompt: 'Pi', messages: [
{ role: 'assistant', content: [{ type: 'toolCall', name: 'read', arguments: { path: 'x' } }] },
{ role: 'toolResult', toolName: 'read', content: [{ type: 'text', text: 'abc' }], isError: false },
] });
assert.equal(messages[1].tool_calls[0].function.arguments.path, 'x');
assert.deepEqual(JSON.parse(messages[2].content), { name: 'read', isError: false, output: 'abc' });
});
test('prefilled thinking is separated, retained across user turns, and never executed', () => {
const reasoning = '<function name="bash"><param name="command">rm -r /</param></function>';
const content = parseCompletion(reasoning + '\n</think>\n\nDone<|im_end|>', tools, { thinkingPrefilled: true });
assert.deepEqual(content, [{ type: 'thinking', thinking: reasoning }, { type: 'text', text: 'Done' }]);
assert.throws(() => parseCompletion(reasoning, tools, { thinkingPrefilled: true }), /before finishing/);
assert.equal(splitThinking('Let me inspect</thi', { thinkingPrefilled: true }).complete, false);
const messages = toMiniMessages({ systemPrompt: 'Pi', messages: [
{ role: 'user', content: 'old' }, { role: 'assistant', content }, { role: 'user', content: 'new' },
] });
assert.equal(messages[2].reasoning_content, reasoning);
assert.equal(messages[2].content, 'Done');
assert.equal(messages[2].tool_calls.length, 0);
});
test('literal thinking tags in code and tool data are preserved', () => {
const literal = '<think>example</think>';
const raw = 'Ready\n</think>\n<function name="write"><param name="path">x</param><param name="content"><![CDATA[' + literal + ']]></param></function>';
const blocks = parseCompletion(raw, tools, { thinkingPrefilled: true });
assert.equal(blocks[1].arguments.content, literal);
assert.deepEqual(parseCompletion('```html\n' + literal + '\n```', tools), [{ type: 'text', text: '```html\n' + literal + '\n```' }]);
});
test('assistant history keeps text/tool order and excludes interrupted thoughts', () => {
const content = [{ type: 'thinking', thinking: 'Inspect both.' }, { type: 'text', text: 'First.\n' },
{ type: 'toolCall', name: 'read', arguments: { path: 'a' } }, { type: 'text', text: '\nSecond.\n' },
{ type: 'toolCall', name: 'read', arguments: { path: 'b' } }];
const messages = toMiniMessages({ systemPrompt: 'Pi', messages: [
{ role: 'assistant', content }, { role: 'assistant', content, stopReason: 'aborted' },
{ role: 'assistant', content, stopReason: 'error' },
] });
assert.equal(messages.length, 2);
assert.equal(messages[1].content, 'First.\n<tool_sep>\nSecond.\n<tool_sep>');
assert.deepEqual(messages[1].tool_calls.map(c => c.function.arguments.path), ['a', 'b']);
});
test('context trimming drops complete older user turns and rejects oversized current turn', () => {
const context = { systemPrompt: '', messages: [
{ role: 'user', content: 'old' }, { role: 'assistant', content: [] },
{ role: 'toolResult', content: [], toolName: 'read' }, { role: 'user', content: 'new' },
] };
const result = fitContext(context, messages => ({ input_ids: { dims: [1, messages.length] } }), 2);
assert.equal(result.dropped, 3);
assert.throws(() => fitContext(context, () => ({ input_ids: { dims: [1, 99] } }), 2), /exceeds/);
});
test('real just-bash executes pipelines and scripts; filesystem survives reload snapshot', async () => {
const workspace = createWorkspace();
const total = "awk 'NR==FNR {price[$1]=$2; next} FNR>1 {split($0,row,\",\"); if (!(row[1] in price)) exit 1; sum += row[2]*price[row[1]]} END {print sum}' <(jq -r '.[] | [.product, .price] | @tsv' products.json) sales.csv";
let result = await workspace.bash.exec(total + ' > total.txt; cat total.txt');
assert.equal(result.stdout.trim(), '144'); assert.equal(result.exitCode, 0);
const products = JSON.parse(await workspace.read('products.json')); products[0].price = 6;
await workspace.write('products.json', JSON.stringify(products));
assert.equal((await workspace.bash.exec(total)).stdout.trim(), '156');
await workspace.write('sales.csv', (await workspace.read('sales.csv')).replace('Notebook,12', 'Notebook,13'));
assert.equal((await workspace.bash.exec(total)).stdout.trim(), '162');
await workspace.tools.find(t => t.name === 'write').execute('1', { path: 'hello.sh', content: 'printf "Hello from the browser\\n"' });
result = await workspace.bash.exec('bash hello.sh'); assert.equal(result.stdout.trim(), 'Hello from the browser');
await workspace.tools.find(t => t.name === 'edit').execute('2', { path: 'hello.sh', oldText: 'Hello', newText: '$& literal' });
assert.match(await workspace.read('hello.sh'), /\$& literal/);
const restored = createWorkspace(await workspace.snapshot()); assert.equal(await restored.read('total.txt'), '144\n');
const denied = await workspace.bash.exec('curl http://example.com');
assert.notEqual(denied.exitCode, 0); assert.match(denied.stderr, /Only HTTPS/);
assert.equal((await workspace.bash.exec('node -v')).exitCode, 127);
});
test('workspace persistence preserves executable modes, empty directories, symlinks and bytes', async () => {
const w = createWorkspace();
await w.bash.exec('mkdir empty; printf "echo ok\\n" > run.sh; chmod +x run.sh; ln -s run.sh link.sh');
await w.bash.fs.writeFile('/workspace/bytes', new Uint8Array([0, 255, 128, 65]));
const restored = createWorkspace(undefined, await w.serialize());
await restored.ready;
assert.equal((await restored.bash.exec('./link.sh')).stdout, 'ok\n');
assert.equal((await restored.bash.fs.stat('/workspace/empty')).isDirectory, true);
assert.deepEqual(await restored.bash.fs.readFileBuffer('/workspace/bytes'), new Uint8Array([0, 255, 128, 65]));
});