merge: server architecture modularization
This commit is contained in:
@@ -0,0 +1,367 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
bootstrapPortalGatewayServices,
|
||||
createPortalRunDeliverablesValidator,
|
||||
} from './portal-gateway-services-bootstrap.mjs';
|
||||
|
||||
function createSetup(overrides = {}) {
|
||||
const calls = [];
|
||||
const pool = { id: 'pool' };
|
||||
const userAuth = { id: 'user-auth' };
|
||||
const sessionAccess = { id: 'session-access' };
|
||||
const llmProviderService = { id: 'llm' };
|
||||
const memoryV2 = {
|
||||
async observePersonalMemory(options) {
|
||||
calls.push(['observe-memory', options]);
|
||||
},
|
||||
};
|
||||
const tkmindProxy = { id: 'proxy' };
|
||||
const toolGateway = { id: 'tool-gateway' };
|
||||
const agentRunGateway = { id: 'agent-run-gateway' };
|
||||
let proxyOptions;
|
||||
let toolOptions;
|
||||
let agentOptions;
|
||||
let validatorOptions;
|
||||
const options = {
|
||||
pool,
|
||||
h5Root: '/app',
|
||||
env: {
|
||||
MEMIND_AGENT_RUN_AUTODISPATCH: 'yes',
|
||||
MEMIND_AGENT_RUN_QUEUE_CONCURRENCY: '3',
|
||||
MEMIND_AGENT_RUN_TIMEOUT_MS: '9000',
|
||||
},
|
||||
apiTarget: 'http://primary',
|
||||
apiTargets: ['http://primary', 'http://secondary'],
|
||||
apiSecret: 'secret',
|
||||
userAuth,
|
||||
sessionAccess,
|
||||
sessionStreamStore: { id: 'stream-store' },
|
||||
llmProviderService,
|
||||
subscriptionService: { id: 'subscription' },
|
||||
sessionSnapshotService: { id: 'snapshot' },
|
||||
conversationMemoryService: {
|
||||
id: 'conversation-memory',
|
||||
},
|
||||
memoryV2,
|
||||
systemDisclosurePolicyService: {
|
||||
id: 'system-disclosure-policy',
|
||||
},
|
||||
mindSpaceAssets: {
|
||||
async readAsset(userId, assetId) {
|
||||
calls.push(['read-asset', userId, assetId]);
|
||||
return {
|
||||
asset: { mimeType: 'image/png' },
|
||||
path: '/assets/image.png',
|
||||
};
|
||||
},
|
||||
},
|
||||
directChatService: { id: 'direct-chat' },
|
||||
chatIntentRouter: { id: 'intent-router' },
|
||||
async syncUserGeneratedPages(userId, options) {
|
||||
calls.push(['sync-pages', userId, options]);
|
||||
return { synced: true };
|
||||
},
|
||||
isSessionPageDeliveryActive(sessionId) {
|
||||
calls.push(['delivery-active', sessionId]);
|
||||
return sessionId === 'busy-session';
|
||||
},
|
||||
createTkmindProxyFn(receivedOptions) {
|
||||
calls.push(['proxy']);
|
||||
proxyOptions = receivedOptions;
|
||||
return tkmindProxy;
|
||||
},
|
||||
createToolGatewayFn(receivedOptions) {
|
||||
calls.push(['tool-gateway']);
|
||||
toolOptions = receivedOptions;
|
||||
return toolGateway;
|
||||
},
|
||||
createAgentRunGatewayFn(receivedOptions) {
|
||||
calls.push(['agent-run-gateway']);
|
||||
agentOptions = receivedOptions;
|
||||
return agentRunGateway;
|
||||
},
|
||||
createRunDeliverablesValidatorFn(receivedOptions) {
|
||||
calls.push(['validator']);
|
||||
validatorOptions = receivedOptions;
|
||||
return async () => ({ errors: [] });
|
||||
},
|
||||
async readAssetFileFn(assetPath) {
|
||||
calls.push(['read-asset-file', assetPath]);
|
||||
return Buffer.from('image');
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
return {
|
||||
calls,
|
||||
options,
|
||||
pool,
|
||||
userAuth,
|
||||
sessionAccess,
|
||||
llmProviderService,
|
||||
memoryV2,
|
||||
tkmindProxy,
|
||||
toolGateway,
|
||||
agentRunGateway,
|
||||
getCaptured() {
|
||||
return {
|
||||
proxyOptions,
|
||||
toolOptions,
|
||||
agentOptions,
|
||||
validatorOptions,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('requires the gateway dependencies', () => {
|
||||
assert.throws(
|
||||
() => bootstrapPortalGatewayServices(),
|
||||
/requires gateway dependencies/,
|
||||
);
|
||||
});
|
||||
|
||||
test('preserves Proxy, Tool, and Agent gateway wiring', () => {
|
||||
const setup = createSetup();
|
||||
const result = bootstrapPortalGatewayServices(
|
||||
setup.options,
|
||||
);
|
||||
const captured = setup.getCaptured();
|
||||
|
||||
assert.deepEqual(
|
||||
setup.calls.map(([name]) => name),
|
||||
['proxy', 'tool-gateway', 'validator', 'agent-run-gateway'],
|
||||
);
|
||||
assert.equal(result.tkmindProxy, setup.tkmindProxy);
|
||||
assert.equal(result.toolGateway, setup.toolGateway);
|
||||
assert.equal(
|
||||
result.agentRunGateway,
|
||||
setup.agentRunGateway,
|
||||
);
|
||||
assert.equal(captured.proxyOptions.userAuth, setup.userAuth);
|
||||
assert.equal(
|
||||
captured.proxyOptions.sessionAccess,
|
||||
setup.sessionAccess,
|
||||
);
|
||||
assert.equal(
|
||||
captured.proxyOptions.llmProviderService,
|
||||
setup.llmProviderService,
|
||||
);
|
||||
assert.equal(
|
||||
captured.proxyOptions.systemDisclosurePolicyService,
|
||||
setup.options.systemDisclosurePolicyService,
|
||||
);
|
||||
assert.deepEqual(captured.toolOptions, {
|
||||
llmProviderService: setup.llmProviderService,
|
||||
});
|
||||
assert.deepEqual(captured.validatorOptions, {
|
||||
h5Root: '/app',
|
||||
});
|
||||
assert.equal(captured.agentOptions.tkmindProxy, setup.tkmindProxy);
|
||||
assert.equal(captured.agentOptions.toolGateway, setup.toolGateway);
|
||||
assert.equal(
|
||||
captured.agentOptions.systemDisclosurePolicyService,
|
||||
setup.options.systemDisclosurePolicyService,
|
||||
);
|
||||
assert.equal(captured.agentOptions.autoDispatch, true);
|
||||
assert.equal(captured.agentOptions.maxConcurrentRuns, 3);
|
||||
assert.equal(captured.agentOptions.runTimeoutMs, 9000);
|
||||
});
|
||||
|
||||
test('preserves local asset reads and optional asset absence', async () => {
|
||||
const setup = createSetup();
|
||||
bootstrapPortalGatewayServices(setup.options);
|
||||
const { proxyOptions } = setup.getCaptured();
|
||||
|
||||
assert.deepEqual(
|
||||
await proxyOptions.localFetchAsset('user-1', 'asset-1'),
|
||||
{
|
||||
buffer: Buffer.from('image'),
|
||||
mimeType: 'image/png',
|
||||
},
|
||||
);
|
||||
assert.deepEqual(
|
||||
setup.calls.slice(-2),
|
||||
[
|
||||
['read-asset', 'user-1', 'asset-1'],
|
||||
['read-asset-file', '/assets/image.png'],
|
||||
],
|
||||
);
|
||||
|
||||
const withoutAssets = createSetup({
|
||||
mindSpaceAssets: null,
|
||||
});
|
||||
bootstrapPortalGatewayServices(withoutAssets.options);
|
||||
assert.equal(
|
||||
withoutAssets.getCaptured().proxyOptions.localFetchAsset,
|
||||
null,
|
||||
);
|
||||
});
|
||||
|
||||
test('preserves memory, page sync, and busy callbacks', async () => {
|
||||
const setup = createSetup();
|
||||
bootstrapPortalGatewayServices(setup.options);
|
||||
const { agentOptions } = setup.getCaptured();
|
||||
|
||||
await agentOptions.observePersonalMemoryOnSuccess({
|
||||
userId: 'user-1',
|
||||
sessionId: 'session-1',
|
||||
userMessage: { role: 'user', content: 'remember' },
|
||||
});
|
||||
assert.deepEqual(setup.calls.at(-1), [
|
||||
'observe-memory',
|
||||
{
|
||||
userId: 'user-1',
|
||||
sessionId: 'session-1',
|
||||
messages: [
|
||||
{ role: 'user', content: 'remember' },
|
||||
],
|
||||
},
|
||||
]);
|
||||
assert.deepEqual(
|
||||
await agentOptions.syncUserPagesOnSuccess({
|
||||
userId: 'user-1',
|
||||
sessionId: 'session-1',
|
||||
runStartedAtMs: 123,
|
||||
}),
|
||||
{ synced: true },
|
||||
);
|
||||
assert.equal(
|
||||
agentOptions.isSessionExternallyBusy({
|
||||
sessionId: 'busy-session',
|
||||
}),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test('keeps memory observation optional and parses disabled dispatch', async () => {
|
||||
const setup = createSetup({
|
||||
env: {
|
||||
MEMIND_AGENT_RUN_AUTODISPATCH: 'off',
|
||||
},
|
||||
memoryV2: {},
|
||||
});
|
||||
bootstrapPortalGatewayServices(setup.options);
|
||||
const { agentOptions } = setup.getCaptured();
|
||||
|
||||
await agentOptions.observePersonalMemoryOnSuccess({
|
||||
userId: 'user-1',
|
||||
sessionId: 'session-1',
|
||||
userMessage: 'hello',
|
||||
});
|
||||
assert.equal(agentOptions.autoDispatch, false);
|
||||
assert.equal(agentOptions.maxConcurrentRuns, 1);
|
||||
assert.equal(agentOptions.runTimeoutMs, 15 * 60 * 1000);
|
||||
});
|
||||
|
||||
test('validates Page Data issues, policy grants, and browser storage', async () => {
|
||||
const policyChecks = [];
|
||||
const validator = createPortalRunDeliverablesValidator({
|
||||
h5Root: '/app',
|
||||
resolveMindSpaceUserPublishDirFn: () =>
|
||||
'/publish/user-1',
|
||||
normalizeWorkspaceRelativePathFn: (value) => value,
|
||||
resolvePathFn: (...parts) => parts.join('/').replaceAll('//', '/'),
|
||||
pathSeparator: '/',
|
||||
existsSyncFn: () => true,
|
||||
readFileSyncFn: () => '<html>page data</html>',
|
||||
evaluatePageDataHtmlContentFn: () => ({
|
||||
usesPageDataApi: true,
|
||||
issues: ['dataset_binding_missing'],
|
||||
}),
|
||||
readPageAccessPolicyFn: () => ({ id: 'policy' }),
|
||||
detectPageDataDatasetUsageFromHtmlFn: () => [
|
||||
[
|
||||
'orders',
|
||||
{ read: true, insert: true },
|
||||
],
|
||||
],
|
||||
policyAllowsActionFn(policy, dataset, action) {
|
||||
policyChecks.push([policy, dataset, action]);
|
||||
return action === 'read';
|
||||
},
|
||||
scanWorkspaceFilesForProhibitedBrowserStorageFn:
|
||||
(options) => {
|
||||
assert.deepEqual(options, {
|
||||
publishDir: '/publish/user-1',
|
||||
relativePaths: ['public/orders.html'],
|
||||
});
|
||||
return [
|
||||
{
|
||||
relativePath: 'public/orders.html',
|
||||
apis: ['localStorage', 'indexedDB'],
|
||||
},
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
const result = await validator({
|
||||
userId: 'user-1',
|
||||
deliverables: {
|
||||
pages: [
|
||||
{
|
||||
pageId: 'page-1',
|
||||
workspaceRelativePath: 'public/orders.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(policyChecks, [
|
||||
[{ id: 'policy' }, 'orders', 'read'],
|
||||
[{ id: 'policy' }, 'orders', 'insert'],
|
||||
]);
|
||||
assert.deepEqual(
|
||||
result.errors.map(({ code }) => code),
|
||||
[
|
||||
'dataset_binding_missing',
|
||||
'page_data_policy_action_missing',
|
||||
'browser_storage_forbidden',
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
test('skips non-public, missing, and path-escaping Page Data files', async () => {
|
||||
const readPaths = [];
|
||||
const validator = createPortalRunDeliverablesValidator({
|
||||
h5Root: '/app',
|
||||
resolveMindSpaceUserPublishDirFn: () =>
|
||||
'/publish/user-1',
|
||||
normalizeWorkspaceRelativePathFn: (value) => value,
|
||||
resolvePathFn: (...parts) => {
|
||||
const joined = parts.join('/');
|
||||
if (joined.includes('escape')) return '/outside/file.html';
|
||||
return joined.replaceAll('//', '/');
|
||||
},
|
||||
pathSeparator: '/',
|
||||
existsSyncFn: (filePath) =>
|
||||
!filePath.includes('missing'),
|
||||
readFileSyncFn(filePath) {
|
||||
readPaths.push(filePath);
|
||||
return '<html></html>';
|
||||
},
|
||||
evaluatePageDataHtmlContentFn: () => ({
|
||||
usesPageDataApi: false,
|
||||
issues: [],
|
||||
}),
|
||||
scanWorkspaceFilesForProhibitedBrowserStorageFn:
|
||||
() => [],
|
||||
});
|
||||
|
||||
const result = await validator({
|
||||
userId: 'user-1',
|
||||
deliverables: {
|
||||
pages: [
|
||||
{ workspaceRelativePath: 'private/a.html' },
|
||||
{ workspaceRelativePath: 'public/missing.html' },
|
||||
{ workspaceRelativePath: 'public/escape.html' },
|
||||
{ workspaceRelativePath: 'public/valid.html' },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(readPaths, [
|
||||
'/publish/user-1/public/valid.html',
|
||||
]);
|
||||
assert.deepEqual(result, { errors: [] });
|
||||
});
|
||||
Reference in New Issue
Block a user