import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import { createPageService, pageInternals, normalizeWorkspaceRelativePath } from './mindspace-pages.mjs';
import { PUBLISH_ROOT_DIR } from './user-publish.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: '
Hi',
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(
'Hi
',
);
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: '麻婆豆腐麻婆豆腐
',
});
assert.match(html, /麻婆豆腐<\/h1>/);
assert.doesNotMatch(html, /<h1>/);
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('pageResponse includes publication metrics when publish row is present', () => {
const response = pageInternals.pageResponse(
{
id: 'page-1',
user_id: 'user-1',
category_id: 'cat-1',
category_code: 'public',
source_session_id: null,
source_message_id: null,
source_asset_id: null,
title: '成果页面',
summary: '摘要',
page_type: 'html',
template_id: 'static-html',
status: 'published',
visibility: 'public',
current_version_id: 'ver-1',
version_no: 3,
pub_access_mode: 'public',
pub_public_url: 'https://m.tkmind.cn/u/john/pages/demo',
pub_id: 'pub-1',
pub_status: 'online',
pub_view_count: 128,
pub_published_at: 1_700_000_000_000,
created_at: 1_699_000_000_000,
updated_at: 1_700_100_000_000,
},
{ includeContent: false },
);
assert.equal(response.publicationId, 'pub-1');
assert.equal(response.publicationStatus, 'online');
assert.equal(response.viewCount, 128);
assert.equal(response.clickCount, null);
assert.equal(response.statsSource, 'publication');
assert.equal(response.publishedAt, 1_700_000_000_000);
assert.equal(response.publicationUrl, 'https://m.tkmind.cn/u/john/pages/demo');
});
test('preview rendering escapes active HTML and emits a restrictive CSP', () => {
const html = pageInternals.renderPreviewHtml({
title: '',
summary: '
',
content: '# 标题\n\n',
templateId: 'report',
versionNo: 2,
});
assert.match(html, /Content-Security-Policy/);
assert.match(html, /default-src 'none'/);
assert.doesNotMatch(html, /