Files
memind/mindspace-public-finish-sync.test.mjs
T
john 0ae7a6a677 fix(mindspace): 公开页上传图片物化到 static 路径供转发访问
Finish 同步与首次访问时将私有资产 API 图片复制到 public/.tmp-images,
避免其他用户转发链接后因无登录态无法加载页面内图片。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 17:03:47 +08:00

748 lines
24 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.
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import {
hasRecentOwnPublicHtmlReference,
isStubPublicHtmlContent,
collectOwnPublicHtmlArtifactRefs,
collectOwnPublicHtmlRelativePaths,
materializeMissingPublicHtmlWrites,
materializePublicHtmlWritesFromSessionEvent,
syncPublicDocxDownloads,
syncPublicHtmlAfterFinish,
} from './mindspace-public-finish-sync.mjs';
const CURRENT_USER = { id: 'a6fb1e97-2b0f-447b-b138-4561d8e5c53e', username: 'john' };
test('detects a recent own public html link', () => {
const messages = [
{
role: 'assistant',
content: [
{
type: 'text',
text: '[Hello](https://m.tkmind.cn/MindSpace/a6fb1e97-2b0f-447b-b138-4561d8e5c53e/public/hello.html)',
},
],
metadata: { userVisible: true },
},
];
assert.equal(hasRecentOwnPublicHtmlReference(messages, CURRENT_USER), true);
});
test('detects a recent public html tool write hint without a full url', () => {
const messages = [
{
role: 'user',
content: [
{
type: 'toolResponse',
toolResult: {
value: {
content: [{ type: 'text', text: '已写入 public/guizhou-guide.html862 行)' }],
},
},
},
],
metadata: { userVisible: true, displayText: '已写入 public/guizhou-guide.html862 行)' },
},
];
assert.equal(hasRecentOwnPublicHtmlReference(messages, CURRENT_USER), true);
});
test('detects a public html tool request even when the message has no text', () => {
const messages = [
{
role: 'assistant',
content: [
{
type: 'toolRequest',
toolCall: {
value: {
name: 'sandbox-fs__write_file',
arguments: {
path: 'public/winter-melon-soup-recipe.html',
content: '<!doctype html><title>Winter melon soup</title>',
},
},
},
},
],
metadata: { userVisible: true },
},
];
assert.equal(hasRecentOwnPublicHtmlReference(messages, CURRENT_USER), true);
});
test('detects public html references beyond the old eight message window', () => {
const messages = [
{
role: 'assistant',
content: [
{
type: 'toolRequest',
toolCall: {
value: {
name: 'sandbox-fs__write_file',
arguments: {
path: 'public/long-flow.html',
content: '<!doctype html><title>Long flow</title>',
},
},
},
},
],
},
...Array.from({ length: 12 }, (_, index) => ({
role: index % 2 === 0 ? 'assistant' : 'user',
content: [{ type: 'text', text: `validation step ${index + 1}` }],
})),
];
assert.equal(hasRecentOwnPublicHtmlReference(messages, CURRENT_USER), true);
});
test('ignores other users public html links', () => {
const messages = [
{
role: 'assistant',
content: [
{
type: 'text',
text: '[Hello](https://m.tkmind.cn/MindSpace/other-user/public/hello.html)',
},
],
metadata: { userVisible: true },
},
];
assert.equal(hasRecentOwnPublicHtmlReference(messages, CURRENT_USER), false);
});
test('materializeMissingPublicHtmlWrites writes html from sandbox write_file tool calls', () => {
const publishDir = fs.mkdtempSync(path.join(os.tmpdir(), 'public-finish-sync-'));
try {
const messages = [
{
id: 'msg-public-1',
role: 'assistant',
content: [
{
type: 'toolRequest',
toolCall: {
value: {
name: 'sandbox-fs__write_file',
arguments: {
path: 'public/summer-essay.html',
content: '<!doctype html><title>Summer</title>',
},
},
},
},
],
metadata: { userVisible: true },
},
];
const result = materializeMissingPublicHtmlWrites({ messages, publishDir });
assert.deepEqual(result.materialized, ['public/summer-essay.html']);
assert.equal(
fs.readFileSync(path.join(publishDir, 'public/summer-essay.html'), 'utf8'),
'<!doctype html><title>Summer</title>',
);
} finally {
fs.rmSync(publishDir, { recursive: true, force: true });
}
});
test('syncPublicHtmlAfterFinish forwards session source to workspace sync', async () => {
const publishDir = fs.mkdtempSync(path.join(os.tmpdir(), 'public-finish-sync-'));
try {
const calls = [];
const messages = [
{
id: 'msg-public-1',
role: 'assistant',
content: [
{
type: 'toolRequest',
toolCall: {
value: {
name: 'write_file',
arguments: {
path: 'public/session-page.html',
content: '<!doctype html><title>Session page</title>',
},
},
},
},
],
},
];
const result = await syncPublicHtmlAfterFinish({
messages,
currentUser: CURRENT_USER,
publishDir,
sessionId: 'session-1',
syncWorkspaceAssets: async (...args) => {
calls.push(args);
},
});
assert.equal(result.synced, true);
assert.deepEqual(calls, [[
CURRENT_USER.id,
{
categoryCode: 'public',
sourceSessionId: 'session-1',
sourceMessageId: 'msg-public-1',
},
]]);
} finally {
fs.rmSync(publishDir, { recursive: true, force: true });
}
});
test('syncPublicHtmlAfterFinish registers existing public html artifacts for the session', async () => {
const publishDir = fs.mkdtempSync(path.join(os.tmpdir(), 'public-finish-sync-'));
try {
fs.mkdirSync(path.join(publishDir, 'public'), { recursive: true });
fs.writeFileSync(path.join(publishDir, 'public/123.html'), '<!doctype html><title>一二三</title>');
const calls = [];
const messages = [
{
id: 'msg-public-1',
role: 'assistant',
content: [
{
type: 'text',
text: `[一二三](https://m.tkmind.cn/MindSpace/${CURRENT_USER.id}/public/123.html)`,
},
],
},
];
const result = await syncPublicHtmlAfterFinish({
messages,
currentUser: CURRENT_USER,
publishDir,
sessionId: 'session-1',
syncWorkspaceAssets: async () => {},
registerPublicHtmlArtifacts: async (...args) => {
calls.push(args);
},
});
assert.deepEqual(result.publicHtmlRelativePaths, ['public/123.html']);
assert.deepEqual(calls, [
[
CURRENT_USER.id,
{
sessionId: 'session-1',
relativePaths: ['public/123.html'],
artifactRefs: [{ relativePath: 'public/123.html', messageId: 'msg-public-1' }],
},
],
]);
} finally {
fs.rmSync(publishDir, { recursive: true, force: true });
}
});
test('syncPublicHtmlAfterFinish materializes private image assets into public html files', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'public-finish-sync-assets-'));
const publishDir = path.join(root, 'MindSpace', CURRENT_USER.id);
const storageRoot = path.join(root, 'storage');
const assetId = '55555555-5555-4555-8555-555555555555';
const storageKey = `users/${CURRENT_USER.id}/assets/${assetId}/versions/v1`;
fs.mkdirSync(path.dirname(path.join(storageRoot, storageKey)), { recursive: true });
fs.writeFileSync(path.join(storageRoot, storageKey), Buffer.from([0xff, 0xd8, 0xff, 0xd9]));
try {
fs.mkdirSync(path.join(publishDir, 'public'), { recursive: true });
fs.writeFileSync(
path.join(publishDir, 'public/tea.html'),
`<!doctype html><img src="/api/mindspace/v1/assets/${assetId}/download?inline=1">`,
);
const messages = [
{
id: 'msg-tea-1',
role: 'assistant',
content: [
{
type: 'text',
text: `[茶馆](https://m.tkmind.cn/MindSpace/${CURRENT_USER.id}/public/tea.html)`,
},
],
},
];
const pool = {
query: async () => [
[{ id: assetId, mime_type: 'image/jpeg', original_filename: 'tea.jpg', storage_key: storageKey }],
],
};
const result = await syncPublicHtmlAfterFinish({
messages,
currentUser: CURRENT_USER,
publishDir,
sessionId: 'session-tea',
pool,
storageRoot,
h5Root: root,
syncWorkspaceAssets: async () => {},
});
assert.equal(result.assetMaterialization.materialized.length, 1);
const saved = fs.readFileSync(path.join(publishDir, 'public/tea.html'), 'utf8');
assert.match(saved, /\.tmp-images\/55555555-5555-4555-8555-555555555555\.jpg/);
assert.ok(fs.existsSync(path.join(publishDir, 'public/.tmp-images', `${assetId}.jpg`)));
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});
test('collectOwnPublicHtmlArtifactRefs includes source message ids when available', () => {
const publishDir = fs.mkdtempSync(path.join(os.tmpdir(), 'public-finish-sync-'));
try {
fs.mkdirSync(path.join(publishDir, 'public'), { recursive: true });
fs.writeFileSync(path.join(publishDir, 'public/own.html'), '<!doctype html><title>Own</title>');
const refs = collectOwnPublicHtmlArtifactRefs({
publishDir,
currentUser: CURRENT_USER,
messages: [
{
id: 'assistant-msg-1',
content: [
{
type: 'text',
text: `https://m.tkmind.cn/MindSpace/${CURRENT_USER.id}/public/own.html`,
},
],
},
],
});
assert.deepEqual(refs, [{ relativePath: 'public/own.html', messageId: 'assistant-msg-1' }]);
} finally {
fs.rmSync(publishDir, { recursive: true, force: true });
}
});
test('collectOwnPublicHtmlRelativePaths ignores missing and other-user public html links', () => {
const publishDir = fs.mkdtempSync(path.join(os.tmpdir(), 'public-finish-sync-'));
try {
fs.mkdirSync(path.join(publishDir, 'public'), { recursive: true });
fs.writeFileSync(path.join(publishDir, 'public/own.html'), '<!doctype html><title>Own</title>');
const paths = collectOwnPublicHtmlRelativePaths({
publishDir,
currentUser: CURRENT_USER,
messages: [
{
content: [
{
type: 'text',
text: [
`https://m.tkmind.cn/MindSpace/${CURRENT_USER.id}/public/own.html`,
`https://m.tkmind.cn/MindSpace/${CURRENT_USER.id}/public/missing.html`,
'https://m.tkmind.cn/MindSpace/other-user/public/other.html',
].join('\n'),
},
],
},
],
});
assert.deepEqual(paths, ['public/own.html']);
} finally {
fs.rmSync(publishDir, { recursive: true, force: true });
}
});
test('materializeMissingPublicHtmlWrites writes html from developer write tool calls', () => {
const publishDir = fs.mkdtempSync(path.join(os.tmpdir(), 'public-finish-sync-'));
try {
const messages = [
{
role: 'assistant',
content: [
{
type: 'toolRequest',
toolCall: {
value: {
name: 'write',
arguments: {
path: 'public/guizhou-guide.html',
content: '<!doctype html><title>Guizhou</title>',
},
},
},
},
],
},
];
const result = materializeMissingPublicHtmlWrites({ messages, publishDir });
assert.deepEqual(result.materialized, ['public/guizhou-guide.html']);
assert.equal(
fs.readFileSync(path.join(publishDir, 'public/guizhou-guide.html'), 'utf8'),
'<!doctype html><title>Guizhou</title>',
);
} finally {
fs.rmSync(publishDir, { recursive: true, force: true });
}
});
test('materializeMissingPublicHtmlWrites replaces stub placeholder html', () => {
const publishDir = fs.mkdtempSync(path.join(os.tmpdir(), 'public-finish-sync-'));
try {
const stubPath = path.join(publishDir, 'public/guide.html');
fs.mkdirSync(path.dirname(stubPath), { recursive: true });
fs.writeFileSync(stubPath, '<html>根据你的要求临时补出的简版页面</html>', 'utf8');
assert.equal(isStubPublicHtmlContent(fs.readFileSync(stubPath, 'utf8')), true);
const messages = [
{
role: 'assistant',
content: [
{
type: 'toolRequest',
toolCall: {
value: {
name: 'write_file',
arguments: {
path: 'public/guide.html',
content: '<!doctype html><title>Real Guide</title><body>Full page</body>',
},
},
},
},
],
},
];
const result = materializeMissingPublicHtmlWrites({ messages, publishDir });
assert.deepEqual(result.materialized, ['public/guide.html']);
assert.match(fs.readFileSync(stubPath, 'utf8'), /Real Guide/);
} finally {
fs.rmSync(publishDir, { recursive: true, force: true });
}
});
test('syncPublicDocxDownloads copies newer complete oa docx into public download targets', () => {
const publishDir = fs.mkdtempSync(path.join(os.tmpdir(), 'public-docx-sync-'));
try {
const oaDir = path.join(publishDir, 'oa');
const publicDir = path.join(publishDir, 'public');
fs.mkdirSync(oaDir, { recursive: true });
fs.mkdirSync(publicDir, { recursive: true });
fs.writeFileSync(path.join(oaDir, 'latest-plan.docx'), 'x'.repeat(9000));
fs.writeFileSync(path.join(publicDir, 'download.html'), '<a href="old-plan.docx">download</a>');
fs.writeFileSync(path.join(publicDir, 'old-plan.docx'), 'stub');
const result = syncPublicDocxDownloads({ publishDir, minCompleteSize: 8000 });
assert.deepEqual(result.synced, ['public/old-plan.docx']);
assert.equal(result.source, 'latest-plan.docx');
assert.equal(fs.readFileSync(path.join(publicDir, 'old-plan.docx'), 'utf8').length, 9000);
} finally {
fs.rmSync(publishDir, { recursive: true, force: true });
}
});
test('syncPublicDocxDownloads skips public docx that is already up to date', () => {
const publishDir = fs.mkdtempSync(path.join(os.tmpdir(), 'public-docx-sync-'));
try {
const oaDir = path.join(publishDir, 'oa');
const publicDir = path.join(publishDir, 'public');
fs.mkdirSync(oaDir, { recursive: true });
fs.mkdirSync(publicDir, { recursive: true });
fs.writeFileSync(path.join(oaDir, 'latest-plan.docx'), 'x'.repeat(9000));
fs.writeFileSync(path.join(publicDir, 'download.html'), '<a href="old-plan.docx">download</a>');
fs.writeFileSync(path.join(publicDir, 'old-plan.docx'), 'x'.repeat(9000));
const result = syncPublicDocxDownloads({ publishDir, minCompleteSize: 8000 });
assert.deepEqual(result.synced, []);
assert.deepEqual(result.skipped, ['public/old-plan.docx']);
} finally {
fs.rmSync(publishDir, { recursive: true, force: true });
}
});
test('syncPublicDocxDownloads copies same-named oa docx even when it is small', () => {
const publishDir = fs.mkdtempSync(path.join(os.tmpdir(), 'public-docx-sync-'));
try {
const oaDir = path.join(publishDir, 'oa');
const publicDir = path.join(publishDir, 'public');
fs.mkdirSync(oaDir, { recursive: true });
fs.mkdirSync(publicDir, { recursive: true });
fs.writeFileSync(path.join(oaDir, 'industry-distribution-report.docx'), 'x'.repeat(3294));
fs.writeFileSync(
path.join(publicDir, 'industry-distribution-report.html'),
'<a href="industry-distribution-report.docx" download>download</a>',
);
const result = syncPublicDocxDownloads({ publishDir, minCompleteSize: 1024 });
assert.deepEqual(result.synced, ['public/industry-distribution-report.docx']);
assert.deepEqual(result.missing, []);
assert.equal(result.source, 'industry-distribution-report.docx');
assert.equal(
fs.readFileSync(path.join(publicDir, 'industry-distribution-report.docx'), 'utf8').length,
3294,
);
} finally {
fs.rmSync(publishDir, { recursive: true, force: true });
}
});
test('syncPublicDocxDownloads reports missing public docx when no safe source exists', () => {
const publishDir = fs.mkdtempSync(path.join(os.tmpdir(), 'public-docx-sync-'));
try {
const oaDir = path.join(publishDir, 'oa');
const publicDir = path.join(publishDir, 'public');
fs.mkdirSync(oaDir, { recursive: true });
fs.mkdirSync(publicDir, { recursive: true });
fs.writeFileSync(path.join(oaDir, '暑假计划.docx'), 'x'.repeat(4084));
fs.writeFileSync(path.join(oaDir, '韩国旅游攻略.docx'), 'x'.repeat(5889));
fs.writeFileSync(
path.join(publicDir, 'industry-distribution-report.html'),
'<a href="industry-distribution-report.docx" download>download</a>',
);
const result = syncPublicDocxDownloads({ publishDir, minCompleteSize: 1024 });
assert.deepEqual(result.synced, []);
assert.deepEqual(result.missing, ['public/industry-distribution-report.docx']);
assert.equal(
fs.existsSync(path.join(publicDir, 'industry-distribution-report.docx')),
false,
);
} finally {
fs.rmSync(publishDir, { recursive: true, force: true });
}
});
test('materializeMissingPublicHtmlWrites replaces existing real html when content changed', () => {
const publishDir = fs.mkdtempSync(path.join(os.tmpdir(), 'public-finish-sync-'));
try {
const htmlPath = path.join(publishDir, 'public/guide.html');
fs.mkdirSync(path.dirname(htmlPath), { recursive: true });
fs.writeFileSync(htmlPath, '<!doctype html><title>Existing</title>', 'utf8');
const result = materializeMissingPublicHtmlWrites({
messages: [
{
role: 'assistant',
content: [
{
type: 'toolRequest',
toolCall: {
value: {
name: 'write_file',
arguments: {
path: 'public/guide.html',
content: '<!doctype html><title>Replacement</title>',
},
},
},
},
],
},
],
publishDir,
});
assert.deepEqual(result.materialized, ['public/guide.html']);
assert.deepEqual(result.skipped, []);
assert.match(fs.readFileSync(htmlPath, 'utf8'), /Replacement/);
} finally {
fs.rmSync(publishDir, { recursive: true, force: true });
}
});
test('materializeMissingPublicHtmlWrites skips existing real html when content is unchanged', () => {
const publishDir = fs.mkdtempSync(path.join(os.tmpdir(), 'public-finish-sync-'));
try {
const htmlPath = path.join(publishDir, 'public/guide.html');
const html = '<!doctype html><title>Existing</title>';
fs.mkdirSync(path.dirname(htmlPath), { recursive: true });
fs.writeFileSync(htmlPath, html, 'utf8');
const result = materializeMissingPublicHtmlWrites({
messages: [
{
role: 'assistant',
content: [
{
type: 'toolRequest',
toolCall: {
value: {
name: 'write_file',
arguments: {
path: 'public/guide.html',
content: html,
},
},
},
},
],
},
],
publishDir,
});
assert.deepEqual(result.materialized, []);
assert.deepEqual(result.skipped, ['public/guide.html']);
assert.match(fs.readFileSync(htmlPath, 'utf8'), /Existing/);
} finally {
fs.rmSync(publishDir, { recursive: true, force: true });
}
});
test('detects a recent edit_file tool request on an existing public html page', () => {
const publishDir = fs.mkdtempSync(path.join(os.tmpdir(), 'public-finish-sync-'));
try {
const htmlPath = path.join(publishDir, 'public/guide.html');
fs.mkdirSync(path.dirname(htmlPath), { recursive: true });
fs.writeFileSync(htmlPath, '<body>一二三</body>', 'utf8');
const messages = [
{
role: 'assistant',
content: [
{
type: 'toolRequest',
toolCall: {
value: {
name: 'edit_file',
arguments: {
path: 'public/guide.html',
old_str: '一二三',
new_str: '二三四',
},
},
},
},
],
metadata: { userVisible: true },
},
];
assert.equal(hasRecentOwnPublicHtmlReference(messages, CURRENT_USER, { publishDir }), true);
} finally {
fs.rmSync(publishDir, { recursive: true, force: true });
}
});
test('materializeMissingPublicHtmlWrites applies edit_file patches onto existing public html', () => {
const publishDir = fs.mkdtempSync(path.join(os.tmpdir(), 'public-finish-sync-'));
try {
const htmlPath = path.join(publishDir, 'public/guide.html');
fs.mkdirSync(path.dirname(htmlPath), { recursive: true });
fs.writeFileSync(htmlPath, '<!doctype html><title>Existing</title><body>一二三</body>', 'utf8');
const result = materializeMissingPublicHtmlWrites({
messages: [
{
role: 'assistant',
content: [
{
type: 'toolRequest',
toolCall: {
value: {
name: 'edit_file',
arguments: {
path: 'public/guide.html',
old_str: '一二三',
new_str: '二三四',
},
},
},
},
],
},
],
publishDir,
});
assert.deepEqual(result.materialized, ['public/guide.html']);
assert.match(fs.readFileSync(htmlPath, 'utf8'), /二三四/);
assert.doesNotMatch(fs.readFileSync(htmlPath, 'utf8'), /一二三/);
} finally {
fs.rmSync(publishDir, { recursive: true, force: true });
}
});
test('materializeMissingPublicHtmlWrites chains multiple edit_file patches for the same html', () => {
const publishDir = fs.mkdtempSync(path.join(os.tmpdir(), 'public-finish-sync-'));
try {
const htmlPath = path.join(publishDir, 'public/guide.html');
fs.mkdirSync(path.dirname(htmlPath), { recursive: true });
fs.writeFileSync(htmlPath, '<body>一 · 二三</body>', 'utf8');
const result = materializeMissingPublicHtmlWrites({
messages: [
{
role: 'assistant',
content: [
{
type: 'toolRequest',
toolCall: {
value: {
name: 'edit_file',
arguments: {
path: 'public/guide.html',
old_str: '一',
new_str: '二',
},
},
},
},
{
type: 'toolRequest',
toolCall: {
value: {
name: 'edit_file',
arguments: {
path: 'public/guide.html',
old_str: '二三',
new_str: '三四',
},
},
},
},
],
},
],
publishDir,
});
assert.deepEqual(result.materialized, ['public/guide.html']);
assert.match(fs.readFileSync(htmlPath, 'utf8'), /二 · 三四/);
} finally {
fs.rmSync(publishDir, { recursive: true, force: true });
}
});
test('materializePublicHtmlWritesFromSessionEvent writes public html from message event before finish', () => {
const publishDir = fs.mkdtempSync(path.join(os.tmpdir(), 'public-event-sync-'));
try {
const result = materializePublicHtmlWritesFromSessionEvent(
{
type: 'Message',
message: {
id: 'assistant-1',
role: 'assistant',
metadata: { userVisible: true },
content: [
{
type: 'toolRequest',
toolCall: {
value: {
name: 'write',
arguments: {
path: 'public/shenwu-gate.html',
content: '<!doctype html><html><body>神口门</body></html>',
},
},
},
},
],
},
},
{ publishDir },
);
assert.deepEqual(result.materialized, ['public/shenwu-gate.html']);
assert.match(
fs.readFileSync(path.join(publishDir, 'public/shenwu-gate.html'), 'utf8'),
/神口门/,
);
} finally {
fs.rmSync(publishDir, { recursive: true, force: true });
}
});