#!/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'; 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, /