Files
memind/mindspace-page-sync.test.mjs
john ea6c49f046 feat(mindspace): 全部页面管理、级联删除与页面同步去重
支持全部页面分页/多选/删除/分享,删除时可选移除广场帖并清理工作区附件;修复并发同步重复创建页面,并补齐预览下载链接重写与 iframe 下载权限。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 18:08:06 +08:00

179 lines
5.2 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 { syncGeneratedPagesFromPublicAssets } from './mindspace-page-sync.mjs';
test('syncGeneratedPagesFromPublicAssets registers html files from publish public dir', async () => {
const publishDir = await fs.mkdtemp(path.join(os.tmpdir(), 'page-sync-publish-'));
const publicDir = path.join(publishDir, 'public');
await fs.mkdir(publicDir, { recursive: true });
await fs.writeFile(
path.join(publicDir, 'demo.html'),
'<html><head><title>Demo Page</title></head><body>Hi</body></html>',
'utf8',
);
const created = [];
const pool = {
async query(sql) {
if (sql.includes('FROM h5_page_records p')) return [[]];
if (sql.includes('FROM h5_assets a')) return [[]];
return [[]];
},
};
const pageService = {
async createFromChat(userId, input, source) {
created.push({ userId, input, source });
return { id: 'page-new' };
},
async updatePage() {
throw new Error('should not update');
},
};
const result = await syncGeneratedPagesFromPublicAssets({
pool,
pageService,
assetService: null,
userId: 'user-1',
publishDir,
});
assert.equal(result.created, 1);
assert.equal(created.length, 1);
assert.equal(created[0].input.title, 'Demo Page');
assert.equal(created[0].source.snapshot.relative_path, 'public/demo.html');
});
test('syncGeneratedPagesFromPublicAssets uses file mtime as createdAt', async () => {
const publishDir = await fs.mkdtemp(path.join(os.tmpdir(), 'page-sync-mtime-'));
const publicDir = path.join(publishDir, 'public');
await fs.mkdir(publicDir, { recursive: true });
const htmlPath = path.join(publicDir, 'timed.html');
await fs.writeFile(
htmlPath,
'<html><head><title>Timed</title></head><body>Hi</body></html>',
'utf8',
);
const mtimeMs = Date.parse('2026-06-20T10:00:00.000Z');
await fs.utimes(htmlPath, mtimeMs / 1000, mtimeMs / 1000);
const created = [];
const pool = {
async query(sql) {
if (sql.includes('FROM h5_page_records p')) return [[]];
if (sql.includes('FROM h5_assets a')) return [[]];
return [[]];
},
};
const pageService = {
async createFromChat(userId, input, source) {
created.push({ userId, input, source });
return { id: 'page-new' };
},
async updatePage() {
throw new Error('should not update');
},
};
await syncGeneratedPagesFromPublicAssets({
pool,
pageService,
assetService: null,
userId: 'user-1',
publishDir,
});
assert.equal(created.length, 1);
assert.equal(created[0].source.createdAt, mtimeMs);
});
test('syncGeneratedPagesFromPublicAssets creates pages for unlinked public html assets', async () => {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'page-sync-'));
const htmlPath = path.join(tempDir, 'demo.html');
await fs.writeFile(htmlPath, '<html><head><title>Demo</title></head><body>Hi</body></html>', 'utf8');
const created = [];
const pool = {
async query(sql) {
if (sql.includes('FROM h5_page_records p')) return [[]];
if (sql.includes('FROM h5_assets a')) {
return [[{ id: 'asset-1', display_name: 'demo.html', original_filename: 'demo.html', updated_at: 100 }]];
}
return [[]];
},
};
const pageService = {
async createFromChat(userId, input, source) {
created.push({ userId, input, source });
return { id: 'page-new' };
},
async updatePage() {
throw new Error('should not update');
},
};
const assetService = {
async readAsset() {
return { path: htmlPath };
},
};
const result = await syncGeneratedPagesFromPublicAssets({
pool,
pageService,
assetService,
userId: 'user-1',
});
assert.equal(result.created, 1);
assert.equal(created[0].source.assetId, 'asset-1');
});
test('syncGeneratedPagesFromPublicAssets reuses db page when memory index is empty', async () => {
const publishDir = await fs.mkdtemp(path.join(os.tmpdir(), 'page-sync-dedupe-'));
const publicDir = path.join(publishDir, 'public');
await fs.mkdir(publicDir, { recursive: true });
await fs.writeFile(
path.join(publicDir, 'demo.html'),
'<html><head><title>Demo Page</title></head><body>Hi</body></html>',
'utf8',
);
const created = [];
const updated = [];
const pool = {
async query(sql) {
if (sql.includes('JSON_UNQUOTE(JSON_EXTRACT(pv.source_snapshot_json')) {
return [[{ id: 'page-existing', updated_at: 100 }]];
}
if (sql.includes('FROM h5_page_records p')) return [[]];
if (sql.includes('FROM h5_assets a')) return [[]];
return [[]];
},
};
const pageService = {
async createFromChat() {
created.push(true);
return { id: 'page-new' };
},
async updatePage(userId, pageId) {
updated.push({ userId, pageId });
return { id: pageId };
},
};
const result = await syncGeneratedPagesFromPublicAssets({
pool,
pageService,
assetService: null,
userId: 'user-1',
publishDir,
});
assert.equal(result.created, 0);
assert.equal(created.length, 0);
assert.equal(updated.length, 1);
assert.equal(updated[0].pageId, 'page-existing');
});