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