import assert from 'node:assert/strict'; import test from 'node:test'; import { bootstrapPortalMemorySessionServices } from './portal-memory-session-services-bootstrap.mjs'; function createSetup(overrides = {}) { const calls = []; const pool = { id: 'pool' }; const llmProviderService = { id: 'llm' }; const userAuth = { id: 'user-auth' }; const sessionAccess = { id: 'session-access' }; const memoryV2ConfigService = { async getRuntimeState() { calls.push(['runtime-state']); return { overrides: { MEMORY_BACKEND: 'pgvector', SHARED: 'override', }, }; }, }; const conversationMemoryService = { id: 'conversation-memory', }; const memoryV2 = { id: 'memory-v2' }; const episodicMemoryService = { id: 'episodic', async ensureSchema() { calls.push(['episodic-schema']); }, }; const sessionSnapshotService = { id: 'snapshot' }; const sessionStreamStore = { id: 'stream-store' }; const directChatService = { id: 'direct-chat' }; const chatIntentRouter = { id: 'intent-router' }; const systemDisclosurePolicyService = { id: 'system-disclosure-policy', async initialize() { calls.push(['system-disclosure-initialize']); }, }; let skillRuntimeOptions; let agentCodeRunOptions; let conversationOptions; let memoryV2Options; let episodicOptions; let snapshotOptions; let streamStoreOptions; let directChatOptions; let routerOptions; const options = { pool, h5Root: '/app', env: { BASE_ONLY: 'base', SHARED: 'base', }, llmProviderService, userAuth, sessionAccess, logger: { warn(...args) { calls.push(['warn', ...args]); }, }, createMemoryV2AdminConfigServiceFn(receivedPool) { assert.equal(receivedPool, pool); calls.push(['memory-config']); return memoryV2ConfigService; }, createSkillRuntimeAdminConfigServiceFn( receivedPool, receivedOptions, ) { assert.equal(receivedPool, pool); calls.push(['skill-config']); skillRuntimeOptions = receivedOptions; return { id: 'skill-config' }; }, createSystemDisclosurePolicyServiceFn( receivedPool, ) { assert.equal(receivedPool, pool); calls.push(['system-disclosure-config']); return systemDisclosurePolicyService; }, createAgentCodeRunAdminConfigServiceFn( receivedPool, receivedOptions, ) { assert.equal(receivedPool, pool); calls.push(['agent-code-run-config']); agentCodeRunOptions = receivedOptions; return { id: 'agent-code-run-config' }; }, createWechatScheduleLlmConfigServiceFn( receivedPool, ) { assert.equal(receivedPool, pool); calls.push(['wechat-schedule-config']); return { id: 'wechat-schedule-config' }; }, createConversationMemoryServiceFn( receivedPool, receivedOptions, ) { assert.equal(receivedPool, pool); calls.push(['conversation-memory']); conversationOptions = receivedOptions; return conversationMemoryService; }, async createManagedMemoryV2RuntimeFn( receivedOptions, ) { calls.push(['memory-runtime']); memoryV2Options = receivedOptions; return memoryV2; }, createEpisodicMemoryServiceFn( receivedPool, receivedOptions, ) { assert.equal(receivedPool, pool); calls.push(['episodic-memory']); episodicOptions = receivedOptions; return episodicMemoryService; }, createSessionSnapshotServiceFn( receivedPool, receivedOptions, ) { assert.equal(receivedPool, pool); calls.push(['snapshot']); snapshotOptions = receivedOptions; return sessionSnapshotService; }, isSessionStreamReplayEnabledFn() { calls.push(['replay-enabled']); return true; }, createSessionStreamStoreFn(receivedOptions) { calls.push(['stream-store']); streamStoreOptions = receivedOptions; return sessionStreamStore; }, createDirectChatServiceFn(receivedOptions) { calls.push(['direct-chat']); directChatOptions = receivedOptions; return directChatService; }, createManagedChatIntentRouterFn( receivedOptions, ) { calls.push(['intent-router']); routerOptions = receivedOptions; return chatIntentRouter; }, ...overrides, }; return { calls, options, pool, llmProviderService, userAuth, sessionAccess, memoryV2ConfigService, conversationMemoryService, memoryV2, episodicMemoryService, systemDisclosurePolicyService, sessionSnapshotService, sessionStreamStore, getCaptured() { return { skillRuntimeOptions, agentCodeRunOptions, conversationOptions, memoryV2Options, episodicOptions, snapshotOptions, streamStoreOptions, directChatOptions, routerOptions, }; }, }; } test('requires the Memory and Session dependencies', async () => { await assert.rejects( bootstrapPortalMemorySessionServices(), /requires memory and session dependencies/, ); }); test('preserves Memory, Session, and chat assembly order', async () => { const setup = createSetup(); const result = await bootstrapPortalMemorySessionServices( setup.options, ); assert.deepEqual( setup.calls.map(([name]) => name), [ 'memory-config', 'skill-config', 'system-disclosure-config', 'system-disclosure-initialize', 'agent-code-run-config', 'wechat-schedule-config', 'conversation-memory', 'memory-runtime', 'episodic-memory', 'episodic-schema', 'snapshot', 'replay-enabled', 'stream-store', 'direct-chat', 'intent-router', ], ); assert.equal( result.memoryV2ConfigService, setup.memoryV2ConfigService, ); assert.equal( result.systemDisclosurePolicyService, setup.systemDisclosurePolicyService, ); assert.equal(result.agentCodeRunPolicyService.id, 'agent-code-run-config'); assert.equal( result.sessionStreamStore, setup.sessionStreamStore, ); assert.equal(result.directChatService.id, 'direct-chat'); assert.equal(result.chatIntentRouter.id, 'intent-router'); }); test('preserves configuration and runtime service wiring', async () => { const setup = createSetup(); await bootstrapPortalMemorySessionServices( setup.options, ); const captured = setup.getCaptured(); assert.deepEqual(captured.skillRuntimeOptions, { h5Root: '/app', }); assert.deepEqual(captured.agentCodeRunOptions, { env: { BASE_ONLY: 'base', SHARED: 'base', }, }); assert.equal( captured.conversationOptions.llmProviderService, setup.llmProviderService, ); assert.deepEqual(captured.memoryV2Options, { legacyMemoryService: setup.conversationMemoryService, configService: setup.memoryV2ConfigService, mysqlPool: setup.pool, }); assert.equal( captured.episodicOptions.logger, setup.options.logger, ); assert.deepEqual(captured.snapshotOptions, { conversationMemoryService: setup.conversationMemoryService, memoryV2: setup.memoryV2, episodicMemoryService: setup.episodicMemoryService, }); assert.deepEqual(captured.streamStoreOptions, { pool: setup.pool, }); }); test('merges runtime overrides and falls back to the base environment', async () => { const setup = createSetup(); const result = await bootstrapPortalMemorySessionServices( setup.options, ); const captured = setup.getCaptured(); assert.deepEqual( await captured.conversationOptions.getEffectiveEnv(), { BASE_ONLY: 'base', SHARED: 'override', MEMORY_BACKEND: 'pgvector', }, ); assert.equal( captured.conversationOptions.getEffectiveEnv, captured.episodicOptions.getEffectiveEnv, ); setup.memoryV2ConfigService.getRuntimeState = async () => { throw new Error('config unavailable'); }; assert.deepEqual(await result.getEffectiveEnv(), { BASE_ONLY: 'base', SHARED: 'base', }); }); test('preserves Direct Chat and Intent Router dependencies', async () => { const setup = createSetup(); await bootstrapPortalMemorySessionServices( setup.options, ); const captured = setup.getCaptured(); assert.deepEqual(captured.directChatOptions, { userAuth: setup.userAuth, sessionAccess: setup.sessionAccess, llmProviderService: setup.llmProviderService, sessionSnapshotService: setup.sessionSnapshotService, memoryV2: setup.memoryV2, conversationMemoryService: setup.conversationMemoryService, episodicMemoryService: setup.episodicMemoryService, }); assert.deepEqual(captured.routerOptions, { llmProviderService: setup.llmProviderService, memoryV2: setup.memoryV2, conversationMemoryService: setup.conversationMemoryService, episodicMemoryService: setup.episodicMemoryService, configService: setup.memoryV2ConfigService, }); }); test('degrades episodic schema failures and can disable replay storage', async () => { const setup = createSetup({ createEpisodicMemoryServiceFn() { return { async ensureSchema() { throw new Error('schema unavailable'); }, }; }, isSessionStreamReplayEnabledFn: () => false, }); const result = await bootstrapPortalMemorySessionServices( setup.options, ); assert.equal(result.sessionStreamStore, null); assert.ok( setup.calls.some( ([name, message, detail]) => name === 'warn' && message === '[episodic-memory] schema setup degraded; snapshot fallback remains available:' && detail === 'schema unavailable', ), ); });