mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-04 14:57:42 +00:00
🛂 test: Cover Tool Approval Workflows End to End (#14427)
* test: cover tool approval workflows end to end * fix: preserve tool approval state across resume * fix: preserve agent context in mock stream responses * fix: preserve nested approvals in collapsed groups
This commit is contained in:
parent
f3159f9891
commit
d8427ffc5e
42 changed files with 2960 additions and 145 deletions
|
|
@ -101,6 +101,7 @@ const {
|
|||
processRequiredActions,
|
||||
resolveAgentCapabilities,
|
||||
} = require('../ToolService');
|
||||
const { createOnSearchResults } = require('~/server/services/Tools/search');
|
||||
const { reinitMCPServer } = require('~/server/services/Tools/mcp');
|
||||
const { PENDING_STALE_MS } = require('@librechat/api');
|
||||
|
||||
|
|
@ -425,6 +426,66 @@ describe('ToolService - Action Capability Gating', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it('fences resumable MCP OAuth definition events to the owning job epoch', async () => {
|
||||
const req = createMockReq([AgentCapabilities.tools]);
|
||||
const res = { writableEnded: false };
|
||||
const serverName = 'Epoch-Server';
|
||||
const streamId = 'stream-epoch';
|
||||
const jobCreatedAt = 1234;
|
||||
const mcpTool = `search${Constants.mcp_delimiter}${serverName}`;
|
||||
mockGetEndpointsConfig.mockResolvedValue(createEndpointsConfig([AgentCapabilities.tools]));
|
||||
mockResolveConfigServers.mockResolvedValue({
|
||||
[serverName]: {
|
||||
type: 'streamable-http',
|
||||
url: `https://mcp.example.com/${serverName}`,
|
||||
requiresOAuth: true,
|
||||
},
|
||||
});
|
||||
mockLoadToolDefinitions
|
||||
.mockImplementationOnce(async (_args, deps) => {
|
||||
await deps.getOrFetchMCPServerTools(req.user.id, serverName);
|
||||
return {
|
||||
toolDefinitions: [],
|
||||
toolRegistry: new Map(),
|
||||
hasDeferredTools: false,
|
||||
};
|
||||
})
|
||||
.mockResolvedValue({
|
||||
toolDefinitions: [mcpTool],
|
||||
toolRegistry: new Map(),
|
||||
hasDeferredTools: false,
|
||||
});
|
||||
reinitMCPServer.mockImplementation(async ({ returnOnOAuth, oauthStart, oauthEnd }) => {
|
||||
await oauthStart(`https://auth.example.com/${serverName}`);
|
||||
if (returnOnOAuth === false) {
|
||||
await oauthEnd();
|
||||
return { availableTools: { [mcpTool]: {} } };
|
||||
}
|
||||
return { availableTools: null };
|
||||
});
|
||||
|
||||
await loadAgentTools({
|
||||
req,
|
||||
res,
|
||||
agent: { id: 'agent_123', tools: [mcpTool] },
|
||||
definitionsOnly: true,
|
||||
streamId,
|
||||
jobCreatedAt,
|
||||
});
|
||||
|
||||
expect(mockSendEvent).not.toHaveBeenCalled();
|
||||
expect(mockEmitChunk).toHaveBeenCalledTimes(3);
|
||||
expect(mockEmitChunk.mock.calls.map(([, event]) => event.event)).toEqual([
|
||||
'on_run_step',
|
||||
'on_run_step_delta',
|
||||
'on_run_step_completed',
|
||||
]);
|
||||
for (const [emittedStreamId, , options] of mockEmitChunk.mock.calls) {
|
||||
expect(emittedStreamId).toBe(streamId);
|
||||
expect(options).toEqual({ expectedCreatedAt: jobCreatedAt });
|
||||
}
|
||||
});
|
||||
|
||||
it('should not expose cached MCP tool definitions when the registry lookup fails', async () => {
|
||||
const serverName = 'private-server';
|
||||
const mcpTool = `search${Constants.mcp_delimiter}${serverName}`;
|
||||
|
|
@ -968,6 +1029,24 @@ describe('ToolService - Action Capability Gating', () => {
|
|||
const actionToolName = `get_weather${actionDelimiter}api_example_com`;
|
||||
const regularTool = 'calculator';
|
||||
|
||||
it('threads the owning job epoch into web-search attachment callbacks', async () => {
|
||||
const capabilities = [AgentCapabilities.tools, AgentCapabilities.web_search];
|
||||
const req = createMockReq(capabilities);
|
||||
const res = {};
|
||||
mockGetEndpointsConfig.mockResolvedValue(createEndpointsConfig(capabilities));
|
||||
|
||||
await loadAgentTools({
|
||||
req,
|
||||
res,
|
||||
streamId: 'conversation-1',
|
||||
jobCreatedAt: 1234,
|
||||
agent: { id: 'agent_123', tools: [Tools.web_search] },
|
||||
definitionsOnly: false,
|
||||
});
|
||||
|
||||
expect(createOnSearchResults).toHaveBeenCalledWith(res, 'conversation-1', 1234);
|
||||
});
|
||||
|
||||
it('should not load action sets when actions capability is disabled', async () => {
|
||||
const capabilities = [AgentCapabilities.tools, AgentCapabilities.web_search];
|
||||
const req = createMockReq(capabilities);
|
||||
|
|
@ -1003,6 +1082,25 @@ describe('ToolService - Action Capability Gating', () => {
|
|||
const actionToolName = `get_weather${actionDelimiter}api_example_com`;
|
||||
const regularTool = Tools.web_search;
|
||||
|
||||
it('threads the owning job epoch into web-search attachment callbacks', async () => {
|
||||
const capabilities = [AgentCapabilities.tools, AgentCapabilities.web_search];
|
||||
const req = createMockReq(capabilities);
|
||||
const res = {};
|
||||
mockGetEndpointsConfig.mockResolvedValue(createEndpointsConfig(capabilities));
|
||||
|
||||
await loadToolsForExecution({
|
||||
req,
|
||||
res,
|
||||
streamId: 'conversation-1',
|
||||
jobCreatedAt: 1234,
|
||||
agent: { id: 'agent_123', tools: [Tools.web_search] },
|
||||
toolNames: [Tools.web_search],
|
||||
actionsEnabled: false,
|
||||
});
|
||||
|
||||
expect(createOnSearchResults).toHaveBeenCalledWith(res, 'conversation-1', 1234);
|
||||
});
|
||||
|
||||
it('does not load code execution tools that were not registered for the agent', async () => {
|
||||
const capabilities = [
|
||||
AgentCapabilities.tools,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue