// @lifecycle canonical - Unit tests for ExecutionModeService.
/**
* ExecutionModeService Unit Tests
*
* Tests the execution mode filtering service including:
* - Confirmation filtering (confirm: true)
* - Confirmation response building
* - Explicit tool request handling
*/
import {
ExecutionModeService,
createExecutionModeService,
getDefaultExecutionModeService,
resetDefaultExecutionModeService,
} from '../../../../src/modules/automation/execution/execution-mode-service.js';
import { resetDefaultPendingConfirmationTracker } from '../../../../src/modules/automation/execution/pending-confirmation-tracker.js';
import type {
LoadedScriptTool,
ToolDetectionMatch,
} from '../../../../src/modules/automation/types.js';
import { DEFAULT_EXECUTION_CONFIG } from '../../../../src/modules/automation/types.js';
describe('ExecutionModeService', () => {
let service: ExecutionModeService;
beforeEach(() => {
service = createExecutionModeService({ debug: false });
});
afterEach(() => {
resetDefaultExecutionModeService();
resetDefaultPendingConfirmationTracker();
});
const createMockTool = (overrides: Partial<LoadedScriptTool> = {}): LoadedScriptTool => ({
id: 'test_tool',
name: 'Test Tool',
description: 'A test tool',
scriptPath: 'script.py',
runtime: 'python',
inputSchema: { type: 'object', properties: {} },
toolDir: '/tmp/tools/test_tool',
absoluteScriptPath: '/tmp/tools/test_tool/script.py',
promptId: 'test_prompt',
descriptionContent: 'Test tool description',
enabled: true,
execution: { ...DEFAULT_EXECUTION_CONFIG },
...overrides,
});
const createMockMatch = (overrides: Partial<ToolDetectionMatch> = {}): ToolDetectionMatch => ({
toolId: 'test_tool',
promptId: 'test_prompt',
priority: 0.9,
matchReason: 'schema_match',
extractedInputs: {},
requiresConfirmation: false,
explicitRequest: false,
...overrides,
});
describe('filterByExecutionMode', () => {
it('should pass non-confirm tools to ready for execution', () => {
const tool = createMockTool({ execution: { ...DEFAULT_EXECUTION_CONFIG, confirm: false } });
const match = createMockMatch({ requiresConfirmation: false });
const result = service.filterByExecutionMode([match], [tool], 'test_prompt');
expect(result.readyForExecution).toHaveLength(1);
expect(result.readyForExecution[0].toolId).toBe('test_tool');
expect(result.skippedManual).toHaveLength(0);
expect(result.pendingConfirmation).toHaveLength(0);
expect(result.requiresConfirmation).toBe(false);
});
it('should add confirm tools to pending confirmation', () => {
const tool = createMockTool({
id: 'confirm_tool',
name: 'Confirm Tool',
execution: {
...DEFAULT_EXECUTION_CONFIG,
confirm: true,
confirmMessage: 'Run expensive analysis?',
},
});
const match = createMockMatch({
toolId: 'confirm_tool',
requiresConfirmation: true,
explicitRequest: false,
});
const result = service.filterByExecutionMode([match], [tool], 'test_prompt');
expect(result.readyForExecution).toHaveLength(0);
expect(result.pendingConfirmation).toHaveLength(1);
expect(result.pendingConfirmation[0].toolId).toBe('confirm_tool');
expect(result.pendingConfirmation[0].message).toBe('Run expensive analysis?');
expect(result.requiresConfirmation).toBe(true);
});
it('should execute confirm tools with explicit request', () => {
const tool = createMockTool({
id: 'confirm_tool',
execution: { ...DEFAULT_EXECUTION_CONFIG, confirm: true },
});
const match = createMockMatch({
toolId: 'confirm_tool',
requiresConfirmation: true,
explicitRequest: true, // User explicitly approved
});
const result = service.filterByExecutionMode([match], [tool], 'test_prompt');
expect(result.readyForExecution).toHaveLength(1);
expect(result.readyForExecution[0].toolId).toBe('confirm_tool');
expect(result.pendingConfirmation).toHaveLength(0);
expect(result.requiresConfirmation).toBe(false);
});
it('should use default confirmation message when not specified', () => {
const tool = createMockTool({
id: 'confirm_tool',
name: 'My Tool',
execution: { ...DEFAULT_EXECUTION_CONFIG, confirm: true },
// No confirmMessage specified
});
const match = createMockMatch({
toolId: 'confirm_tool',
requiresConfirmation: true,
});
const result = service.filterByExecutionMode([match], [tool], 'test_prompt');
expect(result.pendingConfirmation[0].message).toBe('Execute My Tool?');
});
it('should handle mixed confirmation requirements in a single request', () => {
const autoTool = createMockTool({
id: 'auto_tool',
execution: { ...DEFAULT_EXECUTION_CONFIG, confirm: false },
});
const confirmTool = createMockTool({
id: 'confirm_tool',
name: 'Confirm Tool',
execution: { ...DEFAULT_EXECUTION_CONFIG, confirm: true },
});
const explicitConfirmTool = createMockTool({
id: 'confirm_tool_explicit',
name: 'Confirm Tool Explicit',
execution: { ...DEFAULT_EXECUTION_CONFIG, confirm: true },
});
const matches = [
createMockMatch({ toolId: 'auto_tool', requiresConfirmation: false }),
createMockMatch({ toolId: 'confirm_tool', requiresConfirmation: true }),
createMockMatch({
toolId: 'confirm_tool_explicit',
requiresConfirmation: true,
explicitRequest: true,
}),
];
const result = service.filterByExecutionMode(
matches,
[autoTool, confirmTool, explicitConfirmTool],
'test_prompt'
);
expect(result.readyForExecution).toHaveLength(2);
expect(result.readyForExecution.map((m) => m.toolId).sort()).toEqual(
['auto_tool', 'confirm_tool_explicit'].sort()
);
expect(result.skippedManual).toHaveLength(0);
expect(result.pendingConfirmation).toHaveLength(1);
expect(result.pendingConfirmation[0].toolId).toBe('confirm_tool');
});
it('should skip matches with no matching tool', () => {
const tool = createMockTool({ id: 'existing_tool' });
const match = createMockMatch({ toolId: 'nonexistent_tool' });
const result = service.filterByExecutionMode([match], [tool], 'test_prompt');
expect(result.readyForExecution).toHaveLength(0);
expect(result.skippedManual).toHaveLength(0);
expect(result.pendingConfirmation).toHaveLength(0);
});
it('should default requiresConfirmation to false when undefined', () => {
const tool = createMockTool();
const match = createMockMatch({ requiresConfirmation: undefined });
const result = service.filterByExecutionMode([match], [tool], 'test_prompt');
expect(result.readyForExecution).toHaveLength(1);
});
});
describe('buildConfirmationResponse', () => {
it('should build structured confirmation response with re-run syntax', () => {
const filterResult = {
readyForExecution: [],
skippedManual: [],
pendingConfirmation: [
{
toolId: 'tool1',
toolName: 'Tool One',
message: 'Run Tool One?',
resumeCommand: '>>test_prompt',
},
{
toolId: 'tool2',
toolName: 'Tool Two',
message: 'Run Tool Two?',
resumeCommand: '>>test_prompt',
},
],
requiresConfirmation: true,
};
const response = service.buildConfirmationResponse(filterResult, 'test_prompt');
expect(response.type).toBe('confirmation_required');
expect(response.tools).toHaveLength(2);
// Resume command is now just the prompt ID (re-run to approve)
expect(response.resumeCommand).toBe('>>test_prompt');
expect(response.message).toContain('Tool One, Tool Two');
expect(response.message).toContain('re-run the same command');
});
});
describe('factory functions', () => {
it('should create service with default config', () => {
const service = createExecutionModeService();
expect(service).toBeInstanceOf(ExecutionModeService);
});
it('should create service with custom config', () => {
const service = createExecutionModeService({ debug: true });
expect(service).toBeInstanceOf(ExecutionModeService);
});
});
describe('default instance management', () => {
it('should return same instance on multiple calls', () => {
const instance1 = getDefaultExecutionModeService();
const instance2 = getDefaultExecutionModeService();
expect(instance1).toBe(instance2);
});
it('should create new instance after reset', () => {
const instance1 = getDefaultExecutionModeService();
resetDefaultExecutionModeService();
const instance2 = getDefaultExecutionModeService();
expect(instance1).not.toBe(instance2);
});
});
describe('re-run to approve flow', () => {
it('should auto-approve on re-run with same inputs', () => {
const tool = createMockTool({
id: 'confirm_tool',
name: 'Confirm Tool',
execution: { ...DEFAULT_EXECUTION_CONFIG, confirm: true },
});
const match = createMockMatch({
toolId: 'confirm_tool',
requiresConfirmation: true,
extractedInputs: { key: 'value' },
});
// First call: should require confirmation
const result1 = service.filterByExecutionMode([match], [tool], 'test_prompt');
expect(result1.readyForExecution).toHaveLength(0);
expect(result1.pendingConfirmation).toHaveLength(1);
expect(result1.requiresConfirmation).toBe(true);
// Second call (re-run): should auto-approve
const result2 = service.filterByExecutionMode([match], [tool], 'test_prompt');
expect(result2.readyForExecution).toHaveLength(1);
expect(result2.readyForExecution[0].toolId).toBe('confirm_tool');
expect(result2.pendingConfirmation).toHaveLength(0);
expect(result2.requiresConfirmation).toBe(false);
});
it('should require new confirmation when inputs change', () => {
const tool = createMockTool({
id: 'confirm_tool',
name: 'Confirm Tool',
execution: { ...DEFAULT_EXECUTION_CONFIG, confirm: true },
});
// First call with original inputs
const match1 = createMockMatch({
toolId: 'confirm_tool',
requiresConfirmation: true,
extractedInputs: { key: 'value1' },
});
const result1 = service.filterByExecutionMode([match1], [tool], 'test_prompt');
expect(result1.pendingConfirmation).toHaveLength(1);
// Second call with different inputs
const match2 = createMockMatch({
toolId: 'confirm_tool',
requiresConfirmation: true,
extractedInputs: { key: 'value2' }, // Different value
});
const result2 = service.filterByExecutionMode([match2], [tool], 'test_prompt');
// Should still require confirmation (inputs don't match)
expect(result2.pendingConfirmation).toHaveLength(1);
expect(result2.requiresConfirmation).toBe(true);
});
it('should clear pending after successful auto-approve (single use)', () => {
const tool = createMockTool({
id: 'confirm_tool',
execution: { ...DEFAULT_EXECUTION_CONFIG, confirm: true },
});
const match = createMockMatch({
toolId: 'confirm_tool',
requiresConfirmation: true,
extractedInputs: { key: 'value' },
});
// First call: pending
service.filterByExecutionMode([match], [tool], 'test_prompt');
// Second call: auto-approve
const result2 = service.filterByExecutionMode([match], [tool], 'test_prompt');
expect(result2.readyForExecution).toHaveLength(1);
// Third call: should require confirmation again (pending was cleared)
const result3 = service.filterByExecutionMode([match], [tool], 'test_prompt');
expect(result3.pendingConfirmation).toHaveLength(1);
expect(result3.requiresConfirmation).toBe(true);
});
});
});