Files
memind/mindspace-workspace-tool-service.test.mjs
2026-07-27 15:34:35 +08:00

478 lines
10 KiB
JavaScript

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:
'<!doctype html><title>Report</title>',
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',
),
'<!doctype html><title>Report</title>',
);
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',
),
'<!doctype html><title>Report</title>',
);
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:
'<script>localStorage.setItem("x", "y")</script>',
}),
/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',
);
});