File size: 9,101 Bytes
39371ea 9c380c2 39371ea | 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 | 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\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&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]));
});
|