Files
memind/server/portal-agent-runtime-routes.test.mjs
2026-07-24 19:18:14 +08:00

377 lines
10 KiB
JavaScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { attachPortalAgentRuntimeRoutes } from './portal-agent-runtime-routes.mjs';
function createRouterRecorder() {
const routes = new Map();
return {
routes,
get(path, handler) {
routes.set(`GET ${path}`, handler);
},
post(path, handler) {
routes.set(`POST ${path}`, handler);
},
};
}
function createResponseRecorder() {
return {
statusCode: 200,
body: undefined,
status(code) {
this.statusCode = code;
return this;
},
json(body) {
this.body = body;
return this;
},
};
}
function createRequest(overrides = {}) {
return {
body: {},
params: {},
query: {},
...overrides,
};
}
function createProxy(calls) {
const handler = (name) => (req, _res, next) => {
calls.push({ kind: 'handler', name, req });
next();
};
return {
requireUser: handler('require-user'),
ensureChatAllowed: handler('ensure-chat-allowed'),
handlers: {
'POST /agent/start': [handler('start-1'), handler('start-2')],
'POST /agent/resume': [handler('resume')],
'GET /sessions': [handler('sessions')],
},
};
}
test('Agent runtime module preserves route inventory and order', () => {
const api = createRouterRecorder();
attachPortalAgentRuntimeRoutes(api);
assert.deepEqual([...api.routes.keys()], [
'POST /internal/deep-search/llm',
'POST /llm/apply-local-fallback',
'POST /agent/start',
'POST /agent/runs',
'GET /agent/runs/:runId',
'GET /agent/runs/:runId/events',
'POST /agent/resume',
'GET /sessions',
]);
});
test('deep search LLM gateway preserves internal auth, payload, response, and errors', async () => {
const calls = [];
const api = createRouterRecorder();
const logger = {
warn(...args) {
calls.push(['warn', ...args]);
},
};
attachPortalAgentRuntimeRoutes(api, {
getLlmProviderService: () => ({ id: 'llm' }),
getDeepSearchInternalSecret: () => 'expected-secret',
bearerToken: () => 'provided-secret',
async executeDeepSearchLlmGatewayFn(options) {
calls.push(['gateway', options]);
return {
status: 202,
body: { ok: true, accepted: true },
};
},
logger,
});
const req = createRequest({
body: { prompt: 'research' },
get: () => 'header-secret',
});
const res = createResponseRecorder();
await api.routes.get('POST /internal/deep-search/llm')(
req,
res,
);
assert.equal(res.statusCode, 202);
assert.deepEqual(res.body, {
ok: true,
accepted: true,
});
assert.deepEqual(calls[0], [
'gateway',
{
llmProviderService: { id: 'llm' },
expectedSecret: 'expected-secret',
providedSecret: 'provided-secret',
input: { prompt: 'research' },
},
]);
attachPortalAgentRuntimeRoutes(api, {
async executeDeepSearchLlmGatewayFn() {
throw new Error('gateway unavailable');
},
logger,
});
const errorRes = createResponseRecorder();
await api.routes.get('POST /internal/deep-search/llm')(
req,
errorRes,
);
assert.equal(errorRes.statusCode, 502);
assert.deepEqual(errorRes.body, {
ok: false,
message: 'gateway unavailable',
});
assert.equal(calls.at(-1)[0], 'warn');
});
test('local fallback preserves readiness, auth, ownership, and status gates', async () => {
const scenarios = [
{
name: 'unavailable',
dependencies: {},
expectedStatus: 503,
expectedBody: { message: '未启用 LLM 配置' },
},
{
name: 'signed out',
dependencies: {
getUserAuth: () => ({ getMe: async () => null }),
getLlmProviderService: () => ({}),
getTkmindProxy: () => ({}),
},
expectedStatus: 401,
expectedBody: { message: '未登录' },
},
{
name: 'missing session',
dependencies: {
getUserAuth: () => ({ getMe: async () => ({ id: 'user-1' }) }),
getLlmProviderService: () => ({}),
getTkmindProxy: () => ({}),
},
expectedStatus: 400,
expectedBody: { message: '缺少 session_id' },
},
{
name: 'not owner',
request: createRequest({ body: { session_id: 'session-1' } }),
dependencies: {
getUserAuth: () => ({ getMe: async () => ({ id: 'user-1' }) }),
getLlmProviderService: () => ({}),
getTkmindProxy: () => ({}),
},
expectedStatus: 403,
expectedBody: { message: '无权访问该会话' },
},
];
for (const scenario of scenarios) {
let ready = false;
const api = createRouterRecorder();
attachPortalAgentRuntimeRoutes(api, {
waitForUserAuthReady: async () => {
ready = true;
},
...scenario.dependencies,
});
const res = createResponseRecorder();
await api.routes.get('POST /llm/apply-local-fallback')(
scenario.request ?? createRequest(),
res,
);
assert.equal(ready, true, scenario.name);
assert.equal(res.statusCode, scenario.expectedStatus, scenario.name);
assert.deepEqual(res.body, scenario.expectedBody, scenario.name);
}
});
test('local fallback preserves token lookup, success, upstream failure, and exceptions', async () => {
const calls = [];
const proxy = {
async applyLocalFallbackForSession(sessionId) {
calls.push({ kind: 'fallback', sessionId });
return { ok: true, provider: 'local' };
},
};
const api = createRouterRecorder();
attachPortalAgentRuntimeRoutes(api, {
getUserAuth: () => ({
async getMe(token) {
calls.push({ kind: 'get-me', token });
return { id: 'user-1' };
},
}),
getLlmProviderService: () => ({}),
getTkmindProxy: () => proxy,
getUserToken: () => 'token-1',
async ownsAgentSession(userId, sessionId) {
calls.push({ kind: 'owns', userId, sessionId });
return true;
},
});
const req = createRequest({ body: { session_id: ' session-1 ' } });
const res = createResponseRecorder();
await api.routes.get('POST /llm/apply-local-fallback')(req, res);
assert.equal(res.statusCode, 200);
assert.deepEqual(res.body, { ok: true, provider: 'local' });
assert.deepEqual(calls, [
{ kind: 'get-me', token: 'token-1' },
{ kind: 'owns', userId: 'user-1', sessionId: 'session-1' },
{ kind: 'fallback', sessionId: 'session-1' },
]);
proxy.applyLocalFallbackForSession = async () => ({
ok: false,
reason: 'relay_unavailable',
});
const unavailableRes = createResponseRecorder();
await api.routes.get('POST /llm/apply-local-fallback')(req, unavailableRes);
assert.equal(unavailableRes.statusCode, 503);
assert.deepEqual(unavailableRes.body, {
ok: false,
reason: 'relay_unavailable',
});
proxy.applyLocalFallbackForSession = async () => {
throw new Error('switch failed');
};
const errorRes = createResponseRecorder();
await api.routes.get('POST /llm/apply-local-fallback')(req, errorRes);
assert.equal(errorRes.statusCode, 500);
assert.deepEqual(errorRes.body, { message: 'switch failed' });
});
test('proxy-backed routes preserve handler order and fall through after completion', async () => {
const calls = [];
const api = createRouterRecorder();
const proxy = createProxy(calls);
attachPortalAgentRuntimeRoutes(api, {
getUserAuth: () => ({ id: 'auth' }),
getTkmindProxy: () => proxy,
getAgentRunGateway: () => ({ id: 'gateway' }),
getSessionAccess: () => ({ id: 'session-access' }),
getMindSpaceAssetAgent: () => ({ id: 'asset-agent' }),
getCodeRunPolicyService: () => ({ id: 'code-run-policy' }),
createPostAgentRunsHandlerFn(dependencies) {
calls.push({ kind: 'create-post', dependencies });
return (req, _res, next) => {
calls.push({ kind: 'handler', name: 'post-run', req });
next();
};
},
createGetAgentRunHandlerFn(dependencies) {
calls.push({ kind: 'create-get', dependencies });
return (req, _res, next) => {
calls.push({ kind: 'handler', name: 'get-run', req });
next();
};
},
createAgentRunEventsHandlerFn(dependencies) {
calls.push({ kind: 'create-events', dependencies });
return (req, _res, next) => {
calls.push({ kind: 'handler', name: 'run-events', req });
next();
};
},
});
for (const route of [
'POST /agent/start',
'POST /agent/runs',
'GET /agent/runs/:runId',
'GET /agent/runs/:runId/events',
'POST /agent/resume',
'GET /sessions',
]) {
const req = createRequest({ route });
let nextCount = 0;
await api.routes.get(route)(req, createResponseRecorder(), () => {
nextCount += 1;
});
assert.equal(nextCount, 0, `${route} does not fall through after a handled chain`);
}
assert.deepEqual(
calls.filter((call) => call.kind === 'handler').map((call) => call.name),
[
'start-1',
'start-2',
'require-user',
'ensure-chat-allowed',
'post-run',
'require-user',
'get-run',
'require-user',
'run-events',
'resume',
'sessions',
],
);
assert.deepEqual(
calls.find((call) => call.kind === 'create-post').dependencies,
{
userAuth: { id: 'auth' },
sessionAccess: { id: 'session-access' },
agentRunGateway: { id: 'gateway' },
mindSpaceAssetAgent: { id: 'asset-agent' },
codeRunPolicyService: { id: 'code-run-policy' },
},
);
});
test('proxy-backed routes preserve unavailable fallthrough and middleware errors', async () => {
const api = createRouterRecorder();
attachPortalAgentRuntimeRoutes(api);
for (const route of [
'POST /agent/start',
'POST /agent/runs',
'GET /agent/runs/:runId',
'GET /agent/runs/:runId/events',
'POST /agent/resume',
'GET /sessions',
]) {
let nextCount = 0;
await api.routes.get(route)(
createRequest(),
createResponseRecorder(),
() => {
nextCount += 1;
},
);
assert.equal(nextCount, 1, route);
}
const failure = new Error('middleware failed');
const errorApi = createRouterRecorder();
attachPortalAgentRuntimeRoutes(errorApi, {
getUserAuth: () => ({}),
getTkmindProxy: () => ({
handlers: {
'POST /agent/start': [
(_req, _res, next) => next(failure),
() => assert.fail('chain must stop after an error'),
],
},
}),
});
let forwarded;
await errorApi.routes.get('POST /agent/start')(
createRequest(),
createResponseRecorder(),
(error) => {
forwarded = error;
},
);
assert.equal(forwarded, failure);
});