🛂 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:
Danny Avila 2026-07-26 21:58:25 -04:00 committed by GitHub
parent f3159f9891
commit d8427ffc5e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
42 changed files with 2960 additions and 145 deletions

View file

@ -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,