Files
memind/mindspace-pages.test.mjs
T
2026-07-02 22:01:35 +08:00

211 lines
7.2 KiB
JavaScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { pageInternals, normalizeWorkspaceRelativePath } from './mindspace-pages.mjs';
test('parseJsonColumn accepts mysql json objects and strings', () => {
const objectValue = { relative_path: 'public/demo.html', auto_synced: true };
assert.deepEqual(pageInternals.parseJsonColumn(objectValue), objectValue);
assert.deepEqual(
pageInternals.parseJsonColumn(JSON.stringify(objectValue)),
objectValue,
);
assert.deepEqual(pageInternals.parseJsonColumn(null, { ok: true }), { ok: true });
});
test('normalizePageInput trims values and constrains templates', () => {
const result = pageInternals.normalizePageInput({
title: ' 页面标题 ',
content: ' 页面正文 ',
templateId: 'unknown-template',
});
assert.equal(result.title, '页面标题');
assert.equal(result.content, '页面正文');
assert.equal(result.templateId, 'editorial');
assert.equal(result.contentBytes, Buffer.byteLength('页面正文'));
});
test('normalizePageInput rejects empty and oversized page content', () => {
assert.throws(
() => pageInternals.normalizePageInput({ title: '', content: '正文' }),
{ code: 'invalid_page_input' },
);
assert.throws(
() =>
pageInternals.normalizePageInput({
title: '页面',
content: '文'.repeat(400_000),
}),
{ code: 'page_content_too_large' },
);
});
test('normalizePageInput maps html content to static-html template', () => {
const result = pageInternals.normalizePageInput({
title: '页面',
content: '<!doctype html><html><body>Hi</body></html>',
contentFormat: 'html',
});
assert.equal(result.contentFormat, 'html');
assert.equal(result.templateId, 'static-html');
assert.equal(result.pageType, 'html');
});
test('renderHtmlPreview injects restrictive CSP', () => {
const html = pageInternals.renderHtmlPreview(
'<!doctype html><html><head></head><body><h1>Hi</h1></body></html>',
);
assert.match(html, /Content-Security-Policy/);
assert.match(html, /script-src 'none'/);
});
test('renderPublicationHtml serves html pages without escaping markup', () => {
const html = pageInternals.renderPublicationHtml({
page_type: 'html',
title: '麻婆豆腐',
summary: '川菜',
template_id: 'static-html',
version_no: 1,
content: '<!doctype html><html><head><title>麻婆豆腐</title></head><body><h1>麻婆豆腐</h1></body></html>',
});
assert.match(html, /<h1>麻婆豆腐<\/h1>/);
assert.doesNotMatch(html, /&lt;h1&gt;/);
assert.doesNotMatch(html, /MINDSPACE/);
});
test('normalizeListPageFilters clamps limit and offset', () => {
assert.deepEqual(pageInternals.normalizeListPageFilters({ limit: 999, offset: -5 }), {
limit: 100,
offset: 0,
});
assert.deepEqual(pageInternals.normalizeListPageFilters({ limit: 15, offset: 30 }), {
limit: 15,
offset: 30,
});
});
test('preview rendering escapes active HTML and emits a restrictive CSP', () => {
const html = pageInternals.renderPreviewHtml({
title: '<script>alert(1)</script>',
summary: '<img src=x onerror=alert(1)>',
content: '# 标题\n\n<script>alert(2)</script>',
templateId: 'report',
versionNo: 2,
});
assert.match(html, /Content-Security-Policy/);
assert.match(html, /default-src 'none'/);
assert.doesNotMatch(html, /<script>alert/);
assert.match(html, /&lt;script&gt;alert/);
assert.match(html, /DRAFT · V2/);
});
test('previewContentSecurityPolicy allows external assets for html pages', () => {
const htmlCsp = pageInternals.previewContentSecurityPolicy('html');
assert.match(htmlCsp, /style-src 'unsafe-inline' https:/);
assert.match(htmlCsp, /img-src data: https:/);
const markdownCsp = pageInternals.previewContentSecurityPolicy('markdown');
assert.match(markdownCsp, /style-src 'unsafe-inline'/);
assert.doesNotMatch(markdownCsp, /https:/);
});
test('buildPageDeleteSummary lists cascade delete impact', () => {
const lines = pageInternals.buildPageDeleteSummary({
page: { title: '麻婆豆腐', versionCount: 2 },
onlinePublication: { publicUrl: '/u/john/pages/mapo' },
offlinePublicationCount: 1,
plazaPosts: [{ id: 'post-1', title: '广场帖', status: 'published', publicationId: 'pub-1' }],
linkedAssets: [{ id: 'a1' }, { id: 'a2' }],
linkedAssetBytes: 2048,
linkedAgentJobCount: 1,
preservesSourceAsset: true,
});
assert.match(lines[0], /麻婆豆腐/);
assert.ok(lines.some((line) => line.includes('在线公开链接')));
assert.ok(lines.some((line) => line.includes('广场')));
assert.ok(lines.some((line) => line.includes('原始上传资料不会被删除')));
});
test('normalizeWorkspaceRelativePath unifies bare html filenames under public/', () => {
assert.equal(normalizeWorkspaceRelativePath('ai-robot-report.html'), 'public/ai-robot-report.html');
assert.equal(normalizeWorkspaceRelativePath('public/ai-robot-report.html'), 'public/ai-robot-report.html');
assert.equal(normalizeWorkspaceRelativePath('/public/demo.html'), 'public/demo.html');
});
test('registerPageArtifactForConversation records page artifact and refreshes manifest', async () => {
const calls = [];
const registry = {
async ensurePackage(input) {
calls.push(['ensurePackage', input]);
return { id: 'cp_session-1' };
},
async recordArtifact(input) {
calls.push(['recordArtifact', input]);
return input;
},
async writeManifestForSession(input) {
calls.push(['writeManifestForSession', input]);
return { storageKey: 'users/user-1/conversations/session-1/manifest.json' };
},
};
const result = await pageInternals.registerPageArtifactForConversation({
registry,
userId: 'user-1',
source: { sessionId: 'session-1', messageId: 'message-1' },
page: {
id: 'page-1',
currentVersionId: 'version-1',
title: '旅行计划',
sourceMessageId: 'message-fallback',
},
contentAssetId: 'asset-1',
contentMimeType: 'text/html',
contentBytes: 128,
storageKey: 'users/user-1/pages/page-1/versions/v1.html',
versionNo: 1,
now: 1000,
});
assert.equal(result.id, 'ca_page-1_version-1');
assert.deepEqual(calls[0], [
'ensurePackage',
{
userId: 'user-1',
sessionId: 'session-1',
title: '旅行计划',
now: 1000,
},
]);
assert.equal(calls[1][1].artifactKind, 'page');
assert.equal(calls[1][1].messageId, 'message-1');
assert.equal(calls[1][1].storageKey, 'users/user-1/pages/page-1/versions/v1.html');
assert.equal(calls[1][1].canonicalUrl, '/api/mindspace/v1/pages/page-1/preview');
assert.deepEqual(calls[2], [
'writeManifestForSession',
{ userId: 'user-1', sessionId: 'session-1' },
]);
});
test('registerPageArtifactForConversation skips missing registry or session safely', async () => {
assert.equal(
await pageInternals.registerPageArtifactForConversation({
registry: null,
userId: 'user-1',
source: { sessionId: 'session-1' },
page: { id: 'page-1' },
}),
null,
);
assert.equal(
await pageInternals.registerPageArtifactForConversation({
registry: { ensurePackage: async () => assert.fail('should not be called') },
userId: 'user-1',
source: {},
page: { id: 'page-1' },
}),
null,
);
});