377 lines
10 KiB
JavaScript
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);
|
|
});
|