834 lines
24 KiB
JavaScript
834 lines
24 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';
|
|
import { once } from 'node:events';
|
|
import http from 'node:http';
|
|
import { syncPublicDocxDownloads } from './mindspace-public-finish-sync.mjs';
|
|
|
|
function copyDocxGenerateSkill(root) {
|
|
const srcDir = path.join(process.cwd(), 'skills', 'docx-generate');
|
|
const destDir = path.join(root, '.agents', 'skills', 'docx-generate');
|
|
fs.mkdirSync(destDir, { recursive: true });
|
|
for (const name of ['generate_docx.py', 'SKILL.md']) {
|
|
fs.copyFileSync(path.join(srcDir, name), path.join(destDir, name));
|
|
}
|
|
}
|
|
|
|
function summarySections(caseName) {
|
|
return [
|
|
{
|
|
heading: '一、项目概述',
|
|
paragraphs: [
|
|
`${caseName}项目面向医疗服务流程中的核心痛点,通过数字化与 AI 能力重构诊前、诊中、诊后体验。`,
|
|
'本摘要基于创新三角评估框架,从创新、成果、市场、团队四个维度提炼关键结论。',
|
|
],
|
|
},
|
|
{
|
|
heading: '二、解决方案与技术创新',
|
|
paragraphs: [
|
|
'项目以数据驱动和智能辅助决策为主线,形成可复制的技术体系与流程再造方案。',
|
|
'在技术创新、商业模式与政策吻合度方面均具备行业示范价值。',
|
|
],
|
|
},
|
|
{
|
|
heading: '三、成果与价值',
|
|
paragraphs: [
|
|
'核心指标显示候诊效率、临床质量或安全水平获得显著提升,患者与医护双侧受益。',
|
|
'项目已具备向医联体与区域平台输出的标准化能力。',
|
|
],
|
|
},
|
|
{
|
|
heading: '四、壁垒、风险与未来展望',
|
|
paragraphs: [
|
|
'竞争壁垒来自临床数据积累、流程嵌入深度与专科Know-how。',
|
|
'未来可在专科深化、基层赋能与支付模式创新方向持续扩展。',
|
|
],
|
|
},
|
|
];
|
|
}
|
|
|
|
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('generate_image calls the authenticated Memind internal route and returns MindSpace asset data', async (t) => {
|
|
const requests = [];
|
|
const api = http.createServer((req, res) => {
|
|
let body = '';
|
|
req.setEncoding('utf8');
|
|
req.on('data', (chunk) => { body += chunk; });
|
|
req.on('end', () => {
|
|
requests.push({ url: req.url, authorization: req.headers.authorization, body: JSON.parse(body) });
|
|
res.setHeader('content-type', 'application/json');
|
|
res.end(JSON.stringify({
|
|
data: {
|
|
ok: true,
|
|
purpose: 'hero',
|
|
asset: {
|
|
id: 'asset-1',
|
|
publicUrl: 'https://m.example/image.webp',
|
|
workspaceRelativePath: 'public/images/2026-07-19/image.webp',
|
|
},
|
|
},
|
|
}));
|
|
});
|
|
});
|
|
api.listen(0, '127.0.0.1');
|
|
await once(api, 'listening');
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mindspace-sandbox-generate-image-'));
|
|
const sandbox = startSandbox(root, {
|
|
ALLOWED_TOOLS: 'generate_image',
|
|
PRIVATE_DATA_USER_ID: 'user-1',
|
|
MINDSPACE_AGENT_API_BASE_URL: `http://127.0.0.1:${api.address().port}/api`,
|
|
MINDSPACE_INTERNAL_AGENT_SECRET: 'internal-secret',
|
|
});
|
|
t.after(() => sandbox.child.kill());
|
|
t.after(() => api.close());
|
|
|
|
await sandbox.request('initialize');
|
|
const called = await sandbox.request('tools/call', {
|
|
name: 'generate_image',
|
|
arguments: { purpose: 'hero', prompt: 'calm reading room', idempotency_key: 'imgreq-1' },
|
|
});
|
|
assert.equal(called.result.isError, false);
|
|
const result = JSON.parse(called.result.content[0].text);
|
|
assert.equal(result.asset.id, 'asset-1');
|
|
assert.deepEqual(requests, [{
|
|
url: '/api/agent/mindspace_image_generate',
|
|
authorization: 'Bearer internal-secret',
|
|
body: {
|
|
user_id: 'user-1',
|
|
purpose: 'hero',
|
|
prompt: 'calm reading room',
|
|
negative_prompt: '',
|
|
},
|
|
}]);
|
|
});
|
|
|
|
test('sandbox MCP rejects browser storage in generated page code', async (t) => {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mindspace-sandbox-storage-ban-'));
|
|
const server = startSandbox(root, { ALLOWED_TOOLS: 'write_file,edit_file' });
|
|
t.after(() => server.child.kill());
|
|
|
|
await server.request('initialize');
|
|
const rejectedWrite = await server.request('tools/call', {
|
|
name: 'write_file',
|
|
arguments: {
|
|
path: 'public/ledger.html',
|
|
content: '<script>localStorage.setItem("ledger", "[]")</script>',
|
|
},
|
|
});
|
|
assert.equal(rejectedWrite.result.isError, true);
|
|
assert.match(rejectedWrite.result.content[0].text, /BROWSER_STORAGE_FORBIDDEN|禁止使用 localStorage/);
|
|
assert.equal(fs.existsSync(path.join(root, 'public', 'ledger.html')), false);
|
|
|
|
const acceptedWrite = await server.request('tools/call', {
|
|
name: 'write_file',
|
|
arguments: { path: 'public/ledger.html', content: '<script>client.insertRow("ledger", row)</script>' },
|
|
});
|
|
assert.equal(acceptedWrite.result.isError, false);
|
|
|
|
const rejectedEdit = await server.request('tools/call', {
|
|
name: 'edit_file',
|
|
arguments: {
|
|
path: 'public/ledger.html',
|
|
old_str: 'client.insertRow("ledger", row)',
|
|
new_str: 'indexedDB.open("ledger")',
|
|
},
|
|
});
|
|
assert.equal(rejectedEdit.result.isError, true);
|
|
assert.doesNotMatch(fs.readFileSync(path.join(root, 'public', 'ledger.html'), 'utf8'), /indexedDB/);
|
|
});
|
|
|
|
test('sandbox MCP delegates scoped workspace tools to MindSpace without local file writes', async (t) => {
|
|
const requests = [];
|
|
const api = http.createServer((req, res) => {
|
|
let body = '';
|
|
req.setEncoding('utf8');
|
|
req.on('data', (chunk) => {
|
|
body += chunk;
|
|
});
|
|
req.on('end', () => {
|
|
const parsedBody = JSON.parse(body);
|
|
requests.push({
|
|
url: req.url,
|
|
authorization:
|
|
req.headers.authorization,
|
|
body: parsedBody,
|
|
});
|
|
const tool = req.url.split('/').at(-1);
|
|
if (
|
|
tool === 'generate_long_image' &&
|
|
parsedBody.arguments
|
|
?.output_path ===
|
|
'public/fail.long.png'
|
|
) {
|
|
res.statusCode = 503;
|
|
res.setHeader(
|
|
'content-type',
|
|
'application/json',
|
|
);
|
|
res.end(
|
|
JSON.stringify({
|
|
ok: false,
|
|
code:
|
|
'long_image_unavailable',
|
|
}),
|
|
);
|
|
return;
|
|
}
|
|
const results = {
|
|
write_file: {
|
|
relativePath: 'public/page.html',
|
|
packageId: 'cp_session-1',
|
|
sizeBytes: 20,
|
|
},
|
|
read_file: {
|
|
relativePath: 'public/page.html',
|
|
packageId: 'cp_session-1',
|
|
content: '<html>remote</html>',
|
|
},
|
|
publish_page: {
|
|
relativePath: 'public/page.html',
|
|
packageId: 'cp_session-1',
|
|
canonicalUrl:
|
|
'https://example.com/MindSpace/user-1/public/page.html',
|
|
},
|
|
generate_long_image: {
|
|
relativePath:
|
|
'public/page.long.png',
|
|
packageId: 'cp_session-1',
|
|
sizeBytes: 4096,
|
|
canonicalUrl:
|
|
'https://example.com/MindSpace/user-1/public/page.long.png',
|
|
},
|
|
};
|
|
res.setHeader(
|
|
'content-type',
|
|
'application/json',
|
|
);
|
|
res.end(
|
|
JSON.stringify({
|
|
ok: true,
|
|
result: results[tool] ?? {},
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
api.listen(0, '127.0.0.1');
|
|
await once(api, 'listening');
|
|
const root = fs.mkdtempSync(
|
|
path.join(
|
|
os.tmpdir(),
|
|
'mindspace-sandbox-rpc-',
|
|
),
|
|
);
|
|
const sandbox = startSandbox(root, {
|
|
ALLOWED_TOOLS:
|
|
'read_file,write_file,publish_page,generate_long_image',
|
|
MINDSPACE_MCP_BASE_URL:
|
|
`http://127.0.0.1:${api.address().port}`,
|
|
MINDSPACE_MCP_SCOPED_TOKEN:
|
|
'scoped-token',
|
|
MINDSPACE_WORKSPACE_REF:
|
|
'mindspace://users/user-1/workspace',
|
|
MINDSPACE_SESSION_ID: 'session-1',
|
|
MINDSPACE_PACKAGE_ID: 'cp_session-1',
|
|
});
|
|
t.after(() => sandbox.child.kill());
|
|
t.after(() => api.close());
|
|
|
|
await sandbox.request('initialize');
|
|
const written = await sandbox.request(
|
|
'tools/call',
|
|
{
|
|
name: 'write_file',
|
|
arguments: {
|
|
path: 'public/page.html',
|
|
content: '<html>remote</html>',
|
|
},
|
|
},
|
|
);
|
|
const read = await sandbox.request(
|
|
'tools/call',
|
|
{
|
|
name: 'read_file',
|
|
arguments: {
|
|
path: 'public/page.html',
|
|
},
|
|
},
|
|
);
|
|
const published = await sandbox.request(
|
|
'tools/call',
|
|
{
|
|
name: 'publish_page',
|
|
arguments: {
|
|
path: 'public/page.html',
|
|
},
|
|
},
|
|
);
|
|
const longImage = await sandbox.request(
|
|
'tools/call',
|
|
{
|
|
name: 'generate_long_image',
|
|
arguments: {
|
|
html_path: 'public/page.html',
|
|
output_path:
|
|
'public/page.long.png',
|
|
},
|
|
},
|
|
);
|
|
const failedLongImage =
|
|
await sandbox.request(
|
|
'tools/call',
|
|
{
|
|
name: 'generate_long_image',
|
|
arguments: {
|
|
html_path:
|
|
'public/page.html',
|
|
output_path:
|
|
'public/fail.long.png',
|
|
},
|
|
},
|
|
);
|
|
|
|
assert.equal(written.result.isError, false);
|
|
assert.match(
|
|
written.result.content[0].text,
|
|
/package cp_session-1/,
|
|
);
|
|
assert.equal(
|
|
read.result.content[0].text,
|
|
'<html>remote</html>',
|
|
);
|
|
assert.match(
|
|
published.result.content[0].text,
|
|
/canonicalUrl/,
|
|
);
|
|
assert.match(
|
|
longImage.result.content[0].text,
|
|
/page\.long\.png/,
|
|
);
|
|
assert.equal(
|
|
failedLongImage.result.isError,
|
|
true,
|
|
);
|
|
assert.equal(
|
|
fs.existsSync(
|
|
path.join(root, 'public', 'page.html'),
|
|
),
|
|
false,
|
|
);
|
|
assert.equal(
|
|
fs.existsSync(
|
|
path.join(
|
|
root,
|
|
'public',
|
|
'page.long.png',
|
|
),
|
|
),
|
|
false,
|
|
);
|
|
assert.equal(
|
|
fs.existsSync(
|
|
path.join(
|
|
root,
|
|
'public',
|
|
'fail.long.png',
|
|
),
|
|
),
|
|
false,
|
|
);
|
|
assert.deepEqual(
|
|
requests.map((request) => request.url),
|
|
[
|
|
'/mindspace/v1/mcp/write_file',
|
|
'/mindspace/v1/mcp/read_file',
|
|
'/mindspace/v1/mcp/publish_page',
|
|
'/mindspace/v1/mcp/generate_long_image',
|
|
'/mindspace/v1/mcp/generate_long_image',
|
|
],
|
|
);
|
|
assert.ok(
|
|
requests.every(
|
|
(request) =>
|
|
request.authorization ===
|
|
'Bearer scoped-token',
|
|
),
|
|
);
|
|
assert.deepEqual(requests[0].body, {
|
|
arguments: {
|
|
path: 'public/page.html',
|
|
content: '<html>remote</html>',
|
|
},
|
|
});
|
|
});
|
|
|
|
test('generate_docx uploads its binary through scoped MindSpace RPC without writing the target workspace path', async (t) => {
|
|
const requests = [];
|
|
const api = http.createServer((req, res) => {
|
|
let body = '';
|
|
req.setEncoding('utf8');
|
|
req.on('data', (chunk) => {
|
|
body += chunk;
|
|
});
|
|
req.on('end', () => {
|
|
requests.push({
|
|
url: req.url,
|
|
authorization:
|
|
req.headers.authorization,
|
|
body: JSON.parse(body),
|
|
});
|
|
res.setHeader(
|
|
'content-type',
|
|
'application/json',
|
|
);
|
|
res.end(
|
|
JSON.stringify({
|
|
ok: true,
|
|
result: {
|
|
relativePath:
|
|
'public/report.docx',
|
|
packageId:
|
|
'cp_session-1',
|
|
sizeBytes: 1024,
|
|
},
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
api.listen(0, '127.0.0.1');
|
|
await once(api, 'listening');
|
|
const root = fs.mkdtempSync(
|
|
path.join(
|
|
os.tmpdir(),
|
|
'mindspace-sandbox-docx-rpc-',
|
|
),
|
|
);
|
|
copyDocxGenerateSkill(root);
|
|
const sandbox = startSandbox(root, {
|
|
ALLOWED_TOOLS: 'generate_docx',
|
|
MINDSPACE_MCP_BASE_URL:
|
|
`http://127.0.0.1:${api.address().port}`,
|
|
MINDSPACE_MCP_SCOPED_TOKEN:
|
|
'scoped-token',
|
|
MINDSPACE_WORKSPACE_REF:
|
|
'mindspace://users/user-1/workspace',
|
|
MINDSPACE_SESSION_ID: 'session-1',
|
|
MINDSPACE_PACKAGE_ID: 'cp_session-1',
|
|
});
|
|
t.after(() => sandbox.child.kill());
|
|
t.after(() => api.close());
|
|
|
|
await sandbox.request('initialize');
|
|
const generated = await sandbox.request(
|
|
'tools/call',
|
|
{
|
|
name: 'generate_docx',
|
|
arguments: {
|
|
output_path:
|
|
'public/report.docx',
|
|
title: 'Scoped report',
|
|
sections:
|
|
summarySections('Scoped report'),
|
|
},
|
|
},
|
|
);
|
|
|
|
assert.equal(
|
|
generated.result.isError,
|
|
false,
|
|
generated.result.content?.[0]?.text,
|
|
);
|
|
assert.equal(
|
|
fs.existsSync(
|
|
path.join(
|
|
root,
|
|
'public',
|
|
'report.docx',
|
|
),
|
|
),
|
|
false,
|
|
);
|
|
assert.equal(
|
|
requests[0].url,
|
|
'/mindspace/v1/mcp/write_binary_file',
|
|
);
|
|
assert.equal(
|
|
requests[0].authorization,
|
|
'Bearer scoped-token',
|
|
);
|
|
assert.equal(
|
|
requests[0].body.arguments.path,
|
|
'public/report.docx',
|
|
);
|
|
const uploaded = Buffer.from(
|
|
requests[0].body.arguments
|
|
.bodyBase64,
|
|
'base64',
|
|
);
|
|
assert.equal(
|
|
uploaded.subarray(0, 2).toString(
|
|
'utf8',
|
|
),
|
|
'PK',
|
|
);
|
|
assert.ok(uploaded.length > 500);
|
|
});
|
|
|
|
test('scoped DOCX generation fails closed when MindSpace rejects the binary write', async (t) => {
|
|
const api = http.createServer(
|
|
(req, res) => {
|
|
req.resume();
|
|
req.on('end', () => {
|
|
res.statusCode = 503;
|
|
res.setHeader(
|
|
'content-type',
|
|
'application/json',
|
|
);
|
|
res.end(
|
|
JSON.stringify({
|
|
ok: false,
|
|
code:
|
|
'mindspace_unavailable',
|
|
}),
|
|
);
|
|
});
|
|
},
|
|
);
|
|
api.listen(0, '127.0.0.1');
|
|
await once(api, 'listening');
|
|
const root = fs.mkdtempSync(
|
|
path.join(
|
|
os.tmpdir(),
|
|
'mindspace-sandbox-docx-fail-',
|
|
),
|
|
);
|
|
copyDocxGenerateSkill(root);
|
|
const sandbox = startSandbox(root, {
|
|
ALLOWED_TOOLS: 'generate_docx',
|
|
MINDSPACE_MCP_BASE_URL:
|
|
`http://127.0.0.1:${api.address().port}`,
|
|
MINDSPACE_MCP_SCOPED_TOKEN:
|
|
'scoped-token',
|
|
MINDSPACE_WORKSPACE_REF:
|
|
'mindspace://users/user-1/workspace',
|
|
MINDSPACE_SESSION_ID: 'session-1',
|
|
MINDSPACE_PACKAGE_ID: 'cp_session-1',
|
|
});
|
|
t.after(() => sandbox.child.kill());
|
|
t.after(() => api.close());
|
|
|
|
await sandbox.request('initialize');
|
|
const generated = await sandbox.request(
|
|
'tools/call',
|
|
{
|
|
name: 'generate_docx',
|
|
arguments: {
|
|
output_path:
|
|
'public/report.docx',
|
|
title: 'Rejected report',
|
|
sections:
|
|
summarySections(
|
|
'Rejected report',
|
|
),
|
|
},
|
|
},
|
|
);
|
|
|
|
assert.equal(
|
|
generated.result.isError,
|
|
true,
|
|
);
|
|
assert.match(
|
|
generated.result.content[0].text,
|
|
/mindspace_unavailable/,
|
|
);
|
|
assert.equal(
|
|
fs.existsSync(
|
|
path.join(
|
|
root,
|
|
'public',
|
|
'report.docx',
|
|
),
|
|
),
|
|
false,
|
|
);
|
|
});
|
|
|
|
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 registers datasets and page policies for Page Data API', async (t) => {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mindspace-sandbox-page-data-'));
|
|
const server = startSandbox(root, {
|
|
ALLOWED_TOOLS: 'private_data_execute,private_data_register_dataset,private_data_set_page_policy,private_data_close_page_dataset',
|
|
PRIVATE_DATA_USER_ID: 'user-1',
|
|
});
|
|
t.after(() => server.child.kill());
|
|
|
|
await server.request('initialize');
|
|
|
|
const create = await server.request('tools/call', {
|
|
name: 'private_data_execute',
|
|
arguments: {
|
|
sql: `CREATE TABLE signups (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL,
|
|
phone TEXT
|
|
);`,
|
|
},
|
|
});
|
|
assert.equal(create.result.isError, false);
|
|
|
|
const register = await server.request('tools/call', {
|
|
name: 'private_data_register_dataset',
|
|
arguments: {
|
|
name: 'signups',
|
|
table: 'signups',
|
|
actions: ['read', 'insert'],
|
|
columns: {
|
|
read: ['id', 'name', 'phone'],
|
|
insert: ['name', 'phone'],
|
|
},
|
|
},
|
|
});
|
|
assert.equal(register.result.isError, false);
|
|
assert.match(register.result.content[0].text, /"name": "signups"/);
|
|
|
|
const policy = await server.request('tools/call', {
|
|
name: 'private_data_set_page_policy',
|
|
arguments: {
|
|
pageId: 'page-1',
|
|
accessMode: 'public',
|
|
datasets: {
|
|
signups: {
|
|
insert: true,
|
|
columns: { insert: ['name', 'phone'] },
|
|
},
|
|
},
|
|
},
|
|
});
|
|
assert.equal(policy.result.isError, false);
|
|
assert.equal(
|
|
fs.existsSync(path.join(root, '.mindspace', 'page-data-policies', 'page-1.json')),
|
|
true,
|
|
);
|
|
const saved = JSON.parse(fs.readFileSync(path.join(root, '.mindspace', 'page-data-policies', 'page-1.json'), 'utf8'));
|
|
assert.equal(saved.pageId, 'page-1');
|
|
assert.equal(saved.datasets.signups.insert, true);
|
|
|
|
const fictional = await server.request('tools/call', {
|
|
name: 'private_data_set_page_policy',
|
|
arguments: {
|
|
pageId: 'page-fictional',
|
|
accessMode: 'password',
|
|
datasets: {
|
|
split_crud_uat_20260724: {
|
|
read: true,
|
|
insert: true,
|
|
columns: { read: ['id'], insert: ['title'] },
|
|
},
|
|
},
|
|
},
|
|
});
|
|
assert.equal(fictional.result.isError, true);
|
|
assert.match(fictional.result.content[0].text, /dataset 未注册/);
|
|
assert.equal(
|
|
fs.existsSync(path.join(root, '.mindspace', 'page-data-policies', 'page-fictional.json')),
|
|
false,
|
|
);
|
|
|
|
const wrongOwner = await server.request('tools/call', {
|
|
name: 'private_data_set_page_policy',
|
|
arguments: {
|
|
pageId: 'page-wrong-owner',
|
|
ownerUserId: 'user-2',
|
|
accessMode: 'public',
|
|
datasets: {
|
|
signups: {
|
|
insert: true,
|
|
columns: { insert: ['name'] },
|
|
},
|
|
},
|
|
},
|
|
});
|
|
assert.equal(wrongOwner.result.isError, true);
|
|
assert.match(wrongOwner.result.content[0].text, /当前会话用户/);
|
|
|
|
const close = await server.request('tools/call', {
|
|
name: 'private_data_close_page_dataset',
|
|
arguments: { pageId: 'page-1', dataset: 'signups' },
|
|
});
|
|
assert.equal(close.result.isError, false);
|
|
const closed = JSON.parse(close.result.content[0].text);
|
|
assert.equal(closed.closed, true);
|
|
assert.equal(closed.insert, false);
|
|
});
|
|
|
|
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'],
|
|
);
|
|
});
|
|
|
|
test('generate_docx writes public Word files for Mark-style summary request', async (t) => {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mindspace-sandbox-docx-'));
|
|
fs.mkdirSync(path.join(root, 'public'), { recursive: true });
|
|
copyDocxGenerateSkill(root);
|
|
|
|
const server = startSandbox(root, {
|
|
ALLOWED_TOOLS: 'generate_docx,write_file,list_dir',
|
|
});
|
|
t.after(() => server.child.kill());
|
|
|
|
await server.request('initialize');
|
|
const listed = await server.request('tools/list');
|
|
assert.ok(listed.result.tools.some((tool) => tool.name === 'generate_docx'));
|
|
|
|
const cases = [
|
|
{
|
|
docx: 'public/协和智慧门诊研究摘要.docx',
|
|
html: 'public/协和智慧门诊研究摘要.html',
|
|
title: '北京协和医院「智慧门诊 + AI 预问诊」研究摘要',
|
|
},
|
|
{
|
|
docx: 'public/广州妇儿CDSS研究摘要.docx',
|
|
html: 'public/广州妇儿CDSS研究摘要.html',
|
|
title: '广州妇儿中心「数据驱动 + AI 临床决策」研究摘要',
|
|
},
|
|
];
|
|
|
|
for (const item of cases) {
|
|
const generated = await server.request('tools/call', {
|
|
name: 'generate_docx',
|
|
arguments: {
|
|
output_path: item.docx,
|
|
title: item.title,
|
|
sections: summarySections(item.title),
|
|
},
|
|
});
|
|
assert.equal(generated.result.isError, false, generated.result.content?.[0]?.text);
|
|
assert.match(generated.result.content[0].text, /已生成 public\/.+\.docx/);
|
|
assert.ok(fs.statSync(path.join(root, item.docx)).size > 500);
|
|
}
|
|
|
|
for (const item of cases) {
|
|
const docxName = path.basename(item.docx);
|
|
await server.request('tools/call', {
|
|
name: 'write_file',
|
|
arguments: {
|
|
path: item.html,
|
|
content: `<!doctype html><html><body><a href="${docxName}" download>下载 Word</a></body></html>`,
|
|
},
|
|
});
|
|
}
|
|
|
|
const listedPublic = await server.request('tools/call', {
|
|
name: 'list_dir',
|
|
arguments: { path: 'public' },
|
|
});
|
|
assert.equal(listedPublic.result.isError, false);
|
|
assert.match(listedPublic.result.content[0].text, /协和智慧门诊研究摘要\.docx/);
|
|
assert.match(listedPublic.result.content[0].text, /广州妇儿CDSS研究摘要\.docx/);
|
|
|
|
const sync = syncPublicDocxDownloads({ publishDir: root });
|
|
assert.deepEqual(sync.missing, []);
|
|
assert.deepEqual(sync.synced, []);
|
|
|
|
for (const item of cases) {
|
|
const abs = path.join(root, item.docx);
|
|
assert.ok(fs.existsSync(abs));
|
|
const zipHeader = fs.readFileSync(abs).subarray(0, 2).toString('utf8');
|
|
assert.equal(zipHeader, 'PK', `${item.docx} should be a valid zip/docx`);
|
|
}
|
|
});
|