/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { CoderAgentExecutor } from './executor.js';
import { EventEmitter } from 'node:events';
import { requestStorage } from '../http/requestStorage.js';
// Mocks for constructor dependencies
vi.mock('../config/config.js', () => ({
    loadConfig: vi.fn().mockReturnValue({
        getSessionId: () => 'test-session',
        getTargetDir: () => '/tmp',
        getCheckpointingEnabled: () => false,
    }),
    loadEnvironment: vi.fn(),
    setTargetDir: vi.fn().mockReturnValue('/tmp'),
}));
vi.mock('../config/settings.js', () => ({
    loadSettings: vi.fn().mockReturnValue({}),
}));
vi.mock('../config/extension.js', () => ({
    loadExtensions: vi.fn().mockReturnValue([]),
}));
vi.mock('../http/requestStorage.js', () => ({
    requestStorage: {
        getStore: vi.fn(),
    },
}));
vi.mock('./task.js', () => {
    const mockTaskInstance = (taskId, contextId) => ({
        id: taskId,
        contextId,
        taskState: 'working',
        acceptUserMessage: vi
            .fn()
            .mockImplementation(async function* (context, aborted) {
            const isConfirmation = context.userMessage.parts.some((p) => p.kind === 'confirmation');
            // Hang only for main user messages (text), allow confirmations to finish quickly
            if (!isConfirmation && aborted) {
                await new Promise((resolve) => {
                    aborted.addEventListener('abort', resolve, { once: true });
                });
            }
            yield { type: 'content', value: 'hello' };
        }),
        acceptAgentMessage: vi.fn().mockResolvedValue(undefined),
        scheduleToolCalls: vi.fn().mockResolvedValue(undefined),
        waitForPendingTools: vi.fn().mockResolvedValue(undefined),
        getAndClearCompletedTools: vi.fn().mockReturnValue([]),
        addToolResponsesToHistory: vi.fn(),
        sendCompletedToolsToLlm: vi.fn().mockImplementation(async function* () { }),
        cancelPendingTools: vi.fn(),
        setTaskStateAndPublishUpdate: vi.fn(),
        dispose: vi.fn(),
        getMetadata: vi.fn().mockResolvedValue({}),
        geminiClient: {
            initialize: vi.fn().mockResolvedValue(undefined),
        },
        toSDKTask: () => ({
            id: taskId,
            contextId,
            kind: 'task',
            status: { state: 'working', timestamp: new Date().toISOString() },
            metadata: {},
            history: [],
            artifacts: [],
        }),
    });
    const MockTask = vi.fn().mockImplementation(mockTaskInstance);
    MockTask.create = vi
        .fn()
        .mockImplementation(async (taskId, contextId) => mockTaskInstance(taskId, contextId));
    return { Task: MockTask };
});
describe('CoderAgentExecutor', () => {
    let executor;
    let mockTaskStore;
    let mockEventBus;
    beforeEach(() => {
        vi.clearAllMocks();
        mockTaskStore = {
            save: vi.fn().mockResolvedValue(undefined),
            load: vi.fn().mockResolvedValue(undefined),
            delete: vi.fn().mockResolvedValue(undefined),
            list: vi.fn().mockResolvedValue([]),
        };
        mockEventBus = new EventEmitter();
        mockEventBus.publish = vi.fn();
        mockEventBus.finished = vi.fn();
        executor = new CoderAgentExecutor(mockTaskStore);
    });
    it('should distinguish between primary and secondary execution', async () => {
        const taskId = 'test-task';
        const contextId = 'test-context';
        const mockSocket = new EventEmitter();
        const requestContext = {
            userMessage: {
                messageId: 'msg-1',
                taskId,
                contextId,
                parts: [{ kind: 'text', text: 'hi' }],
                metadata: {
                    coderAgent: { kind: 'agent-settings', workspacePath: '/tmp' },
                },
            },
        };
        // Mock requestStorage for primary
        requestStorage.getStore.mockReturnValue({
            req: { socket: mockSocket },
        });
        // First execution (Primary)
        const primaryPromise = executor.execute(requestContext, mockEventBus);
        // Give it enough time to reach line 490 in executor.ts
        await new Promise((resolve) => setTimeout(resolve, 50));
        expect(executor.executingTasks.has(taskId)).toBe(true);
        const wrapper = executor.getTask(taskId);
        expect(wrapper).toBeDefined();
        // Mock requestStorage for secondary
        const secondarySocket = new EventEmitter();
        requestStorage.getStore.mockReturnValue({
            req: { socket: secondarySocket },
        });
        const secondaryRequestContext = {
            userMessage: {
                messageId: 'msg-2',
                taskId,
                contextId,
                parts: [{ kind: 'confirmation', callId: '1', outcome: 'proceed' }],
                metadata: {
                    coderAgent: { kind: 'agent-settings', workspacePath: '/tmp' },
                },
            },
        };
        const secondaryPromise = executor.execute(secondaryRequestContext, mockEventBus);
        // Secondary execution should NOT add to executingTasks (already there)
        // and should return early after its loop
        await secondaryPromise;
        // Task should still be in executingTasks and NOT disposed
        expect(executor.executingTasks.has(taskId)).toBe(true);
        expect(wrapper?.task.dispose).not.toHaveBeenCalled();
        // Now simulate secondary socket closure - it should NOT affect primary
        secondarySocket.emit('end');
        expect(executor.executingTasks.has(taskId)).toBe(true);
        expect(wrapper?.task.dispose).not.toHaveBeenCalled();
        // Set to terminal state to verify disposal on finish
        wrapper.task.taskState = 'completed';
        // Now close primary socket
        mockSocket.emit('end');
        await primaryPromise;
        expect(executor.executingTasks.has(taskId)).toBe(false);
        expect(wrapper?.task.dispose).toHaveBeenCalled();
    });
    it('should evict task from cache when it reaches terminal state', async () => {
        const taskId = 'test-task-terminal';
        const contextId = 'test-context';
        const mockSocket = new EventEmitter();
        requestStorage.getStore.mockReturnValue({
            req: { socket: mockSocket },
        });
        const requestContext = {
            userMessage: {
                messageId: 'msg-1',
                taskId,
                contextId,
                parts: [{ kind: 'text', text: 'hi' }],
                metadata: {
                    coderAgent: { kind: 'agent-settings', workspacePath: '/tmp' },
                },
            },
        };
        const primaryPromise = executor.execute(requestContext, mockEventBus);
        await new Promise((resolve) => setTimeout(resolve, 50));
        const wrapper = executor.getTask(taskId);
        expect(wrapper).toBeDefined();
        // Simulate terminal state
        wrapper.task.taskState = 'completed';
        // Finish primary execution
        mockSocket.emit('end');
        await primaryPromise;
        expect(executor.getTask(taskId)).toBeUndefined();
        expect(wrapper.task.dispose).toHaveBeenCalled();
    });
});
//# sourceMappingURL=executor.test.js.map