import assert from 'node:assert/strict'; import test from 'node:test'; import { createSessionBroker, createSessionAccess, isSessionBrokerEnabled } from './session-broker.mjs'; function createMockUserAuth(overrides = {}) { const store = new Map(); return { async registerAgentSession(userId, sessionId, target = 0) { const isLegacyIndex = typeof target === 'number' || /^\d+$/.test(String(target)); const node = isLegacyIndex ? Number(target) : 0; const targetUrl = isLegacyIndex ? null : String(target); store.set(sessionId, { userId, node, target: targetUrl, origin: 'h5', }); }, async getSessionTarget(sessionId) { const row = store.get(sessionId); return { target: row?.target ?? null, node: row?.node ?? 0 }; }, async ownsSession(userId, sessionId) { return store.get(sessionId)?.userId === userId; }, async unregisterAgentSession(userId, sessionId) { if (store.get(sessionId)?.userId === userId) store.delete(sessionId); }, async setSessionOrigin(sessionId, origin) { const row = store.get(sessionId); if (row) row.origin = origin; }, async getSessionOrigins(sessionIds = []) { return new Map( sessionIds .map((sessionId) => { const row = store.get(sessionId); return row ? [sessionId, row.origin] : null; }) .filter(Boolean), ); }, ...overrides, _store: store, }; } test('isSessionBrokerEnabled defaults to false', () => { assert.equal(isSessionBrokerEnabled({}), false); assert.equal(isSessionBrokerEnabled({ MEMIND_SESSION_BROKER_ENABLED: '0' }), false); assert.equal(isSessionBrokerEnabled({ MEMIND_SESSION_BROKER_ENABLED: '1' }), true); }); test('validateOwnership delegates to userAuth', async () => { const userAuth = createMockUserAuth(); const broker = createSessionBroker({ userAuth }); await userAuth.registerAgentSession('user-1', 'sess-1', 'https://127.0.0.1:18006'); assert.equal(await broker.validateOwnership('user-1', 'sess-1'), true); assert.equal(await broker.validateOwnership('user-2', 'sess-1'), false); assert.equal(await broker.validateOwnership('user-1', ''), false); }); test('registerSession records direct chat h5-direct target', async () => { const userAuth = createMockUserAuth(); const broker = createSessionBroker({ userAuth }); await broker.registerSession({ userId: 'user-1', sessionId: 'h5direct_abc', target: 'h5-direct', origin: 'h5', }); assert.equal(await broker.validateOwnership('user-1', 'h5direct_abc'), true); const resolved = await broker.resolveSessionTarget('h5direct_abc'); assert.equal(resolved.target, 'h5-direct'); assert.equal(resolved.node, 0); }); test('resolveSessionTarget prefers goosed_target over legacy node', async () => { const userAuth = createMockUserAuth(); const broker = createSessionBroker({ userAuth }); await userAuth.registerAgentSession('user-1', 'sess-url', 'https://127.0.0.1:18007'); assert.deepEqual(await broker.resolveSessionTarget('sess-url'), { target: 'https://127.0.0.1:18007', node: 0, }); await userAuth.registerAgentSession('user-1', 'sess-node', 2); assert.deepEqual(await broker.resolveSessionTarget('sess-node'), { target: null, node: 2, }); }); test('resolveSession includes origin for wechat sessions', async () => { const userAuth = createMockUserAuth(); const broker = createSessionBroker({ userAuth }); await broker.registerSession({ userId: 'user-wx', sessionId: 'wx-sess-1', target: 'https://127.0.0.1:18006', origin: 'wechat', }); assert.deepEqual(await broker.resolveSession('wx-sess-1'), { sessionId: 'wx-sess-1', target: 'https://127.0.0.1:18006', node: 0, origin: 'wechat', }); }); test('unregisterSession removes ownership', async () => { const userAuth = createMockUserAuth(); const broker = createSessionBroker({ userAuth }); await broker.registerSession({ userId: 'user-1', sessionId: 'sess-del', target: 1, }); assert.equal(await broker.validateOwnership('user-1', 'sess-del'), true); await broker.unregisterSession({ userId: 'user-1', sessionId: 'sess-del' }); assert.equal(await broker.validateOwnership('user-1', 'sess-del'), false); }); test('createSessionBroker requires userAuth session helpers', () => { assert.throws(() => createSessionBroker({ userAuth: {} }), /registerAgentSession/); }); test('createSessionAccess with enabled=false uses userAuth directly', async () => { const userAuth = createMockUserAuth(); const access = createSessionAccess({ userAuth, enabled: false }); assert.equal(access.enabled, false); await access.registerAgentSession('user-1', 'sess-fallback', 'https://127.0.0.1:18006'); assert.equal(await access.validateOwnership('user-1', 'sess-fallback'), true); assert.deepEqual(await access.getSessionTarget('sess-fallback'), { target: 'https://127.0.0.1:18006', node: 0, }); }); test('createSessionAccess with enabled=true routes through broker', async () => { const userAuth = createMockUserAuth(); const access = createSessionAccess({ userAuth, enabled: true }); assert.equal(access.enabled, true); await access.registerSession({ userId: 'user-1', sessionId: 'sess-broker', target: 'h5-direct', origin: 'wechat', }); assert.equal(await access.validateOwnership('user-1', 'sess-broker'), true); const resolved = await access.resolveSession('sess-broker'); assert.equal(resolved.origin, 'wechat'); assert.equal(resolved.target, 'h5-direct'); }); test('createSessionBroker emits metrics when enabled', async () => { const userAuth = createMockUserAuth(); const lines = []; const broker = createSessionBroker({ userAuth, metricsEnabled: true, logger: { log: (line) => lines.push(line) }, }); await broker.registerSession({ userId: 'user-1', sessionId: 'sess-metrics', target: 'https://127.0.0.1:18006', }); await broker.registerSession({ userId: 'user-1', sessionId: 'sess-metrics', target: 'https://127.0.0.1:18006', }); assert.equal(await broker.validateOwnership('user-2', 'sess-metrics'), false); await userAuth.registerAgentSession('user-1', 'sess-node-only', 2); await broker.resolveSessionTarget('sess-node-only'); assert.ok(lines.some((line) => line.includes('register.duplicate'))); assert.ok(lines.some((line) => line.includes('ownership.denied'))); assert.ok(lines.some((line) => line.includes('resolve_target.miss'))); });