9b4a25799f
Replace fixed ackText with a rule-based AckProvider that picks response templates by message type and intent (translate, summary, rewrite, poster, ppt, mindmap, code, search, schedule). Pure sync, zero I/O, auto-falls back to config.ackText on any error. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
105 lines
3.7 KiB
JavaScript
105 lines
3.7 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { spawn } from 'node:child_process';
|
|
|
|
function startSandbox(root, envOverrides = {}) {
|
|
const child = spawn(process.execPath, ['mindspace-sandbox-mcp.mjs', root], {
|
|
cwd: process.cwd(),
|
|
env: {
|
|
...process.env,
|
|
ALLOWED_TOOLS:
|
|
'private_data_info,private_data_schema,private_data_query,private_data_execute',
|
|
PRIVATE_DATA_MAX_BYTES: String(1024 * 1024),
|
|
...envOverrides,
|
|
},
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
});
|
|
const pending = new Map();
|
|
let nextId = 1;
|
|
let buffer = '';
|
|
child.stdout.setEncoding('utf8');
|
|
child.stdout.on('data', (chunk) => {
|
|
buffer += chunk;
|
|
let idx;
|
|
while ((idx = buffer.indexOf('\n')) >= 0) {
|
|
const line = buffer.slice(0, idx);
|
|
buffer = buffer.slice(idx + 1);
|
|
if (!line.trim()) continue;
|
|
const msg = JSON.parse(line);
|
|
pending.get(msg.id)?.(msg);
|
|
pending.delete(msg.id);
|
|
}
|
|
});
|
|
const request = (method, params = {}) =>
|
|
new Promise((resolve) => {
|
|
const id = nextId++;
|
|
pending.set(id, resolve);
|
|
child.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n');
|
|
});
|
|
return { child, request };
|
|
}
|
|
|
|
test('sandbox MCP exposes and protects the user private data space', async (t) => {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mindspace-sandbox-'));
|
|
const server = startSandbox(root);
|
|
t.after(() => server.child.kill());
|
|
|
|
await server.request('initialize');
|
|
const listed = await server.request('tools/list');
|
|
assert.deepEqual(
|
|
listed.result.tools.map((tool) => tool.name),
|
|
['private_data_info', 'private_data_schema', 'private_data_query', 'private_data_execute'],
|
|
);
|
|
|
|
const create = await server.request('tools/call', {
|
|
name: 'private_data_execute',
|
|
arguments: {
|
|
sql: `CREATE TABLE surveys (id INTEGER PRIMARY KEY, title TEXT NOT NULL);
|
|
INSERT INTO surveys (title) VALUES ('满意度调查');`,
|
|
},
|
|
});
|
|
assert.equal(create.result.isError, false);
|
|
|
|
const query = await server.request('tools/call', {
|
|
name: 'private_data_query',
|
|
arguments: { sql: 'SELECT id, title FROM surveys ORDER BY id' },
|
|
});
|
|
assert.equal(query.result.isError, false);
|
|
assert.deepEqual(JSON.parse(query.result.content[0].text), [{ id: 1, title: '满意度调查' }]);
|
|
assert.equal(fs.existsSync(path.join(root, '.mindspace', 'private-data.sqlite')), true);
|
|
|
|
const rejected = await server.request('tools/call', {
|
|
name: 'private_data_query',
|
|
arguments: { sql: "ATTACH DATABASE '/tmp/other.sqlite' AS other" },
|
|
});
|
|
assert.equal(rejected.result.isError, true);
|
|
assert.match(rejected.result.content[0].text, /不允许|只允许/);
|
|
|
|
const dotCommand = await server.request('tools/call', {
|
|
name: 'private_data_execute',
|
|
arguments: { sql: `.open /tmp/other.sqlite` },
|
|
});
|
|
assert.equal(dotCommand.result.isError, true);
|
|
assert.match(dotCommand.result.content[0].text, /dot command/);
|
|
});
|
|
|
|
test('sandbox MCP exposes schedule tools only when schedule env is configured', async (t) => {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mindspace-sandbox-schedule-'));
|
|
const server = startSandbox(root, {
|
|
ALLOWED_TOOLS: 'schedule_create_item,schedule_create_reminder,schedule_list_items',
|
|
PRIVATE_DATA_USER_ID: 'user-1',
|
|
DATABASE_URL: 'mysql://boot:pass@127.0.0.1:3306/test',
|
|
});
|
|
t.after(() => server.child.kill());
|
|
|
|
await server.request('initialize');
|
|
const listed = await server.request('tools/list');
|
|
assert.deepEqual(
|
|
listed.result.tools.map((tool) => tool.name),
|
|
['schedule_create_item', 'schedule_create_reminder', 'schedule_list_items'],
|
|
);
|
|
});
|