import assert from 'node:assert/strict'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import test from 'node:test'; import { createMindSpaceWorkspaceToolService, normalizeMindSpaceWorkspacePath, parseMindSpaceWorkspaceRef, } from './mindspace-workspace-tool-service.mjs'; function createSetup() { const calls = []; const workspaceRoot = fs.mkdtempSync( path.join( os.tmpdir(), 'mindspace-workspace-tools-', ), ); fs.mkdirSync( path.join(workspaceRoot, 'public'), { recursive: true }, ); const service = createMindSpaceWorkspaceToolService({ h5Root: '/srv/h5', resolveMindSpaceUserPublishDirFn() { return workspaceRoot; }, async syncWorkspaceAssets( userId, options, ) { calls.push({ kind: 'asset-sync', userId, options, }); return { imported: 1, updated: 0, skipped: 0, }; }, conversationArtifactService: { async registerPublicHtmlArtifacts( input, ) { calls.push({ kind: 'public-html', input, }); return [ { artifactId: 'artifact-1' }, ]; }, async registerWorkspaceFileArtifact( input, ) { calls.push({ kind: 'workspace-artifact', input, }); return { id: 'artifact-workspace-1', }; }, }, workspacePageDeliveryService: { async syncAndDeliver( userId, options, ) { calls.push({ kind: 'deliver', userId, options, }); return { publish: { published: 1, }, }; }, }, buildMindSpacePublicUrlForUserFn({ user, relativePath, }) { return `https://example.com/MindSpace/${user.id}/${relativePath}`; }, async renderLongImageFn({ url, outputPath, }) { calls.push({ kind: 'render-long-image', url, outputPath, }); fs.writeFileSync( outputPath, Buffer.from([ 0x89, 0x50, 0x4e, 0x47, ]), ); return { bytes: 4 }; }, }); return { calls, service, workspaceRoot, scope: { workspaceRef: 'mindspace://users/user-1/workspace', sessionId: 'session-1', packageId: 'cp_session-1', }, }; } test('workspace tool service reads, writes, edits, and registers generated artifacts', async (t) => { const setup = createSetup(); t.after(() => { fs.rmSync(setup.workspaceRoot, { recursive: true, force: true, }); }); const written = await setup.service.writeFile({ ...setup.scope, path: 'public/report.html', content: 'Report', messageId: 'message-1', }); const edited = await setup.service.editFile({ ...setup.scope, path: 'public/report.html', old_str: 'Report', new_str: 'Updated', messageId: 'message-1', }); const read = await setup.service.readFile({ ...setup.scope, path: 'public/report.html', }); const listed = await setup.service.listDirectory({ ...setup.scope, path: 'public', }); assert.equal( written.relativePath, 'public/report.html', ); assert.equal( edited.packageId, 'cp_session-1', ); assert.match(read.content, /Updated/); assert.deepEqual(listed.entries, [ { name: 'report.html', type: 'file' }, ]); assert.deepEqual( setup.calls .map((call) => call.kind), [ 'public-html', 'asset-sync', 'public-html', 'asset-sync', ], ); assert.deepEqual( setup.calls[1].options.onlyRelativePaths, ['public/report.html'], ); }); test('non-public workspace writes are synchronized and registered even when asset sync may skip unchanged content', async (t) => { const setup = createSetup(); t.after(() => { fs.rmSync(setup.workspaceRoot, { recursive: true, force: true, }); }); const result = await setup.service.writeFile({ ...setup.scope, path: 'oa/notes.txt', content: 'generated notes', messageId: 'message-2', }); assert.deepEqual( setup.calls.map((call) => call.kind), ['workspace-artifact', 'asset-sync'], ); assert.equal( setup.calls[0].input.relativePath, 'oa/notes.txt', ); assert.equal( setup.calls[0].input.body, 'generated notes', ); assert.equal( result.registration.workspaceFileArtifact.id, 'artifact-workspace-1', ); }); test('binary workspace writes persist and register package artifacts without exposing physical paths', async (t) => { const setup = createSetup(); t.after(() => { fs.rmSync(setup.workspaceRoot, { recursive: true, force: true, }); }); const body = Buffer.from( 'PK generated docx', 'utf8', ); const result = await setup.service.writeBinaryFile({ ...setup.scope, path: 'public/report.docx', bodyBase64: body.toString('base64'), messageId: 'message-docx', }); assert.deepEqual( fs.readFileSync( path.join( setup.workspaceRoot, 'public', 'report.docx', ), ), body, ); assert.deepEqual( setup.calls.map((call) => call.kind), ['workspace-artifact', 'asset-sync'], ); assert.equal( Buffer.isBuffer( setup.calls[0].input.body, ), true, ); assert.equal(result.sizeBytes, body.length); assert.doesNotMatch( JSON.stringify(result), new RegExp(setup.workspaceRoot), ); }); test('long-image generation runs inside MindSpace and registers the generated PNG', async (t) => { const setup = createSetup(); t.after(() => { fs.rmSync(setup.workspaceRoot, { recursive: true, force: true, }); }); fs.writeFileSync( path.join( setup.workspaceRoot, 'public', 'report.html', ), 'Report', ); const result = await setup.service.generateLongImage({ ...setup.scope, htmlPath: 'public/report.html', outputPath: 'public/report.long.png', messageId: 'message-image', }); assert.deepEqual( setup.calls.map((call) => call.kind), [ 'render-long-image', 'workspace-artifact', 'asset-sync', ], ); assert.equal( setup.calls[1].input.relativePath, 'public/report.long.png', ); assert.equal( setup.calls[0].url, 'https://example.com/MindSpace/user-1/public/report.html', ); assert.equal( result.canonicalUrl, 'https://example.com/MindSpace/user-1/public/report.long.png', ); assert.doesNotMatch( JSON.stringify(result), new RegExp(setup.workspaceRoot), ); }); test('workspace tool service refuses to start without direct package artifact authority', () => { assert.throws( () => createMindSpaceWorkspaceToolService({ h5Root: '/srv/h5', conversationArtifactService: { async registerPublicHtmlArtifacts() { return []; }, }, }), /registerWorkspaceFileArtifact/, ); }); test('publish_page returns a canonical logical URL and delivers only the selected page', async (t) => { const setup = createSetup(); t.after(() => { fs.rmSync(setup.workspaceRoot, { recursive: true, force: true, }); }); fs.writeFileSync( path.join( setup.workspaceRoot, 'public', 'report.html', ), 'Report', ); const result = await setup.service.publishPage({ ...setup.scope, path: 'public/report.html', }); assert.equal( result.canonicalUrl, 'https://example.com/MindSpace/user-1/public/report.html', ); assert.equal( setup.calls.at(-1).kind, 'deliver', ); assert.deepEqual( setup.calls.at(-1).options .pageDataRelativePaths, ['public/report.html'], ); }); test('workspace tool service rejects path, package, browser-storage, and symlink escapes', async (t) => { const setup = createSetup(); const outsideRoot = fs.mkdtempSync( path.join(os.tmpdir(), 'mindspace-outside-'), ); t.after(() => { fs.rmSync(setup.workspaceRoot, { recursive: true, force: true, }); fs.rmSync(outsideRoot, { recursive: true, force: true, }); }); fs.symlinkSync( outsideRoot, path.join(setup.workspaceRoot, 'escape'), ); await assert.rejects( setup.service.writeFile({ ...setup.scope, path: '../outside.html', content: 'bad', }), (error) => error?.code === 'invalid_workspace_path', ); await assert.rejects( setup.service.writeFile({ ...setup.scope, packageId: 'cp_other-session', path: 'public/report.html', content: 'bad', }), (error) => error?.code === 'invalid_workspace_package_scope', ); await assert.rejects( setup.service.writeFile({ ...setup.scope, path: 'public/report.html', content: '', }), /localStorage/, ); await assert.rejects( setup.service.writeFile({ ...setup.scope, path: 'escape/report.html', content: 'bad', }), (error) => error?.code === 'invalid_workspace_path', ); await assert.rejects( setup.service.writeBinaryFile({ ...setup.scope, path: 'public/report.docx', bodyBase64: 'not-base64', }), (error) => error?.code === 'invalid_workspace_binary', ); }); test('workspace refs and logical paths normalize without exposing physical roots', () => { assert.deepEqual( parseMindSpaceWorkspaceRef( 'mindspace://users/user-1/workspace', ), { workspaceRef: 'mindspace://users/user-1/workspace', userId: 'user-1', }, ); assert.equal( normalizeMindSpaceWorkspacePath( 'public\\report.html', ), 'public/report.html', ); assert.throws( () => normalizeMindSpaceWorkspacePath( '/srv/h5/MindSpace/user-1/public/report.html', ), (error) => error?.code === 'invalid_workspace_path', ); assert.throws( () => parseMindSpaceWorkspaceRef( 'mindspace://users/user%2Fother/workspace', ), (error) => error?.code === 'invalid_workspace_ref', ); });