🧭 test: Cover Agent Handoffs End to End (#14428)
Some checks failed
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Has been cancelled
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Has been cancelled
GitNexus Index / index (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Has been cancelled
Sync Helm Chart Tags / Ignore non-main push (push) Has been cancelled
Sync Helm Chart Tags / Sync chart tags (push) Has been cancelled
GitNexus Index / post-index (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Has been cancelled

* test: cover agent handoffs end to end

* style: sort handoff imports

* fix: normalize missing agent handoff edges

* chore: update package dependencies and versions in package-lock.json and package.json

* chore: bump agents SDK
This commit is contained in:
Danny Avila 2026-07-27 08:47:15 -04:00 committed by GitHub
parent 8374b8416a
commit a53936d273
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 3160 additions and 527 deletions

View file

@ -8,6 +8,7 @@ const {
agentUpdateSchema,
refreshListAvatars,
collectEdgeAgentIds,
replaceEdgeSourceId,
mergeDeploymentSkillIds,
mergeAgentOcrConversion,
sanitizeModelParameters,
@ -143,18 +144,30 @@ const classifyAgentReferences = async (agentIds, userId, userRole) => {
};
/**
* Validates VIEW access for every agent referenced in `edges`.
* Missing ids are NOT errors here at create time a self-referential
* `from` often names the agent being built, which has no DB record
* yet. Only unauthorized (existing but unviewable) ids are returned.
* Validates that every agent referenced in `edges` exists and is viewable.
* The create path may allow its newly generated self id because that agent
* has not been inserted yet; all other missing references are invalid.
* @param {GraphEdge[]} edges
* @param {string} userId
* @param {string} userRole
* @param {Set<string>} [allowedMissingIds]
* @returns {Promise<{ missing: string[], unauthorized: string[] }>}
*/
const validateEdgeAgentAccess = async (edges, userId, userRole) => {
const { unauthorized } = await classifyAgentReferences(
const validateEdgeAgentReferences = async (
edges,
userId,
userRole,
allowedMissingIds = new Set(),
) => {
const { missing, unauthorized } = await classifyAgentReferences(
collectEdgeAgentIds(edges),
userId,
userRole,
);
return unauthorized;
return {
missing: missing.filter((id) => !allowedMissingIds.has(id)),
unauthorized,
};
};
/**
@ -366,6 +379,8 @@ const createAgentHandler = async (req, res) => {
}
const { id: userId, role: userRole } = req.user;
agentData.id = `agent_${nanoid()}`;
agentData.edges = replaceEdgeSourceId(agentData.edges, '', agentData.id);
if (agentData.tool_resources) {
await pruneToolResourceFileIdsForAgent({
@ -376,7 +391,18 @@ const createAgentHandler = async (req, res) => {
}
if (agentData.edges?.length) {
const unauthorized = await validateEdgeAgentAccess(agentData.edges, userId, userRole);
const { missing, unauthorized } = await validateEdgeAgentReferences(
agentData.edges,
userId,
userRole,
new Set([agentData.id]),
);
if (missing.length > 0) {
return res.status(400).json({
error: 'One or more agents referenced in edges do not exist',
agent_ids: missing,
});
}
if (unauthorized.length > 0) {
return res.status(403).json({
error: 'You do not have access to one or more agents referenced in edges',
@ -423,7 +449,6 @@ const createAgentHandler = async (req, res) => {
}
}
agentData.id = `agent_${nanoid()}`;
agentData.author = userId;
agentData.tools = [];
@ -629,9 +654,23 @@ const updateAgentHandler = async (req, res) => {
updateData.avatar = avatarField;
}
if (updateData.edges !== undefined) {
updateData.edges = replaceEdgeSourceId(updateData.edges, '', id);
}
if (updateData.edges?.length) {
const { id: userId, role: userRole } = req.user;
const unauthorized = await validateEdgeAgentAccess(updateData.edges, userId, userRole);
const { missing, unauthorized } = await validateEdgeAgentReferences(
updateData.edges,
userId,
userRole,
);
if (missing.length > 0) {
return res.status(400).json({
error: 'One or more agents referenced in edges do not exist',
agent_ids: missing,
});
}
if (unauthorized.length > 0) {
return res.status(403).json({
error: 'You do not have access to one or more agents referenced in edges',
@ -802,7 +841,7 @@ const updateAgentHandler = async (req, res) => {
*/
const duplicateAgentHandler = async (req, res) => {
const { id } = req.params;
const { id: userId } = req.user;
const { id: userId, role: userRole } = req.user;
const sensitiveFields = ['api_key', 'oauth_client_id', 'oauth_client_secret'];
try {
@ -852,6 +891,29 @@ const duplicateAgentHandler = async (req, res) => {
id: newAgentId,
author: userId,
});
newAgentData.edges = replaceEdgeSourceId(newAgentData.edges, id, newAgentId);
newAgentData.edges = replaceEdgeSourceId(newAgentData.edges, '', newAgentId);
if (newAgentData.edges?.length) {
const { missing, unauthorized } = await validateEdgeAgentReferences(
newAgentData.edges,
userId,
userRole,
new Set([newAgentId]),
);
if (missing.length > 0) {
return res.status(400).json({
error: 'One or more agents referenced in edges do not exist',
agent_ids: missing,
});
}
if (unauthorized.length > 0) {
return res.status(403).json({
error: 'You do not have access to one or more agents referenced in edges',
agent_ids: unauthorized,
});
}
}
const newActionsList = [];
const originalActions = (await db.getActions({ agent_id: id }, true)) ?? [];
@ -1274,10 +1336,42 @@ const revertAgentVersionHandler = async (req, res) => {
return res.status(404).json({ error: 'Agent not found' });
}
const revertVersion = existingAgent.versions?.[version_index];
const storedRevertEdges = Array.isArray(revertVersion?.edges) ? revertVersion.edges : [];
const revertEdges = replaceEdgeSourceId(storedRevertEdges, '', id);
const hasLegacyEdgeSource = storedRevertEdges.some((edge) =>
Array.isArray(edge.from) ? edge.from.includes('') : edge.from === '',
);
if (revertEdges.length > 0) {
const { missing, unauthorized } = await validateEdgeAgentReferences(
revertEdges,
req.user.id,
req.user.role,
);
if (missing.length > 0) {
return res.status(400).json({
error: 'One or more agents referenced in edges do not exist',
agent_ids: missing,
});
}
if (unauthorized.length > 0) {
return res.status(403).json({
error: 'You do not have access to one or more agents referenced in edges',
agent_ids: unauthorized,
});
}
}
// Permissions are enforced via route middleware (ACL EDIT)
let updatedAgent = await db.revertAgentVersion({ id }, version_index);
const revertUpdates = {};
if (
revertVersion &&
(hasLegacyEdgeSource || (!Array.isArray(revertVersion.edges) && updatedAgent.edges?.length))
) {
revertUpdates.edges = revertEdges;
}
if (updatedAgent.tools?.length) {
const [availableTools, configServers] = await Promise.all([

View file

@ -2475,7 +2475,7 @@ describe('Agent Controllers - Mass Assignment Protection', () => {
name: 'Attacker Agent',
provider: 'openai',
model: 'gpt-4',
edges: [{ from: 'self_placeholder', to: targetAgent.id, edgeType: 'handoff' }],
edges: [{ from: '', to: targetAgent.id, edgeType: 'handoff' }],
};
await createAgentHandler(mockReq, mockRes);
@ -2493,25 +2493,33 @@ describe('Agent Controllers - Mass Assignment Protection', () => {
name: 'Legit Agent',
provider: 'openai',
model: 'gpt-4',
edges: [{ from: 'self_placeholder', to: targetAgent.id, edgeType: 'handoff' }],
edges: [{ from: '', to: targetAgent.id, edgeType: 'handoff' }],
};
await createAgentHandler(mockReq, mockRes);
expect(mockRes.status).toHaveBeenCalledWith(201);
const response = mockRes.json.mock.calls[0][0];
expect(response.edges).toEqual([
{ from: response.id, to: targetAgent.id, edgeType: 'handoff' },
]);
});
test('createAgentHandler should allow edges referencing non-existent agents (self-reference at create time)', async () => {
test('createAgentHandler should reject a non-existent handoff target', async () => {
mockReq.body = {
name: 'Self-Ref Agent',
name: 'Dangling Edge Agent',
provider: 'openai',
model: 'gpt-4',
edges: [{ from: 'agent_does_not_exist_yet', to: 'agent_also_new', edgeType: 'handoff' }],
edges: [{ from: '', to: 'agent_missing_target', edgeType: 'handoff' }],
};
await createAgentHandler(mockReq, mockRes);
expect(mockRes.status).toHaveBeenCalledWith(201);
expect(mockRes.status).toHaveBeenCalledWith(400);
expect(mockRes.json).toHaveBeenCalledWith({
error: 'One or more agents referenced in edges do not exist',
agent_ids: ['agent_missing_target'],
});
});
test('updateAgentHandler should return 403 when user lacks VIEW on an edge-referenced agent', async () => {
@ -2540,6 +2548,42 @@ describe('Agent Controllers - Mass Assignment Protection', () => {
expect(response.agent_ids).not.toContain(ownedAgent.id);
});
test('updateAgentHandler should repair a legacy empty handoff source', async () => {
const ownedAgent = await Agent.create({
id: `agent_${nanoid()}`,
author: mockReq.user.id,
name: 'Legacy Router',
provider: 'openai',
model: 'gpt-4',
tools: [],
edges: [{ from: '', to: targetAgent.id, edgeType: 'handoff' }],
});
getResourcePermissionsMap.mockResolvedValueOnce(
new Map([
[ownedAgent._id.toString(), PermissionBits.VIEW],
[targetAgent._id.toString(), PermissionBits.VIEW],
]),
);
mockReq.params = { id: ownedAgent.id };
mockReq.body = {
edges: [{ from: '', to: targetAgent.id, edgeType: 'handoff' }],
};
await updateAgentHandler(mockReq, mockRes);
expect(mockRes.status).not.toHaveBeenCalledWith(400);
expect(mockRes.json).toHaveBeenCalledWith(
expect.objectContaining({
edges: [{ from: ownedAgent.id, to: targetAgent.id, edgeType: 'handoff' }],
}),
);
const persisted = await Agent.findOne({ id: ownedAgent.id }).lean();
expect(persisted.edges).toEqual([
{ from: ownedAgent.id, to: targetAgent.id, edgeType: 'handoff' },
]);
});
test('updateAgentHandler should succeed when edges field is absent from payload', async () => {
const ownedAgent = await Agent.create({
id: `agent_${nanoid()}`,
@ -2559,5 +2603,238 @@ describe('Agent Controllers - Mass Assignment Protection', () => {
const response = mockRes.json.mock.calls[0][0];
expect(response.name).toBe('Renamed Agent');
});
test('duplicateAgentHandler should move current and legacy handoff sources to the clone', async () => {
const sourceAgentId = `agent_${nanoid()}`;
const secondTarget = await Agent.create({
id: `agent_${nanoid()}`,
author: new mongoose.Types.ObjectId().toString(),
name: 'Second Target Agent',
provider: 'openai',
model: 'gpt-4',
tools: [],
});
const sourceAgent = await Agent.create({
id: sourceAgentId,
author: mockReq.user.id,
name: 'Legacy Clone Source',
provider: 'openai',
model: 'gpt-4',
tools: [],
edges: [
{ from: sourceAgentId, to: targetAgent.id, edgeType: 'handoff' },
{ from: '', to: secondTarget.id, edgeType: 'handoff' },
],
});
getResourcePermissionsMap.mockResolvedValueOnce(
new Map([
[targetAgent._id.toString(), PermissionBits.VIEW],
[secondTarget._id.toString(), PermissionBits.VIEW],
]),
);
jest.spyOn(require('~/models'), 'getActions').mockResolvedValueOnce([]);
mockReq.params = { id: sourceAgent.id };
await duplicateAgentHandler(mockReq, mockRes);
expect(mockRes.status).toHaveBeenCalledWith(201);
const { agent } = mockRes.json.mock.calls[0][0];
expect(agent.edges).toEqual([
{ from: agent.id, to: targetAgent.id, edgeType: 'handoff' },
{ from: agent.id, to: secondTarget.id, edgeType: 'handoff' },
]);
});
test('duplicateAgentHandler should return 400 for a missing handoff target', async () => {
const missingTargetId = `agent_${nanoid()}`;
const sourceAgent = await Agent.create({
id: `agent_${nanoid()}`,
author: mockReq.user.id,
name: 'Stale Clone Source',
provider: 'openai',
model: 'gpt-4',
tools: [],
edges: [{ from: '', to: missingTargetId, edgeType: 'handoff' }],
});
mockReq.params = { id: sourceAgent.id };
await duplicateAgentHandler(mockReq, mockRes);
expect(mockRes.status).toHaveBeenCalledWith(400);
expect(mockRes.json).toHaveBeenCalledWith({
error: 'One or more agents referenced in edges do not exist',
agent_ids: [missingTargetId],
});
expect(await Agent.countDocuments()).toBe(2);
});
test('duplicateAgentHandler should return 403 without VIEW access to a handoff target', async () => {
const sourceAgentId = `agent_${nanoid()}`;
const sourceAgent = await Agent.create({
id: sourceAgentId,
author: mockReq.user.id,
name: 'Restricted Clone Source',
provider: 'openai',
model: 'gpt-4',
tools: [],
edges: [{ from: sourceAgentId, to: targetAgent.id, edgeType: 'handoff' }],
});
getResourcePermissionsMap.mockResolvedValueOnce(new Map());
mockReq.params = { id: sourceAgent.id };
await duplicateAgentHandler(mockReq, mockRes);
expect(mockRes.status).toHaveBeenCalledWith(403);
expect(mockRes.json).toHaveBeenCalledWith({
error: 'You do not have access to one or more agents referenced in edges',
agent_ids: [targetAgent.id],
});
expect(await Agent.countDocuments()).toBe(2);
});
test('revertAgentVersionHandler should clear handoffs when the historical version has none', async () => {
const agentId = `agent_${nanoid()}`;
await Agent.create({
id: agentId,
author: mockReq.user.id,
name: 'Current Router',
provider: 'openai',
model: 'gpt-4',
tools: [],
edges: [{ from: agentId, to: targetAgent.id, edgeType: 'handoff' }],
versions: [
{
name: 'Historical Router',
provider: 'openai',
model: 'gpt-4',
tools: [],
},
],
});
mockReq.params = { id: agentId };
mockReq.body = { version_index: 0 };
await revertAgentVersionHandler(mockReq, mockRes);
expect(mockRes.status).not.toHaveBeenCalledWith(400);
const persisted = await Agent.findOne({ id: agentId }).lean();
expect(persisted.name).toBe('Historical Router');
expect(persisted.edges).toEqual([]);
});
test('revertAgentVersionHandler should restore accessible historical handoffs', async () => {
const agentId = `agent_${nanoid()}`;
const sourceAgent = await Agent.create({
id: agentId,
author: mockReq.user.id,
name: 'Current Router',
provider: 'openai',
model: 'gpt-4',
tools: [],
edges: [],
versions: [
{
name: 'Historical Router',
provider: 'openai',
model: 'gpt-4',
tools: [],
edges: [{ from: '', to: targetAgent.id, edgeType: 'handoff' }],
},
],
});
getResourcePermissionsMap.mockResolvedValueOnce(
new Map([
[sourceAgent._id.toString(), PermissionBits.VIEW],
[targetAgent._id.toString(), PermissionBits.VIEW],
]),
);
mockReq.params = { id: agentId };
mockReq.body = { version_index: 0 };
await revertAgentVersionHandler(mockReq, mockRes);
expect(mockRes.status).not.toHaveBeenCalledWith(400);
expect(mockRes.status).not.toHaveBeenCalledWith(403);
const persisted = await Agent.findOne({ id: agentId }).lean();
expect(persisted.name).toBe('Historical Router');
expect(persisted.edges).toEqual([{ from: agentId, to: targetAgent.id, edgeType: 'handoff' }]);
});
test('revertAgentVersionHandler should return 400 before restoring a missing handoff target', async () => {
const agentId = `agent_${nanoid()}`;
const missingTargetId = `agent_${nanoid()}`;
await Agent.create({
id: agentId,
author: mockReq.user.id,
name: 'Current Router',
provider: 'openai',
model: 'gpt-4',
tools: [],
versions: [
{
name: 'Stale Historical Router',
provider: 'openai',
model: 'gpt-4',
tools: [],
edges: [{ from: agentId, to: missingTargetId, edgeType: 'handoff' }],
},
],
});
mockReq.params = { id: agentId };
mockReq.body = { version_index: 0 };
await revertAgentVersionHandler(mockReq, mockRes);
expect(mockRes.status).toHaveBeenCalledWith(400);
expect(mockRes.json).toHaveBeenCalledWith({
error: 'One or more agents referenced in edges do not exist',
agent_ids: [missingTargetId],
});
const persisted = await Agent.findOne({ id: agentId }).lean();
expect(persisted.name).toBe('Current Router');
});
test('revertAgentVersionHandler should return 403 before restoring a restricted handoff target', async () => {
const agentId = `agent_${nanoid()}`;
const sourceAgent = await Agent.create({
id: agentId,
author: mockReq.user.id,
name: 'Current Router',
provider: 'openai',
model: 'gpt-4',
tools: [],
versions: [
{
name: 'Restricted Historical Router',
provider: 'openai',
model: 'gpt-4',
tools: [],
edges: [{ from: agentId, to: targetAgent.id, edgeType: 'handoff' }],
},
],
});
getResourcePermissionsMap.mockResolvedValueOnce(
new Map([[sourceAgent._id.toString(), PermissionBits.VIEW]]),
);
mockReq.params = { id: agentId };
mockReq.body = { version_index: 0 };
await revertAgentVersionHandler(mockReq, mockRes);
expect(mockRes.status).toHaveBeenCalledWith(403);
expect(mockRes.json).toHaveBeenCalledWith({
error: 'You do not have access to one or more agents referenced in edges',
agent_ids: [targetAgent.id],
});
const persisted = await Agent.findOne({ id: agentId }).lean();
expect(persisted.name).toBe('Current Router');
});
});
});