Files
memind/scripts/fruit-theme-john-e2e.mjs
T
John 2e14873f2d Initial commit: Memind H5 portal with MindSpace, Plaza, and agent jobs.
Track application source and tests; exclude local env, user workspaces, and runtime data via .gitignore.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-15 15:04:43 -07:00

186 lines
7.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* john 账号水果主题 E2E(仅本地):工作区 HTML → 预览图 → 保存到空间 → 可点击链接
* 用法:node scripts/fruit-theme-john-e2e.mjs
* 环境:自动启动本地 serverMINDSPACE_E2E_PORT);勿对生产 URL 跑本脚本
*/
import assert from 'node:assert/strict';
import { spawn } from 'node:child_process';
import fs from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { loadH5Environment } from './load-env.mjs';
import { createDbPool } from '../db.mjs';
import { createUserAuth } from '../user-auth.mjs';
import { buildPublicUrl } from '../user-publish.mjs';
import {
ensureWorkspaceHtmlThumbnail,
workspaceThumbnailRelativePath,
} from '../mindspace-workspace-thumbnails.mjs';
import { isModernFeedThumbnail } from '../mindspace-thumbnails.mjs';
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
loadH5Environment(import.meta.dirname);
if (/go\.tkmind\.cn|goo\.tkmind\.cn|120\.26\.184\.105/i.test(process.env.BASE_URL ?? '')) {
throw new Error('本脚本仅用于本地验证,请勿设置生产 BASE_URL');
}
const portalPort = Number(process.env.MINDSPACE_E2E_PORT ?? 18140);
const BASE_URL = `http://127.0.0.1:${portalPort}`;
const HTML_NAME = 'fruit-theme-test.html';
const USERNAME = 'john';
const JOHN_PASSWORD = process.env.JOHN_PASSWORD ?? process.env.H5_ACCESS_PASSWORD ?? '981122tj';
const publishDir = path.join(root, 'MindSpace', USERNAME);
const htmlPath = path.join(publishDir, HTML_NAME);
const thumbRel = workspaceThumbnailRelativePath(HTML_NAME);
const publicPageUrl = buildPublicUrl(BASE_URL, USERNAME, HTML_NAME);
const publicThumbUrl = buildPublicUrl(BASE_URL, USERNAME, thumbRel);
const agentReplyLink = `[夏日鲜果指南 · 水果主题测试](${publicPageUrl})`;
const portal = spawn(process.execPath, ['server.mjs'], {
cwd: root,
env: {
...process.env,
H5_PORT: String(portalPort),
H5_PUBLIC_BASE_URL: BASE_URL,
TKMIND_API_TARGET: 'http://127.0.0.1:9',
},
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 < 80; attempt += 1) {
if (portal.exitCode != null) throw new Error(`本地 server 提前退出\n${portalLogs}`);
try {
const response = await fetch(`${BASE_URL}/auth/status`);
if (response.ok) return;
} catch {
// still starting
}
await new Promise((resolve) => setTimeout(resolve, 150));
}
throw new Error(`本地 server 启动超时\n${portalLogs}`);
}
function cookieHeader(setCookie) {
if (!setCookie) return '';
const parts = Array.isArray(setCookie) ? setCookie : [setCookie];
return parts.map((c) => c.split(';')[0]).join('; ');
}
async function request(pathname, { method = 'GET', body, cookie } = {}) {
const headers = {};
if (body) headers['Content-Type'] = 'application/json';
if (cookie) headers.Cookie = cookie;
const response = await fetch(`${BASE_URL}${pathname}`, { method, headers, body: body ? JSON.stringify(body) : undefined });
const type = response.headers.get('content-type') ?? '';
const payload = type.includes('json') ? await response.json() : await response.text();
return { response, payload, setCookie: response.headers.getSetCookie?.() ?? response.headers.get('set-cookie') };
}
async function ensureJohnPassword() {
const pool = createDbPool();
const auth = createUserAuth(pool, { h5Root: root });
let login = await auth.login({ username: USERNAME, password: JOHN_PASSWORD, ip: '127.0.0.1' });
if (!login.ok) {
const [rows] = await pool.query(`SELECT id, email FROM h5_users WHERE username = ? LIMIT 1`, [
USERNAME,
]);
const user = rows[0];
if (!user) throw new Error('本地数据库无 john 用户,请先注册或导入数据');
const email = user.email ?? `${USERNAME}@local.test`;
if (!user.email) {
await pool.query(`UPDATE h5_users SET email = ? WHERE id = ?`, [email, user.id]);
}
const reset = await auth.resetPassword({ username: USERNAME, email, password: JOHN_PASSWORD });
assert.equal(reset.ok, true, reset.message ?? '重置 john 本地密码失败');
login = await auth.login({ username: USERNAME, password: JOHN_PASSWORD, ip: '127.0.0.1' });
}
assert.equal(login.ok, true, login.error ?? 'john 本地登录失败');
const { response, setCookie } = await request('/auth/login', {
method: 'POST',
body: { username: USERNAME, password: JOHN_PASSWORD },
});
assert.equal(response.status, 200, 'john HTTP 登录应成功');
await pool.end();
return cookieHeader(setCookie);
}
async function main() {
console.log('=== john 水果主题 E2E(本地) ===');
await waitForPortal();
console.log(`BASE_URL: ${BASE_URL}`);
const html = await fs.readFile(htmlPath, 'utf8');
assert.match(html, /mindspace-cover/, 'HTML 应包含 mindspace-cover');
const svg = await ensureWorkspaceHtmlThumbnail(publishDir, HTML_NAME, html, {
title: '夏日鲜果指南',
subtitle: '水果主题测试',
});
assert.equal(isModernFeedThumbnail(svg), true, '工作区预览图应为新版 3:4');
assert.match(svg, /美食|鲜果|FRUIT/i, '预览图应体现水果/美食主题');
const hasPhoto = /preserveAspectRatio="xMidYMid slice"/.test(svg);
console.log(`✓ 工作区预览图已生成: ${thumbRel}(含主图: ${hasPhoto ? '是' : '否'}`);
const pageCheck = await request(`/MindSpace/${USERNAME}/${HTML_NAME}`);
assert.equal(pageCheck.response.status, 200, `公网 HTML 应可访问: ${publicPageUrl}`);
console.log(`✓ 公网 HTML 200: ${publicPageUrl}`);
const thumbCheck = await request(`/MindSpace/${USERNAME}/${thumbRel}`);
assert.equal(thumbCheck.response.status, 200, `公网预览图应可访问: ${publicThumbUrl}`);
assert.match(String(thumbCheck.payload), /width="540" height="720"/, '线上预览图为 3:4');
console.log(`✓ 公网预览图 200: ${publicThumbUrl}`);
assert.match(agentReplyLink, /^\[.+\]\(https?:\/\/.+\)$/);
console.log(`✓ Agent 应回复的可点击链接:\n ${agentReplyLink}`);
const cookie = await ensureJohnPassword();
console.log('✓ john 本地登录成功');
const { response: spaceRes, payload: spacePayload } = await request('/api/mindspace/v1/space', {
cookie,
});
assert.equal(spaceRes.status, 200, JSON.stringify(spacePayload));
const saveBody = {
title: '夏日鲜果指南 · 水果主题测试',
summary: 'TKMind 水果主题预览图 E2E',
content: html,
content_format: 'html',
page_type: 'html',
category_code: 'draft',
};
const created = await request('/api/mindspace/v1/pages', {
method: 'POST',
body: saveBody,
cookie,
});
assert.equal(created.response.status, 201, JSON.stringify(created.payload));
const pageId = created.payload?.data?.id ?? created.payload?.data?.page?.id;
assert.ok(pageId, '应创建页面记录');
await new Promise((r) => setTimeout(r, 800));
const thumbApi = await request(`/api/mindspace/v1/pages/${pageId}/thumbnail`, { cookie });
assert.equal(thumbApi.response.status, 200, '页面缩略图 API 应返回 200');
assert.match(String(thumbApi.payload), /width="540" height="720"/);
console.log(`✓ 我的空间缩略图 API 200: /api/mindspace/v1/pages/${pageId}/thumbnail`);
console.log('\n=== 全部通过 ===');
}
try {
await main();
} finally {
portal.kill('SIGTERM');
}