397 lines
11 KiB
JavaScript
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: [] },
|
|
}),
|
|
/交付验证服务未启用/,
|
|
);
|
|
});
|