/**
 * @license
 * Copyright 2026 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import { describe, it, expect, vi, beforeEach, afterEach, } from 'vitest';
import { randomUUID } from 'node:crypto';
vi.mock('node:crypto', () => ({
    randomUUID: vi.fn(),
}));
vi.mock('../telemetry/trace.js', () => ({
    runInDevTraceSpan: vi.fn(async (_opts, fn) => fn({ metadata: { input: {}, output: {} } })),
}));
import { logToolCall } from '../telemetry/loggers.js';
import { ToolCallEvent } from '../telemetry/types.js';
vi.mock('../telemetry/loggers.js', () => ({
    logToolCall: vi.fn(),
}));
vi.mock('../telemetry/types.js', () => ({
    ToolCallEvent: vi.fn().mockImplementation((call) => ({ ...call })),
}));
vi.mock('../core/coreToolHookTriggers.js', () => ({
    fireToolNotificationHook: vi.fn(),
}));
import { SchedulerStateManager } from './state-manager.js';
import { resolveConfirmation } from './confirmation.js';
import { checkPolicy, updatePolicy } from './policy.js';
import { ToolExecutor } from './tool-executor.js';
import { ToolModificationHandler } from './tool-modifier.js';
vi.mock('./state-manager.js');
vi.mock('./confirmation.js');
vi.mock('./policy.js');
vi.mock('./tool-executor.js');
vi.mock('./tool-modifier.js');
import { Scheduler } from './scheduler.js';
import { PolicyDecision } from '../policy/types.js';
import { ToolConfirmationOutcome, } from '../tools/tools.js';
import { ToolErrorType } from '../tools/tool-error.js';
import * as ToolUtils from '../utils/tool-utils.js';
describe('Scheduler (Orchestrator)', () => {
    let scheduler;
    let signal;
    let abortController;
    // Mocked Services (Injected via Config/Options)
    let mockConfig;
    let mockMessageBus;
    let mockPolicyEngine;
    let mockToolRegistry;
    let getPreferredEditor;
    // Mocked Sub-components (Instantiated by Scheduler)
    let mockStateManager;
    let mockExecutor;
    let mockModifier;
    // Test Data
    const req1 = {
        callId: 'call-1',
        name: 'test-tool',
        args: { foo: 'bar' },
        isClientInitiated: false,
        prompt_id: 'prompt-1',
    };
    const req2 = {
        callId: 'call-2',
        name: 'test-tool',
        args: { foo: 'baz' },
        isClientInitiated: false,
        prompt_id: 'prompt-1',
    };
    const mockTool = {
        name: 'test-tool',
        build: vi.fn(),
    };
    const mockInvocation = {
        shouldConfirmExecute: vi.fn(),
    };
    beforeEach(() => {
        vi.mocked(randomUUID).mockReturnValue('123e4567-e89b-12d3-a456-426614174000');
        abortController = new AbortController();
        signal = abortController.signal;
        // --- Setup Injected Mocks ---
        mockPolicyEngine = {
            check: vi.fn().mockResolvedValue({ decision: PolicyDecision.ALLOW }),
        };
        mockToolRegistry = {
            getTool: vi.fn().mockReturnValue(mockTool),
            getAllToolNames: vi.fn().mockReturnValue(['test-tool']),
        };
        mockConfig = {
            getPolicyEngine: vi.fn().mockReturnValue(mockPolicyEngine),
            getToolRegistry: vi.fn().mockReturnValue(mockToolRegistry),
            isInteractive: vi.fn().mockReturnValue(true),
            getEnableHooks: vi.fn().mockReturnValue(true),
            setApprovalMode: vi.fn(),
        };
        mockMessageBus = {
            publish: vi.fn(),
            subscribe: vi.fn(),
        };
        getPreferredEditor = vi.fn().mockReturnValue('vim');
        // --- Setup Sub-component Mocks ---
        mockStateManager = {
            enqueue: vi.fn(),
            dequeue: vi.fn(),
            getToolCall: vi.fn(),
            updateStatus: vi.fn(),
            finalizeCall: vi.fn(),
            updateArgs: vi.fn(),
            setOutcome: vi.fn(),
            cancelAllQueued: vi.fn(),
            clearBatch: vi.fn(),
        };
        // Define getters for accessors idiomatically
        Object.defineProperty(mockStateManager, 'isActive', {
            get: vi.fn().mockReturnValue(false),
            configurable: true,
        });
        Object.defineProperty(mockStateManager, 'queueLength', {
            get: vi.fn().mockReturnValue(0),
            configurable: true,
        });
        Object.defineProperty(mockStateManager, 'firstActiveCall', {
            get: vi.fn().mockReturnValue(undefined),
            configurable: true,
        });
        Object.defineProperty(mockStateManager, 'completedBatch', {
            get: vi.fn().mockReturnValue([]),
            configurable: true,
        });
        vi.spyOn(mockStateManager, 'cancelAllQueued').mockImplementation(() => { });
        vi.spyOn(mockStateManager, 'clearBatch').mockImplementation(() => { });
        vi.mocked(resolveConfirmation).mockReset();
        vi.mocked(checkPolicy).mockReset();
        vi.mocked(updatePolicy).mockReset();
        mockExecutor = {
            execute: vi.fn(),
        };
        mockModifier = {
            handleModifyWithEditor: vi.fn(),
            applyInlineModify: vi.fn(),
        };
        // Wire up class constructors to return our mock instances
        vi.mocked(SchedulerStateManager).mockReturnValue(mockStateManager);
        vi.mocked(ToolExecutor).mockReturnValue(mockExecutor);
        vi.mocked(ToolModificationHandler).mockReturnValue(mockModifier);
        // Initialize Scheduler
        scheduler = new Scheduler({
            config: mockConfig,
            messageBus: mockMessageBus,
            getPreferredEditor,
        });
        // Reset Tool build behavior
        vi.mocked(mockTool.build).mockReturnValue(mockInvocation);
    });
    afterEach(() => {
        vi.clearAllMocks();
    });
    describe('Phase 1: Ingestion & Resolution', () => {
        it('should create an ErroredToolCall if tool is not found', async () => {
            vi.mocked(mockToolRegistry.getTool).mockReturnValue(undefined);
            vi.spyOn(ToolUtils, 'getToolSuggestion').mockReturnValue(' (Did you mean "test-tool"?)');
            await scheduler.schedule(req1, signal);
            // Verify it was enqueued with an error status
            expect(mockStateManager.enqueue).toHaveBeenCalledWith(expect.arrayContaining([
                expect.objectContaining({
                    status: 'error',
                    response: expect.objectContaining({
                        errorType: ToolErrorType.TOOL_NOT_REGISTERED,
                    }),
                }),
            ]));
        });
        it('should create an ErroredToolCall if tool.build throws (invalid args)', async () => {
            vi.mocked(mockTool.build).mockImplementation(() => {
                throw new Error('Invalid schema');
            });
            await scheduler.schedule(req1, signal);
            expect(mockStateManager.enqueue).toHaveBeenCalledWith(expect.arrayContaining([
                expect.objectContaining({
                    status: 'error',
                    response: expect.objectContaining({
                        errorType: ToolErrorType.INVALID_TOOL_PARAMS,
                    }),
                }),
            ]));
        });
        it('should correctly build ValidatingToolCalls for happy path', async () => {
            await scheduler.schedule(req1, signal);
            expect(mockStateManager.enqueue).toHaveBeenCalledWith(expect.arrayContaining([
                expect.objectContaining({
                    status: 'validating',
                    request: req1,
                    tool: mockTool,
                    invocation: mockInvocation,
                }),
            ]));
        });
    });
    describe('Phase 2: Queue Management', () => {
        it('should drain the queue if multiple calls are scheduled', async () => {
            const validatingCall = {
                status: 'validating',
                request: req1,
                tool: mockTool,
                invocation: mockInvocation,
            };
            // Setup queue simulation: two items
            Object.defineProperty(mockStateManager, 'queueLength', {
                get: vi
                    .fn()
                    .mockReturnValueOnce(2)
                    .mockReturnValueOnce(1)
                    .mockReturnValue(0),
                configurable: true,
            });
            Object.defineProperty(mockStateManager, 'isActive', {
                get: vi.fn().mockReturnValue(false),
                configurable: true,
            });
            mockStateManager.dequeue.mockReturnValue(validatingCall);
            vi.mocked(mockStateManager.dequeue).mockReturnValue(validatingCall);
            Object.defineProperty(mockStateManager, 'firstActiveCall', {
                get: vi.fn().mockReturnValue(validatingCall),
                configurable: true,
            });
            // Execute is the end of the loop, stub it
            mockExecutor.execute.mockResolvedValue({
                status: 'success',
            });
            await scheduler.schedule(req1, signal);
            // Verify loop ran twice
            expect(mockStateManager.dequeue).toHaveBeenCalledTimes(2);
            expect(mockStateManager.finalizeCall).toHaveBeenCalledTimes(2);
        });
        it('should execute tool calls sequentially (first completes before second starts)', async () => {
            // Setup queue simulation: two items
            Object.defineProperty(mockStateManager, 'queueLength', {
                get: vi
                    .fn()
                    .mockReturnValueOnce(2)
                    .mockReturnValueOnce(1)
                    .mockReturnValue(0),
                configurable: true,
            });
            Object.defineProperty(mockStateManager, 'isActive', {
                get: vi.fn().mockReturnValue(false),
                configurable: true,
            });
            const validatingCall1 = {
                status: 'validating',
                request: req1,
                tool: mockTool,
                invocation: mockInvocation,
            };
            const validatingCall2 = {
                status: 'validating',
                request: req2,
                tool: mockTool,
                invocation: mockInvocation,
            };
            vi.mocked(mockStateManager.dequeue)
                .mockReturnValueOnce(validatingCall1)
                .mockReturnValueOnce(validatingCall2)
                .mockReturnValue(undefined);
            Object.defineProperty(mockStateManager, 'firstActiveCall', {
                get: vi
                    .fn()
                    .mockReturnValueOnce(validatingCall1) // Used in loop check for call 1
                    .mockReturnValueOnce(validatingCall1) // Used in _execute for call 1
                    .mockReturnValueOnce(validatingCall2) // Used in loop check for call 2
                    .mockReturnValueOnce(validatingCall2), // Used in _execute for call 2
                configurable: true,
            });
            const executionLog = [];
            // Mock executor to push to log with a deterministic microtask delay
            mockExecutor.execute.mockImplementation(async ({ call }) => {
                const id = call.request.callId;
                executionLog.push(`start-${id}`);
                // Yield to the event loop deterministically using queueMicrotask
                await new Promise((resolve) => queueMicrotask(resolve));
                executionLog.push(`end-${id}`);
                return { status: 'success' };
            });
            // Action: Schedule batch of 2 tools
            await scheduler.schedule([req1, req2], signal);
            // Assert: The second tool only started AFTER the first one ended
            expect(executionLog).toEqual([
                'start-call-1',
                'end-call-1',
                'start-call-2',
                'end-call-2',
            ]);
        });
        it('should queue and process multiple schedule() calls made synchronously', async () => {
            const validatingCall1 = {
                status: 'validating',
                request: req1,
                tool: mockTool,
                invocation: mockInvocation,
            };
            const validatingCall2 = {
                status: 'validating',
                request: req2, // Second request
                tool: mockTool,
                invocation: mockInvocation,
            };
            // Mock state responses dynamically
            Object.defineProperty(mockStateManager, 'isActive', {
                get: vi.fn().mockReturnValue(false),
                configurable: true,
            });
            // Queue state responses for the two batches:
            // Batch 1: length 1 -> 0
            // Batch 2: length 1 -> 0
            Object.defineProperty(mockStateManager, 'queueLength', {
                get: vi
                    .fn()
                    .mockReturnValueOnce(1)
                    .mockReturnValueOnce(0)
                    .mockReturnValueOnce(1)
                    .mockReturnValue(0),
                configurable: true,
            });
            vi.mocked(mockStateManager.dequeue)
                .mockReturnValueOnce(validatingCall1)
                .mockReturnValueOnce(validatingCall2);
            Object.defineProperty(mockStateManager, 'firstActiveCall', {
                get: vi
                    .fn()
                    .mockReturnValueOnce(validatingCall1)
                    .mockReturnValueOnce(validatingCall1)
                    .mockReturnValueOnce(validatingCall2)
                    .mockReturnValueOnce(validatingCall2),
                configurable: true,
            });
            // Executor succeeds instantly
            mockExecutor.execute.mockResolvedValue({
                status: 'success',
            });
            // ACT: Call schedule twice synchronously (without awaiting the first)
            const promise1 = scheduler.schedule(req1, signal);
            const promise2 = scheduler.schedule(req2, signal);
            await Promise.all([promise1, promise2]);
            // ASSERT: Both requests were eventually pulled from the queue and executed
            expect(mockExecutor.execute).toHaveBeenCalledTimes(2);
            expect(mockStateManager.finalizeCall).toHaveBeenCalledWith('call-1');
            expect(mockStateManager.finalizeCall).toHaveBeenCalledWith('call-2');
        });
        it('should queue requests when scheduler is busy (overlapping batches)', async () => {
            const validatingCall1 = {
                status: 'validating',
                request: req1,
                tool: mockTool,
                invocation: mockInvocation,
            };
            const validatingCall2 = {
                status: 'validating',
                request: req2, // Second request
                tool: mockTool,
                invocation: mockInvocation,
            };
            // 1. Setup State Manager for 2 sequential batches
            Object.defineProperty(mockStateManager, 'isActive', {
                get: vi.fn().mockReturnValue(false),
                configurable: true,
            });
            Object.defineProperty(mockStateManager, 'queueLength', {
                get: vi
                    .fn()
                    .mockReturnValueOnce(1) // Batch 1
                    .mockReturnValueOnce(0)
                    .mockReturnValueOnce(1) // Batch 2
                    .mockReturnValue(0),
                configurable: true,
            });
            vi.mocked(mockStateManager.dequeue)
                .mockReturnValueOnce(validatingCall1)
                .mockReturnValueOnce(validatingCall2);
            Object.defineProperty(mockStateManager, 'firstActiveCall', {
                get: vi
                    .fn()
                    .mockReturnValueOnce(validatingCall1)
                    .mockReturnValueOnce(validatingCall1)
                    .mockReturnValueOnce(validatingCall2)
                    .mockReturnValueOnce(validatingCall2),
                configurable: true,
            });
            // 2. Setup Executor with a controllable lock for the first batch
            const executionLog = [];
            let finishFirstBatch;
            const firstBatchPromise = new Promise((resolve) => {
                finishFirstBatch = resolve;
            });
            mockExecutor.execute.mockImplementationOnce(async () => {
                executionLog.push('start-batch-1');
                await firstBatchPromise; // Simulating long-running tool execution
                executionLog.push('end-batch-1');
                return { status: 'success' };
            });
            mockExecutor.execute.mockImplementationOnce(async () => {
                executionLog.push('start-batch-2');
                executionLog.push('end-batch-2');
                return { status: 'success' };
            });
            // 3. ACTIONS
            // Start Batch 1 (it will block indefinitely inside execution)
            const promise1 = scheduler.schedule(req1, signal);
            // Schedule Batch 2 WHILE Batch 1 is executing
            const promise2 = scheduler.schedule(req2, signal);
            // Yield event loop to let promise2 hit the queue
            await new Promise((r) => setTimeout(r, 0));
            // At this point, Batch 2 should NOT have started
            expect(executionLog).not.toContain('start-batch-2');
            // Now resolve Batch 1, which should trigger the request queue drain
            finishFirstBatch({});
            await Promise.all([promise1, promise2]);
            // 4. ASSERTIONS
            // Verify complete sequential ordering of the two overlapping batches
            expect(executionLog).toEqual([
                'start-batch-1',
                'end-batch-1',
                'start-batch-2',
                'end-batch-2',
            ]);
        });
        it('should cancel all queues if AbortSignal is triggered during loop', async () => {
            Object.defineProperty(mockStateManager, 'queueLength', {
                get: vi.fn().mockReturnValue(1),
                configurable: true,
            });
            abortController.abort(); // Signal aborted
            await scheduler.schedule(req1, signal);
            expect(mockStateManager.cancelAllQueued).toHaveBeenCalledWith('Operation cancelled');
            expect(mockStateManager.dequeue).not.toHaveBeenCalled(); // Loop broke
        });
        it('cancelAll() should cancel active call and clear queue', () => {
            const activeCall = {
                status: 'validating',
                request: req1,
                tool: mockTool,
                invocation: mockInvocation,
            };
            Object.defineProperty(mockStateManager, 'firstActiveCall', {
                get: vi.fn().mockReturnValue(activeCall),
                configurable: true,
            });
            scheduler.cancelAll();
            expect(mockStateManager.updateStatus).toHaveBeenCalledWith('call-1', 'cancelled', 'Operation cancelled by user');
            // finalizeCall is handled by the processing loop, not synchronously by cancelAll
            // expect(mockStateManager.finalizeCall).toHaveBeenCalledWith('call-1');
            expect(mockStateManager.cancelAllQueued).toHaveBeenCalledWith('Operation cancelled by user');
        });
        it('cancelAll() should clear the requestQueue and reject pending promises', async () => {
            // 1. Setup a busy scheduler with one batch processing
            Object.defineProperty(mockStateManager, 'isActive', {
                get: vi.fn().mockReturnValue(true),
                configurable: true,
            });
            const promise1 = scheduler.schedule(req1, signal);
            // Catch promise1 to avoid unhandled rejection when we cancelAll
            promise1.catch(() => { });
            // 2. Queue another batch while the first is busy
            const promise2 = scheduler.schedule(req2, signal);
            // 3. ACT: Cancel everything
            scheduler.cancelAll();
            // 4. ASSERT: The second batch's promise should be rejected
            await expect(promise2).rejects.toThrow('Operation cancelled by user');
        });
    });
    describe('Phase 3: Policy & Confirmation Loop', () => {
        const validatingCall = {
            status: 'validating',
            request: req1,
            tool: mockTool,
            invocation: mockInvocation,
        };
        beforeEach(() => {
            Object.defineProperty(mockStateManager, 'queueLength', {
                get: vi.fn().mockReturnValueOnce(1).mockReturnValue(0),
                configurable: true,
            });
            vi.mocked(mockStateManager.dequeue).mockReturnValue(validatingCall);
            Object.defineProperty(mockStateManager, 'firstActiveCall', {
                get: vi.fn().mockReturnValue(validatingCall),
                configurable: true,
            });
        });
        it('should update state to error with POLICY_VIOLATION if Policy returns DENY', async () => {
            vi.mocked(checkPolicy).mockResolvedValue(PolicyDecision.DENY);
            await scheduler.schedule(req1, signal);
            expect(mockStateManager.updateStatus).toHaveBeenCalledWith('call-1', 'error', expect.objectContaining({
                errorType: ToolErrorType.POLICY_VIOLATION,
            }));
            // Deny shouldn't throw, execution is just skipped, state is updated
            expect(mockExecutor.execute).not.toHaveBeenCalled();
        });
        it('should handle errors from checkPolicy (e.g. non-interactive ASK_USER)', async () => {
            const error = new Error('Not interactive');
            vi.mocked(checkPolicy).mockRejectedValue(error);
            await scheduler.schedule(req1, signal);
            expect(mockStateManager.updateStatus).toHaveBeenCalledWith('call-1', 'error', expect.objectContaining({
                errorType: ToolErrorType.UNHANDLED_EXCEPTION,
                responseParts: expect.arrayContaining([
                    expect.objectContaining({
                        functionResponse: expect.objectContaining({
                            response: { error: 'Not interactive' },
                        }),
                    }),
                ]),
            }));
        });
        it('should bypass confirmation and ProceedOnce if Policy returns ALLOW (YOLO/AllowedTools)', async () => {
            vi.mocked(checkPolicy).mockResolvedValue(PolicyDecision.ALLOW);
            // Provide a mock execute to finish the loop
            mockExecutor.execute.mockResolvedValue({
                status: 'success',
            });
            await scheduler.schedule(req1, signal);
            // Never called coordinator
            expect(resolveConfirmation).not.toHaveBeenCalled();
            // State recorded as ProceedOnce
            expect(mockStateManager.setOutcome).toHaveBeenCalledWith('call-1', ToolConfirmationOutcome.ProceedOnce);
            // Triggered execution
            expect(mockStateManager.updateStatus).toHaveBeenCalledWith('call-1', 'executing');
            expect(mockExecutor.execute).toHaveBeenCalled();
        });
        it('should auto-approve remaining identical tools in batch after ProceedAlways', async () => {
            // Setup: two identical tools
            const validatingCall1 = {
                status: 'validating',
                request: req1,
                tool: mockTool,
                invocation: mockInvocation,
            };
            const validatingCall2 = {
                status: 'validating',
                request: req2,
                tool: mockTool,
                invocation: mockInvocation,
            };
            vi.mocked(mockStateManager.dequeue)
                .mockReturnValueOnce(validatingCall1)
                .mockReturnValueOnce(validatingCall2)
                .mockReturnValue(undefined);
            vi.spyOn(mockStateManager, 'queueLength', 'get')
                .mockReturnValueOnce(2)
                .mockReturnValueOnce(1)
                .mockReturnValue(0);
            // First call requires confirmation, second is auto-approved (simulating policy update)
            vi.mocked(checkPolicy)
                .mockResolvedValueOnce(PolicyDecision.ASK_USER)
                .mockResolvedValueOnce(PolicyDecision.ALLOW);
            vi.mocked(resolveConfirmation).mockResolvedValue({
                outcome: ToolConfirmationOutcome.ProceedAlways,
                lastDetails: undefined,
            });
            mockExecutor.execute.mockResolvedValue({
                status: 'success',
            });
            await scheduler.schedule([req1, req2], signal);
            // resolveConfirmation only called ONCE
            expect(resolveConfirmation).toHaveBeenCalledTimes(1);
            // updatePolicy called for the first tool
            expect(updatePolicy).toHaveBeenCalled();
            // execute called TWICE
            expect(mockExecutor.execute).toHaveBeenCalledTimes(2);
        });
        it('should call resolveConfirmation and updatePolicy when ASK_USER', async () => {
            vi.mocked(checkPolicy).mockResolvedValue(PolicyDecision.ASK_USER);
            const resolution = {
                outcome: ToolConfirmationOutcome.ProceedAlways,
                lastDetails: {
                    type: 'info',
                    title: 'Title',
                    prompt: 'Confirm?',
                },
            };
            vi.mocked(resolveConfirmation).mockResolvedValue(resolution);
            mockExecutor.execute.mockResolvedValue({
                status: 'success',
            });
            await scheduler.schedule(req1, signal);
            expect(resolveConfirmation).toHaveBeenCalledWith(expect.anything(), // toolCall
            signal, expect.objectContaining({
                config: mockConfig,
                messageBus: mockMessageBus,
                state: mockStateManager,
            }));
            expect(updatePolicy).toHaveBeenCalledWith(mockTool, resolution.outcome, resolution.lastDetails, expect.objectContaining({
                config: mockConfig,
                messageBus: mockMessageBus,
            }));
            expect(mockExecutor.execute).toHaveBeenCalled();
        });
        it('should cancel and NOT execute if resolveConfirmation returns Cancel', async () => {
            vi.mocked(checkPolicy).mockResolvedValue(PolicyDecision.ASK_USER);
            const resolution = {
                outcome: ToolConfirmationOutcome.Cancel,
                lastDetails: undefined,
            };
            vi.mocked(resolveConfirmation).mockResolvedValue(resolution);
            await scheduler.schedule(req1, signal);
            expect(mockStateManager.updateStatus).toHaveBeenCalledWith('call-1', 'cancelled', 'User denied execution.');
            expect(mockStateManager.cancelAllQueued).toHaveBeenCalledWith('User cancelled operation');
            expect(mockExecutor.execute).not.toHaveBeenCalled();
        });
        it('should mark as cancelled (not errored) when abort happens during confirmation error', async () => {
            vi.mocked(checkPolicy).mockResolvedValue(PolicyDecision.ASK_USER);
            // Simulate shouldConfirmExecute logic throwing while aborted
            vi.mocked(resolveConfirmation).mockImplementation(async () => {
                // Trigger abort
                abortController.abort();
                throw new Error('Some internal network abort error');
            });
            await scheduler.schedule(req1, signal);
            // Verify execution did NOT happen
            expect(mockExecutor.execute).not.toHaveBeenCalled();
            // Because the signal is aborted, the catch block should convert the error to a cancellation
            expect(mockStateManager.updateStatus).toHaveBeenCalledWith('call-1', 'cancelled', 'Operation cancelled');
        });
        it('should preserve confirmation details (e.g. diff) in cancelled state', async () => {
            vi.mocked(checkPolicy).mockResolvedValue(PolicyDecision.ASK_USER);
            const confirmDetails = {
                type: 'edit',
                title: 'Edit',
                fileName: 'file.txt',
                fileDiff: 'diff content',
                filePath: '/path/to/file.txt',
                originalContent: 'old',
                newContent: 'new',
            };
            const resolution = {
                outcome: ToolConfirmationOutcome.Cancel,
                lastDetails: confirmDetails,
            };
            vi.mocked(resolveConfirmation).mockResolvedValue(resolution);
            await scheduler.schedule(req1, signal);
            expect(mockStateManager.updateStatus).toHaveBeenCalledWith('call-1', 'cancelled', 'User denied execution.');
            // We assume the state manager stores these details.
            // Since we mock state manager, we just verify the flow passed the details.
            // In a real integration, StateManager.updateStatus would merge these.
        });
    });
    describe('Phase 4: Execution Outcomes', () => {
        const validatingCall = {
            status: 'validating',
            request: req1,
            tool: mockTool,
            invocation: mockInvocation,
        };
        beforeEach(() => {
            vi.spyOn(mockStateManager, 'queueLength', 'get')
                .mockReturnValueOnce(1)
                .mockReturnValue(0);
            mockStateManager.dequeue.mockReturnValue(validatingCall);
            vi.spyOn(mockStateManager, 'firstActiveCall', 'get').mockReturnValue(validatingCall);
            mockPolicyEngine.check.mockResolvedValue({
                decision: PolicyDecision.ALLOW,
            }); // Bypass confirmation
        });
        it('should update state to success on successful execution', async () => {
            const mockResponse = {
                callId: 'call-1',
                responseParts: [],
            };
            mockExecutor.execute.mockResolvedValue({
                status: 'success',
                response: mockResponse,
            });
            await scheduler.schedule(req1, signal);
            expect(mockStateManager.updateStatus).toHaveBeenCalledWith('call-1', 'success', mockResponse);
        });
        it('should update state to cancelled when executor returns cancelled status', async () => {
            mockExecutor.execute.mockResolvedValue({
                status: 'cancelled',
                response: { callId: 'call-1', responseParts: [] },
            });
            await scheduler.schedule(req1, signal);
            expect(mockStateManager.updateStatus).toHaveBeenCalledWith('call-1', 'cancelled', 'Operation cancelled');
        });
        it('should update state to error on execution failure', async () => {
            const mockResponse = {
                callId: 'call-1',
                error: new Error('fail'),
            };
            mockExecutor.execute.mockResolvedValue({
                status: 'error',
                response: mockResponse,
            });
            await scheduler.schedule(req1, signal);
            expect(mockStateManager.updateStatus).toHaveBeenCalledWith('call-1', 'error', mockResponse);
        });
        it('should log telemetry for terminal states in the queue processor', async () => {
            const mockResponse = {
                callId: 'call-1',
                responseParts: [],
            };
            // Mock the execution so the state advances
            mockExecutor.execute.mockResolvedValue({
                status: 'success',
                response: mockResponse,
            });
            // Mock the state manager to return a SUCCESS state when getToolCall is
            // called
            const successfulCall = {
                status: 'success',
                request: req1,
                response: mockResponse,
                tool: mockTool,
                invocation: mockInvocation,
            };
            mockStateManager.getToolCall.mockReturnValue(successfulCall);
            Object.defineProperty(mockStateManager, 'completedBatch', {
                get: vi.fn().mockReturnValue([successfulCall]),
                configurable: true,
            });
            await scheduler.schedule(req1, signal);
            // Verify the finalizer and logger were called
            expect(mockStateManager.finalizeCall).toHaveBeenCalledWith('call-1');
            expect(ToolCallEvent).toHaveBeenCalledWith(successfulCall);
            expect(logToolCall).toHaveBeenCalledWith(mockConfig, expect.objectContaining(successfulCall));
        });
        it('should not double-report completed tools when concurrent completions occur', async () => {
            // Simulate a race where execution finishes but cancelAll is called immediately after
            const response = {
                callId: 'call-1',
                responseParts: [],
                resultDisplay: undefined,
                error: undefined,
                errorType: undefined,
                contentLength: 0,
            };
            mockExecutor.execute.mockResolvedValue({
                status: 'success',
                response,
            });
            const promise = scheduler.schedule(req1, signal);
            scheduler.cancelAll();
            await promise;
            // finalizeCall should be called exactly once for this ID
            expect(mockStateManager.finalizeCall).toHaveBeenCalledTimes(1);
            expect(mockStateManager.finalizeCall).toHaveBeenCalledWith('call-1');
        });
    });
});
//# sourceMappingURL=scheduler.test.js.map