mirror of
https://github.com/lobehub/lobehub
synced 2026-04-21 17:47:27 +00:00
🐛 fix: default execAgent approval mode to headless (#13873)
* 🐛 fix: default execAgent approval mode to headless Backend execAgent calls should run headlessly by default since only frontend scenarios require manual human approval. This prevents cron jobs and other server-side triggers from unexpectedly waiting for human intervention. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * ✅ test: add regression test for headless approval default Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4203e32dc7
commit
ab05020f62
2 changed files with 174 additions and 1 deletions
|
|
@ -0,0 +1,173 @@
|
|||
import type * as ModelBankModule from 'model-bank';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { AiAgentService } from '../index';
|
||||
|
||||
const { mockCreateOperation, mockGetAgentConfig, mockMessageCreate } = vi.hoisted(() => ({
|
||||
mockCreateOperation: vi.fn(),
|
||||
mockGetAgentConfig: vi.fn(),
|
||||
mockMessageCreate: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/libs/trusted-client', () => ({
|
||||
generateTrustedClientToken: vi.fn().mockReturnValue(undefined),
|
||||
getTrustedClientTokenForSession: vi.fn().mockResolvedValue(undefined),
|
||||
isTrustedClientEnabled: vi.fn().mockReturnValue(false),
|
||||
}));
|
||||
|
||||
vi.mock('@/database/models/message', () => ({
|
||||
MessageModel: vi.fn().mockImplementation(() => ({
|
||||
create: mockMessageCreate,
|
||||
query: vi.fn().mockResolvedValue([]),
|
||||
update: vi.fn().mockResolvedValue({}),
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock('@/database/models/agent', () => ({
|
||||
AgentModel: vi.fn().mockImplementation(() => ({
|
||||
getAgentConfig: vi.fn(),
|
||||
queryAgents: vi.fn().mockResolvedValue([]),
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock('@/server/services/agent', () => ({
|
||||
AgentService: vi.fn().mockImplementation(() => ({
|
||||
getAgentConfig: mockGetAgentConfig,
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock('@/database/models/plugin', () => ({
|
||||
PluginModel: vi.fn().mockImplementation(() => ({
|
||||
query: vi.fn().mockResolvedValue([]),
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock('@/database/models/topic', () => ({
|
||||
TopicModel: vi.fn().mockImplementation(() => ({
|
||||
create: vi.fn().mockResolvedValue({ id: 'topic-1' }),
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock('@/database/models/thread', () => ({
|
||||
ThreadModel: vi.fn().mockImplementation(() => ({
|
||||
create: vi.fn(),
|
||||
findById: vi.fn(),
|
||||
update: vi.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock('@/server/services/agentRuntime', () => ({
|
||||
AgentRuntimeService: vi.fn().mockImplementation(() => ({
|
||||
createOperation: mockCreateOperation,
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock('@/server/services/market', () => ({
|
||||
MarketService: vi.fn().mockImplementation(() => ({
|
||||
getLobehubSkillManifests: vi.fn().mockResolvedValue([]),
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock('@/server/services/klavis', () => ({
|
||||
KlavisService: vi.fn().mockImplementation(() => ({
|
||||
getKlavisManifests: vi.fn().mockResolvedValue([]),
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock('@/server/services/file', () => ({
|
||||
FileService: vi.fn().mockImplementation(() => ({
|
||||
uploadFromUrl: vi.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock('@/server/modules/Mecha', () => ({
|
||||
createServerAgentToolsEngine: vi.fn().mockReturnValue({
|
||||
generateToolsDetailed: vi.fn().mockReturnValue({ enabledToolIds: [], tools: [] }),
|
||||
getEnabledPluginManifests: vi.fn().mockReturnValue(new Map()),
|
||||
}),
|
||||
serverMessagesEngine: vi.fn().mockResolvedValue([{ content: 'test', role: 'user' }]),
|
||||
}));
|
||||
|
||||
vi.mock('@/server/services/toolExecution/deviceProxy', () => ({
|
||||
deviceProxy: {
|
||||
isConfigured: false,
|
||||
queryDeviceList: vi.fn().mockResolvedValue([]),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('model-bank', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof ModelBankModule>();
|
||||
return {
|
||||
...actual,
|
||||
LOBE_DEFAULT_MODEL_LIST: [
|
||||
{
|
||||
abilities: { functionCall: true, video: false, vision: true },
|
||||
id: 'gpt-4',
|
||||
providerId: 'openai',
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
describe('AiAgentService.execAgent - headless approval default', () => {
|
||||
let service: AiAgentService;
|
||||
const mockDb = {} as any;
|
||||
const userId = 'test-user-id';
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockMessageCreate.mockResolvedValue({ id: 'msg-1' });
|
||||
mockCreateOperation.mockResolvedValue({
|
||||
autoStarted: true,
|
||||
messageId: 'queue-msg-1',
|
||||
operationId: 'op-123',
|
||||
success: true,
|
||||
});
|
||||
mockGetAgentConfig.mockResolvedValue({
|
||||
chatConfig: {},
|
||||
id: 'agent-1',
|
||||
model: 'gpt-4',
|
||||
plugins: [],
|
||||
provider: 'openai',
|
||||
systemRole: '',
|
||||
});
|
||||
service = new AiAgentService(mockDb, userId);
|
||||
});
|
||||
|
||||
it('should default to headless approval mode when userInterventionConfig is not provided', async () => {
|
||||
await service.execAgent({
|
||||
agentId: 'agent-1',
|
||||
prompt: 'Hello',
|
||||
});
|
||||
|
||||
expect(mockCreateOperation).toHaveBeenCalledTimes(1);
|
||||
const callArgs = mockCreateOperation.mock.calls[0][0];
|
||||
expect(callArgs.userInterventionConfig).toEqual({ approvalMode: 'headless' });
|
||||
});
|
||||
|
||||
it('should respect explicit userInterventionConfig when provided', async () => {
|
||||
await service.execAgent({
|
||||
agentId: 'agent-1',
|
||||
prompt: 'Hello',
|
||||
userInterventionConfig: { approvalMode: 'manual' },
|
||||
});
|
||||
|
||||
expect(mockCreateOperation).toHaveBeenCalledTimes(1);
|
||||
const callArgs = mockCreateOperation.mock.calls[0][0];
|
||||
expect(callArgs.userInterventionConfig).toEqual({ approvalMode: 'manual' });
|
||||
});
|
||||
|
||||
it('should respect explicit allow-list approval mode with allowList', async () => {
|
||||
const config = { allowList: ['tool-a', 'tool-b'], approvalMode: 'allow-list' as const };
|
||||
|
||||
await service.execAgent({
|
||||
agentId: 'agent-1',
|
||||
prompt: 'Hello',
|
||||
userInterventionConfig: config,
|
||||
});
|
||||
|
||||
expect(mockCreateOperation).toHaveBeenCalledTimes(1);
|
||||
const callArgs = mockCreateOperation.mock.calls[0][0];
|
||||
expect(callArgs.userInterventionConfig).toEqual(config);
|
||||
});
|
||||
});
|
||||
|
|
@ -260,7 +260,7 @@ export class AiAgentService {
|
|||
maxSteps,
|
||||
initialStepCount,
|
||||
signal,
|
||||
userInterventionConfig,
|
||||
userInterventionConfig = { approvalMode: 'headless' },
|
||||
queueRetries,
|
||||
queueRetryDelay,
|
||||
parentMessageId,
|
||||
|
|
|
|||
Loading…
Reference in a new issue