import assert from 'node:assert/strict'; import test from 'node:test'; import { attachPortalRuntimeRoutes } from './portal-runtime-routes.mjs'; function createRouterRecorder() { const routes = new Map(); return { routes, get(path, handler) { routes.set(path, handler); }, }; } function createResponseRecorder() { return { headers: new Map(), statusCode: 200, body: undefined, responseType: null, status(code) { this.statusCode = code; return this; }, send(body) { this.responseType = 'send'; this.body = body; return this; }, json(body) { this.responseType = 'json'; this.body = body; return this; }, setHeader(name, value) { this.headers.set(String(name).toLowerCase(), String(value)); return this; }, }; } test('portal runtime route module preserves the status route inventory', () => { const api = createRouterRecorder(); attachPortalRuntimeRoutes(api); assert.deepEqual([...api.routes.keys()], ['/status', '/runtime/status']); }); test('GET /status preserves upstream status and body', async () => { const api = createRouterRecorder(); let waited = false; attachPortalRuntimeRoutes(api, { waitForUserAuthReady: async () => { waited = true; }, getUserAuth: () => ({}), getTkmindProxy: () => ({ async apiFetch(path, init) { assert.equal(path, '/status'); assert.deepEqual(init, { method: 'GET' }); return { status: 207, async text() { return 'upstream-status'; }, }; }, }), }); const res = createResponseRecorder(); let nextCalled = false; await api.routes.get('/status')({}, res, () => { nextCalled = true; }); assert.equal(waited, true); assert.equal(nextCalled, false); assert.equal(res.statusCode, 207); assert.equal(res.responseType, 'send'); assert.equal(res.body, 'upstream-status'); assert.equal(res.headers.get('x-memind-runtime-role'), 'stable'); }); test('GET /status identifies a passive candidate runtime without changing the body', async () => { const api = createRouterRecorder(); attachPortalRuntimeRoutes(api, { env: { MEMIND_RUNTIME_ROLE: 'candidate' }, getUserAuth: () => ({}), getTkmindProxy: () => ({ async apiFetch() { return { status: 200, async text() { return 'ok'; }, }; }, }), }); const res = createResponseRecorder(); await api.routes.get('/status')({}, res, () => {}); assert.equal(res.body, 'ok'); assert.equal(res.headers.get('x-memind-runtime-role'), 'candidate'); }); test('GET /status falls through when multi-user proxy services are unavailable', async () => { const api = createRouterRecorder(); attachPortalRuntimeRoutes(api); const res = createResponseRecorder(); let nextCalled = false; await api.routes.get('/status')({}, res, () => { nextCalled = true; }); assert.equal(nextCalled, true); assert.equal(res.body, undefined); }); test('GET /status preserves proxy failure response', async () => { const api = createRouterRecorder(); attachPortalRuntimeRoutes(api, { getUserAuth: () => ({}), getTkmindProxy: () => ({ async apiFetch() { throw new Error('proxy down'); }, }), }); const res = createResponseRecorder(); await api.routes.get('/status')({}, res, () => {}); assert.equal(res.statusCode, 502); assert.deepEqual(res.body, { message: 'proxy down' }); }); test('GET /runtime/status preserves memory, queue, and code-run policy projection', async () => { const api = createRouterRecorder(); attachPortalRuntimeRoutes(api, { getTkmindProxy: () => ({ async getRuntimeStatus() { return { workers: [{ id: 'goosed-1' }], memory: { backend: 'legacy' } }; }, }), getEpisodicMemoryService: () => ({ async getStatus() { return { configuredEnabled: true, mode: 'canary' }; }, }), getAgentRunGateway: () => ({ async getQueueStatus() { return { active: 1, pendingDispatches: 2 }; }, }), env: { MEMIND_AGENT_CODE_RUNS_ENABLED: '1', MEMIND_AGENT_CODE_RUNS_USER_IDS: 'user-a, user-b', MEMIND_AGENT_CODE_RUN_TASK_TYPES: 'page,code', MEMIND_AGENT_CODE_RUNS_REQUIRE_VALIDATION: 'true', }, }); const res = createResponseRecorder(); await api.routes.get('/runtime/status')({}, res); assert.equal(res.statusCode, 200); assert.equal(res.body.ok, true); assert.equal(typeof res.body.timestamp, 'string'); assert.deepEqual(res.body.memory, { backend: 'legacy', episodic: { configuredEnabled: true, mode: 'canary' }, }); assert.deepEqual(res.body.toolRuntime, { codeRunPolicy: { source: 'env', enabled: true, clientEnabled: false, userAllowlist: ['user-a', 'user-b'], taskTypeAllowlist: ['page', 'code'], requireValidation: true, pageDataDevAutodetect: false, generalAutodetect: false, envOverrideActive: false, updatedAt: null, updatedBy: null, }, queue: { active: 1, pendingDispatches: 2 }, }); }); test('GET /runtime/status preserves fail-open dependency projections', async () => { const api = createRouterRecorder(); attachPortalRuntimeRoutes(api, { getTkmindProxy: () => ({ async getRuntimeStatus() { return { memory: { backend: 'legacy' } }; }, }), getEpisodicMemoryService: () => ({ async getStatus() { throw new Error('episodic unavailable'); }, }), getAgentRunGateway: () => ({ async getQueueStatus() { throw new Error('queue unavailable'); }, }), }); const res = createResponseRecorder(); await api.routes.get('/runtime/status')({}, res); assert.deepEqual(res.body.memory, { backend: 'legacy', episodic: { configuredEnabled: false, mode: 'off', error: 'episodic unavailable', }, }); assert.deepEqual(res.body.toolRuntime.queue, { error: 'queue unavailable' }); }); test('GET /runtime/status preserves unavailable and upstream error responses', async () => { const unavailableApi = createRouterRecorder(); attachPortalRuntimeRoutes(unavailableApi); const unavailableRes = createResponseRecorder(); await unavailableApi.routes.get('/runtime/status')({}, unavailableRes); assert.equal(unavailableRes.statusCode, 503); assert.deepEqual(unavailableRes.body, { ok: false, message: 'runtime router unavailable', }); const failingApi = createRouterRecorder(); attachPortalRuntimeRoutes(failingApi, { getTkmindProxy: () => ({ async getRuntimeStatus() { throw new Error('runtime down'); }, }), }); const failingRes = createResponseRecorder(); await failingApi.routes.get('/runtime/status')({}, failingRes); assert.equal(failingRes.statusCode, 502); assert.deepEqual(failingRes.body, { ok: false, message: 'runtime down', }); });