Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb4d6dbe77 |
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Regression: a logged-in user in "My Agents" with no agent created/selected
|
||||
* could type "hi" and get `400 {message:'agent_id is required in request body'}`.
|
||||
*
|
||||
* A missing agent_id on the agents endpoint is an ad-hoc (ephemeral) chat, not an
|
||||
* error. This hermetic test locks the access-middleware fix point (no MongoDB /
|
||||
* winston): canAccessAgentFromBody skips the per-agent ACL and calls next().
|
||||
* The downstream build-options fix point is covered by
|
||||
* ../../services/Endpoints/agents/build.spec.js.
|
||||
*/
|
||||
|
||||
// Mock externals so neither winston (data-schemas) nor MongoDB (~/models/Agent) load.
|
||||
jest.mock('@librechat/data-schemas', () => ({ logger: { error: jest.fn(), debug: jest.fn() } }));
|
||||
|
||||
jest.mock('librechat-data-provider', () => ({
|
||||
Constants: { EPHEMERAL_AGENT_ID: 'ephemeral' },
|
||||
ResourceType: { AGENT: 'agent' },
|
||||
// Real predicate semantics (mirrors packages/data-provider/src):
|
||||
isAgentsEndpoint: (endpoint) => endpoint === 'agents',
|
||||
isEphemeralAgentId: (agentId) => !agentId?.startsWith?.('agent_'),
|
||||
}));
|
||||
|
||||
const mockCanAccessResource = jest.fn(() => (req, res, next) => {
|
||||
req.__aclChecked = true;
|
||||
return next();
|
||||
});
|
||||
jest.mock('./canAccessResource', () => ({ canAccessResource: mockCanAccessResource }));
|
||||
|
||||
jest.mock('~/models/Agent', () => ({ getAgent: jest.fn() }));
|
||||
|
||||
const { canAccessAgentFromBody } = require('./canAccessAgentFromBody');
|
||||
|
||||
const makeReqRes = (body) => {
|
||||
const req = { body, params: {} };
|
||||
const res = {
|
||||
status: jest.fn().mockReturnThis(),
|
||||
json: jest.fn().mockReturnThis(),
|
||||
};
|
||||
const next = jest.fn();
|
||||
return { req, res, next };
|
||||
};
|
||||
|
||||
describe('agent_id missing → ephemeral fallback (no 400)', () => {
|
||||
const mw = canAccessAgentFromBody({ requiredPermission: 1 });
|
||||
|
||||
test('agents endpoint with NO agent_id proceeds without a 400', async () => {
|
||||
const { req, res, next } = makeReqRes({ endpoint: 'agents', text: 'hi' });
|
||||
await mw(req, res, next);
|
||||
|
||||
expect(res.status).not.toHaveBeenCalled();
|
||||
expect(next).toHaveBeenCalledTimes(1);
|
||||
expect(mockCanAccessResource).not.toHaveBeenCalled(); // no per-agent ACL for ephemeral
|
||||
});
|
||||
|
||||
test('non-agents endpoint with no agent_id proceeds (ephemeral)', async () => {
|
||||
const { req, res, next } = makeReqRes({ endpoint: 'openAI', text: 'hi' });
|
||||
await mw(req, res, next);
|
||||
|
||||
expect(res.status).not.toHaveBeenCalled();
|
||||
expect(next).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('explicit ephemeral id proceeds', async () => {
|
||||
const { req, res, next } = makeReqRes({ endpoint: 'agents', agent_id: 'ephemeral_primary' });
|
||||
await mw(req, res, next);
|
||||
|
||||
expect(res.status).not.toHaveBeenCalled();
|
||||
expect(next).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('a real agent_id still runs the per-agent ACL check', async () => {
|
||||
const { req, res, next } = makeReqRes({ endpoint: 'agents', agent_id: 'agent_abc123' });
|
||||
await mw(req, res, next);
|
||||
|
||||
// The middleware builds the ACL check with the real agent id as resourceIdParam,
|
||||
// then runs it (which here calls next via the mock).
|
||||
expect(mockCanAccessResource).toHaveBeenCalledTimes(1);
|
||||
expect(mockCanAccessResource.mock.calls[0][0]).toMatchObject({ resourceType: 'agent' });
|
||||
expect(next).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -60,15 +60,11 @@ const canAccessAgentFromBody = (options) => {
|
||||
agentId = Constants.EPHEMERAL_AGENT_ID;
|
||||
}
|
||||
|
||||
if (!agentId) {
|
||||
return res.status(400).json({
|
||||
error: 'Bad Request',
|
||||
message: 'agent_id is required in request body',
|
||||
});
|
||||
}
|
||||
|
||||
// Skip permission checks for ephemeral agents
|
||||
// Real agent IDs always start with "agent_", so anything else is ephemeral
|
||||
// A missing agent_id on the agents endpoint is an ad-hoc (ephemeral) chat,
|
||||
// not an error: "type a message and get a reply" must work before the user
|
||||
// has created or selected an agent. Ephemeral agents carry no per-agent ACL,
|
||||
// so skip the permission check and fall through to plain chat. A missing id
|
||||
// is ephemeral by the canonical predicate (isEphemeralAgentId(undefined) === true).
|
||||
if (isEphemeralAgentId(agentId)) {
|
||||
return next();
|
||||
}
|
||||
|
||||
@@ -104,13 +104,15 @@ describe('canAccessAgentFromBody middleware', () => {
|
||||
});
|
||||
|
||||
describe('primary agent checks', () => {
|
||||
test('returns 400 when agent_id is missing on agents endpoint', async () => {
|
||||
test('proceeds (ephemeral fallback) when agent_id is missing on agents endpoint', async () => {
|
||||
// No agent created/selected yet → ad-hoc chat. "Type hi and get a reply"
|
||||
// must work out of the box; a missing id is ephemeral, not a 400.
|
||||
req.body.agent_id = undefined;
|
||||
const middleware = canAccessAgentFromBody({ requiredPermission: 1 });
|
||||
await middleware(req, res, next);
|
||||
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
expect(res.status).toHaveBeenCalledWith(400);
|
||||
expect(next).toHaveBeenCalled();
|
||||
expect(res.status).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('proceeds for ephemeral primary agent without addedConvo', async () => {
|
||||
|
||||
@@ -7,7 +7,10 @@ const buildOptions = (req, endpoint, parsedBody, endpointType) => {
|
||||
const agentPromise = loadAgent({
|
||||
req,
|
||||
spec,
|
||||
agent_id: isAgentsEndpoint(endpoint) ? agent_id : Constants.EPHEMERAL_AGENT_ID,
|
||||
// No agent selected on the agents endpoint → ad-hoc (ephemeral) chat, so the
|
||||
// user gets a reply without first creating/selecting an agent. Mirrors the
|
||||
// access middleware, which treats a missing id as ephemeral.
|
||||
agent_id: isAgentsEndpoint(endpoint) && agent_id ? agent_id : Constants.EPHEMERAL_AGENT_ID,
|
||||
endpoint,
|
||||
model_parameters,
|
||||
}).catch((error) => {
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Regression: when no agent is created/selected, the agents endpoint must resolve
|
||||
* to an ephemeral (ad-hoc, plain-model) agent so the user gets a reply — instead
|
||||
* of loadAgent returning null because agent_id was missing.
|
||||
*
|
||||
* Hermetic: mocks data-schemas (no winston) and ~/models/Agent (no MongoDB).
|
||||
*/
|
||||
|
||||
jest.mock('@librechat/data-schemas', () => ({ logger: { error: jest.fn() } }));
|
||||
|
||||
jest.mock('librechat-data-provider', () => ({
|
||||
Constants: { EPHEMERAL_AGENT_ID: 'ephemeral' },
|
||||
isAgentsEndpoint: (endpoint) => endpoint === 'agents',
|
||||
removeNullishValues: (obj) =>
|
||||
Object.fromEntries(Object.entries(obj).filter(([, v]) => v != null)),
|
||||
}));
|
||||
|
||||
const mockLoadAgent = jest.fn(() => Promise.resolve({ id: 'ephemeral', model: 'zen3-nano' }));
|
||||
jest.mock('~/models/Agent', () => ({ loadAgent: mockLoadAgent }));
|
||||
|
||||
const { buildOptions } = require('./build');
|
||||
|
||||
const EPHEMERAL = 'ephemeral';
|
||||
|
||||
describe('agents buildOptions — missing agent_id resolves to ephemeral', () => {
|
||||
beforeEach(() => mockLoadAgent.mockClear());
|
||||
|
||||
test('no agent_id on agents endpoint → loadAgent called with EPHEMERAL_AGENT_ID', async () => {
|
||||
const req = { body: {}, user: { id: 'u1' } };
|
||||
// parsedBody as built from "My Agents" with no agent selected (no agent_id).
|
||||
const options = buildOptions(req, 'agents', { model: 'zen3-nano' });
|
||||
await options.agent;
|
||||
|
||||
expect(mockLoadAgent).toHaveBeenCalledTimes(1);
|
||||
expect(mockLoadAgent.mock.calls[0][0].agent_id).toBe(EPHEMERAL);
|
||||
});
|
||||
|
||||
test('real agent_id on agents endpoint is passed through unchanged', async () => {
|
||||
const req = { body: {}, user: { id: 'u1' } };
|
||||
const options = buildOptions(req, 'agents', { model: 'zen3-nano', agent_id: 'agent_real_42' });
|
||||
await options.agent;
|
||||
|
||||
expect(mockLoadAgent.mock.calls[0][0].agent_id).toBe('agent_real_42');
|
||||
});
|
||||
|
||||
test('non-agents endpoint always resolves to ephemeral', async () => {
|
||||
const req = { body: {}, user: { id: 'u1' } };
|
||||
const options = buildOptions(req, 'openAI', { model: 'zen3-nano', agent_id: 'agent_real_42' });
|
||||
await options.agent;
|
||||
|
||||
expect(mockLoadAgent.mock.calls[0][0].agent_id).toBe(EPHEMERAL);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user