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, hasPublicHtmlWriteRequestInSessionEvent, isStubPublicHtmlContent, collectOwnPublicHtmlArtifactRefs, collectOwnPublicHtmlRelativePaths, materializeMissingPublicHtmlWrites, materializePublicHtmlWritesFromSessionEvent, syncPublicDocxDownloads, syncPublicHtmlAfterFinish, } from './mindspace-public-finish-sync.mjs'; import { createMindSpacePublicFinishService } from './mindspace-public-finish-service.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.html(862 行)' }], }, }, }, ], metadata: { userVisible: true, displayText: '已写入 public/guizhou-guide.html(862 行)' }, }, ]; 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: 'Winter melon soup', }, }, }, }, ], metadata: { userVisible: true }, }, ]; assert.equal(hasRecentOwnPublicHtmlReference(messages, CURRENT_USER), true); }); test('stream event prefilter detects public HTML writes without filesystem access', () => { assert.equal( hasPublicHtmlWriteRequestInSessionEvent({ type: 'Message', message: { role: 'assistant', content: [ { type: 'toolRequest', toolCall: { value: { name: 'edit_file', arguments: { path: 'public/page.html', old_str: 'before', new_str: 'after', }, }, }, }, ], }, }), true, ); assert.equal( hasPublicHtmlWriteRequestInSessionEvent({ type: 'Message', message: { role: 'assistant', content: [{ type: 'text', text: 'done' }], }, }), false, ); }); 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: 'Long flow', }, }, }, }, ], }, ...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: 'Summer', }, }, }, }, ], 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'), 'Summer', ); } 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: 'Session page', }, }, }, }, ], }, ]; 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', onlyRelativePaths: ['public/session-page.html'], }, ]]); } 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'), '一二三'); 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'), ``, ); 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'), 'Own'); 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'), 'Own'); 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: 'Guizhou', }, }, }, }, ], }, ]; 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'), 'Guizhou', ); } 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, '根据你的要求临时补出的简版页面', '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: 'Real GuideFull page', }, }, }, }, ], }, ]; 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'), 'download'); 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'), 'download'); 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'), 'download', ); 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'), 'download', ); 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, 'Existing', 'utf8'); const result = materializeMissingPublicHtmlWrites({ messages: [ { role: 'assistant', content: [ { type: 'toolRequest', toolCall: { value: { name: 'write_file', arguments: { path: 'public/guide.html', content: 'Replacement', }, }, }, }, ], }, ], 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 = 'Existing'; 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, '一二三', '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, 'Existing一二三', '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, '一 · 二三', '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: '神口门', }, }, }, }, ], }, }, { 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 }); } }); function createPublicFinishService(overrides = {}) { const calls = []; const service = createMindSpacePublicFinishService({ pool: { query: async () => [[]] }, h5Root: '/srv/h5', storageRoot: '/srv/storage', env: { H5_PUBLIC_BASE_URL: 'https://example.com', }, conversationArtifactService: { async registerPublicHtmlArtifacts(input) { calls.push({ kind: 'artifacts', input }); return []; }, }, resolveMindSpaceUserPublishDirFn() { return '/srv/h5/MindSpace/user-1'; }, buildMindSpacePublicUrlForUserFn({ user, relativePath, }) { return `https://example.com/MindSpace/${user.id}/${relativePath}`; }, evaluateH5HtmlFinishGuardFn(input) { calls.push({ kind: 'delivery-evaluation', input, }); return { needsRepair: false, missingHtml: [], missingAssets: [], browserStorageViolations: [], htmlGenerationNeedsRetry: false, linkExists() { return true; }, }; }, async preparePageDataAfterFinishFn( input, ) { calls.push({ kind: 'page-data-preparation', input, }); return { autoBind: { bound: [], skipped: [], errors: [], }, evaluation: { structuralPageData: false, relevantFiles: [ { relativePath: 'public/page.html', absolutePath: '/srv/h5/MindSpace/user-1/public/page.html', content: '', }, ], htmlIssues: [], unboundFiles: [], needsRepair: false, }, }; }, ...overrides, }); return { calls, service }; } test('public finish service resolves stream writes and canonical URLs inside MindSpace', async () => { const calls = []; const setup = createPublicFinishService({ materializeSessionEventFn(event, options) { calls.push({ kind: 'materialize', event, options }); return { materialized: ['public/page.html'], skipped: [], }; }, collectArtifactRefsFn(input) { calls.push({ kind: 'collect', input }); return [ { relativePath: 'public/page.html', messageId: 'message-1', }, ]; }, }); const event = { type: 'Message', message: { id: 'message-1', role: 'assistant', }, }; const result = await setup.service.materializeSessionEvent({ userId: 'user-1', event, }); assert.equal( calls[0].options.publishDir, '/srv/h5/MindSpace/user-1', ); assert.deepEqual(result.publicHtmlRelativePaths, [ 'public/page.html', ]); assert.equal( result.publicHtmlArtifacts[0].canonicalUrl, 'https://example.com/MindSpace/user-1/public/page.html', ); }); test('public finish service owns WeChat HTML preparation, sync, and package registration', async () => { const calls = []; const setup = createPublicFinishService({ prepareWechatHtmlDeliveryAtWorkspaceFn( input, ) { calls.push({ kind: 'prepare-html', input, }); return { materialization: { materialized: [ 'public/page.html', ], skipped: [], }, publishedArtifacts: [ { relativePath: 'public/page.html', url: input.buildCanonicalUrl( 'public/page.html', ), exists: true, isStub: false, }, ], expectedArtifacts: [], recentArtifacts: [], confirmedArtifacts: [ { relativePath: 'public/page.html', url: input.buildCanonicalUrl( 'public/page.html', ), exists: true, isStub: false, }, ], verifiedArtifacts: [], validReplyUrls: [], hasValidReplyLink: false, }; }, async syncAfterFinishFn(input) { calls.push({ kind: 'finish-sync', input, }); return { synced: true }; }, async syncWorkspaceAssets( userId, options, ) { calls.push({ kind: 'asset-sync', userId, options, }); }, conversationArtifactService: { async registerPublicHtmlArtifacts( input, ) { calls.push({ kind: 'artifact-register', input, }); return []; }, }, }); const result = await setup.service .prepareWechatHtmlDelivery({ userId: 'user-1', sessionId: 'session-1', reply: { text: 'done', messages: [ { role: 'assistant', }, ], }, intent: { agentText: 'build page', }, }); assert.equal( calls[0].input.publishDir, '/srv/h5/MindSpace/user-1', ); assert.equal( result.confirmedArtifacts[0].url, 'https://example.com/MindSpace/user-1/public/page.html', ); assert.equal( Object.hasOwn( result.confirmedArtifacts[0], 'localPath', ), false, ); assert.ok( calls.some( (call) => call.kind === 'asset-sync' && call.options.onlyRelativePaths .includes('public/page.html'), ), ); assert.ok( calls.some( (call) => call.kind === 'artifact-register' && call.input.sessionId === 'session-1', ), ); }); test('public finish service owns WeChat fresh thumbnail rendering and metadata sync', async () => { const calls = []; const setup = createPublicFinishService({ async ensureWechatFreshPageThumbnailsAtWorkspaceFn( input, ) { calls.push({ kind: 'fresh-thumbnail', input, }); return { ok: true, reason: null, matchRelativePaths: [ 'public/page.html', ], thumbnailRelativePaths: [ 'public/page.thumbnail.svg', ], }; }, async syncWorkspaceAssets( userId, options, ) { calls.push({ kind: 'asset-sync', userId, options, }); }, conversationArtifactService: { async registerPublicHtmlArtifacts( input, ) { calls.push({ kind: 'artifact-register', input, }); return []; }, }, }); const result = await setup.service .ensureWechatFreshPageThumbnails({ userId: 'user-1', sessionId: 'session-1', artifacts: [ { relativePath: 'public/page.html', }, ], images: [ { jobId: 'job-1', }, ], repairEnabled: true, }); assert.equal(result.ok, true); assert.deepEqual( calls.find( (call) => call.kind === 'asset-sync', ).options.onlyRelativePaths, [ 'public/page.html', 'public/page.thumbnail.svg', ], ); assert.deepEqual( calls.find( (call) => call.kind === 'artifact-register', ).input.relativePaths, ['public/page.html'], ); }); test('public finish service owns WeChat Page Data delivery preparation', async () => { const calls = []; let outcomeCount = 0; const setup = createPublicFinishService({ createPageServiceFn(pool, options) { calls.push({ kind: 'page-service', pool, options, }); return { async findPageByRelativePath() { return { id: 'page-1' }; }, }; }, async resolvePageDataCollectOutcomeAsyncFn( input, ) { calls.push({ kind: 'outcome', input }); outcomeCount += 1; return outcomeCount === 1 ? { action: 'retry' } : { action: 'send', evaluation: { relevantFiles: [ { relativePath: 'public/page.html', absolutePath: '/srv/h5/MindSpace/user-1/public/page.html', content: '', }, ], }, }; }, async maybeAutoBindPageDataHtmlPagesFn( input, ) { calls.push({ kind: 'auto-bind', input }); return { bound: [ { relativePath: 'public/page.html', workspaceUrl: 'https://example.com/MindSpace/user-1/public/page.html', }, ], skipped: [], errors: [], }; }, buildPageDataDeliveryArtifactsFromBindResultFn( autoBind, publishDir, options, ) { calls.push({ kind: 'artifacts', autoBind, publishDir, options, }); return [ { relativePath: 'public/page.html', localPath: '/srv/h5/MindSpace/user-1/public/page.html', url: 'https://example.com/MindSpace/user-1/public/page.html', }, ]; }, async ensurePageDataDeliveryReadyFn(input) { calls.push({ kind: 'delivery-check', input, }); return { ok: true, failures: [] }; }, rewritePageDataDeliveryLinksFn( text, artifacts, ) { calls.push({ kind: 'rewrite', text, artifacts, }); return '已发布:workspace-url'; }, }); const result = await setup.service .prepareWechatPageDataDelivery({ userId: 'user-1', reply: { text: '已发布:legacy-url', messages: [], }, intent: { agentText: 'build survey', }, publicBaseUrl: 'https://example.com', requestStartedAt: 123, }); assert.deepEqual( calls .map((call) => call.kind), [ 'outcome', 'page-service', 'auto-bind', 'outcome', 'artifacts', 'delivery-check', 'rewrite', ], ); assert.equal( calls[0].input.publishDir, '/srv/h5/MindSpace/user-1', ); assert.equal( calls[2].input.storageRoot, '/srv/storage', ); assert.equal(result.outcome.action, 'send'); assert.equal(result.deliveryCheck.ok, true); assert.equal( result.rewrittenText, '已发布:workspace-url', ); assert.equal( Object.hasOwn( result.deliveryArtifacts[0], 'localPath', ), false, ); assert.equal( Object.hasOwn( result.outcome.evaluation .relevantFiles[0], 'absolutePath', ), false, ); }); test('public finish service keeps workspace sync and artifact registration inside MindSpace', async () => { const calls = []; const setup = createPublicFinishService({ async syncWorkspaceAssets(userId, options) { calls.push({ kind: 'workspace-assets', userId, options, }); }, async syncAfterFinishFn(input) { calls.push({ kind: 'finish', input }); await input.syncWorkspaceAssets('user-1', { categoryCode: 'public', }); await input.registerPublicHtmlArtifacts( 'user-1', { sessionId: 'session-1', relativePaths: ['public/page.html'], artifactRefs: [ { relativePath: 'public/page.html', messageId: 'message-1', }, ], }, ); return { materialized: ['public/page.html'], skipped: [], publicHtmlRelativePaths: [ 'public/page.html', ], publicHtmlArtifactRefs: [ { relativePath: 'public/page.html', messageId: 'message-1', }, ], }; }, }); const result = await setup.service.syncAfterFinish({ userId: 'user-1', sessionId: 'session-1', messages: [], }); assert.equal(calls[0].kind, 'finish'); assert.equal( calls[0].input.publishDir, '/srv/h5/MindSpace/user-1', ); assert.equal(calls[0].input.storageRoot, '/srv/storage'); assert.equal(calls[1].kind, 'workspace-assets'); assert.equal(setup.calls[0].kind, 'artifacts'); assert.equal( setup.calls[0].input.userId, 'user-1', ); assert.equal( result.publicHtmlArtifacts[0].canonicalUrl, 'https://example.com/MindSpace/user-1/public/page.html', ); assert.equal( result.deliveryEvaluation.needsRepair, false, ); assert.equal( Object.hasOwn( result.deliveryEvaluation, 'linkExists', ), false, ); const pageDataPreparation = await setup.service.preparePageDataAfterFinish({ userId: 'user-1', messages: [], userText: 'build page', }); assert.equal( pageDataPreparation.evaluation .relevantFiles[0].relativePath, 'public/page.html', ); assert.equal( Object.hasOwn( pageDataPreparation.evaluation .relevantFiles[0], 'absolutePath', ), false, ); assert.equal( Object.hasOwn( pageDataPreparation.evaluation .relevantFiles[0], 'content', ), false, ); const preparationCall = setup.calls.find( (call) => call.kind === 'page-data-preparation', ); assert.equal( preparationCall.input.publishDir, '/srv/h5/MindSpace/user-1', ); assert.equal( preparationCall.input.storageRoot, '/srv/storage', ); assert.equal( preparationCall.input.userText, 'build page', ); });