4e21ca937a
Deploy Documentation / deploy (push) Has been cancelled
Canary / Prepare Version (push) Has been cancelled
Canary / build-cli (push) Has been cancelled
Canary / Upload Install Script (push) Has been cancelled
Canary / bundle-desktop (push) Has been cancelled
Canary / bundle-desktop-intel (push) Has been cancelled
Canary / bundle-desktop-linux (push) Has been cancelled
Canary / bundle-desktop-windows (push) Has been cancelled
Canary / bundle-desktop-windows-cuda (push) Has been cancelled
Canary / Release (push) Has been cancelled
Unused Dependencies / machete (push) Has been cancelled
CI / changes (push) Has been cancelled
CI / Check Rust Code Format (push) Has been cancelled
CI / Build and Test Rust Project (push) Has been cancelled
CI / Build Rust Project on Windows (push) Has been cancelled
CI / Check MSRV (push) Has been cancelled
CI / Lint Rust Code (push) Has been cancelled
CI / Check Generated Schemas are Up-to-Date (push) Has been cancelled
CI / Test and Lint Electron Desktop App (push) Has been cancelled
CI / H5 Plaza Tests and Build (push) Has been cancelled
Live Provider Tests / check-fork (push) Has been cancelled
Live Provider Tests / changes (push) Has been cancelled
Live Provider Tests / Build Binary (push) Has been cancelled
Live Provider Tests / Smoke Tests (push) Has been cancelled
Live Provider Tests / Smoke Tests (Code Execution) (push) Has been cancelled
Live Provider Tests / Compaction Tests (push) Has been cancelled
Live Provider Tests / goose server HTTP integration tests (push) Has been cancelled
Publish Ask AI Bot Docker Image / docker (push) Has been cancelled
Publish Docker Image / docker (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
Fork goose with custom MCP widgets, platform extensions (aider, git, web, search), MindSpace H5 backend/frontend, Plaza/Ops UIs, and deploy scripts for tkmind.cn. Co-authored-by: Cursor <cursoragent@cursor.com>
250 lines
8.0 KiB
JavaScript
250 lines
8.0 KiB
JavaScript
#!/usr/bin/env node
|
|
import assert from 'node:assert/strict';
|
|
import { spawn } from 'node:child_process';
|
|
import fs from 'node:fs/promises';
|
|
import http from 'node:http';
|
|
import path from 'node:path';
|
|
import { createDbPool } from '../db.mjs';
|
|
import { loadH5Environment } from './load-env.mjs';
|
|
|
|
loadH5Environment(import.meta.dirname);
|
|
|
|
const suffix = `${Date.now()}${Math.floor(Math.random() * 1000)}`;
|
|
const fakePort = Number(process.env.MINDSPACE_PAGE_FAKE_PORT ?? 18121);
|
|
const portalPort = Number(process.env.MINDSPACE_PAGE_PORT ?? 18122);
|
|
const baseUrl = `http://127.0.0.1:${portalPort}`;
|
|
const storageRoot = path.join('/tmp', `mindspace-pages-${suffix}`);
|
|
const sessionId = `page-e2e-session-${suffix}`;
|
|
const messageId = `assistant-message-${suffix}`;
|
|
const sourceContent = '# 安全页面\n\n这是来自 AI 会话的正文。\n\n<script>alert("blocked")</script>';
|
|
const users = [
|
|
{
|
|
username: `msp_owner_${suffix}`,
|
|
email: `msp-owner-${suffix}@example.test`,
|
|
password: 'MindSpace-Page-Owner-2026',
|
|
},
|
|
{
|
|
username: `msp_other_${suffix}`,
|
|
email: `msp-other-${suffix}@example.test`,
|
|
password: 'MindSpace-Page-Other-2026',
|
|
},
|
|
];
|
|
|
|
const fakeUpstream = http.createServer((req, res) => {
|
|
if (req.method === 'GET' && req.url === `/sessions/${sessionId}`) {
|
|
res.setHeader('Content-Type', 'application/json');
|
|
res.end(
|
|
JSON.stringify({
|
|
id: sessionId,
|
|
name: '页面 E2E 会话',
|
|
conversation: [
|
|
{
|
|
id: messageId,
|
|
role: 'assistant',
|
|
created: Math.floor(Date.now() / 1000),
|
|
metadata: { userVisible: true, agentVisible: true },
|
|
content: [{ type: 'text', text: sourceContent }],
|
|
},
|
|
],
|
|
}),
|
|
);
|
|
return;
|
|
}
|
|
res.statusCode = 404;
|
|
res.end(JSON.stringify({ message: 'not found' }));
|
|
});
|
|
|
|
await new Promise((resolve) => fakeUpstream.listen(fakePort, '127.0.0.1', resolve));
|
|
|
|
const portal = spawn(process.execPath, ['server.mjs'], {
|
|
cwd: path.join(import.meta.dirname, '..'),
|
|
env: {
|
|
...process.env,
|
|
H5_PORT: String(portalPort),
|
|
TKMIND_API_TARGET: `http://127.0.0.1:${fakePort}`,
|
|
MINDSPACE_STORAGE_ROOT: storageRoot,
|
|
},
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
});
|
|
let portalLogs = '';
|
|
portal.stdout.on('data', (chunk) => {
|
|
portalLogs += chunk.toString();
|
|
});
|
|
portal.stderr.on('data', (chunk) => {
|
|
portalLogs += chunk.toString();
|
|
});
|
|
|
|
async function waitForPortal() {
|
|
for (let attempt = 0; attempt < 60; attempt += 1) {
|
|
if (portal.exitCode != null) throw new Error(`Portal 提前退出\n${portalLogs}`);
|
|
try {
|
|
const response = await fetch(`${baseUrl}/auth/status`);
|
|
if (response.ok) return;
|
|
} catch {
|
|
// Portal is still starting.
|
|
}
|
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
}
|
|
throw new Error(`Portal 启动超时\n${portalLogs}`);
|
|
}
|
|
|
|
async function request(pathname, options = {}) {
|
|
const response = await fetch(`${baseUrl}${pathname}`, options);
|
|
const contentType = response.headers.get('content-type') ?? '';
|
|
const body = contentType.includes('application/json')
|
|
? await response.json()
|
|
: await response.text();
|
|
return { response, body };
|
|
}
|
|
|
|
async function registerAndLogin(user) {
|
|
const registration = await request('/auth/register', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ ...user, displayName: user.username }),
|
|
});
|
|
assert.equal(registration.response.status, 200, JSON.stringify(registration.body));
|
|
const login = await request('/auth/login', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ username: user.username, password: user.password }),
|
|
});
|
|
assert.equal(login.response.status, 200, JSON.stringify(login.body));
|
|
return {
|
|
id: registration.body.user.id,
|
|
cookie: login.response.headers.get('set-cookie')?.split(';', 1)[0],
|
|
};
|
|
}
|
|
|
|
const pool = createDbPool();
|
|
|
|
try {
|
|
await waitForPortal();
|
|
const owner = await registerAndLogin(users[0]);
|
|
const other = await registerAndLogin(users[1]);
|
|
assert.ok(owner.cookie && other.cookie);
|
|
|
|
await pool.query(
|
|
`INSERT INTO h5_user_sessions (agent_session_id, user_id, created_at) VALUES (?, ?, ?)`,
|
|
[sessionId, owner.id, Date.now()],
|
|
);
|
|
|
|
const created = await request('/api/mindspace/v1/pages/save-from-chat', {
|
|
method: 'POST',
|
|
headers: {
|
|
Cookie: owner.cookie,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
session_id: sessionId,
|
|
message_id: messageId,
|
|
title: 'AI 会话页面',
|
|
summary: '由会话生成的安全页面',
|
|
template_id: 'report',
|
|
}),
|
|
});
|
|
assert.equal(created.response.status, 201, JSON.stringify(created.body));
|
|
const pageId = created.body.data.page.id;
|
|
assert.equal(created.body.data.kind, 'page');
|
|
assert.equal(created.body.data.page.versionNo, 1);
|
|
assert.equal(created.body.data.page.content, sourceContent);
|
|
|
|
const detail = await request(`/api/mindspace/v1/pages/${pageId}`, {
|
|
headers: { Cookie: owner.cookie },
|
|
});
|
|
assert.equal(detail.response.status, 200);
|
|
assert.equal(detail.body.data.sourceSessionId, sessionId);
|
|
assert.equal(detail.body.data.versions.length, 1);
|
|
|
|
const preview = await request(`/api/mindspace/v1/pages/${pageId}/preview`, {
|
|
headers: { Cookie: owner.cookie },
|
|
});
|
|
assert.equal(preview.response.status, 200);
|
|
assert.match(preview.body, /default-src 'none'/);
|
|
assert.doesNotMatch(preview.body, /<script>alert/);
|
|
assert.match(preview.body, /<script>/);
|
|
|
|
const updatedContent = `${sourceContent}\n\n第二版补充内容。`;
|
|
const updated = await request(`/api/mindspace/v1/pages/${pageId}`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
Cookie: owner.cookie,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
expected_version: 1,
|
|
title: 'AI 会话页面 v2',
|
|
summary: '第二版摘要',
|
|
content: updatedContent,
|
|
template_id: 'knowledge-card',
|
|
change_note: 'E2E 第二版',
|
|
}),
|
|
});
|
|
assert.equal(updated.response.status, 200, JSON.stringify(updated.body));
|
|
assert.equal(updated.body.data.versionNo, 2);
|
|
|
|
const staleUpdate = await request(`/api/mindspace/v1/pages/${pageId}`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
Cookie: owner.cookie,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
expected_version: 1,
|
|
title: '过期修改',
|
|
content: '不应写入',
|
|
template_id: 'editorial',
|
|
}),
|
|
});
|
|
assert.equal(staleUpdate.response.status, 409);
|
|
assert.equal(staleUpdate.body.error.code, 'version_conflict');
|
|
|
|
for (const pathname of [
|
|
`/api/mindspace/v1/pages/${pageId}`,
|
|
`/api/mindspace/v1/pages/${pageId}/preview`,
|
|
]) {
|
|
const foreignRead = await request(pathname, { headers: { Cookie: other.cookie } });
|
|
assert.equal(foreignRead.response.status, 404);
|
|
}
|
|
|
|
const foreignUpdate = await request(`/api/mindspace/v1/pages/${pageId}`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
Cookie: other.cookie,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
expected_version: 2,
|
|
title: '越权修改',
|
|
content: '禁止',
|
|
template_id: 'editorial',
|
|
}),
|
|
});
|
|
assert.equal(foreignUpdate.response.status, 404);
|
|
|
|
const space = await request('/api/mindspace/v1/space', {
|
|
headers: { Cookie: owner.cookie },
|
|
});
|
|
assert.equal(space.response.status, 200);
|
|
assert.equal(
|
|
space.body.data.categories.find((category) => category.code === 'draft').itemCount,
|
|
1,
|
|
);
|
|
assert.equal(
|
|
space.body.data.quota.usedBytes,
|
|
Buffer.byteLength(sourceContent) + Buffer.byteLength(updatedContent),
|
|
);
|
|
|
|
console.log('MindSpace page API E2E passed');
|
|
} finally {
|
|
await pool.query(`DELETE FROM h5_users WHERE username IN (?, ?)`, [
|
|
users[0].username,
|
|
users[1].username,
|
|
]);
|
|
await pool.end();
|
|
portal.kill('SIGTERM');
|
|
await new Promise((resolve) => portal.once('exit', resolve));
|
|
await new Promise((resolve) => fakeUpstream.close(resolve));
|
|
await fs.rm(storageRoot, { recursive: true, force: true });
|
|
}
|