369 lines
9.7 KiB
JavaScript
369 lines
9.7 KiB
JavaScript
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',
|
|
),
|
|
);
|
|
});
|