/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ApprovalMode, BaseDeclarativeTool, BaseToolInvocation, Kind, ToolConfirmationOutcome, DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES, DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD, } from '../index.js';
import { CoreToolScheduler, convertToFunctionResponse, truncateAndSaveToFile, } from './coreToolScheduler.js';
import { MockModifiableTool, MockTool, MOCK_TOOL_SHOULD_CONFIRM_EXECUTE, } from '../test-utils/mock-tool.js';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
vi.mock('fs/promises', () => ({
    writeFile: vi.fn(),
}));
class TestApprovalTool extends BaseDeclarativeTool {
    config;
    static Name = 'testApprovalTool';
    constructor(config) {
        super(TestApprovalTool.Name, 'TestApprovalTool', 'A tool for testing approval logic', Kind.Edit, {
            properties: { id: { type: 'string' } },
            required: ['id'],
            type: 'object',
        });
        this.config = config;
    }
    createInvocation(params) {
        return new TestApprovalInvocation(this.config, params);
    }
}
class TestApprovalInvocation extends BaseToolInvocation {
    config;
    constructor(config, params) {
        super(params);
        this.config = config;
    }
    getDescription() {
        return `Test tool ${this.params.id}`;
    }
    async shouldConfirmExecute() {
        // Need confirmation unless approval mode is AUTO_EDIT
        if (this.config.getApprovalMode() === ApprovalMode.AUTO_EDIT) {
            return false;
        }
        return {
            type: 'edit',
            title: `Confirm Test Tool ${this.params.id}`,
            fileName: `test-${this.params.id}.txt`,
            filePath: `/test-${this.params.id}.txt`,
            fileDiff: 'Test diff content',
            originalContent: '',
            newContent: 'Test content',
            onConfirm: async (outcome) => {
                if (outcome === ToolConfirmationOutcome.ProceedAlways) {
                    this.config.setApprovalMode(ApprovalMode.AUTO_EDIT);
                }
            },
        };
    }
    async execute() {
        return {
            llmContent: `Executed test tool ${this.params.id}`,
            returnDisplay: `Executed test tool ${this.params.id}`,
        };
    }
}
class AbortDuringConfirmationInvocation extends BaseToolInvocation {
    abortController;
    abortError;
    constructor(abortController, abortError, params) {
        super(params);
        this.abortController = abortController;
        this.abortError = abortError;
    }
    async shouldConfirmExecute(_signal) {
        this.abortController.abort();
        throw this.abortError;
    }
    async execute(_abortSignal) {
        throw new Error('execute should not be called when confirmation fails');
    }
    getDescription() {
        return 'Abort during confirmation invocation';
    }
}
class AbortDuringConfirmationTool extends BaseDeclarativeTool {
    abortController;
    abortError;
    constructor(abortController, abortError) {
        super('abortDuringConfirmationTool', 'Abort During Confirmation Tool', 'A tool that aborts while confirming execution.', Kind.Other, {
            type: 'object',
            properties: {},
        });
        this.abortController = abortController;
        this.abortError = abortError;
    }
    createInvocation(params) {
        return new AbortDuringConfirmationInvocation(this.abortController, this.abortError, params);
    }
}
async function waitForStatus(onToolCallsUpdate, status, timeout = 5000) {
    return new Promise((resolve, reject) => {
        const startTime = Date.now();
        const check = () => {
            if (Date.now() - startTime > timeout) {
                const seenStatuses = onToolCallsUpdate.mock.calls
                    .flatMap((call) => call[0])
                    .map((toolCall) => toolCall.status);
                reject(new Error(`Timed out waiting for status "${status}". Seen statuses: ${seenStatuses.join(', ')}`));
                return;
            }
            const foundCall = onToolCallsUpdate.mock.calls
                .flatMap((call) => call[0])
                .find((toolCall) => toolCall.status === status);
            if (foundCall) {
                resolve(foundCall);
            }
            else {
                setTimeout(check, 10); // Check again in 10ms
            }
        };
        check();
    });
}
describe('CoreToolScheduler', () => {
    it('should cancel a tool call if the signal is aborted before confirmation', async () => {
        const mockTool = new MockTool({
            name: 'mockTool',
            shouldConfirmExecute: MOCK_TOOL_SHOULD_CONFIRM_EXECUTE,
        });
        const declarativeTool = mockTool;
        const mockToolRegistry = {
            getTool: () => declarativeTool,
            getFunctionDeclarations: () => [],
            tools: new Map(),
            discovery: {},
            registerTool: () => { },
            getToolByName: () => declarativeTool,
            getToolByDisplayName: () => declarativeTool,
            getTools: () => [],
            discoverTools: async () => { },
            getAllTools: () => [],
            getToolsByServer: () => [],
        };
        const onAllToolCallsComplete = vi.fn();
        const onToolCallsUpdate = vi.fn();
        const mockConfig = {
            getSessionId: () => 'test-session-id',
            getUsageStatisticsEnabled: () => true,
            getDebugMode: () => false,
            getApprovalMode: () => ApprovalMode.DEFAULT,
            getAllowedTools: () => [],
            getContentGeneratorConfig: () => ({
                model: 'test-model',
                authType: 'oauth-personal',
            }),
            getShellExecutionConfig: () => ({
                terminalWidth: 90,
                terminalHeight: 30,
            }),
            storage: {
                getProjectTempDir: () => '/tmp',
            },
            getTruncateToolOutputThreshold: () => DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
            getTruncateToolOutputLines: () => DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES,
            getToolRegistry: () => mockToolRegistry,
            getUseSmartEdit: () => false,
            getUseModelRouter: () => false,
            getGeminiClient: () => null, // No client needed for these tests
        };
        const scheduler = new CoreToolScheduler({
            config: mockConfig,
            onAllToolCallsComplete,
            onToolCallsUpdate,
            getPreferredEditor: () => 'vscode',
            onEditorClose: vi.fn(),
        });
        const abortController = new AbortController();
        const request = {
            callId: '1',
            name: 'mockTool',
            args: {},
            isClientInitiated: false,
            prompt_id: 'prompt-id-1',
        };
        abortController.abort();
        await scheduler.schedule([request], abortController.signal);
        expect(onAllToolCallsComplete).toHaveBeenCalled();
        const completedCalls = onAllToolCallsComplete.mock
            .calls[0][0];
        expect(completedCalls[0].status).toBe('cancelled');
    });
    it('should mark tool call as cancelled when abort happens during confirmation error', async () => {
        const abortController = new AbortController();
        const abortError = new Error('Abort requested during confirmation');
        const declarativeTool = new AbortDuringConfirmationTool(abortController, abortError);
        const mockToolRegistry = {
            getTool: () => declarativeTool,
            getFunctionDeclarations: () => [],
            tools: new Map(),
            discovery: {},
            registerTool: () => { },
            getToolByName: () => declarativeTool,
            getToolByDisplayName: () => declarativeTool,
            getTools: () => [],
            discoverTools: async () => { },
            getAllTools: () => [],
            getToolsByServer: () => [],
        };
        const onAllToolCallsComplete = vi.fn();
        const onToolCallsUpdate = vi.fn();
        const mockConfig = {
            getSessionId: () => 'test-session-id',
            getUsageStatisticsEnabled: () => true,
            getDebugMode: () => false,
            getApprovalMode: () => ApprovalMode.DEFAULT,
            getAllowedTools: () => [],
            getContentGeneratorConfig: () => ({
                model: 'test-model',
                authType: 'oauth-personal',
            }),
            getShellExecutionConfig: () => ({
                terminalWidth: 90,
                terminalHeight: 30,
            }),
            storage: {
                getProjectTempDir: () => '/tmp',
            },
            getTruncateToolOutputThreshold: () => DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
            getTruncateToolOutputLines: () => DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES,
            getToolRegistry: () => mockToolRegistry,
            getUseSmartEdit: () => false,
            getUseModelRouter: () => false,
            getGeminiClient: () => null,
        };
        const scheduler = new CoreToolScheduler({
            config: mockConfig,
            onAllToolCallsComplete,
            onToolCallsUpdate,
            getPreferredEditor: () => 'vscode',
            onEditorClose: vi.fn(),
        });
        const request = {
            callId: 'abort-1',
            name: 'abortDuringConfirmationTool',
            args: {},
            isClientInitiated: false,
            prompt_id: 'prompt-id-abort',
        };
        await scheduler.schedule([request], abortController.signal);
        expect(onAllToolCallsComplete).toHaveBeenCalled();
        const completedCalls = onAllToolCallsComplete.mock
            .calls[0][0];
        expect(completedCalls[0].status).toBe('cancelled');
        const statuses = onToolCallsUpdate.mock.calls.flatMap((call) => call[0].map((toolCall) => toolCall.status));
        expect(statuses).not.toContain('error');
    });
    describe('getToolSuggestion', () => {
        it('should suggest the top N closest tool names for a typo', () => {
            // Create mocked tool registry
            const mockConfig = {
                getToolRegistry: () => mockToolRegistry,
                getUseSmartEdit: () => false,
                getUseModelRouter: () => false,
                getGeminiClient: () => null, // No client needed for these tests
            };
            const mockToolRegistry = {
                getAllToolNames: () => ['list_files', 'read_file', 'write_file'],
            };
            // Create scheduler
            const scheduler = new CoreToolScheduler({
                config: mockConfig,
                getPreferredEditor: () => 'vscode',
                onEditorClose: vi.fn(),
            });
            // Test that the right tool is selected, with only 1 result, for typos
            // @ts-expect-error accessing private method
            const misspelledTool = scheduler.getToolSuggestion('list_fils', 1);
            expect(misspelledTool).toBe(' Did you mean "list_files"?');
            // Test that the right tool is selected, with only 1 result, for prefixes
            // @ts-expect-error accessing private method
            const prefixedTool = scheduler.getToolSuggestion('github.list_files', 1);
            expect(prefixedTool).toBe(' Did you mean "list_files"?');
            // Test that the right tool is first
            // @ts-expect-error accessing private method
            const suggestionMultiple = scheduler.getToolSuggestion('list_fils');
            expect(suggestionMultiple).toBe(' Did you mean one of: "list_files", "read_file", "write_file"?');
        });
    });
});
describe('CoreToolScheduler with payload', () => {
    it('should update args and diff and execute tool when payload is provided', async () => {
        const mockTool = new MockModifiableTool();
        mockTool.executeFn = vi.fn();
        const declarativeTool = mockTool;
        const mockToolRegistry = {
            getTool: () => declarativeTool,
            getFunctionDeclarations: () => [],
            tools: new Map(),
            discovery: {},
            registerTool: () => { },
            getToolByName: () => declarativeTool,
            getToolByDisplayName: () => declarativeTool,
            getTools: () => [],
            discoverTools: async () => { },
            getAllTools: () => [],
            getToolsByServer: () => [],
        };
        const onAllToolCallsComplete = vi.fn();
        const onToolCallsUpdate = vi.fn();
        const mockConfig = {
            getSessionId: () => 'test-session-id',
            getUsageStatisticsEnabled: () => true,
            getDebugMode: () => false,
            getApprovalMode: () => ApprovalMode.DEFAULT,
            getAllowedTools: () => [],
            getContentGeneratorConfig: () => ({
                model: 'test-model',
                authType: 'oauth-personal',
            }),
            getShellExecutionConfig: () => ({
                terminalWidth: 90,
                terminalHeight: 30,
            }),
            storage: {
                getProjectTempDir: () => '/tmp',
            },
            getTruncateToolOutputThreshold: () => DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
            getTruncateToolOutputLines: () => DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES,
            getToolRegistry: () => mockToolRegistry,
            getUseSmartEdit: () => false,
            getUseModelRouter: () => false,
            getGeminiClient: () => null, // No client needed for these tests
        };
        const scheduler = new CoreToolScheduler({
            config: mockConfig,
            onAllToolCallsComplete,
            onToolCallsUpdate,
            getPreferredEditor: () => 'vscode',
            onEditorClose: vi.fn(),
        });
        const abortController = new AbortController();
        const request = {
            callId: '1',
            name: 'mockModifiableTool',
            args: {},
            isClientInitiated: false,
            prompt_id: 'prompt-id-2',
        };
        await scheduler.schedule([request], abortController.signal);
        const awaitingCall = (await waitForStatus(onToolCallsUpdate, 'awaiting_approval'));
        const confirmationDetails = awaitingCall.confirmationDetails;
        if (confirmationDetails) {
            const payload = { newContent: 'final version' };
            await confirmationDetails.onConfirm(ToolConfirmationOutcome.ProceedOnce, payload);
        }
        // Wait for the tool execution to complete
        await vi.waitFor(() => {
            expect(onAllToolCallsComplete).toHaveBeenCalled();
        });
        const completedCalls = onAllToolCallsComplete.mock
            .calls[0][0];
        expect(completedCalls[0].status).toBe('success');
        expect(mockTool.executeFn).toHaveBeenCalledWith({
            newContent: 'final version',
        });
    });
});
describe('convertToFunctionResponse', () => {
    const toolName = 'testTool';
    const callId = 'call1';
    it('should handle simple string llmContent', () => {
        const llmContent = 'Simple text output';
        const result = convertToFunctionResponse(toolName, callId, llmContent);
        expect(result).toEqual([
            {
                functionResponse: {
                    name: toolName,
                    id: callId,
                    response: { output: 'Simple text output' },
                },
            },
        ]);
    });
    it('should handle llmContent as a single Part with text', () => {
        const llmContent = { text: 'Text from Part object' };
        const result = convertToFunctionResponse(toolName, callId, llmContent);
        expect(result).toEqual([
            {
                functionResponse: {
                    name: toolName,
                    id: callId,
                    response: { output: 'Text from Part object' },
                },
            },
        ]);
    });
    it('should handle llmContent as a PartListUnion array with a single text Part', () => {
        const llmContent = [{ text: 'Text from array' }];
        const result = convertToFunctionResponse(toolName, callId, llmContent);
        expect(result).toEqual([
            {
                functionResponse: {
                    name: toolName,
                    id: callId,
                    response: { output: 'Text from array' },
                },
            },
        ]);
    });
    it('should handle llmContent with inlineData', () => {
        const llmContent = {
            inlineData: { mimeType: 'image/png', data: 'base64...' },
        };
        const result = convertToFunctionResponse(toolName, callId, llmContent);
        expect(result).toEqual([
            {
                functionResponse: {
                    name: toolName,
                    id: callId,
                    response: {
                        output: 'Binary content of type image/png was processed.',
                    },
                },
            },
            llmContent,
        ]);
    });
    it('should handle llmContent with fileData', () => {
        const llmContent = {
            fileData: { mimeType: 'application/pdf', fileUri: 'gs://...' },
        };
        const result = convertToFunctionResponse(toolName, callId, llmContent);
        expect(result).toEqual([
            {
                functionResponse: {
                    name: toolName,
                    id: callId,
                    response: {
                        output: 'Binary content of type application/pdf was processed.',
                    },
                },
            },
            llmContent,
        ]);
    });
    it('should handle llmContent as an array of multiple Parts (text and inlineData)', () => {
        const llmContent = [
            { text: 'Some textual description' },
            { inlineData: { mimeType: 'image/jpeg', data: 'base64data...' } },
            { text: 'Another text part' },
        ];
        const result = convertToFunctionResponse(toolName, callId, llmContent);
        expect(result).toEqual([
            {
                functionResponse: {
                    name: toolName,
                    id: callId,
                    response: { output: 'Tool execution succeeded.' },
                },
            },
            ...llmContent,
        ]);
    });
    it('should handle llmContent as an array with a single inlineData Part', () => {
        const llmContent = [
            { inlineData: { mimeType: 'image/gif', data: 'gifdata...' } },
        ];
        const result = convertToFunctionResponse(toolName, callId, llmContent);
        expect(result).toEqual([
            {
                functionResponse: {
                    name: toolName,
                    id: callId,
                    response: {
                        output: 'Binary content of type image/gif was processed.',
                    },
                },
            },
            ...llmContent,
        ]);
    });
    it('should handle llmContent as a generic Part (not text, inlineData, or fileData)', () => {
        const llmContent = { functionCall: { name: 'test', args: {} } };
        const result = convertToFunctionResponse(toolName, callId, llmContent);
        expect(result).toEqual([
            {
                functionResponse: {
                    name: toolName,
                    id: callId,
                    response: { output: 'Tool execution succeeded.' },
                },
            },
        ]);
    });
    it('should handle empty string llmContent', () => {
        const llmContent = '';
        const result = convertToFunctionResponse(toolName, callId, llmContent);
        expect(result).toEqual([
            {
                functionResponse: {
                    name: toolName,
                    id: callId,
                    response: { output: '' },
                },
            },
        ]);
    });
    it('should handle llmContent as an empty array', () => {
        const llmContent = [];
        const result = convertToFunctionResponse(toolName, callId, llmContent);
        expect(result).toEqual([
            {
                functionResponse: {
                    name: toolName,
                    id: callId,
                    response: { output: 'Tool execution succeeded.' },
                },
            },
        ]);
    });
    it('should handle llmContent as a Part with undefined inlineData/fileData/text', () => {
        const llmContent = {}; // An empty part object
        const result = convertToFunctionResponse(toolName, callId, llmContent);
        expect(result).toEqual([
            {
                functionResponse: {
                    name: toolName,
                    id: callId,
                    response: { output: 'Tool execution succeeded.' },
                },
            },
        ]);
    });
});
class MockEditToolInvocation extends BaseToolInvocation {
    constructor(params) {
        super(params);
    }
    getDescription() {
        return 'A mock edit tool invocation';
    }
    async shouldConfirmExecute(_abortSignal) {
        return {
            type: 'edit',
            title: 'Confirm Edit',
            fileName: 'test.txt',
            filePath: 'test.txt',
            fileDiff: '--- test.txt\n+++ test.txt\n@@ -1,1 +1,1 @@\n-old content\n+new content',
            originalContent: 'old content',
            newContent: 'new content',
            onConfirm: async () => { },
        };
    }
    async execute(_abortSignal) {
        return {
            llmContent: 'Edited successfully',
            returnDisplay: 'Edited successfully',
        };
    }
}
class MockEditTool extends BaseDeclarativeTool {
    constructor() {
        super('mockEditTool', 'mockEditTool', 'A mock edit tool', Kind.Edit, {});
    }
    createInvocation(params) {
        return new MockEditToolInvocation(params);
    }
}
describe('CoreToolScheduler edit cancellation', () => {
    it('should preserve diff when an edit is cancelled', async () => {
        const mockEditTool = new MockEditTool();
        const mockToolRegistry = {
            getTool: () => mockEditTool,
            getFunctionDeclarations: () => [],
            tools: new Map(),
            discovery: {},
            registerTool: () => { },
            getToolByName: () => mockEditTool,
            getToolByDisplayName: () => mockEditTool,
            getTools: () => [],
            discoverTools: async () => { },
            getAllTools: () => [],
            getToolsByServer: () => [],
        };
        const onAllToolCallsComplete = vi.fn();
        const onToolCallsUpdate = vi.fn();
        const mockConfig = {
            getSessionId: () => 'test-session-id',
            getUsageStatisticsEnabled: () => true,
            getDebugMode: () => false,
            getApprovalMode: () => ApprovalMode.DEFAULT,
            getAllowedTools: () => [],
            getContentGeneratorConfig: () => ({
                model: 'test-model',
                authType: 'oauth-personal',
            }),
            getShellExecutionConfig: () => ({
                terminalWidth: 90,
                terminalHeight: 30,
            }),
            storage: {
                getProjectTempDir: () => '/tmp',
            },
            getToolRegistry: () => mockToolRegistry,
            getUseSmartEdit: () => false,
            getUseModelRouter: () => false,
            getGeminiClient: () => null, // No client needed for these tests
        };
        const scheduler = new CoreToolScheduler({
            config: mockConfig,
            onAllToolCallsComplete,
            onToolCallsUpdate,
            getPreferredEditor: () => 'vscode',
            onEditorClose: vi.fn(),
        });
        const abortController = new AbortController();
        const request = {
            callId: '1',
            name: 'mockEditTool',
            args: {},
            isClientInitiated: false,
            prompt_id: 'prompt-id-1',
        };
        await scheduler.schedule([request], abortController.signal);
        const awaitingCall = (await waitForStatus(onToolCallsUpdate, 'awaiting_approval'));
        // Cancel the edit
        const confirmationDetails = awaitingCall.confirmationDetails;
        if (confirmationDetails) {
            await confirmationDetails.onConfirm(ToolConfirmationOutcome.Cancel);
        }
        expect(onAllToolCallsComplete).toHaveBeenCalled();
        const completedCalls = onAllToolCallsComplete.mock
            .calls[0][0];
        expect(completedCalls[0].status).toBe('cancelled');
        // Check that the diff is preserved
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        const cancelledCall = completedCalls[0];
        expect(cancelledCall.response.resultDisplay).toBeDefined();
        expect(cancelledCall.response.resultDisplay.fileDiff).toBe('--- test.txt\n+++ test.txt\n@@ -1,1 +1,1 @@\n-old content\n+new content');
        expect(cancelledCall.response.resultDisplay.fileName).toBe('test.txt');
    });
});
describe('CoreToolScheduler YOLO mode', () => {
    it('should execute tool requiring confirmation directly without waiting', async () => {
        // Arrange
        const executeFn = vi.fn().mockResolvedValue({
            llmContent: 'Tool executed',
            returnDisplay: 'Tool executed',
        });
        const mockTool = new MockTool({
            name: 'mockTool',
            execute: executeFn,
            shouldConfirmExecute: MOCK_TOOL_SHOULD_CONFIRM_EXECUTE,
        });
        const declarativeTool = mockTool;
        const mockToolRegistry = {
            getTool: () => declarativeTool,
            getToolByName: () => declarativeTool,
            // Other properties are not needed for this test but are included for type consistency.
            getFunctionDeclarations: () => [],
            tools: new Map(),
            discovery: {},
            registerTool: () => { },
            getToolByDisplayName: () => declarativeTool,
            getTools: () => [],
            discoverTools: async () => { },
            getAllTools: () => [],
            getToolsByServer: () => [],
        };
        const onAllToolCallsComplete = vi.fn();
        const onToolCallsUpdate = vi.fn();
        // Configure the scheduler for YOLO mode.
        const mockConfig = {
            getSessionId: () => 'test-session-id',
            getUsageStatisticsEnabled: () => true,
            getDebugMode: () => false,
            getApprovalMode: () => ApprovalMode.YOLO,
            getAllowedTools: () => [],
            getContentGeneratorConfig: () => ({
                model: 'test-model',
                authType: 'oauth-personal',
            }),
            getShellExecutionConfig: () => ({
                terminalWidth: 90,
                terminalHeight: 30,
            }),
            storage: {
                getProjectTempDir: () => '/tmp',
            },
            getToolRegistry: () => mockToolRegistry,
            getTruncateToolOutputThreshold: () => DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
            getTruncateToolOutputLines: () => DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES,
            getUseSmartEdit: () => false,
            getUseModelRouter: () => false,
            getGeminiClient: () => null, // No client needed for these tests
        };
        const scheduler = new CoreToolScheduler({
            config: mockConfig,
            onAllToolCallsComplete,
            onToolCallsUpdate,
            getPreferredEditor: () => 'vscode',
            onEditorClose: vi.fn(),
        });
        const abortController = new AbortController();
        const request = {
            callId: '1',
            name: 'mockTool',
            args: { param: 'value' },
            isClientInitiated: false,
            prompt_id: 'prompt-id-yolo',
        };
        // Act
        await scheduler.schedule([request], abortController.signal);
        // Wait for the tool execution to complete
        await vi.waitFor(() => {
            expect(onAllToolCallsComplete).toHaveBeenCalled();
        });
        // Assert
        // 1. The tool's execute method was called directly.
        expect(executeFn).toHaveBeenCalledWith({ param: 'value' });
        // 2. The tool call status never entered 'awaiting_approval'.
        const statusUpdates = onToolCallsUpdate.mock.calls
            .map((call) => call[0][0]?.status)
            .filter(Boolean);
        expect(statusUpdates).not.toContain('awaiting_approval');
        expect(statusUpdates).toEqual([
            'validating',
            'scheduled',
            'executing',
            'success',
        ]);
        // 3. The final callback indicates the tool call was successful.
        const completedCalls = onAllToolCallsComplete.mock
            .calls[0][0];
        expect(completedCalls).toHaveLength(1);
        const completedCall = completedCalls[0];
        expect(completedCall.status).toBe('success');
        if (completedCall.status === 'success') {
            expect(completedCall.response.resultDisplay).toBe('Tool executed');
        }
    });
});
describe('CoreToolScheduler cancellation during executing with live output', () => {
    it('sets status to cancelled and preserves last output', async () => {
        class StreamingInvocation extends BaseToolInvocation {
            getDescription() {
                return `Streaming tool ${this.params.id}`;
            }
            async execute(signal, updateOutput) {
                updateOutput?.('hello');
                // Wait until aborted to emulate a long-running task
                await new Promise((resolve) => {
                    if (signal.aborted)
                        return resolve();
                    const onAbort = () => {
                        signal.removeEventListener('abort', onAbort);
                        resolve();
                    };
                    signal.addEventListener('abort', onAbort, { once: true });
                });
                // Return a normal (non-error) result; scheduler should still mark cancelled
                return { llmContent: 'done', returnDisplay: 'done' };
            }
        }
        class StreamingTool extends BaseDeclarativeTool {
            constructor() {
                super('stream-tool', 'Stream Tool', 'Emits live output and waits for abort', Kind.Other, {
                    type: 'object',
                    properties: { id: { type: 'string' } },
                    required: ['id'],
                }, true, true);
            }
            createInvocation(params) {
                return new StreamingInvocation(params);
            }
        }
        const tool = new StreamingTool();
        const mockToolRegistry = {
            getTool: () => tool,
            getFunctionDeclarations: () => [],
            tools: new Map(),
            discovery: {},
            registerTool: () => { },
            getToolByName: () => tool,
            getToolByDisplayName: () => tool,
            getTools: () => [],
            discoverTools: async () => { },
            getAllTools: () => [],
            getToolsByServer: () => [],
        };
        const onAllToolCallsComplete = vi.fn();
        const onToolCallsUpdate = vi.fn();
        const mockConfig = {
            getSessionId: () => 'test-session-id',
            getUsageStatisticsEnabled: () => true,
            getDebugMode: () => false,
            getApprovalMode: () => ApprovalMode.DEFAULT,
            getContentGeneratorConfig: () => ({
                model: 'test-model',
                authType: 'oauth-personal',
            }),
            getToolRegistry: () => mockToolRegistry,
            getShellExecutionConfig: () => ({
                terminalWidth: 90,
                terminalHeight: 30,
            }),
        };
        const scheduler = new CoreToolScheduler({
            config: mockConfig,
            onAllToolCallsComplete,
            onToolCallsUpdate,
            getPreferredEditor: () => 'vscode',
            onEditorClose: vi.fn(),
        });
        const abortController = new AbortController();
        const request = {
            callId: '1',
            name: 'stream-tool',
            args: { id: 'x' },
            isClientInitiated: true,
            prompt_id: 'prompt-stream',
        };
        const schedulePromise = scheduler.schedule([request], abortController.signal);
        // Wait until executing
        await vi.waitFor(() => {
            const calls = onToolCallsUpdate.mock.calls;
            const last = calls[calls.length - 1]?.[0][0];
            expect(last?.status).toBe('executing');
        });
        // Now abort
        abortController.abort();
        await schedulePromise;
        await vi.waitFor(() => {
            expect(onAllToolCallsComplete).toHaveBeenCalled();
        });
        const completedCalls = onAllToolCallsComplete.mock
            .calls[0][0];
        expect(completedCalls[0].status).toBe('cancelled');
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        const cancelled = completedCalls[0];
        expect(cancelled.response.resultDisplay).toBe('hello');
    });
});
describe('CoreToolScheduler request queueing', () => {
    it('should queue a request if another is running', async () => {
        let resolveFirstCall;
        const firstCallPromise = new Promise((resolve) => {
            resolveFirstCall = resolve;
        });
        const executeFn = vi.fn().mockImplementation(() => firstCallPromise);
        const mockTool = new MockTool({ name: 'mockTool', execute: executeFn });
        const declarativeTool = mockTool;
        const mockToolRegistry = {
            getTool: () => declarativeTool,
            getToolByName: () => declarativeTool,
            getFunctionDeclarations: () => [],
            tools: new Map(),
            discovery: {},
            registerTool: () => { },
            getToolByDisplayName: () => declarativeTool,
            getTools: () => [],
            discoverTools: async () => { },
            getAllTools: () => [],
            getToolsByServer: () => [],
        };
        const onAllToolCallsComplete = vi.fn();
        const onToolCallsUpdate = vi.fn();
        const mockConfig = {
            getSessionId: () => 'test-session-id',
            getUsageStatisticsEnabled: () => true,
            getDebugMode: () => false,
            getApprovalMode: () => ApprovalMode.YOLO, // Use YOLO to avoid confirmation prompts
            getAllowedTools: () => [],
            getContentGeneratorConfig: () => ({
                model: 'test-model',
                authType: 'oauth-personal',
            }),
            getShellExecutionConfig: () => ({
                terminalWidth: 90,
                terminalHeight: 30,
            }),
            storage: {
                getProjectTempDir: () => '/tmp',
            },
            getTruncateToolOutputThreshold: () => DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
            getTruncateToolOutputLines: () => DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES,
            getToolRegistry: () => mockToolRegistry,
            getUseSmartEdit: () => false,
            getUseModelRouter: () => false,
            getGeminiClient: () => null, // No client needed for these tests
        };
        const scheduler = new CoreToolScheduler({
            config: mockConfig,
            onAllToolCallsComplete,
            onToolCallsUpdate,
            getPreferredEditor: () => 'vscode',
            onEditorClose: vi.fn(),
        });
        const abortController = new AbortController();
        const request1 = {
            callId: '1',
            name: 'mockTool',
            args: { a: 1 },
            isClientInitiated: false,
            prompt_id: 'prompt-1',
        };
        const request2 = {
            callId: '2',
            name: 'mockTool',
            args: { b: 2 },
            isClientInitiated: false,
            prompt_id: 'prompt-2',
        };
        // Schedule the first call, which will pause execution.
        scheduler.schedule([request1], abortController.signal);
        // Wait for the first call to be in the 'executing' state.
        await waitForStatus(onToolCallsUpdate, 'executing');
        // Schedule the second call while the first is "running".
        const schedulePromise2 = scheduler.schedule([request2], abortController.signal);
        // Ensure the second tool call hasn't been executed yet.
        expect(executeFn).toHaveBeenCalledWith({ a: 1 });
        // Complete the first tool call.
        resolveFirstCall({
            llmContent: 'First call complete',
            returnDisplay: 'First call complete',
        });
        // Wait for the second schedule promise to resolve.
        await schedulePromise2;
        // Let the second call finish.
        const secondCallResult = {
            llmContent: 'Second call complete',
            returnDisplay: 'Second call complete',
        };
        // Since the mock is shared, we need to resolve the current promise.
        // In a real scenario, a new promise would be created for the second call.
        resolveFirstCall(secondCallResult);
        await vi.waitFor(() => {
            // Now the second tool call should have been executed.
            expect(executeFn).toHaveBeenCalledTimes(2);
        });
        expect(executeFn).toHaveBeenCalledWith({ b: 2 });
        // Wait for the second completion.
        await vi.waitFor(() => {
            expect(onAllToolCallsComplete).toHaveBeenCalledTimes(2);
        });
        // Verify the completion callbacks were called correctly.
        expect(onAllToolCallsComplete.mock.calls[0][0][0].status).toBe('success');
        expect(onAllToolCallsComplete.mock.calls[1][0][0].status).toBe('success');
    });
    it('should auto-approve a tool call if it is on the allowedTools list', async () => {
        // Arrange
        const executeFn = vi.fn().mockResolvedValue({
            llmContent: 'Tool executed',
            returnDisplay: 'Tool executed',
        });
        const mockTool = new MockTool({
            name: 'mockTool',
            execute: executeFn,
            shouldConfirmExecute: MOCK_TOOL_SHOULD_CONFIRM_EXECUTE,
        });
        const declarativeTool = mockTool;
        const toolRegistry = {
            getTool: () => declarativeTool,
            getToolByName: () => declarativeTool,
            getFunctionDeclarations: () => [],
            tools: new Map(),
            discovery: {},
            registerTool: () => { },
            getToolByDisplayName: () => declarativeTool,
            getTools: () => [],
            discoverTools: async () => { },
            getAllTools: () => [],
            getToolsByServer: () => [],
        };
        const onAllToolCallsComplete = vi.fn();
        const onToolCallsUpdate = vi.fn();
        // Configure the scheduler to auto-approve the specific tool call.
        const mockConfig = {
            getSessionId: () => 'test-session-id',
            getUsageStatisticsEnabled: () => true,
            getDebugMode: () => false,
            getApprovalMode: () => ApprovalMode.DEFAULT, // Not YOLO mode
            getAllowedTools: () => ['mockTool'], // Auto-approve this tool
            getToolRegistry: () => toolRegistry,
            getContentGeneratorConfig: () => ({
                model: 'test-model',
                authType: 'oauth-personal',
            }),
            getShellExecutionConfig: () => ({
                terminalWidth: 80,
                terminalHeight: 24,
            }),
            getTerminalWidth: vi.fn(() => 80),
            getTerminalHeight: vi.fn(() => 24),
            storage: {
                getProjectTempDir: () => '/tmp',
            },
            getTruncateToolOutputThreshold: () => DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
            getTruncateToolOutputLines: () => DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES,
            getUseSmartEdit: () => false,
            getUseModelRouter: () => false,
            getGeminiClient: () => null, // No client needed for these tests
        };
        const scheduler = new CoreToolScheduler({
            config: mockConfig,
            onAllToolCallsComplete,
            onToolCallsUpdate,
            getPreferredEditor: () => 'vscode',
            onEditorClose: vi.fn(),
        });
        const abortController = new AbortController();
        const request = {
            callId: '1',
            name: 'mockTool',
            args: { param: 'value' },
            isClientInitiated: false,
            prompt_id: 'prompt-auto-approved',
        };
        // Act
        await scheduler.schedule([request], abortController.signal);
        // Wait for the tool execution to complete
        await vi.waitFor(() => {
            expect(onAllToolCallsComplete).toHaveBeenCalled();
        });
        // Assert
        // 1. The tool's execute method was called directly.
        expect(executeFn).toHaveBeenCalledWith({ param: 'value' });
        // 2. The tool call status never entered 'awaiting_approval'.
        const statusUpdates = onToolCallsUpdate.mock.calls
            .map((call) => call[0][0]?.status)
            .filter(Boolean);
        expect(statusUpdates).not.toContain('awaiting_approval');
        expect(statusUpdates).toEqual([
            'validating',
            'scheduled',
            'executing',
            'success',
        ]);
        // 3. The final callback indicates the tool call was successful.
        expect(onAllToolCallsComplete).toHaveBeenCalled();
        const completedCalls = onAllToolCallsComplete.mock
            .calls[0][0];
        expect(completedCalls).toHaveLength(1);
        const completedCall = completedCalls[0];
        expect(completedCall.status).toBe('success');
        if (completedCall.status === 'success') {
            expect(completedCall.response.resultDisplay).toBe('Tool executed');
        }
    });
    it('should handle two synchronous calls to schedule', async () => {
        const executeFn = vi.fn().mockResolvedValue({
            llmContent: 'Tool executed',
            returnDisplay: 'Tool executed',
        });
        const mockTool = new MockTool({ name: 'mockTool', execute: executeFn });
        const declarativeTool = mockTool;
        const mockToolRegistry = {
            getTool: () => declarativeTool,
            getToolByName: () => declarativeTool,
            getFunctionDeclarations: () => [],
            tools: new Map(),
            discovery: {},
            registerTool: () => { },
            getToolByDisplayName: () => declarativeTool,
            getTools: () => [],
            discoverTools: async () => { },
            getAllTools: () => [],
            getToolsByServer: () => [],
        };
        const onAllToolCallsComplete = vi.fn();
        const onToolCallsUpdate = vi.fn();
        const mockConfig = {
            getSessionId: () => 'test-session-id',
            getUsageStatisticsEnabled: () => true,
            getDebugMode: () => false,
            getApprovalMode: () => ApprovalMode.YOLO,
            getAllowedTools: () => [],
            getContentGeneratorConfig: () => ({
                model: 'test-model',
                authType: 'oauth-personal',
            }),
            getShellExecutionConfig: () => ({
                terminalWidth: 90,
                terminalHeight: 30,
            }),
            storage: {
                getProjectTempDir: () => '/tmp',
            },
            getTruncateToolOutputThreshold: () => DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
            getTruncateToolOutputLines: () => DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES,
            getToolRegistry: () => mockToolRegistry,
            getUseSmartEdit: () => false,
            getUseModelRouter: () => false,
            getGeminiClient: () => null, // No client needed for these tests
        };
        const scheduler = new CoreToolScheduler({
            config: mockConfig,
            onAllToolCallsComplete,
            onToolCallsUpdate,
            getPreferredEditor: () => 'vscode',
            onEditorClose: vi.fn(),
        });
        const abortController = new AbortController();
        const request1 = {
            callId: '1',
            name: 'mockTool',
            args: { a: 1 },
            isClientInitiated: false,
            prompt_id: 'prompt-1',
        };
        const request2 = {
            callId: '2',
            name: 'mockTool',
            args: { b: 2 },
            isClientInitiated: false,
            prompt_id: 'prompt-2',
        };
        // Schedule two calls synchronously.
        const schedulePromise1 = scheduler.schedule([request1], abortController.signal);
        const schedulePromise2 = scheduler.schedule([request2], abortController.signal);
        // Wait for both promises to resolve.
        await Promise.all([schedulePromise1, schedulePromise2]);
        // Ensure the tool was called twice with the correct arguments.
        expect(executeFn).toHaveBeenCalledTimes(2);
        expect(executeFn).toHaveBeenCalledWith({ a: 1 });
        expect(executeFn).toHaveBeenCalledWith({ b: 2 });
        // Ensure completion callbacks were called twice.
        expect(onAllToolCallsComplete).toHaveBeenCalledTimes(2);
    });
    it('should auto-approve remaining tool calls when first tool call is approved with ProceedAlways', async () => {
        let approvalMode = ApprovalMode.DEFAULT;
        const mockConfig = {
            getSessionId: () => 'test-session-id',
            getUsageStatisticsEnabled: () => true,
            getDebugMode: () => false,
            getApprovalMode: () => approvalMode,
            getAllowedTools: () => [],
            setApprovalMode: (mode) => {
                approvalMode = mode;
            },
            getShellExecutionConfig: () => ({
                terminalWidth: 90,
                terminalHeight: 30,
            }),
            storage: {
                getProjectTempDir: () => '/tmp',
            },
            getTruncateToolOutputThreshold: () => DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
            getTruncateToolOutputLines: () => DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES,
            getUseSmartEdit: () => false,
            getUseModelRouter: () => false,
            getGeminiClient: () => null, // No client needed for these tests
        };
        const testTool = new TestApprovalTool(mockConfig);
        const toolRegistry = {
            getTool: () => testTool,
            getFunctionDeclarations: () => [],
            getFunctionDeclarationsFiltered: () => [],
            registerTool: () => { },
            discoverAllTools: async () => { },
            discoverMcpTools: async () => { },
            discoverToolsForServer: async () => { },
            removeMcpToolsByServer: () => { },
            getAllTools: () => [],
            getToolsByServer: () => [],
            tools: new Map(),
            config: mockConfig,
            mcpClientManager: undefined,
            getToolByName: () => testTool,
            getToolByDisplayName: () => testTool,
            getTools: () => [],
            discoverTools: async () => { },
            discovery: {},
        };
        mockConfig.getToolRegistry = () => toolRegistry;
        const onAllToolCallsComplete = vi.fn();
        const onToolCallsUpdate = vi.fn();
        const pendingConfirmations = [];
        const scheduler = new CoreToolScheduler({
            config: mockConfig,
            onAllToolCallsComplete,
            onToolCallsUpdate: (toolCalls) => {
                onToolCallsUpdate(toolCalls);
                // Capture confirmation handlers for awaiting_approval tools
                toolCalls.forEach((call) => {
                    if (call.status === 'awaiting_approval') {
                        const waitingCall = call;
                        if (waitingCall.confirmationDetails?.onConfirm) {
                            const originalHandler = pendingConfirmations.find((h) => h === waitingCall.confirmationDetails.onConfirm);
                            if (!originalHandler) {
                                pendingConfirmations.push(waitingCall.confirmationDetails.onConfirm);
                            }
                        }
                    }
                });
            },
            getPreferredEditor: () => 'vscode',
            onEditorClose: vi.fn(),
        });
        const abortController = new AbortController();
        // Schedule multiple tools that need confirmation
        const requests = [
            {
                callId: '1',
                name: 'testApprovalTool',
                args: { id: 'first' },
                isClientInitiated: false,
                prompt_id: 'prompt-1',
            },
            {
                callId: '2',
                name: 'testApprovalTool',
                args: { id: 'second' },
                isClientInitiated: false,
                prompt_id: 'prompt-2',
            },
            {
                callId: '3',
                name: 'testApprovalTool',
                args: { id: 'third' },
                isClientInitiated: false,
                prompt_id: 'prompt-3',
            },
        ];
        await scheduler.schedule(requests, abortController.signal);
        // Wait for all tools to be awaiting approval
        await vi.waitFor(() => {
            const calls = onToolCallsUpdate.mock.calls.at(-1)?.[0];
            expect(calls?.length).toBe(3);
            expect(calls?.every((call) => call.status === 'awaiting_approval')).toBe(true);
        });
        expect(pendingConfirmations.length).toBe(3);
        // Approve the first tool with ProceedAlways
        const firstConfirmation = pendingConfirmations[0];
        firstConfirmation(ToolConfirmationOutcome.ProceedAlways);
        // Wait for all tools to be completed
        await vi.waitFor(() => {
            expect(onAllToolCallsComplete).toHaveBeenCalled();
            const completedCalls = onAllToolCallsComplete.mock.calls.at(-1)?.[0];
            expect(completedCalls?.length).toBe(3);
            expect(completedCalls?.every((call) => call.status === 'success')).toBe(true);
        });
        // Verify approval mode was changed
        expect(approvalMode).toBe(ApprovalMode.AUTO_EDIT);
    });
});
describe('CoreToolScheduler Sequential Execution', () => {
    it('should execute tool calls in a batch sequentially', async () => {
        // Arrange
        let firstCallFinished = false;
        const executeFn = vi
            .fn()
            .mockImplementation(async (args) => {
            if (args.call === 1) {
                // First call, wait for a bit to simulate work
                await new Promise((resolve) => setTimeout(resolve, 50));
                firstCallFinished = true;
                return { llmContent: 'First call done' };
            }
            if (args.call === 2) {
                // Second call, should only happen after the first is finished
                if (!firstCallFinished) {
                    throw new Error('Second tool call started before the first one finished!');
                }
                return { llmContent: 'Second call done' };
            }
            return { llmContent: 'default' };
        });
        const mockTool = new MockTool({ name: 'mockTool', execute: executeFn });
        const declarativeTool = mockTool;
        const mockToolRegistry = {
            getTool: () => declarativeTool,
            getToolByName: () => declarativeTool,
            getFunctionDeclarations: () => [],
            tools: new Map(),
            discovery: {},
            registerTool: () => { },
            getToolByDisplayName: () => declarativeTool,
            getTools: () => [],
            discoverTools: async () => { },
            getAllTools: () => [],
            getToolsByServer: () => [],
        };
        const onAllToolCallsComplete = vi.fn();
        const onToolCallsUpdate = vi.fn();
        const mockConfig = {
            getSessionId: () => 'test-session-id',
            getUsageStatisticsEnabled: () => true,
            getDebugMode: () => false,
            getApprovalMode: () => ApprovalMode.YOLO, // Use YOLO to avoid confirmation prompts
            getAllowedTools: () => [],
            getContentGeneratorConfig: () => ({
                model: 'test-model',
                authType: 'oauth-personal',
            }),
            getShellExecutionConfig: () => ({
                terminalWidth: 90,
                terminalHeight: 30,
            }),
            storage: {
                getProjectTempDir: () => '/tmp',
            },
            getToolRegistry: () => mockToolRegistry,
            getTruncateToolOutputThreshold: () => DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
            getTruncateToolOutputLines: () => DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES,
            getUseSmartEdit: () => false,
            getUseModelRouter: () => false,
            getGeminiClient: () => null,
        };
        const scheduler = new CoreToolScheduler({
            config: mockConfig,
            onAllToolCallsComplete,
            onToolCallsUpdate,
            getPreferredEditor: () => 'vscode',
            onEditorClose: vi.fn(),
        });
        const abortController = new AbortController();
        const requests = [
            {
                callId: '1',
                name: 'mockTool',
                args: { call: 1 },
                isClientInitiated: false,
                prompt_id: 'prompt-1',
            },
            {
                callId: '2',
                name: 'mockTool',
                args: { call: 2 },
                isClientInitiated: false,
                prompt_id: 'prompt-1',
            },
        ];
        // Act
        await scheduler.schedule(requests, abortController.signal);
        // Assert
        await vi.waitFor(() => {
            expect(onAllToolCallsComplete).toHaveBeenCalled();
        });
        // Check that execute was called twice
        expect(executeFn).toHaveBeenCalledTimes(2);
        // Check the order of calls
        const calls = executeFn.mock.calls;
        expect(calls[0][0]).toEqual({ call: 1 });
        expect(calls[1][0]).toEqual({ call: 2 });
        // The onAllToolCallsComplete should be called once with both results
        const completedCalls = onAllToolCallsComplete.mock
            .calls[0][0];
        expect(completedCalls).toHaveLength(2);
        expect(completedCalls[0].status).toBe('success');
        expect(completedCalls[1].status).toBe('success');
    });
    it('should cancel subsequent tools when the signal is aborted.', async () => {
        // Arrange
        const abortController = new AbortController();
        let secondCallStarted = false;
        const executeFn = vi
            .fn()
            .mockImplementation(async (args) => {
            if (args.call === 1) {
                return { llmContent: 'First call done' };
            }
            if (args.call === 2) {
                secondCallStarted = true;
                // This call will be cancelled while it's "running".
                await new Promise((resolve) => setTimeout(resolve, 100));
                // It should not return a value because it will be cancelled.
                return { llmContent: 'Second call should not complete' };
            }
            if (args.call === 3) {
                return { llmContent: 'Third call done' };
            }
            return { llmContent: 'default' };
        });
        const mockTool = new MockTool({ name: 'mockTool', execute: executeFn });
        const declarativeTool = mockTool;
        const mockToolRegistry = {
            getTool: () => declarativeTool,
            getToolByName: () => declarativeTool,
            getFunctionDeclarations: () => [],
            tools: new Map(),
            discovery: {},
            registerTool: () => { },
            getToolByDisplayName: () => declarativeTool,
            getTools: () => [],
            discoverTools: async () => { },
            getAllTools: () => [],
            getToolsByServer: () => [],
        };
        const onAllToolCallsComplete = vi.fn();
        const onToolCallsUpdate = vi.fn();
        const mockConfig = {
            getSessionId: () => 'test-session-id',
            getUsageStatisticsEnabled: () => true,
            getDebugMode: () => false,
            getApprovalMode: () => ApprovalMode.YOLO,
            getAllowedTools: () => [],
            getContentGeneratorConfig: () => ({
                model: 'test-model',
                authType: 'oauth-personal',
            }),
            getShellExecutionConfig: () => ({
                terminalWidth: 90,
                terminalHeight: 30,
            }),
            storage: {
                getProjectTempDir: () => '/tmp',
            },
            getToolRegistry: () => mockToolRegistry,
            getTruncateToolOutputThreshold: () => DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
            getTruncateToolOutputLines: () => DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES,
            getUseSmartEdit: () => false,
            getUseModelRouter: () => false,
            getGeminiClient: () => null,
        };
        const scheduler = new CoreToolScheduler({
            config: mockConfig,
            onAllToolCallsComplete,
            onToolCallsUpdate,
            getPreferredEditor: () => 'vscode',
            onEditorClose: vi.fn(),
        });
        const requests = [
            {
                callId: '1',
                name: 'mockTool',
                args: { call: 1 },
                isClientInitiated: false,
                prompt_id: 'prompt-1',
            },
            {
                callId: '2',
                name: 'mockTool',
                args: { call: 2 },
                isClientInitiated: false,
                prompt_id: 'prompt-1',
            },
            {
                callId: '3',
                name: 'mockTool',
                args: { call: 3 },
                isClientInitiated: false,
                prompt_id: 'prompt-1',
            },
        ];
        // Act
        const schedulePromise = scheduler.schedule(requests, abortController.signal);
        // Wait for the second call to start, then abort.
        await vi.waitFor(() => {
            expect(secondCallStarted).toBe(true);
        });
        abortController.abort();
        await schedulePromise;
        // Assert
        await vi.waitFor(() => {
            expect(onAllToolCallsComplete).toHaveBeenCalled();
        });
        // Check that execute was called for all three tools initially
        expect(executeFn).toHaveBeenCalledTimes(3);
        expect(executeFn).toHaveBeenCalledWith({ call: 1 });
        expect(executeFn).toHaveBeenCalledWith({ call: 2 });
        expect(executeFn).toHaveBeenCalledWith({ call: 3 });
        const completedCalls = onAllToolCallsComplete.mock
            .calls[0][0];
        expect(completedCalls).toHaveLength(3);
        const call1 = completedCalls.find((c) => c.request.callId === '1');
        const call2 = completedCalls.find((c) => c.request.callId === '2');
        const call3 = completedCalls.find((c) => c.request.callId === '3');
        expect(call1?.status).toBe('success');
        expect(call2?.status).toBe('cancelled');
        expect(call3?.status).toBe('cancelled');
    });
});
describe('truncateAndSaveToFile', () => {
    const mockWriteFile = vi.mocked(fs.writeFile);
    const THRESHOLD = 40_000;
    const TRUNCATE_LINES = 1000;
    beforeEach(() => {
        vi.clearAllMocks();
    });
    it('should return content unchanged if below threshold', async () => {
        const content = 'Short content';
        const callId = 'test-call-id';
        const projectTempDir = '/tmp';
        const result = await truncateAndSaveToFile(content, callId, projectTempDir, THRESHOLD, TRUNCATE_LINES);
        expect(result).toEqual({ content });
        expect(mockWriteFile).not.toHaveBeenCalled();
    });
    it('should truncate content by lines when content has many lines', async () => {
        // Create content that exceeds 100,000 character threshold with many lines
        const lines = Array(2000).fill('x'.repeat(100)); // 100 chars per line * 2000 lines = 200,000 chars
        const content = lines.join('\n');
        const callId = 'test-call-id';
        const projectTempDir = '/tmp';
        mockWriteFile.mockResolvedValue(undefined);
        const result = await truncateAndSaveToFile(content, callId, projectTempDir, THRESHOLD, TRUNCATE_LINES);
        expect(result.outputFile).toBe(path.join(projectTempDir, `${callId}.output`));
        expect(mockWriteFile).toHaveBeenCalledWith(path.join(projectTempDir, `${callId}.output`), content);
        // Should contain the first and last lines with 1/5 head and 4/5 tail
        const head = Math.floor(TRUNCATE_LINES / 5);
        const beginning = lines.slice(0, head);
        const end = lines.slice(-(TRUNCATE_LINES - head));
        const expectedTruncated = beginning.join('\n') + '\n... [CONTENT TRUNCATED] ...\n' + end.join('\n');
        expect(result.content).toContain('Tool output was too large and has been truncated');
        expect(result.content).toContain('Truncated part of the output:');
        expect(result.content).toContain(expectedTruncated);
    });
    it('should wrap and truncate content when content has few but long lines', async () => {
        const content = 'a'.repeat(200_000); // A single very long line
        const callId = 'test-call-id';
        const projectTempDir = '/tmp';
        const wrapWidth = 120;
        mockWriteFile.mockResolvedValue(undefined);
        // Manually wrap the content to generate the expected file content
        const wrappedLines = [];
        for (let i = 0; i < content.length; i += wrapWidth) {
            wrappedLines.push(content.substring(i, i + wrapWidth));
        }
        const expectedFileContent = wrappedLines.join('\n');
        const result = await truncateAndSaveToFile(content, callId, projectTempDir, THRESHOLD, TRUNCATE_LINES);
        expect(result.outputFile).toBe(path.join(projectTempDir, `${callId}.output`));
        // Check that the file was written with the wrapped content
        expect(mockWriteFile).toHaveBeenCalledWith(path.join(projectTempDir, `${callId}.output`), expectedFileContent);
        // Should contain the first and last lines with 1/5 head and 4/5 tail of the wrapped content
        const head = Math.floor(TRUNCATE_LINES / 5);
        const beginning = wrappedLines.slice(0, head);
        const end = wrappedLines.slice(-(TRUNCATE_LINES - head));
        const expectedTruncated = beginning.join('\n') + '\n... [CONTENT TRUNCATED] ...\n' + end.join('\n');
        expect(result.content).toContain('Tool output was too large and has been truncated');
        expect(result.content).toContain('Truncated part of the output:');
        expect(result.content).toContain(expectedTruncated);
    });
    it('should handle file write errors gracefully', async () => {
        const content = 'a'.repeat(2_000_000);
        const callId = 'test-call-id';
        const projectTempDir = '/tmp';
        mockWriteFile.mockRejectedValue(new Error('File write failed'));
        const result = await truncateAndSaveToFile(content, callId, projectTempDir, THRESHOLD, TRUNCATE_LINES);
        expect(result.outputFile).toBeUndefined();
        expect(result.content).toContain('[Note: Could not save full output to file]');
        expect(mockWriteFile).toHaveBeenCalled();
    });
    it('should save to correct file path with call ID', async () => {
        const content = 'a'.repeat(200_000);
        const callId = 'unique-call-123';
        const projectTempDir = '/custom/temp/dir';
        const wrapWidth = 120;
        mockWriteFile.mockResolvedValue(undefined);
        // Manually wrap the content to generate the expected file content
        const wrappedLines = [];
        for (let i = 0; i < content.length; i += wrapWidth) {
            wrappedLines.push(content.substring(i, i + wrapWidth));
        }
        const expectedFileContent = wrappedLines.join('\n');
        const result = await truncateAndSaveToFile(content, callId, projectTempDir, THRESHOLD, TRUNCATE_LINES);
        const expectedPath = path.join(projectTempDir, `${callId}.output`);
        expect(result.outputFile).toBe(expectedPath);
        expect(mockWriteFile).toHaveBeenCalledWith(expectedPath, expectedFileContent);
    });
    it('should include helpful instructions in truncated message', async () => {
        const content = 'a'.repeat(2_000_000);
        const callId = 'test-call-id';
        const projectTempDir = '/tmp';
        mockWriteFile.mockResolvedValue(undefined);
        const result = await truncateAndSaveToFile(content, callId, projectTempDir, THRESHOLD, TRUNCATE_LINES);
        expect(result.content).toContain('Tool output was too large and has been truncated');
        expect(result.content).toContain('The full output has been saved to:');
        expect(result.content).toContain('To read the complete output, use the read_file tool with the absolute file path above');
        expect(result.content).toContain('The truncated output below shows the beginning and end of the content');
    });
    it('should sanitize callId to prevent path traversal', async () => {
        const content = 'a'.repeat(200_000);
        const callId = '../../../../../etc/passwd';
        const projectTempDir = '/tmp/safe_dir';
        const wrapWidth = 120;
        mockWriteFile.mockResolvedValue(undefined);
        // Manually wrap the content to generate the expected file content
        const wrappedLines = [];
        for (let i = 0; i < content.length; i += wrapWidth) {
            wrappedLines.push(content.substring(i, i + wrapWidth));
        }
        const expectedFileContent = wrappedLines.join('\n');
        await truncateAndSaveToFile(content, callId, projectTempDir, THRESHOLD, TRUNCATE_LINES);
        const expectedPath = path.join(projectTempDir, 'passwd.output');
        expect(mockWriteFile).toHaveBeenCalledWith(expectedPath, expectedFileContent);
    });
});
//# sourceMappingURL=coreToolScheduler.test.js.map