705 lines
19 KiB
JavaScript
705 lines
19 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import {
|
|
createMindSpaceRemoteServerAdapter,
|
|
mindspaceRemoteServerAdapterInternals,
|
|
validateMindSpaceRemoteContract,
|
|
} from './mindspace-remote-server-adapter.mjs';
|
|
import {
|
|
MINDSPACE_SERVER_ADAPTER_BINDINGS,
|
|
MINDSPACE_SERVER_ADAPTER_CONTRACT_VERSION,
|
|
MINDSPACE_SERVER_ADAPTER_REQUIRED_CAPABILITIES,
|
|
} from './mindspace-server-adapter-contract.mjs';
|
|
|
|
test('buildRemoteOperationUrl normalizes endpoint and encodes operation path', () => {
|
|
assert.equal(
|
|
mindspaceRemoteServerAdapterInternals.buildRemoteOperationUrl(
|
|
'https://mindspace.example.com/',
|
|
'assetService',
|
|
'renderAssetPreview',
|
|
'/internal/mindspace-adapter',
|
|
),
|
|
'https://mindspace.example.com/internal/mindspace-adapter/assetService/renderAssetPreview',
|
|
);
|
|
});
|
|
|
|
test('createMindSpaceRemoteServerAdapter validates endpoint at startup', async () => {
|
|
const adapter = createMindSpaceRemoteServerAdapter({
|
|
fetchFn: async () => {
|
|
throw new Error('should not fetch');
|
|
},
|
|
});
|
|
await assert.rejects(() => adapter.assertReady(), /MINDSPACE_REMOTE_BASE_URL/);
|
|
});
|
|
|
|
test('createMindSpaceRemoteServerAdapter validates remote contract before startup succeeds', async () => {
|
|
const calls = [];
|
|
const adapter = createMindSpaceRemoteServerAdapter({
|
|
endpoint: 'https://mindspace.example.com/',
|
|
authToken: 'secret-token',
|
|
fetchFn: async (url, init) => {
|
|
calls.push({ url, init });
|
|
return {
|
|
ok: true,
|
|
status: 200,
|
|
async text() {
|
|
return JSON.stringify({
|
|
contractVersion:
|
|
MINDSPACE_SERVER_ADAPTER_CONTRACT_VERSION,
|
|
bindings: MINDSPACE_SERVER_ADAPTER_BINDINGS,
|
|
requiredCapabilities:
|
|
MINDSPACE_SERVER_ADAPTER_REQUIRED_CAPABILITIES,
|
|
buildId: 'mindspace-test',
|
|
gitSha: 'abc123',
|
|
});
|
|
},
|
|
};
|
|
},
|
|
logger: { log() {}, warn() {}, error() {} },
|
|
});
|
|
|
|
await assert.doesNotReject(() => adapter.assertReady());
|
|
assert.equal(
|
|
calls[0].url,
|
|
'https://mindspace.example.com/mindspace/v1/contract',
|
|
);
|
|
assert.equal(calls[0].init.method, 'GET');
|
|
assert.equal(
|
|
calls[0].init.headers.Authorization,
|
|
'Bearer secret-token',
|
|
);
|
|
});
|
|
|
|
test('createMindSpaceRemoteServerAdapter rejects stale remote contracts', async () => {
|
|
const adapter = createMindSpaceRemoteServerAdapter({
|
|
endpoint: 'https://mindspace.example.com/',
|
|
fetchFn: async () => ({
|
|
ok: true,
|
|
status: 200,
|
|
async text() {
|
|
return JSON.stringify({
|
|
contractVersion: 1,
|
|
bindings: {
|
|
assetService: ['renderAssetPreview'],
|
|
},
|
|
requiredCapabilities: [],
|
|
});
|
|
},
|
|
}),
|
|
logger: { log() {}, warn() {}, error() {} },
|
|
});
|
|
|
|
await assert.rejects(
|
|
() => adapter.assertReady(),
|
|
(error) =>
|
|
error?.code === 'mindspace_contract_incompatible' &&
|
|
/workspacePublicationDeliveryService/.test(error.message),
|
|
);
|
|
});
|
|
|
|
test('validateMindSpaceRemoteContract reports missing capabilities and methods', () => {
|
|
assert.throws(
|
|
() =>
|
|
validateMindSpaceRemoteContract({
|
|
contractVersion:
|
|
MINDSPACE_SERVER_ADAPTER_CONTRACT_VERSION,
|
|
bindings: {
|
|
chatSaveService: ['createSharedHtml'],
|
|
},
|
|
requiredCapabilities: ['chat-save-authority'],
|
|
}),
|
|
/missing method chatSaveService\.materializeWorkspaceHtml/,
|
|
);
|
|
});
|
|
|
|
test('createMindSpaceRemoteServerAdapter proxies server binding methods through fetch transport', async () => {
|
|
const calls = [];
|
|
const adapter = createMindSpaceRemoteServerAdapter({
|
|
endpoint: 'https://mindspace.example.com/',
|
|
authToken: 'secret-token',
|
|
operationBasePath: '/internal/mindspace-adapter/',
|
|
timeoutMs: 3000,
|
|
fetchFn: async (url, init) => {
|
|
calls.push({ url, init });
|
|
return {
|
|
ok: true,
|
|
status: 200,
|
|
async text() {
|
|
return JSON.stringify({ ok: true, source: 'remote' });
|
|
},
|
|
};
|
|
},
|
|
logger: { log() {}, warn() {}, error() {} },
|
|
});
|
|
|
|
const result = await adapter.assetService.renderAssetPreview({ userId: 'u1', assetId: 'a1' });
|
|
|
|
assert.deepEqual(result, { ok: true, source: 'remote' });
|
|
assert.equal(calls.length, 1);
|
|
assert.equal(
|
|
calls[0].url,
|
|
'https://mindspace.example.com/internal/mindspace-adapter/assetService/renderAssetPreview',
|
|
);
|
|
assert.equal(calls[0].init.method, 'POST');
|
|
assert.equal(calls[0].init.headers.Authorization, 'Bearer secret-token');
|
|
assert.deepEqual(JSON.parse(calls[0].init.body), {
|
|
args: [{ userId: 'u1', assetId: 'a1' }],
|
|
});
|
|
});
|
|
|
|
test('createMindSpaceRemoteServerAdapter exposes conversation package read methods', async () => {
|
|
const calls = [];
|
|
const adapter = createMindSpaceRemoteServerAdapter({
|
|
endpoint: 'https://mindspace.example.com/',
|
|
authToken: 'secret-token',
|
|
fetchFn: async (url, init) => {
|
|
calls.push({ url, init });
|
|
return {
|
|
ok: true,
|
|
status: 200,
|
|
async text() {
|
|
return JSON.stringify({ packageId: 'cp-session-1' });
|
|
},
|
|
};
|
|
},
|
|
logger: { log() {}, warn() {}, error() {} },
|
|
});
|
|
|
|
await adapter.conversationPackageRegistry.readManifestForSession({
|
|
userId: 'user-1',
|
|
sessionId: 'session-1',
|
|
});
|
|
await adapter.conversationPackageRegistry.readArtifactObject({
|
|
userId: 'user-1',
|
|
sessionId: 'session-1',
|
|
artifactId: 'artifact-1',
|
|
});
|
|
|
|
assert.match(calls[0].url, /conversationPackageRegistry\/readManifestForSession$/);
|
|
assert.match(calls[1].url, /conversationPackageRegistry\/readArtifactObject$/);
|
|
assert.deepEqual(JSON.parse(calls[0].init.body), {
|
|
args: [{ userId: 'user-1', sessionId: 'session-1' }],
|
|
});
|
|
assert.deepEqual(JSON.parse(calls[1].init.body), {
|
|
args: [{ userId: 'user-1', sessionId: 'session-1', artifactId: 'artifact-1' }],
|
|
});
|
|
assert.equal(calls[0].init.headers.Authorization, 'Bearer secret-token');
|
|
assert.equal(calls[1].init.headers.Authorization, 'Bearer secret-token');
|
|
});
|
|
|
|
test('createMindSpaceRemoteServerAdapter exposes the chat save write authority', async () => {
|
|
const calls = [];
|
|
const adapter = createMindSpaceRemoteServerAdapter({
|
|
endpoint: 'https://mindspace.example.com/',
|
|
authToken: 'secret-token',
|
|
fetchFn: async (url, init) => {
|
|
calls.push({ url, init });
|
|
return {
|
|
ok: true,
|
|
status: 200,
|
|
async text() {
|
|
return JSON.stringify({
|
|
html: '<html>ready</html>',
|
|
relativePath: 'public/report.html',
|
|
changed: true,
|
|
});
|
|
},
|
|
};
|
|
},
|
|
logger: { log() {}, warn() {}, error() {} },
|
|
});
|
|
const input = {
|
|
userId: 'user-1',
|
|
sourceContent: 'source',
|
|
html: '<html>draft</html>',
|
|
relativePath: 'public/report.html',
|
|
};
|
|
|
|
const result =
|
|
await adapter.chatSaveService.materializeWorkspaceHtml(input);
|
|
|
|
assert.equal(result.changed, true);
|
|
assert.match(
|
|
calls[0].url,
|
|
/chatSaveService\/materializeWorkspaceHtml$/,
|
|
);
|
|
assert.deepEqual(JSON.parse(calls[0].init.body), {
|
|
args: [input],
|
|
});
|
|
assert.equal(
|
|
calls[0].init.headers.Authorization,
|
|
'Bearer secret-token',
|
|
);
|
|
});
|
|
|
|
test('createMindSpaceRemoteServerAdapter creates shared HTML through MindSpace', async () => {
|
|
const calls = [];
|
|
const adapter = createMindSpaceRemoteServerAdapter({
|
|
endpoint: 'https://mindspace.example.com/',
|
|
authToken: 'secret-token',
|
|
fetchFn: async (url, init) => {
|
|
calls.push({ url, init });
|
|
return {
|
|
ok: true,
|
|
status: 200,
|
|
async text() {
|
|
return JSON.stringify({
|
|
publicUrl:
|
|
'https://example.com/MindSpace/user-1/public/shared/page.html',
|
|
filename: 'page.html',
|
|
});
|
|
},
|
|
};
|
|
},
|
|
logger: { log() {}, warn() {}, error() {} },
|
|
});
|
|
const input = {
|
|
userId: 'user-1',
|
|
html: '<html>page</html>',
|
|
sourceFilename: 'page.html',
|
|
};
|
|
|
|
const result =
|
|
await adapter.chatSaveService.createSharedHtml(input);
|
|
|
|
assert.equal(result.filename, 'page.html');
|
|
assert.match(
|
|
calls[0].url,
|
|
/chatSaveService\/createSharedHtml$/,
|
|
);
|
|
assert.deepEqual(JSON.parse(calls[0].init.body), {
|
|
args: [input],
|
|
});
|
|
|
|
const workspaceInput = {
|
|
userId: 'user-1',
|
|
sessionId: 'session-1',
|
|
relativePath: 'oa/notes.txt',
|
|
body: 'generated notes',
|
|
};
|
|
await adapter.conversationArtifactService
|
|
.registerWorkspaceFileArtifact(
|
|
workspaceInput,
|
|
);
|
|
assert.match(
|
|
calls[1].url,
|
|
/conversationArtifactService\/registerWorkspaceFileArtifact$/,
|
|
);
|
|
assert.deepEqual(JSON.parse(calls[1].init.body), {
|
|
args: [workspaceInput],
|
|
});
|
|
});
|
|
|
|
test('createMindSpaceRemoteServerAdapter exposes conversation artifact write authority', async () => {
|
|
const calls = [];
|
|
const adapter = createMindSpaceRemoteServerAdapter({
|
|
endpoint: 'https://mindspace.example.com/',
|
|
authToken: 'secret-token',
|
|
fetchFn: async (url, init) => {
|
|
calls.push({ url, init });
|
|
return {
|
|
ok: true,
|
|
status: 200,
|
|
async text() {
|
|
return JSON.stringify([
|
|
{ artifactId: 'artifact-1' },
|
|
]);
|
|
},
|
|
};
|
|
},
|
|
logger: { log() {}, warn() {}, error() {} },
|
|
});
|
|
const input = {
|
|
userId: 'user-1',
|
|
sessionId: 'session-1',
|
|
artifactRefs: [
|
|
{
|
|
relativePath: 'public/report.html',
|
|
messageId: 'message-1',
|
|
},
|
|
],
|
|
};
|
|
|
|
const result =
|
|
await adapter.conversationArtifactService
|
|
.registerPublicHtmlArtifacts(input);
|
|
|
|
assert.deepEqual(result, [
|
|
{ artifactId: 'artifact-1' },
|
|
]);
|
|
assert.match(
|
|
calls[0].url,
|
|
/conversationArtifactService\/registerPublicHtmlArtifacts$/,
|
|
);
|
|
assert.deepEqual(JSON.parse(calls[0].init.body), {
|
|
args: [input],
|
|
});
|
|
});
|
|
|
|
test('createMindSpaceRemoteServerAdapter exposes public Finish write authority', async () => {
|
|
const calls = [];
|
|
const adapter = createMindSpaceRemoteServerAdapter({
|
|
endpoint: 'https://mindspace.example.com/',
|
|
authToken: 'secret-token',
|
|
fetchFn: async (url, init) => {
|
|
calls.push({ url, init });
|
|
return {
|
|
ok: true,
|
|
status: 200,
|
|
async text() {
|
|
return JSON.stringify({
|
|
materialized: ['public/report.html'],
|
|
publicHtmlRelativePaths: [
|
|
'public/report.html',
|
|
],
|
|
});
|
|
},
|
|
};
|
|
},
|
|
logger: { log() {}, warn() {}, error() {} },
|
|
});
|
|
const input = {
|
|
userId: 'user-1',
|
|
sessionId: 'session-1',
|
|
messages: [],
|
|
};
|
|
|
|
const result =
|
|
await adapter.publicFinishService
|
|
.syncAfterFinish(input);
|
|
|
|
assert.deepEqual(result.publicHtmlRelativePaths, [
|
|
'public/report.html',
|
|
]);
|
|
assert.match(
|
|
calls[0].url,
|
|
/publicFinishService\/syncAfterFinish$/,
|
|
);
|
|
assert.deepEqual(JSON.parse(calls[0].init.body), {
|
|
args: [input],
|
|
});
|
|
});
|
|
|
|
test('createMindSpaceRemoteServerAdapter exposes WeChat Page Data delivery authority', async () => {
|
|
const calls = [];
|
|
const adapter = createMindSpaceRemoteServerAdapter({
|
|
endpoint: 'https://mindspace.example.com/',
|
|
authToken: 'secret-token',
|
|
fetchFn: async (url, init) => {
|
|
calls.push({ url, init });
|
|
return {
|
|
ok: true,
|
|
status: 200,
|
|
async text() {
|
|
return JSON.stringify({
|
|
outcome: { action: 'send' },
|
|
deliveryArtifacts: [
|
|
{
|
|
relativePath:
|
|
'public/report.html',
|
|
url: 'https://example.com/MindSpace/user-1/public/report.html',
|
|
},
|
|
],
|
|
});
|
|
},
|
|
};
|
|
},
|
|
logger: { log() {}, warn() {}, error() {} },
|
|
});
|
|
const input = {
|
|
userId: 'user-1',
|
|
reply: {
|
|
text: 'published',
|
|
messages: [],
|
|
},
|
|
intent: {
|
|
agentText: 'build a survey',
|
|
},
|
|
publicBaseUrl: 'https://example.com',
|
|
};
|
|
|
|
const result =
|
|
await adapter.publicFinishService
|
|
.prepareWechatPageDataDelivery(input);
|
|
|
|
assert.equal(result.outcome.action, 'send');
|
|
assert.match(
|
|
calls[0].url,
|
|
/publicFinishService\/prepareWechatPageDataDelivery$/,
|
|
);
|
|
assert.deepEqual(JSON.parse(calls[0].init.body), {
|
|
args: [input],
|
|
});
|
|
});
|
|
|
|
test('createMindSpaceRemoteServerAdapter exposes WeChat HTML and thumbnail authority', async () => {
|
|
const calls = [];
|
|
const adapter =
|
|
createMindSpaceRemoteServerAdapter({
|
|
endpoint:
|
|
'https://mindspace.example.com/',
|
|
authToken: 'secret-token',
|
|
fetchFn: async (url, init) => {
|
|
calls.push({ url, init });
|
|
const thumbnail =
|
|
String(url).endsWith(
|
|
'/ensureWechatFreshPageThumbnails',
|
|
);
|
|
return {
|
|
ok: true,
|
|
status: 200,
|
|
async text() {
|
|
return JSON.stringify(
|
|
thumbnail
|
|
? { ok: true }
|
|
: {
|
|
confirmedArtifacts: [
|
|
{
|
|
relativePath:
|
|
'public/page.html',
|
|
url:
|
|
'https://example.com/MindSpace/user-1/public/page.html',
|
|
},
|
|
],
|
|
},
|
|
);
|
|
},
|
|
};
|
|
},
|
|
logger: {
|
|
log() {},
|
|
warn() {},
|
|
error() {},
|
|
},
|
|
});
|
|
const prepareInput = {
|
|
userId: 'user-1',
|
|
sessionId: 'session-1',
|
|
reply: {
|
|
text: 'done',
|
|
messages: [],
|
|
},
|
|
};
|
|
const thumbnailInput = {
|
|
userId: 'user-1',
|
|
sessionId: 'session-1',
|
|
artifacts: [
|
|
{
|
|
relativePath:
|
|
'public/page.html',
|
|
},
|
|
],
|
|
};
|
|
|
|
const prepared =
|
|
await adapter.publicFinishService
|
|
.prepareWechatHtmlDelivery(
|
|
prepareInput,
|
|
);
|
|
const thumbnail =
|
|
await adapter.publicFinishService
|
|
.ensureWechatFreshPageThumbnails(
|
|
thumbnailInput,
|
|
);
|
|
|
|
assert.equal(
|
|
prepared.confirmedArtifacts[0]
|
|
.relativePath,
|
|
'public/page.html',
|
|
);
|
|
assert.equal(thumbnail.ok, true);
|
|
assert.match(
|
|
calls[0].url,
|
|
/publicFinishService\/prepareWechatHtmlDelivery$/,
|
|
);
|
|
assert.match(
|
|
calls[1].url,
|
|
/publicFinishService\/ensureWechatFreshPageThumbnails$/,
|
|
);
|
|
assert.deepEqual(
|
|
JSON.parse(calls[0].init.body),
|
|
{ args: [prepareInput] },
|
|
);
|
|
assert.deepEqual(
|
|
JSON.parse(calls[1].init.body),
|
|
{ args: [thumbnailInput] },
|
|
);
|
|
});
|
|
|
|
test('createMindSpaceRemoteServerAdapter exposes logical workspace delivery authority', async () => {
|
|
const calls = [];
|
|
const adapter =
|
|
createMindSpaceRemoteServerAdapter({
|
|
endpoint:
|
|
'https://mindspace.example.com/',
|
|
authToken: 'secret-token',
|
|
fetchFn: async (url, init) => {
|
|
calls.push({ url, init });
|
|
return {
|
|
ok: true,
|
|
status: 200,
|
|
async text() {
|
|
return JSON.stringify({
|
|
action: 'deliver',
|
|
kind: 'html',
|
|
ownerId: 'user-1',
|
|
relativePath:
|
|
'public/report.html',
|
|
html: '<html></html>',
|
|
});
|
|
},
|
|
};
|
|
},
|
|
logger: {
|
|
log() {},
|
|
warn() {},
|
|
error() {},
|
|
},
|
|
});
|
|
const input = {
|
|
requestPath:
|
|
'/user-1/public/report.html',
|
|
};
|
|
|
|
const result =
|
|
await adapter
|
|
.workspacePublicationDeliveryService
|
|
.resolveWorkspaceRequest(input);
|
|
|
|
assert.equal(result.kind, 'html');
|
|
assert.match(
|
|
calls[0].url,
|
|
/workspacePublicationDeliveryService\/resolveWorkspaceRequest$/,
|
|
);
|
|
assert.deepEqual(
|
|
JSON.parse(calls[0].init.body),
|
|
{ args: [input] },
|
|
);
|
|
});
|
|
|
|
test('createMindSpaceRemoteServerAdapter exposes binary and long-image workspace tools', async () => {
|
|
const calls = [];
|
|
const adapter =
|
|
createMindSpaceRemoteServerAdapter({
|
|
endpoint:
|
|
'https://mindspace.example.com/',
|
|
authToken: 'secret-token',
|
|
fetchFn: async (url, init) => {
|
|
calls.push({ url, init });
|
|
return {
|
|
ok: true,
|
|
status: 200,
|
|
async text() {
|
|
return JSON.stringify({
|
|
relativePath:
|
|
'public/output.bin',
|
|
});
|
|
},
|
|
};
|
|
},
|
|
logger: {
|
|
log() {},
|
|
warn() {},
|
|
error() {},
|
|
},
|
|
});
|
|
const binaryInput = {
|
|
workspaceRef:
|
|
'mindspace://users/user-1/workspace',
|
|
sessionId: 'session-1',
|
|
packageId: 'cp_session-1',
|
|
path: 'public/report.docx',
|
|
bodyBase64:
|
|
Buffer.from('PK').toString('base64'),
|
|
};
|
|
const imageInput = {
|
|
workspaceRef:
|
|
'mindspace://users/user-1/workspace',
|
|
sessionId: 'session-1',
|
|
packageId: 'cp_session-1',
|
|
htmlPath: 'public/report.html',
|
|
};
|
|
|
|
await adapter.workspaceToolService
|
|
.writeBinaryFile(binaryInput);
|
|
await adapter.workspaceToolService
|
|
.generateLongImage(imageInput);
|
|
|
|
assert.match(
|
|
calls[0].url,
|
|
/workspaceToolService\/writeBinaryFile$/,
|
|
);
|
|
assert.match(
|
|
calls[1].url,
|
|
/workspaceToolService\/generateLongImage$/,
|
|
);
|
|
assert.deepEqual(
|
|
JSON.parse(calls[0].init.body),
|
|
{ args: [binaryInput] },
|
|
);
|
|
assert.deepEqual(
|
|
JSON.parse(calls[1].init.body),
|
|
{ args: [imageInput] },
|
|
);
|
|
});
|
|
|
|
test('createMindSpaceRemoteServerAdapter rejects unknown binding methods', () => {
|
|
const adapter = createMindSpaceRemoteServerAdapter({
|
|
endpoint: 'https://mindspace.example.com',
|
|
fetchFn: async () => ({ ok: true, status: 204, text: async () => '' }),
|
|
logger: { log() {}, warn() {}, error() {} },
|
|
});
|
|
|
|
assert.throws(() => adapter.assetService.notARealMethod, /does not expose/);
|
|
assert.throws(
|
|
() => adapter.conversationPackageRegistry.recordArtifact,
|
|
/does not expose/,
|
|
);
|
|
});
|
|
|
|
test('createMindSpaceRemoteServerAdapter surfaces remote conversation package errors with binding context', async () => {
|
|
const adapter = createMindSpaceRemoteServerAdapter({
|
|
endpoint: 'https://mindspace.example.com/',
|
|
authToken: 'secret-token',
|
|
fetchFn: async () => ({
|
|
ok: false,
|
|
status: 502,
|
|
async text() {
|
|
return 'upstream manifest missing';
|
|
},
|
|
}),
|
|
logger: { log() {}, warn() {}, error() {} },
|
|
});
|
|
|
|
await assert.rejects(
|
|
() => adapter.conversationPackageRegistry.readManifestForSession({
|
|
userId: 'user-1',
|
|
sessionId: 'session-1',
|
|
}),
|
|
/conversationPackageRegistry\.readManifestForSession\(\) failed with 502: upstream manifest missing/,
|
|
);
|
|
});
|
|
|
|
test('createMindSpaceRemoteServerAdapter preserves publication error codes from rpc payload', async () => {
|
|
const adapter = createMindSpaceRemoteServerAdapter({
|
|
endpoint: 'https://mindspace.example.com/',
|
|
authToken: 'secret-token',
|
|
fetchFn: async () => ({
|
|
ok: false,
|
|
status: 404,
|
|
async text() {
|
|
return JSON.stringify({
|
|
message: '公开页面不存在',
|
|
code: 'publication_not_found',
|
|
});
|
|
},
|
|
}),
|
|
logger: { log() {}, warn() {}, error() {} },
|
|
});
|
|
|
|
await assert.rejects(
|
|
() => adapter.publicationService.resolvePublic('john', 'missing-page', null, null, {}),
|
|
(error) => error instanceof Error && error.code === 'publication_not_found',
|
|
);
|
|
});
|