merge: server architecture modularization
This commit is contained in:
@@ -0,0 +1,376 @@
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user