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); 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(-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('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('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: () => 'page data', 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 ''; }, 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: [] }); });