Files
memind/mindspace-publications.test.mjs

871 lines
30 KiB
JavaScript

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 { createPublicationService, publicationInternals } from './mindspace-publications.mjs';
test('normalizes safe long-page slugs', () => {
assert.equal(publicationInternals.normalizeSlug(' product-intro-2026 '), 'product-intro-2026');
assert.throws(
() => publicationInternals.normalizeSlug('../private'),
(error) => error.code === 'invalid_publish_input',
);
assert.throws(
() => publicationInternals.normalizeSlug('中文地址'),
(error) => error.code === 'invalid_publish_input',
);
});
test('accepts all documented access modes', () => {
for (const mode of [
'public',
'password',
'private_link',
'time_limited',
'login_required',
'owner_only',
]) {
assert.equal(publicationInternals.normalizeAccessMode(mode), mode);
}
assert.throws(
() => publicationInternals.normalizeAccessMode('team_only'),
(error) => error.code === 'invalid_publish_input',
);
});
test('getCurrent exposes whether the owner confirmed publication visibility', async () => {
const service = createPublicationService({
async query(sql, params) {
assert.match(sql, /SELECT pr\.\*/);
assert.deepEqual(params, ['page-1', 'user-1']);
return [[{
id: 'pub-1',
page_id: 'page-1',
page_version_id: 'version-1',
url_slug: 'journal',
public_url: '/u/john/pages/journal',
access_mode: 'public',
expires_at: null,
status: 'online',
view_count: 2,
published_at: 1000,
offline_at: null,
user_confirmed_at: 2000,
}]];
},
});
const publication = await service.getCurrent('user-1', 'page-1');
assert.equal(publication.userConfirmedAt, 2000);
});
test('getCurrent returns null confirmation for an unconfirmed publication', async () => {
const service = createPublicationService({
async query() {
return [[{
id: 'pub-1',
page_id: 'page-1',
page_version_id: 'version-1',
url_slug: 'journal',
public_url: '/u/john/pages/journal',
access_mode: 'public',
expires_at: null,
status: 'online',
view_count: 0,
published_at: 1000,
offline_at: null,
user_confirmed_at: null,
}]];
},
});
const publication = await service.getCurrent('user-1', 'page-1');
assert.equal(publication.userConfirmedAt, null);
});
test('cleanupExpiredUnconfirmedPublications falls back to owner-only access', async () => {
let executedSql = '';
const service = createPublicationService({
async query(sql, params) {
executedSql = sql;
assert.deepEqual(params, [3000, 3000]);
return [{ affectedRows: 1 }];
},
});
const result = await service.cleanupExpiredUnconfirmedPublications(3000);
assert.deepEqual(result, { cleaned: 1 });
assert.match(executedSql, /SET access_mode = 'owner_only'/);
assert.doesNotMatch(executedSql, /SET access_mode = 'private'/);
});
test('hashes access passwords with a random salt', () => {
const first = publicationInternals.hashPassword('Publish-Password-2026');
const second = publicationInternals.hashPassword('Publish-Password-2026');
assert.notEqual(first, second);
assert.equal(publicationInternals.verifyPassword('Publish-Password-2026', first), true);
assert.equal(publicationInternals.verifyPassword('wrong-password', first), false);
assert.throws(
() => publicationInternals.normalizePassword('short', true),
(error) => error.code === 'invalid_publish_input',
);
});
test('requires future expiry timestamps for time-limited pages', () => {
const future = Date.now() + 60_000;
assert.equal(publicationInternals.normalizeExpiresAt(future, true), future);
assert.throws(
() => publicationInternals.normalizeExpiresAt(Date.now() - 1, true),
(error) => error.code === 'invalid_publish_input',
);
});
test('classifies devices and strips referrer paths', () => {
assert.equal(
publicationInternals.deviceType('Mozilla/5.0 (iPhone; CPU iPhone OS 18_0) Mobile'),
'mobile',
);
assert.equal(publicationInternals.deviceType('Googlebot/2.1'), 'bot');
assert.equal(publicationInternals.deviceType('Mozilla/5.0 (Macintosh)'), 'desktop');
assert.equal(
publicationInternals.referrerHost('https://example.com/private/path?token=secret'),
'example.com',
);
assert.equal(publicationInternals.referrerHost('not a url'), null);
});
test('warns for contact details and external links without silently hiding them', () => {
const result = publicationInternals.scanContent(
'联系 13812345678 或 hello@example.com,详情 https://example.com/path',
);
assert.equal(result.status, 'warned');
assert.equal(result.riskLevel, 'medium');
assert.equal(result.allowed, true);
assert.deepEqual(
result.findings.map((finding) => finding.type),
['phone', 'email', 'external_link'],
);
assert.doesNotMatch(result.findings[0].sampleMasked, /13812345678/);
});
test('deduplicates the same sensitive value repeated by summary and content', () => {
const result = publicationInternals.scanContent(
'摘要 qa@example.com\n正文 qa@example.com',
);
assert.equal(result.findings[0].occurrenceCount, 1);
});
test('blocks identity and bank-card numbers', () => {
const result = publicationInternals.scanContent(
'身份证 310101199001011234,银行卡 6222021234567890123',
);
assert.equal(result.status, 'blocked');
assert.equal(result.riskLevel, 'high');
assert.equal(result.allowed, false);
assert.ok(result.findings.every((finding) => finding.blocking));
});
test('publicHomepageResponse summarizes public cards and total views', () => {
const result = publicationInternals.publicHomepageResponse(
{
id: 'user-1',
slug: 'john',
username: 'john',
display_name: 'John',
},
[
{
id: 'pub-1',
page_id: 'page-1',
title: '第一篇',
summary: '摘要一',
template_id: 'report',
public_url: '/u/john/pages/one',
url_slug: 'one',
view_count: 7,
published_at: 100,
},
{
id: 'pub-2',
page_id: 'page-2',
title: '第二篇',
summary: '摘要二',
template_id: 'editorial',
public_url: '/u/john/pages/two',
url_slug: 'two',
view_count: 3,
published_at: 200,
},
],
);
assert.equal(result.owner.displayName, 'John');
assert.equal(result.totalViews, 10);
assert.equal(result.pageCount, 2);
assert.equal(result.pages[0].publicUrl, '/u/john/pages/one');
});
test('buildPublicationThumbnailFallback points private resources at the public page thumbnail', () => {
assert.equal(
publicationInternals.buildPublicationThumbnailFallback('john', 'page-037d497f'),
'/u/john/pages/page-037d497f.thumbnail.png',
);
assert.equal(
publicationInternals.buildPublicationThumbnailFallback('木子', 'demo-page'),
'/u/%E6%9C%A8%E5%AD%90/pages/demo-page.thumbnail.png',
);
});
test('resolvePublic returns page provenance for package artifact registration', async () => {
const storageRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'mindspace-public-resolve-'));
const storageKey = 'users/user-1/publications/pub-1/index.html';
await fs.mkdir(path.dirname(path.join(storageRoot, storageKey)), { recursive: true });
await fs.writeFile(path.join(storageRoot, storageKey), '<!doctype html><title>发布页</title>');
const service = createPublicationService(
{
async query(sql) {
if (sql.includes('FROM h5_publish_records pr') && sql.includes('COALESCE(u.slug, u.username)')) {
return [[
{
id: 'pub-1',
user_id: 'user-1',
owner_id: 'user-1',
page_id: 'page-1',
page_version_id: 'version-1',
title: '发布页',
source_session_id: 'session-1',
source_message_id: 'message-1',
url_slug: 'published',
public_url: 'https://m.tkmind.cn/u/john/pages/published',
access_mode: 'public',
status: 'online',
view_count: 0,
published_at: 3000,
storage_key: storageKey,
},
]];
}
return [[]];
},
},
{
storageRoot,
idFactory: () => 'view-1',
},
);
const result = await service.resolvePublic('john', 'published', null, null, {});
assert.equal(result.ownerId, 'user-1');
assert.deepEqual(result.pageSource, {
pageId: 'page-1',
title: '发布页',
sourceSessionId: 'session-1',
sourceMessageId: 'message-1',
});
});
test('prepareHtmlPublishContent inlines owned private image assets before scanning', async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'mindspace-publish-'));
const storageKey = 'users/user-1/assets/asset-1/current.png';
await fs.mkdir(path.dirname(path.join(root, storageKey)), { recursive: true });
await fs.writeFile(path.join(root, storageKey), Buffer.from('png-bytes'));
const pool = {
async query(sql, params) {
if (sql.includes('original_filename')) {
return [[]];
}
assert.match(sql, /h5_assets/);
assert.equal(params[0], 'user-1');
assert.deepEqual(params[1], ['asset-1']);
return [
[
{
id: 'asset-1',
mime_type: 'image/png',
storage_key: storageKey,
},
],
];
},
};
try {
const html = '<!doctype html><img src="/api/mindspace/v1/assets/asset-1/download?inline=1&v=1">';
const prepared = await publicationInternals.prepareHtmlPublishContent({
pool,
userId: 'user-1',
html,
ownerSlug: 'john',
urlSlug: 'korean-daily-look',
absoluteStoragePath: (key) => path.join(root, key),
});
assert.match(prepared, /src="data:image\/png;base64,/);
assert.doesNotMatch(prepared, /\/api\/mindspace\/v1\/assets\//);
const scan = publicationInternals.scanContent(prepared, { format: 'html' });
assert.equal(
scan.findings.some((finding) => finding.type === 'private_resource_reference'),
false,
);
} finally {
await fs.rm(root, { recursive: true, force: true });
}
});
test('prepareHtmlPublishContent signs private image assets when imgproxy signer is enabled', async () => {
const storageKey = 'users/user-1/images/2026-07-03/demo-asset-1.jpg';
const pool = {
async query(sql, params) {
if (sql.includes('original_filename')) {
return [[]];
}
assert.match(sql, /h5_assets/);
assert.equal(params[0], 'user-1');
assert.deepEqual(params[1], ['asset-1']);
return [
[
{
id: 'asset-1',
mime_type: 'image/png',
storage_key: storageKey,
},
],
];
},
};
const imgproxySigner = {
baseUrl: 'https://img.example.test',
buildUrl: (baseUrl, storagePath, preset = 'display') => {
assert.equal(baseUrl, 'https://img.example.test');
assert.equal(storagePath, storageKey);
assert.equal(preset, 'display');
return `https://img.example.test/sig/plain/local:///${storagePath}`;
},
};
const prepared = await publicationInternals.prepareHtmlPublishContent({
pool,
userId: 'user-1',
html: '<!doctype html><img src="/api/mindspace/v1/assets/asset-1/download?inline=1&v=1">',
ownerSlug: 'john',
urlSlug: 'night-market-essay',
absoluteStoragePath: () => {
throw new Error('should not read private storage when imgproxy is enabled');
},
imgproxySigner: {
baseUrl: imgproxySigner.baseUrl,
buildUrl: (storagePath, preset = 'display') =>
imgproxySigner.buildUrl(imgproxySigner.baseUrl, storagePath, preset),
},
});
assert.doesNotMatch(prepared, /\/api\/mindspace\/v1\/assets\//);
assert.doesNotMatch(prepared, /thumbnail\.png/);
assert.doesNotMatch(prepared, /plain\/local:\/\//);
});
test('prepareHtmlPublishContent converts signed private images to public standard urls', async () => {
const storageKey =
'users/user-1/images/2026-07-03/60270b88dda7b7ccb8a49d8e2746e80b-asset-1.jpg';
const pool = {
async query(sql, params) {
if (sql.includes('original_filename')) {
return [[]];
}
assert.match(sql, /h5_assets/);
assert.equal(params[0], 'user-1');
assert.deepEqual(params[1], ['asset-1']);
return [
[
{
id: 'asset-1',
mime_type: 'image/jpeg',
storage_key: storageKey,
},
],
];
},
};
const imgproxySigner = {
baseUrl: 'https://img.example.test',
buildUrl: (storagePath, preset = 'display') =>
`https://img.example.test/sig/rs:fit:1280:1280:0/q:85/plain/local:///${storagePath}@${preset}`,
};
const prepared = await publicationInternals.prepareHtmlPublishContent({
pool,
userId: 'user-1',
html: '<!doctype html><img src="/api/mindspace/v1/assets/asset-1/download?inline=1&v=1">',
ownerSlug: 'john',
urlSlug: 'night-market-essay',
absoluteStoragePath: () => {
throw new Error('should not read private storage when imgproxy is enabled');
},
imgproxySigner,
});
assert.match(prepared, /https:\/\/m\.tkmind\.cn\/MindSpace\/user-1\/public\/images\/2026-07-03\//);
assert.doesNotMatch(prepared, /\/api\/mindspace\/v1\/assets\//);
assert.doesNotMatch(prepared, /thumbnail\.png/);
assert.doesNotMatch(prepared, /plain\/local:\/\//);
});
test('prepareHtmlPublishContent rewrites imgproxy local image urls to public standard images', async () => {
const prepared = await publicationInternals.prepareHtmlPublishContent({
pool: {
async query() {
return [[]];
},
},
userId: 'user-1',
html: '<!doctype html><img src="https://tkmind.ai/sig/rs:fit:1280:1280:0/q:85/plain/local:///users/user-1/images/2026-06-29/hero.jpg@jpg">',
ownerSlug: 'john',
urlSlug: 'story',
absoluteStoragePath: () => {
throw new Error('should not read private storage');
},
});
assert.match(prepared, /https:\/\/m\.tkmind\.cn\/MindSpace\/user-1\/public\/images\/2026-06-29\/hero\.jpg/);
assert.doesNotMatch(prepared, /plain\/local:\/\//);
});
test('registerPublicationArtifactForConversation records public html artifact', 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 publicationInternals.registerPublicationArtifactForConversation({
registry,
userId: 'user-1',
page: {
page_id: 'page-1',
title: '发布页',
source_session_id: 'session-1',
source_message_id: 'message-1',
},
publication: {
id: 'pub-1',
publicUrl: 'https://m.tkmind.cn/u/john/pages/published',
publishedAt: 3000,
},
bundleAssetId: 'asset-pub',
storageKey: 'users/user-1/publications/pub-1/index.html',
htmlBytes: 256,
now: 3000,
});
assert.equal(result.id, 'ca_pub-1');
assert.deepEqual(calls[0], [
'ensurePackage',
{
userId: 'user-1',
sessionId: 'session-1',
title: '发布页',
now: 3000,
},
]);
assert.equal(calls[1][1].artifactKind, 'public_html');
assert.equal(calls[1][1].publicationId, 'pub-1');
assert.equal(calls[1][1].canonicalUrl, 'https://m.tkmind.cn/u/john/pages/published');
assert.deepEqual(calls[2], [
'writeManifestForSession',
{ userId: 'user-1', sessionId: 'session-1' },
]);
});
test('collectPublicationCompanionArtifacts finds real download and long-image files', async () => {
const publishDir = await fs.mkdtemp(path.join(os.tmpdir(), 'publication-companions-'));
await fs.mkdir(path.join(publishDir, 'public'), { recursive: true });
await fs.writeFile(path.join(publishDir, 'public', 'report.docx'), 'docx');
await fs.writeFile(path.join(publishDir, 'public', 'report.long.png'), 'png');
await fs.writeFile(path.join(publishDir, 'public', 'appendix.pdf'), 'pdf');
const artifacts = await publicationInternals.collectPublicationCompanionArtifacts({
publishDir,
htmlRelativePath: 'public/report.html',
html: '<a href="appendix.pdf" download>PDF</a>',
});
assert.deepEqual(
artifacts.map((item) => item.workspacePath).sort(),
['public/appendix.pdf', 'public/report.docx', 'public/report.long.png'],
);
});
test('registerPublicationArtifactForConversation records public companions', async () => {
const publishDir = await fs.mkdtemp(path.join(os.tmpdir(), 'publication-companions-'));
await fs.mkdir(path.join(publishDir, 'public'), { recursive: true });
await fs.writeFile(path.join(publishDir, 'public', 'report.docx'), 'docx');
await fs.writeFile(path.join(publishDir, 'public', 'report.long.png'), 'png');
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' };
},
};
await publicationInternals.registerPublicationArtifactForConversation({
registry,
userId: 'user-1',
page: {
page_id: 'page-1',
title: '发布页',
source_session_id: 'session-1',
source_message_id: 'message-1',
source_relative_path: 'public/report.html',
},
publication: {
id: 'pub-1',
publicUrl: 'https://m.tkmind.cn/u/john/pages/published',
publishedAt: 3000,
},
bundleAssetId: 'asset-pub',
storageKey: 'users/user-1/publications/pub-1/index.html',
htmlBytes: 256,
html: '<!doctype html><title>发布页</title>',
publishDir,
now: 3000,
});
const artifactKinds = calls
.filter(([name]) => name === 'recordArtifact')
.map(([, input]) => input.artifactKind)
.sort();
assert.deepEqual(artifactKinds, ['docx', 'long_image', 'public_html']);
const docx = calls.find(([, input]) => input?.artifactKind === 'docx')?.[1];
assert.equal(docx.displayName, 'report.docx');
assert.match(docx.canonicalUrl, /\/MindSpace\/user-1\/public\/report\.docx$/);
});
test('publish registers public html and companion artifacts for chat-sourced pages', async () => {
const storageRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'mindspace-publish-flow-'));
const h5Root = await fs.mkdtemp(path.join(os.tmpdir(), 'mindspace-h5-root-'));
const pageStorageKey = 'users/user-1/pages/page-1/versions/v1.html';
const pageHtml = '<!doctype html><title>Report</title><main>发布内容</main>';
await fs.mkdir(path.dirname(path.join(storageRoot, pageStorageKey)), { recursive: true });
await fs.writeFile(path.join(storageRoot, pageStorageKey), pageHtml);
const publishDir = path.join(h5Root, 'MindSpace', 'user-1');
await fs.mkdir(path.join(publishDir, 'public'), { recursive: true });
await fs.writeFile(path.join(publishDir, 'public', 'report.docx'), 'docx');
await fs.writeFile(path.join(publishDir, 'public', 'report.long.png'), 'png');
const registryCalls = [];
let idCounter = 0;
const pageRow = {
page_id: 'page-1',
title: 'Report',
summary: '',
page_type: 'html',
template_id: 'static-html',
current_version_id: 'version-1',
source_session_id: 'session-1',
source_message_id: 'message-1',
user_id: 'user-1',
space_id: 'space-1',
page_version_id: 'version-1',
version_no: 1,
bundle_asset_id: null,
storage_key: pageStorageKey,
source_relative_path: 'public/report.html',
};
const pool = {
async query(sql, params = []) {
if (sql.includes('FROM h5_page_records p') && sql.includes('JOIN h5_page_versions')) {
assert.deepEqual(params, ['page-1', 'user-1', 'version-1']);
return [[pageRow]];
}
if (sql.includes('FROM h5_publish_records pr') && sql.includes('pr.page_id <>')) {
return [[]];
}
if (sql.includes('FROM h5_users')) {
return [[{ public_slug: 'john' }]];
}
if (sql.includes('FROM h5_assets') || sql.includes('FROM h5_asset_versions')) {
return [[]];
}
return [[]];
},
async getConnection() {
return {
async beginTransaction() {},
async commit() {},
async rollback() {},
release() {},
async query(sql) {
if (sql.includes('FROM h5_user_spaces') && sql.includes('FOR UPDATE')) {
return [[{ quota_bytes: 1_000_000, used_bytes: 0, reserved_bytes: 0 }]];
}
if (sql.includes('COUNT(DISTINCT page_id)')) {
return [[{ public_page_used: 0, page_already_online: 0 }]];
}
if (sql.includes('FROM h5_space_categories')) {
return [[{ id: 'cat-public' }]];
}
if (sql.includes('SELECT id, page_version_id FROM h5_publish_records')) {
return [[]];
}
return [[]];
},
};
},
};
const service = createPublicationService(pool, {
storageRoot,
h5Root,
idFactory: () => `id-${++idCounter}`,
conversationPackageRegistry: {
async ensurePackage(input) {
registryCalls.push(['ensurePackage', input]);
return { id: 'cp-session-1' };
},
async recordArtifact(input) {
registryCalls.push(['recordArtifact', input]);
return input;
},
async writeManifestForSession(input) {
registryCalls.push(['writeManifestForSession', input]);
return input;
},
},
});
try {
const publication = await service.publish('user-1', 'page-1', {
pageVersionId: 'version-1',
accessMode: 'public',
urlSlug: 'report',
acknowledgedFindingIds: [],
});
assert.equal(publication.id, 'id-4');
const artifactKinds = registryCalls
.filter(([name]) => name === 'recordArtifact')
.map(([, input]) => input.artifactKind)
.sort();
assert.deepEqual(artifactKinds, ['docx', 'long_image', 'public_html']);
const companion = registryCalls.find(([, input]) => input?.artifactKind === 'docx')?.[1];
assert.equal(companion.messageId, 'message-1');
assert.equal(companion.publicationId, 'id-4');
assert.equal(registryCalls.at(-1)[0], 'writeManifestForSession');
} finally {
await fs.rm(storageRoot, { recursive: true, force: true });
await fs.rm(h5Root, { recursive: true, force: true });
}
});
test('registerPublicationArtifactForConversation skips pages without source session', async () => {
assert.equal(
await publicationInternals.registerPublicationArtifactForConversation({
registry: { ensurePackage: async () => assert.fail('should not be called') },
userId: 'user-1',
page: { page_id: 'page-1', title: '发布页' },
publication: { id: 'pub-1' },
}),
null,
);
});
test('prepareHtmlPublishContent rewrites imgproxy urls in srcset and css url contexts', async () => {
const prepared = await publicationInternals.prepareHtmlPublishContent({
pool: {
async query() {
return [[]];
},
},
userId: 'user-1',
html: [
'<!doctype html>',
"<img srcset='https://img.tkmind.cn/sig/rs:fit:256:256:0/q:70/plain/local:///users/user-1/images/2026-06-29/thumb.jpg@jpg 1x, https://tkmind.ai/sig/rs:fit:1280:1280:0/q:85/plain/local:///users/user-1/images/2026-06-29/hero.jpg@jpg 2x'>",
'<section style="background-image:url(http://localhost:20081/unsafe/rs:fit:1280:1280:0/q:85/plain/local:///users/user-1/images/2026-06-29/bg.jpg@jpg)"></section>',
].join(''),
ownerSlug: 'john',
urlSlug: 'story',
absoluteStoragePath: () => {
throw new Error('should not read private storage');
},
});
assert.match(prepared, /\/MindSpace\/user-1\/public\/images\/2026-06-29\/thumb\.jpg 1x/);
assert.match(prepared, /\/MindSpace\/user-1\/public\/images\/2026-06-29\/hero\.jpg 2x/);
assert.match(prepared, /url\(https:\/\/m\.tkmind\.cn\/MindSpace\/user-1\/public\/images\/2026-06-29\/bg\.jpg\)/);
assert.doesNotMatch(prepared, /plain\/local:\/\//);
});
test('prepareHtmlPublishContent preserves absolute MindSpace public image urls', async () => {
const prepared = await publicationInternals.prepareHtmlPublishContent({
pool: {
async query() {
return [[]];
},
},
userId: 'user-1',
html: '<!doctype html><img src="http://127.0.0.1:5173/MindSpace/user-1/public/images/2026-07-05/demo.jpg" alt="demo">',
ownerSlug: 'john',
urlSlug: 'story',
htmlRelativePath: 'public/index.html',
absoluteStoragePath: () => {
throw new Error('should not read private storage');
},
});
assert.match(
prepared,
/src="http:\/\/127\.0\.0\.1:5173\/MindSpace\/user-1\/public\/images\/2026-07-05\/demo\.jpg"/,
);
assert.doesNotMatch(prepared, /\/MindSpace\/user-1\/images\/2026-07-05\//);
});
test('rewriteBrokenMindSpacePublicImageUrls restores missing public segment', () => {
const html =
'<img src="http://127.0.0.1:5173/MindSpace/user-1/images/2026-07-05/demo.jpg" alt="demo">';
const out = publicationInternals.rewriteBrokenMindSpacePublicImageUrls(html);
assert.match(out, /\/MindSpace\/user-1\/public\/images\/2026-07-05\/demo\.jpg/);
assert.doesNotMatch(out, /\/MindSpace\/user-1\/images\/2026-07-05\//);
});
test('prepareHtmlPublishContent rewrites workspace public asset paths relative to html output', async () => {
const prepared = await publicationInternals.prepareHtmlPublishContent({
pool: {
async query() {
return [[]];
},
},
userId: 'user-1',
html: [
'<!doctype html>',
`<meta name="mindspace-cover" content='{"cover":"public/images/2026-06-29/hero.jpg"}'>`,
`<section style="background-image:url('public/images/2026-06-29/bg.jpg')"></section>`,
`<img src="public/.tmp-images/card.png">`,
].join(''),
ownerSlug: 'john',
urlSlug: 'shared-story',
htmlRelativePath: 'public/shared/shared-story.html',
absoluteStoragePath: () => {
throw new Error('should not read private storage');
},
});
assert.match(prepared, /"cover":"\/u\/john\/public\/images\/2026-06-29\/hero\.jpg"/);
assert.match(prepared, /url\('\/u\/john\/public\/images\/2026-06-29\/bg\.jpg'\)/);
assert.match(prepared, /src="\/u\/john\/public\/\.tmp-images\/card\.png"/);
assert.doesNotMatch(prepared, /src="public\//);
assert.doesNotMatch(prepared, /"cover":"public\//);
});
test('prepareHtmlPublishContent rewrites known chart.js cdn scripts to platform asset', async () => {
const prepared = await publicationInternals.prepareHtmlPublishContent({
pool: {
async query() {
return [[]];
},
},
userId: 'user-1',
html: '<!doctype html><body><script src="https://cdn.jsdelivr.net/npm/chart.js/dist/chart.umd.min.js"></script></body></html>',
ownerSlug: 'john',
urlSlug: 'survey-admin',
htmlRelativePath: 'public/survey-admin.html',
absoluteStoragePath: () => {
throw new Error('should not read private storage');
},
});
assert.match(prepared, /src="\/assets\/chart\.umd\.min\.js"/);
assert.doesNotMatch(prepared, /cdn\.jsdelivr\.net/);
});
test('rewritePublicationCanonicalAssetUrls rewrites workspace tmp image paths for /u pages routes', () => {
const html = [
'<img src=".tmp-images/one.jpg">',
'<img src="../.tmp-images/two.jpg">',
`<section style="background-image:url('.tmp-images/three.jpg')"></section>`,
].join('');
const out = publicationInternals.rewritePublicationCanonicalAssetUrls(html, 'john');
assert.match(out, /src="\/u\/john\/public\/\.tmp-images\/one\.jpg"/);
assert.match(out, /src="\/u\/john\/public\/\.tmp-images\/two\.jpg"/);
assert.match(out, /url\('\/u\/john\/public\/\.tmp-images\/three\.jpg'\)/);
});
test('publish rejects republish while page is still online', async () => {
const pageRow = {
page_id: 'page-1',
title: 'Report',
summary: '',
page_type: 'article',
template_id: 'editorial',
current_version_id: 'version-1',
source_session_id: null,
source_message_id: null,
user_id: 'user-1',
space_id: 'space-1',
page_version_id: 'version-1',
version_no: 1,
bundle_asset_id: null,
storage_key: 'users/user-1/pages/page-1/versions/v1.md',
content: '# Report',
};
const pool = {
async query(sql, params = []) {
if (sql.includes('FROM h5_page_records p') && sql.includes('JOIN h5_page_versions')) {
return [[pageRow]];
}
if (
sql.includes('FROM h5_publish_records') &&
sql.includes("status = 'online'") &&
sql.includes('page_id = ?')
) {
return [[{ id: 'pub-online', url_slug: 'report', public_url: '/u/john/pages/report' }]];
}
if (sql.includes('FROM h5_publish_records pr') && sql.includes('pr.page_id <>')) {
return [[]];
}
return [[]];
},
async getConnection() {
throw new Error('publish should fail before opening a transaction');
},
};
const service = createPublicationService(pool, {
storageRoot: '/tmp',
h5Root: '/tmp',
idFactory: () => 'id-1',
});
await assert.rejects(
() =>
service.publish('user-1', 'page-1', {
pageVersionId: 'version-1',
accessMode: 'public',
urlSlug: 'report',
acknowledgedFindingIds: [],
}),
(error) => {
assert.equal(error.code, 'page_already_online');
assert.equal(error.details.publicationId, 'pub-online');
return true;
},
);
});