Files
memind/server/portal-gateway-services-bootstrap.test.mjs

397 lines
11 KiB
JavaScript

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' };
const workspacePublicationDelivery = {
async validateRunDeliverables() {
return { errors: [] };
},
};
const getWorkspacePublicationDelivery =
() => workspacePublicationDelivery;
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 readAssetContent(
userId,
assetId,
) {
calls.push(['read-asset', userId, assetId]);
return {
asset: { mimeType: 'image/png' },
bodyBase64:
Buffer.from('image').toString(
'base64',
),
};
},
},
getWorkspacePublicationDelivery,
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: [] });
},
...overrides,
};
return {
calls,
options,
pool,
userAuth,
sessionAccess,
llmProviderService,
memoryV2,
tkmindProxy,
toolGateway,
agentRunGateway,
workspacePublicationDelivery,
getWorkspacePublicationDelivery,
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, {
getWorkspacePublicationDelivery:
setup.getWorkspacePublicationDelivery,
});
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);
assert.equal(typeof captured.agentOptions.cancelSessionOnRetry, 'function');
assert.equal(captured.agentOptions.observeWorkflowRun, null);
assert.equal(captured.agentOptions.observeWorkflowValidation, null);
assert.equal(captured.agentOptions.enforcePageDataWorkflowValidation, false);
assert.equal(captured.agentOptions.maxConcurrentShadowObservations, 2);
assert.equal(captured.agentOptions.maxQueuedShadowObservations, 100);
});
test('wires Shadow observation only behind the explicit environment gate', () => {
const configService = { id: 'orchestrator-config' };
const observer = async () => ({ observed: false });
const validationObserver = async () => ({ observed: false });
observer.observeValidation = validationObserver;
let receivedObserverOptions = null;
const setup = createSetup({
env: {
MEMIND_ORCHESTRATOR_SHADOW_OBSERVATION_ENABLED: '1',
MEMIND_ORCHESTRATOR_SERVICE_TOKEN: 'shadow-token',
MEMIND_ORCHESTRATOR_SHADOW_MAX_CONCURRENCY: '4',
MEMIND_ORCHESTRATOR_SHADOW_MAX_QUEUE: '25',
MEMIND_ORCHESTRATOR_PAGE_DATA_VALIDATION_GATE_ENABLED: '1',
},
createOrchestratorAdminConfigServiceFn(receivedPool) {
assert.equal(receivedPool, setup.pool);
return configService;
},
createWorkflowShadowObserverFn(options) {
receivedObserverOptions = options;
return observer;
},
});
bootstrapPortalGatewayServices(setup.options);
const { agentOptions } = setup.getCaptured();
assert.equal(receivedObserverOptions.configService, configService);
assert.equal(receivedObserverOptions.serviceToken, 'shadow-token');
assert.equal(agentOptions.observeWorkflowRun, observer);
assert.equal(agentOptions.observeWorkflowValidation, validationObserver);
assert.equal(agentOptions.enforcePageDataWorkflowValidation, true);
assert.equal(agentOptions.maxConcurrentShadowObservations, 4);
assert.equal(agentOptions.maxQueuedShadowObservations, 25);
});
test('keeps the Page Data validation gate disabled without Shadow wiring', () => {
const setup = createSetup({
env: {
MEMIND_ORCHESTRATOR_PAGE_DATA_VALIDATION_GATE_ENABLED: '1',
},
});
bootstrapPortalGatewayServices(setup.options);
const { agentOptions } = setup.getCaptured();
assert.equal(agentOptions.observeWorkflowValidation, null);
assert.equal(agentOptions.enforcePageDataWorkflowValidation, false);
});
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(-1),
[['read-asset', 'user-1', 'asset-1']],
);
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('passive candidate runtime disables the singleton Agent recovery loop', () => {
let recoveryStarts = 0;
const setup = createSetup({
env: {
MEMIND_RUNTIME_ROLE: 'candidate',
MEMIND_CANARY_PASSIVE_RUNTIME: '1',
},
startAgentRunRecoveryLoopFn() {
recoveryStarts += 1;
return { id: 'recovery' };
},
});
const result = bootstrapPortalGatewayServices(setup.options);
assert.equal(recoveryStarts, 0);
assert.equal(result.agentRunRecoveryTimer, null);
});
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('delegates run deliverable validation to MindSpace', async () => {
const calls = [];
const validator = createPortalRunDeliverablesValidator({
getWorkspacePublicationDelivery:
() => ({
async validateRunDeliverables(
input,
) {
calls.push(input);
return {
errors: [
{
code:
'browser_storage_forbidden',
},
],
};
},
}),
});
const input = {
userId: 'user-1',
deliverables: {
pages: [
{
pageId: 'page-1',
workspaceRelativePath: 'public/orders.html',
},
],
},
};
const result = await validator(input);
assert.deepEqual(calls, [input]);
assert.equal(
result.errors[0].code,
'browser_storage_forbidden',
);
});
test('fails closed when MindSpace validation is unavailable', async () => {
const validator = createPortalRunDeliverablesValidator({
getWorkspacePublicationDelivery:
() => null,
});
await assert.rejects(
() =>
validator({
userId: 'user-1',
deliverables: { pages: [] },
}),
/交付验证服务未启用/,
);
});