/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import { GeminiEventType, ApprovalMode, } from '@google/gemini-cli-core';
import express from 'express';
import request from 'supertest';
import { afterAll, afterEach, beforeEach, beforeAll, describe, expect, it, vi, } from 'vitest';
import { createApp, main } from './app.js';
import { commandRegistry } from '../commands/command-registry.js';
import { assertUniqueFinalEventIsLast, assertTaskCreationAndWorkingStatus, createStreamMessageRequest, createMockConfig, } from '../utils/testing_utils.js';
// Import MockTool from specific path to avoid vitest dependency in main core bundle
import { MockTool } from '@google/gemini-cli-core/src/test-utils/mock-tool.js';
const mockToolConfirmationFn = async () => ({});
const streamToSSEEvents = (stream) => stream
    .split('\n\n')
    .filter(Boolean) // Remove empty strings from trailing newlines
    .map((chunk) => {
    const dataLine = chunk
        .split('\n')
        .find((line) => line.startsWith('data: '));
    if (!dataLine) {
        throw new Error(`Invalid SSE chunk found: "${chunk}"`);
    }
    return JSON.parse(dataLine.substring(6));
});
// Mock the logger to avoid polluting test output
// Comment out to debug tests
vi.mock('../utils/logger.js', () => ({
    logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
}));
let config;
const getToolRegistrySpy = vi.fn().mockReturnValue({
    getTool: vi.fn(),
    getAllToolNames: vi.fn().mockReturnValue([]),
    getAllTools: vi.fn().mockReturnValue([]),
    getToolsByServer: vi.fn().mockReturnValue([]),
});
const getApprovalModeSpy = vi.fn();
const getShellExecutionConfigSpy = vi.fn();
const getExtensionsSpy = vi.fn();
vi.mock('../config/config.js', async () => {
    const actual = await vi.importActual('../config/config.js');
    return {
        ...actual,
        loadConfig: vi.fn().mockImplementation(async () => {
            const mockConfig = createMockConfig({
                getToolRegistry: getToolRegistrySpy,
                getApprovalMode: getApprovalModeSpy,
                getShellExecutionConfig: getShellExecutionConfigSpy,
                getExtensions: getExtensionsSpy,
            });
            config = mockConfig;
            return config;
        }),
    };
});
// Mock the GeminiClient to avoid actual API calls
const sendMessageStreamSpy = vi.fn();
vi.mock('@google/gemini-cli-core', async () => {
    const actual = await vi.importActual('@google/gemini-cli-core');
    return {
        ...actual,
        GeminiClient: vi.fn().mockImplementation(() => ({
            sendMessageStream: sendMessageStreamSpy,
            getUserTier: vi.fn().mockReturnValue('free'),
            initialize: vi.fn(),
        })),
        performRestore: vi.fn(),
    };
});
describe('E2E Tests', () => {
    let app;
    let server;
    beforeAll(async () => {
        app = await createApp();
        server = app.listen(0); // Listen on a random available port
    });
    beforeEach(() => {
        getApprovalModeSpy.mockReturnValue(ApprovalMode.DEFAULT);
    });
    afterAll(() => new Promise((resolve) => {
        server.close(() => {
            resolve();
        });
    }));
    afterEach(() => {
        vi.clearAllMocks();
    });
    it('should create a new task and stream status updates (text-content) via POST /', async () => {
        sendMessageStreamSpy.mockImplementation(async function* () {
            yield* [{ type: 'content', value: 'Hello how are you?' }];
        });
        const agent = request.agent(app);
        const res = await agent
            .post('/')
            .send(createStreamMessageRequest('hello', 'a2a-test-message'))
            .set('Content-Type', 'application/json')
            .expect(200);
        const events = streamToSSEEvents(res.text);
        assertTaskCreationAndWorkingStatus(events);
        // Status update: text-content
        const textContentEvent = events[2].result;
        expect(textContentEvent.kind).toBe('status-update');
        expect(textContentEvent.status.state).toBe('working');
        expect(textContentEvent.metadata?.['coderAgent']).toMatchObject({
            kind: 'text-content',
        });
        expect(textContentEvent.status.message?.parts).toMatchObject([
            { kind: 'text', text: 'Hello how are you?' },
        ]);
        // Status update: input-required (final)
        const finalEvent = events[3].result;
        expect(finalEvent.kind).toBe('status-update');
        expect(finalEvent.status?.state).toBe('input-required');
        expect(finalEvent.final).toBe(true);
        assertUniqueFinalEventIsLast(events);
        expect(events.length).toBe(4);
    });
    it('should create a new task, schedule a tool call, and wait for approval', async () => {
        // First call yields the tool request
        sendMessageStreamSpy.mockImplementationOnce(async function* () {
            yield* [
                {
                    type: GeminiEventType.ToolCallRequest,
                    value: {
                        callId: 'test-call-id',
                        name: 'test-tool',
                        args: {},
                    },
                },
            ];
        });
        // Subsequent calls yield nothing
        sendMessageStreamSpy.mockImplementation(async function* () {
            yield* [];
        });
        const mockTool = new MockTool({
            name: 'test-tool',
            shouldConfirmExecute: vi.fn(mockToolConfirmationFn),
        });
        getToolRegistrySpy.mockReturnValue({
            getAllTools: vi.fn().mockReturnValue([mockTool]),
            getToolsByServer: vi.fn().mockReturnValue([]),
            getTool: vi.fn().mockReturnValue(mockTool),
        });
        const agent = request.agent(app);
        const res = await agent
            .post('/')
            .send(createStreamMessageRequest('run a tool', 'a2a-tool-test-message'))
            .set('Content-Type', 'application/json')
            .expect(200);
        const events = streamToSSEEvents(res.text);
        assertTaskCreationAndWorkingStatus(events);
        // Status update: working
        const workingEvent2 = events[2].result;
        expect(workingEvent2.kind).toBe('status-update');
        expect(workingEvent2.status.state).toBe('working');
        expect(workingEvent2.metadata?.['coderAgent']).toMatchObject({
            kind: 'state-change',
        });
        // Status update: tool-call-update
        const toolCallUpdateEvent = events[3].result;
        expect(toolCallUpdateEvent.kind).toBe('status-update');
        expect(toolCallUpdateEvent.status.state).toBe('working');
        expect(toolCallUpdateEvent.metadata?.['coderAgent']).toMatchObject({
            kind: 'tool-call-update',
        });
        expect(toolCallUpdateEvent.status.message?.parts).toMatchObject([
            {
                data: {
                    status: 'scheduled',
                    request: { callId: 'test-call-id' },
                },
            },
        ]);
        // State update: awaiting_approval update
        const toolCallConfirmationEvent = events[4].result;
        expect(toolCallConfirmationEvent.kind).toBe('status-update');
        expect(toolCallConfirmationEvent.metadata?.['coderAgent']).toMatchObject({
            kind: 'tool-call-confirmation',
        });
        expect(toolCallConfirmationEvent.status.message?.parts).toMatchObject([
            {
                data: {
                    status: 'awaiting_approval',
                    request: { callId: 'test-call-id' },
                },
            },
        ]);
        expect(toolCallConfirmationEvent.status?.state).toBe('working');
        assertUniqueFinalEventIsLast(events);
        expect(events.length).toBe(6);
    });
    it('should handle multiple tool calls in a single turn', async () => {
        // First call yields the tool request
        sendMessageStreamSpy.mockImplementationOnce(async function* () {
            yield* [
                {
                    type: GeminiEventType.ToolCallRequest,
                    value: {
                        callId: 'test-call-id-1',
                        name: 'test-tool-1',
                        args: {},
                    },
                },
                {
                    type: GeminiEventType.ToolCallRequest,
                    value: {
                        callId: 'test-call-id-2',
                        name: 'test-tool-2',
                        args: {},
                    },
                },
            ];
        });
        // Subsequent calls yield nothing
        sendMessageStreamSpy.mockImplementation(async function* () {
            yield* [];
        });
        const mockTool1 = new MockTool({
            name: 'test-tool-1',
            displayName: 'Test Tool 1',
            shouldConfirmExecute: vi.fn(mockToolConfirmationFn),
        });
        const mockTool2 = new MockTool({
            name: 'test-tool-2',
            displayName: 'Test Tool 2',
            shouldConfirmExecute: vi.fn(mockToolConfirmationFn),
        });
        getToolRegistrySpy.mockReturnValue({
            getAllTools: vi.fn().mockReturnValue([mockTool1, mockTool2]),
            getToolsByServer: vi.fn().mockReturnValue([]),
            getTool: vi.fn().mockImplementation((name) => {
                if (name === 'test-tool-1')
                    return mockTool1;
                if (name === 'test-tool-2')
                    return mockTool2;
                return undefined;
            }),
        });
        const agent = request.agent(app);
        const res = await agent
            .post('/')
            .send(createStreamMessageRequest('run two tools', 'a2a-multi-tool-test-message'))
            .set('Content-Type', 'application/json')
            .expect(200);
        const events = streamToSSEEvents(res.text);
        assertTaskCreationAndWorkingStatus(events);
        // Second working update
        const workingEvent = events[2].result;
        expect(workingEvent.kind).toBe('status-update');
        expect(workingEvent.status.state).toBe('working');
        // State Update: Validate the first tool call
        const toolCallValidateEvent1 = events[3].result;
        expect(toolCallValidateEvent1.metadata?.['coderAgent']).toMatchObject({
            kind: 'tool-call-update',
        });
        expect(toolCallValidateEvent1.status.message?.parts).toMatchObject([
            {
                data: {
                    status: 'scheduled',
                    request: { callId: 'test-call-id-1' },
                },
            },
        ]);
        // --- Assert the event stream ---
        // 1. Initial "submitted" status.
        expect(events[0].result.status.state).toBe('submitted');
        // 2. "working" status after receiving the user prompt.
        expect(events[1].result.status.state).toBe('working');
        // 3. A "state-change" event from the agent.
        expect(events[2].result.metadata?.['coderAgent']).toMatchObject({
            kind: 'state-change',
        });
        // 4. Tool 1 is scheduled.
        const toolCallUpdate1 = events[3].result;
        expect(toolCallUpdate1.metadata?.['coderAgent']).toMatchObject({
            kind: 'tool-call-update',
        });
        expect(toolCallUpdate1.status.message?.parts).toMatchObject([
            {
                data: {
                    request: { callId: 'test-call-id-1' },
                    status: 'scheduled',
                },
            },
        ]);
        // 5. Tool 2 is scheduled.
        const toolCallUpdate2 = events[4].result;
        expect(toolCallUpdate2.metadata?.['coderAgent']).toMatchObject({
            kind: 'tool-call-update',
        });
        expect(toolCallUpdate2.status.message?.parts).toMatchObject([
            {
                data: {
                    request: { callId: 'test-call-id-2' },
                    status: 'scheduled',
                },
            },
        ]);
        // 6. Tool 1 is awaiting approval.
        const toolCallAwaitEvent1 = events[5].result;
        expect(toolCallAwaitEvent1.metadata?.['coderAgent']).toMatchObject({
            kind: 'tool-call-confirmation',
        });
        expect(toolCallAwaitEvent1.status.message?.parts).toMatchObject([
            {
                data: {
                    request: { callId: 'test-call-id-1' },
                    status: 'awaiting_approval',
                },
            },
        ]);
        // 7. Tool 2 is awaiting approval.
        const toolCallAwaitEvent2 = events[6].result;
        expect(toolCallAwaitEvent2.metadata?.['coderAgent']).toMatchObject({
            kind: 'tool-call-confirmation',
        });
        expect(toolCallAwaitEvent2.status.message?.parts).toMatchObject([
            {
                data: {
                    request: { callId: 'test-call-id-2' },
                    status: 'awaiting_approval',
                },
            },
        ]);
        // 8. The final event is "input-required".
        const finalEvent = events[7].result;
        expect(finalEvent.final).toBe(true);
        expect(finalEvent.status.state).toBe('input-required');
        // The scheduler now waits for approval, so no more events are sent.
        assertUniqueFinalEventIsLast(events);
        expect(events.length).toBe(8);
    });
    it('should handle multiple tool calls sequentially in YOLO mode', async () => {
        // Set YOLO mode to auto-approve tools and test sequential execution.
        getApprovalModeSpy.mockReturnValue(ApprovalMode.YOLO);
        // First call yields the tool request
        sendMessageStreamSpy.mockImplementationOnce(async function* () {
            yield* [
                {
                    type: GeminiEventType.ToolCallRequest,
                    value: {
                        callId: 'test-call-id-1',
                        name: 'test-tool-1',
                        args: {},
                    },
                },
                {
                    type: GeminiEventType.ToolCallRequest,
                    value: {
                        callId: 'test-call-id-2',
                        name: 'test-tool-2',
                        args: {},
                    },
                },
            ];
        });
        // Subsequent calls yield nothing, as the tools will "succeed".
        sendMessageStreamSpy.mockImplementation(async function* () {
            yield* [{ type: 'content', value: 'All tools executed.' }];
        });
        const mockTool1 = new MockTool({
            name: 'test-tool-1',
            displayName: 'Test Tool 1',
            shouldConfirmExecute: vi.fn(mockToolConfirmationFn),
            execute: vi
                .fn()
                .mockResolvedValue({ llmContent: 'tool 1 done', returnDisplay: '' }),
        });
        const mockTool2 = new MockTool({
            name: 'test-tool-2',
            displayName: 'Test Tool 2',
            shouldConfirmExecute: vi.fn(mockToolConfirmationFn),
            execute: vi
                .fn()
                .mockResolvedValue({ llmContent: 'tool 2 done', returnDisplay: '' }),
        });
        getToolRegistrySpy.mockReturnValue({
            getAllTools: vi.fn().mockReturnValue([mockTool1, mockTool2]),
            getToolsByServer: vi.fn().mockReturnValue([]),
            getTool: vi.fn().mockImplementation((name) => {
                if (name === 'test-tool-1')
                    return mockTool1;
                if (name === 'test-tool-2')
                    return mockTool2;
                return undefined;
            }),
        });
        const agent = request.agent(app);
        const res = await agent
            .post('/')
            .send(createStreamMessageRequest('run two tools', 'a2a-multi-tool-test-message'))
            .set('Content-Type', 'application/json')
            .expect(200);
        const events = streamToSSEEvents(res.text);
        assertTaskCreationAndWorkingStatus(events);
        // --- Assert the sequential execution flow ---
        const eventStream = events.slice(2).map((e) => {
            const update = e.result;
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
            const agentData = update.metadata?.['coderAgent'];
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
            const toolData = update.status.message?.parts[0];
            if (!toolData) {
                return { kind: agentData.kind };
            }
            return {
                kind: agentData.kind,
                status: toolData.data?.status,
                callId: toolData.data?.request.callId,
            };
        });
        const expectedFlow = [
            // Initial state change
            { kind: 'state-change', status: undefined, callId: undefined },
            // Tool 1 Lifecycle
            {
                kind: 'tool-call-update',
                status: 'scheduled',
                callId: 'test-call-id-1',
            },
            {
                kind: 'tool-call-update',
                status: 'scheduled',
                callId: 'test-call-id-1',
            },
            {
                kind: 'tool-call-update',
                status: 'executing',
                callId: 'test-call-id-1',
            },
            {
                kind: 'tool-call-update',
                status: 'success',
                callId: 'test-call-id-1',
            },
            // Tool 2 Lifecycle
            {
                kind: 'tool-call-update',
                status: 'scheduled',
                callId: 'test-call-id-2',
            },
            {
                kind: 'tool-call-update',
                status: 'scheduled',
                callId: 'test-call-id-2',
            },
            {
                kind: 'tool-call-update',
                status: 'executing',
                callId: 'test-call-id-2',
            },
            {
                kind: 'tool-call-update',
                status: 'success',
                callId: 'test-call-id-2',
            },
            // Final updates
            { kind: 'state-change', status: undefined, callId: undefined },
            { kind: 'text-content', status: undefined, callId: undefined },
        ];
        // Use `toContainEqual` for flexibility if other events are interspersed.
        expect(eventStream).toEqual(expect.arrayContaining(expectedFlow));
        assertUniqueFinalEventIsLast(events);
    });
    it('should handle tool calls that do not require approval', async () => {
        // First call yields the tool request
        sendMessageStreamSpy.mockImplementationOnce(async function* () {
            yield* [
                {
                    type: GeminiEventType.ToolCallRequest,
                    value: {
                        callId: 'test-call-id-no-approval',
                        name: 'test-tool-no-approval',
                        args: {},
                    },
                },
            ];
        });
        // Second call, after the tool runs, yields the final text
        sendMessageStreamSpy.mockImplementationOnce(async function* () {
            yield* [{ type: 'content', value: 'Tool executed successfully.' }];
        });
        const mockTool = new MockTool({
            name: 'test-tool-no-approval',
            displayName: 'Test Tool No Approval',
            execute: vi.fn().mockResolvedValue({
                llmContent: 'Tool executed successfully.',
                returnDisplay: 'Tool executed successfully.',
            }),
        });
        getToolRegistrySpy.mockReturnValue({
            getAllTools: vi.fn().mockReturnValue([mockTool]),
            getToolsByServer: vi.fn().mockReturnValue([]),
            getTool: vi.fn().mockReturnValue(mockTool),
        });
        const agent = request.agent(app);
        const res = await agent
            .post('/')
            .send(createStreamMessageRequest('run a tool without approval', 'a2a-no-approval-test-message'))
            .set('Content-Type', 'application/json')
            .expect(200);
        const events = streamToSSEEvents(res.text);
        assertTaskCreationAndWorkingStatus(events);
        // Status update: working
        const workingEvent2 = events[2].result;
        expect(workingEvent2.kind).toBe('status-update');
        expect(workingEvent2.status.state).toBe('working');
        // Status update: tool-call-update (scheduled)
        const scheduledEvent1 = events[3].result;
        expect(scheduledEvent1.metadata?.['coderAgent']).toMatchObject({
            kind: 'tool-call-update',
        });
        expect(scheduledEvent1.status.message?.parts).toMatchObject([
            {
                data: {
                    status: 'scheduled',
                    request: { callId: 'test-call-id-no-approval' },
                },
            },
        ]);
        // Status update: tool-call-update (scheduled)
        const scheduledEvent2 = events[4].result;
        expect(scheduledEvent2.metadata?.['coderAgent']).toMatchObject({
            kind: 'tool-call-update',
        });
        expect(scheduledEvent2.status.message?.parts).toMatchObject([
            {
                data: {
                    status: 'scheduled',
                    request: { callId: 'test-call-id-no-approval' },
                },
            },
        ]);
        // Status update: tool-call-update (scheduled)
        const scheduledEvent3 = events[5].result;
        expect(scheduledEvent3.metadata?.['coderAgent']).toMatchObject({
            kind: 'tool-call-update',
        });
        expect(scheduledEvent3.status.message?.parts).toMatchObject([
            {
                data: {
                    status: 'scheduled',
                    request: { callId: 'test-call-id-no-approval' },
                },
            },
        ]);
        // Status update: tool-call-update (executing)
        const executingEvent = events[6].result;
        expect(executingEvent.metadata?.['coderAgent']).toMatchObject({
            kind: 'tool-call-update',
        });
        expect(executingEvent.status.message?.parts).toMatchObject([
            {
                data: {
                    status: 'executing',
                    request: { callId: 'test-call-id-no-approval' },
                },
            },
        ]);
        // Status update: tool-call-update (success)
        const successEvent = events[7].result;
        expect(successEvent.metadata?.['coderAgent']).toMatchObject({
            kind: 'tool-call-update',
        });
        expect(successEvent.status.message?.parts).toMatchObject([
            {
                data: {
                    status: 'success',
                    request: { callId: 'test-call-id-no-approval' },
                },
            },
        ]);
        // Status update: working (before sending tool result to LLM)
        const workingEvent3 = events[8].result;
        expect(workingEvent3.kind).toBe('status-update');
        expect(workingEvent3.status.state).toBe('working');
        // Status update: text-content (final LLM response)
        const textContentEvent = events[9].result;
        expect(textContentEvent.metadata?.['coderAgent']).toMatchObject({
            kind: 'text-content',
        });
        expect(textContentEvent.status.message?.parts).toMatchObject([
            { text: 'Tool executed successfully.' },
        ]);
        assertUniqueFinalEventIsLast(events);
        expect(events.length).toBe(11);
    });
    it('should bypass tool approval in YOLO mode', async () => {
        // First call yields the tool request
        sendMessageStreamSpy.mockImplementationOnce(async function* () {
            yield* [
                {
                    type: GeminiEventType.ToolCallRequest,
                    value: {
                        callId: 'test-call-id-yolo',
                        name: 'test-tool-yolo',
                        args: {},
                    },
                },
            ];
        });
        // Second call, after the tool runs, yields the final text
        sendMessageStreamSpy.mockImplementationOnce(async function* () {
            yield* [{ type: 'content', value: 'Tool executed successfully.' }];
        });
        // Set approval mode to yolo
        getApprovalModeSpy.mockReturnValue(ApprovalMode.YOLO);
        const mockTool = new MockTool({
            name: 'test-tool-yolo',
            displayName: 'Test Tool YOLO',
            execute: vi.fn().mockResolvedValue({
                llmContent: 'Tool executed successfully.',
                returnDisplay: 'Tool executed successfully.',
            }),
        });
        getToolRegistrySpy.mockReturnValue({
            getAllTools: vi.fn().mockReturnValue([mockTool]),
            getToolsByServer: vi.fn().mockReturnValue([]),
            getTool: vi.fn().mockReturnValue(mockTool),
        });
        const agent = request.agent(app);
        const res = await agent
            .post('/')
            .send(createStreamMessageRequest('run a tool in yolo mode', 'a2a-yolo-mode-test-message'))
            .set('Content-Type', 'application/json')
            .expect(200);
        const events = streamToSSEEvents(res.text);
        assertTaskCreationAndWorkingStatus(events);
        // Status update: working
        const workingEvent2 = events[2].result;
        expect(workingEvent2.kind).toBe('status-update');
        expect(workingEvent2.status.state).toBe('working');
        // Status update: tool-call-update (scheduled)
        const scheduledEvent = events[3].result;
        expect(scheduledEvent.metadata?.['coderAgent']).toMatchObject({
            kind: 'tool-call-update',
        });
        expect(scheduledEvent.status.message?.parts).toMatchObject([
            {
                data: {
                    status: 'scheduled',
                    request: { callId: 'test-call-id-yolo' },
                },
            },
        ]);
        // Status update: tool-call-update (scheduled)
        const awaitingEvent = events[4].result;
        expect(awaitingEvent.metadata?.['coderAgent']).toMatchObject({
            kind: 'tool-call-update',
        });
        expect(awaitingEvent.status.message?.parts).toMatchObject([
            {
                data: {
                    status: 'scheduled',
                    request: { callId: 'test-call-id-yolo' },
                },
            },
        ]);
        // Status update: tool-call-update (scheduled)
        const scheduledEvent3 = events[5].result;
        expect(scheduledEvent3.metadata?.['coderAgent']).toMatchObject({
            kind: 'tool-call-update',
        });
        expect(scheduledEvent3.status.message?.parts).toMatchObject([
            {
                data: {
                    status: 'scheduled',
                    request: { callId: 'test-call-id-yolo' },
                },
            },
        ]);
        // Status update: tool-call-update (executing)
        const executingEvent = events[6].result;
        expect(executingEvent.metadata?.['coderAgent']).toMatchObject({
            kind: 'tool-call-update',
        });
        expect(executingEvent.status.message?.parts).toMatchObject([
            {
                data: {
                    status: 'executing',
                    request: { callId: 'test-call-id-yolo' },
                },
            },
        ]);
        // Status update: tool-call-update (success)
        const successEvent = events[7].result;
        expect(successEvent.metadata?.['coderAgent']).toMatchObject({
            kind: 'tool-call-update',
        });
        expect(successEvent.status.message?.parts).toMatchObject([
            {
                data: {
                    status: 'success',
                    request: { callId: 'test-call-id-yolo' },
                },
            },
        ]);
        // Status update: working (before sending tool result to LLM)
        const workingEvent3 = events[8].result;
        expect(workingEvent3.kind).toBe('status-update');
        expect(workingEvent3.status.state).toBe('working');
        // Status update: text-content (final LLM response)
        const textContentEvent = events[9].result;
        expect(textContentEvent.metadata?.['coderAgent']).toMatchObject({
            kind: 'text-content',
        });
        expect(textContentEvent.status.message?.parts).toMatchObject([
            { text: 'Tool executed successfully.' },
        ]);
        assertUniqueFinalEventIsLast(events);
        expect(events.length).toBe(11);
    });
    it('should include traceId in status updates when available', async () => {
        const traceId = 'test-trace-id';
        sendMessageStreamSpy.mockImplementation(async function* () {
            yield* [
                { type: 'content', value: 'Hello', traceId },
                { type: 'thought', value: { subject: 'Thinking...' }, traceId },
            ];
        });
        const agent = request.agent(app);
        const res = await agent
            .post('/')
            .send(createStreamMessageRequest('hello', 'a2a-trace-id-test'))
            .set('Content-Type', 'application/json')
            .expect(200);
        const events = streamToSSEEvents(res.text);
        // The first two events are task-creation and working status
        const textContentEvent = events[2].result;
        expect(textContentEvent.kind).toBe('status-update');
        expect(textContentEvent.metadata?.['traceId']).toBe(traceId);
        const thoughtEvent = events[3].result;
        expect(thoughtEvent.kind).toBe('status-update');
        expect(thoughtEvent.metadata?.['traceId']).toBe(traceId);
    });
    describe('/listCommands', () => {
        it('should return a list of top-level commands', async () => {
            const mockCommands = [
                {
                    name: 'test-command',
                    description: 'A test command',
                    topLevel: true,
                    arguments: [{ name: 'arg1', description: 'Argument 1' }],
                    subCommands: [
                        {
                            name: 'sub-command',
                            description: 'A sub command',
                            topLevel: false,
                            execute: vi.fn(),
                        },
                    ],
                    execute: vi.fn(),
                },
                {
                    name: 'another-command',
                    description: 'Another test command',
                    topLevel: true,
                    execute: vi.fn(),
                },
                {
                    name: 'not-top-level',
                    description: 'Not a top level command',
                    topLevel: false,
                    execute: vi.fn(),
                },
            ];
            const getAllCommandsSpy = vi
                .spyOn(commandRegistry, 'getAllCommands')
                .mockReturnValue(mockCommands);
            const agent = request.agent(app);
            const res = await agent.get('/listCommands').expect(200);
            expect(res.body).toEqual({
                commands: [
                    {
                        name: 'test-command',
                        description: 'A test command',
                        arguments: [{ name: 'arg1', description: 'Argument 1' }],
                        subCommands: [
                            {
                                name: 'sub-command',
                                description: 'A sub command',
                                arguments: [],
                                subCommands: [],
                            },
                        ],
                    },
                    {
                        name: 'another-command',
                        description: 'Another test command',
                        arguments: [],
                        subCommands: [],
                    },
                ],
            });
            expect(getAllCommandsSpy).toHaveBeenCalledOnce();
            getAllCommandsSpy.mockRestore();
        });
        it('should handle cyclic commands gracefully', async () => {
            const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
            const cyclicCommand = {
                name: 'cyclic-command',
                description: 'A cyclic command',
                topLevel: true,
                execute: vi.fn(),
                subCommands: [],
            };
            cyclicCommand.subCommands?.push(cyclicCommand); // Create cycle
            const getAllCommandsSpy = vi
                .spyOn(commandRegistry, 'getAllCommands')
                .mockReturnValue([cyclicCommand]);
            const agent = request.agent(app);
            const res = await agent.get('/listCommands').expect(200);
            expect(res.body.commands[0].name).toBe('cyclic-command');
            expect(res.body.commands[0].subCommands).toEqual([]);
            expect(warnSpy).toHaveBeenCalledWith('Command cyclic-command already inserted in the response, skipping');
            getAllCommandsSpy.mockRestore();
            warnSpy.mockRestore();
        });
    });
    describe('/executeCommand', () => {
        const mockExtensions = [{ name: 'test-extension', version: '0.0.1' }];
        beforeEach(() => {
            getExtensionsSpy.mockReturnValue(mockExtensions);
        });
        afterEach(() => {
            getExtensionsSpy.mockClear();
        });
        it('should return extensions for valid command', async () => {
            const mockExtensionsCommand = {
                name: 'extensions list',
                description: 'a mock command',
                execute: vi.fn(async (context) => {
                    // Simulate the actual command's behavior
                    const extensions = context.config.getExtensions();
                    return { name: 'extensions list', data: extensions };
                }),
            };
            vi.spyOn(commandRegistry, 'get').mockReturnValue(mockExtensionsCommand);
            const agent = request.agent(app);
            const res = await agent
                .post('/executeCommand')
                .send({ command: 'extensions list', args: [] })
                .set('Content-Type', 'application/json')
                .expect(200);
            expect(res.body).toEqual({
                name: 'extensions list',
                data: mockExtensions,
            });
            expect(getExtensionsSpy).toHaveBeenCalled();
        });
        it('should return 404 for invalid command', async () => {
            vi.spyOn(commandRegistry, 'get').mockReturnValue(undefined);
            const agent = request.agent(app);
            const res = await agent
                .post('/executeCommand')
                .send({ command: 'invalid command' })
                .set('Content-Type', 'application/json')
                .expect(404);
            expect(res.body.error).toBe('Command not found: invalid command');
            expect(getExtensionsSpy).not.toHaveBeenCalled();
        });
        it('should return 400 for missing command', async () => {
            const agent = request.agent(app);
            await agent
                .post('/executeCommand')
                .send({ args: [] })
                .set('Content-Type', 'application/json')
                .expect(400);
            expect(getExtensionsSpy).not.toHaveBeenCalled();
        });
        it('should return 400 if args is not an array', async () => {
            const agent = request.agent(app);
            const res = await agent
                .post('/executeCommand')
                .send({ command: 'extensions.list', args: 'not-an-array' })
                .set('Content-Type', 'application/json')
                .expect(400);
            expect(res.body.error).toBe('"args" field must be an array.');
            expect(getExtensionsSpy).not.toHaveBeenCalled();
        });
        it('should execute a command that does not require a workspace when CODER_AGENT_WORKSPACE_PATH is not set', async () => {
            const mockCommand = {
                name: 'test-command',
                description: 'a mock command',
                execute: vi
                    .fn()
                    .mockResolvedValue({ name: 'test-command', data: 'success' }),
            };
            vi.spyOn(commandRegistry, 'get').mockReturnValue(mockCommand);
            delete process.env['CODER_AGENT_WORKSPACE_PATH'];
            const response = await request(app)
                .post('/executeCommand')
                .send({ command: 'test-command', args: [] });
            expect(response.status).toBe(200);
            expect(response.body.data).toBe('success');
        });
        it('should return 400 for a command that requires a workspace when CODER_AGENT_WORKSPACE_PATH is not set', async () => {
            const mockWorkspaceCommand = {
                name: 'workspace-command',
                description: 'A command that requires a workspace',
                requiresWorkspace: true,
                execute: vi
                    .fn()
                    .mockResolvedValue({ name: 'workspace-command', data: 'success' }),
            };
            vi.spyOn(commandRegistry, 'get').mockReturnValue(mockWorkspaceCommand);
            delete process.env['CODER_AGENT_WORKSPACE_PATH'];
            const response = await request(app)
                .post('/executeCommand')
                .send({ command: 'workspace-command', args: [] });
            expect(response.status).toBe(400);
            expect(response.body.error).toBe('Command "workspace-command" requires a workspace, but CODER_AGENT_WORKSPACE_PATH is not set.');
        });
        it('should execute a command that requires a workspace when CODER_AGENT_WORKSPACE_PATH is set', async () => {
            const mockWorkspaceCommand = {
                name: 'workspace-command',
                description: 'A command that requires a workspace',
                requiresWorkspace: true,
                execute: vi
                    .fn()
                    .mockResolvedValue({ name: 'workspace-command', data: 'success' }),
            };
            vi.spyOn(commandRegistry, 'get').mockReturnValue(mockWorkspaceCommand);
            process.env['CODER_AGENT_WORKSPACE_PATH'] = '/tmp/test-workspace';
            const response = await request(app)
                .post('/executeCommand')
                .send({ command: 'workspace-command', args: [] });
            expect(response.status).toBe(200);
            expect(response.body.data).toBe('success');
        });
        it('should include agentExecutor in context', async () => {
            const mockCommand = {
                name: 'context-check-command',
                description: 'checks context',
                execute: vi.fn(async (context) => {
                    if (!context.agentExecutor) {
                        throw new Error('agentExecutor missing');
                    }
                    return { name: 'context-check-command', data: 'success' };
                }),
            };
            vi.spyOn(commandRegistry, 'get').mockReturnValue(mockCommand);
            const agent = request.agent(app);
            const res = await agent
                .post('/executeCommand')
                .send({ command: 'context-check-command', args: [] })
                .set('Content-Type', 'application/json')
                .expect(200);
            expect(res.body.data).toBe('success');
        });
        describe('/executeCommand streaming', () => {
            it('should execute a streaming command and stream back events', (done) => {
                const executeSpy = vi.fn(async (context) => {
                    context.eventBus?.publish({
                        kind: 'status-update',
                        status: { state: 'working' },
                        taskId: 'test-task',
                        contextId: 'test-context',
                        final: false,
                    });
                    context.eventBus?.publish({
                        kind: 'status-update',
                        status: { state: 'completed' },
                        taskId: 'test-task',
                        contextId: 'test-context',
                        final: true,
                    });
                    return { name: 'stream-test', data: 'done' };
                });
                const mockStreamCommand = {
                    name: 'stream-test',
                    description: 'A test streaming command',
                    streaming: true,
                    execute: executeSpy,
                };
                vi.spyOn(commandRegistry, 'get').mockReturnValue(mockStreamCommand);
                const agent = request.agent(app);
                agent
                    .post('/executeCommand')
                    .send({ command: 'stream-test', args: [] })
                    .set('Content-Type', 'application/json')
                    .set('Accept', 'text/event-stream')
                    .on('response', (res) => {
                    let data = '';
                    res.on('data', (chunk) => {
                        data += chunk.toString();
                    });
                    res.on('end', () => {
                        try {
                            const events = streamToSSEEvents(data);
                            expect(events.length).toBe(2);
                            expect(events[0].result).toEqual({
                                kind: 'status-update',
                                status: { state: 'working' },
                                taskId: 'test-task',
                                contextId: 'test-context',
                                final: false,
                            });
                            expect(events[1].result).toEqual({
                                kind: 'status-update',
                                status: { state: 'completed' },
                                taskId: 'test-task',
                                contextId: 'test-context',
                                final: true,
                            });
                            expect(executeSpy).toHaveBeenCalled();
                            done();
                        }
                        catch (e) {
                            done(e);
                        }
                    });
                })
                    .end();
            });
            it('should handle non-streaming commands gracefully', async () => {
                const mockNonStreamCommand = {
                    name: 'non-stream-test',
                    description: 'A test non-streaming command',
                    execute: vi
                        .fn()
                        .mockResolvedValue({ name: 'non-stream-test', data: 'done' }),
                };
                vi.spyOn(commandRegistry, 'get').mockReturnValue(mockNonStreamCommand);
                const agent = request.agent(app);
                const res = await agent
                    .post('/executeCommand')
                    .send({ command: 'non-stream-test', args: [] })
                    .set('Content-Type', 'application/json')
                    .expect(200);
                expect(res.body).toEqual({ name: 'non-stream-test', data: 'done' });
            });
        });
    });
    describe('main', () => {
        it('should listen on localhost only', async () => {
            const listenSpy = vi
                .spyOn(express.application, 'listen')
                .mockImplementation((...args) => {
                // Trigger the callback passed to listen
                const callback = args.find((arg) => typeof arg === 'function');
                if (callback) {
                    callback();
                }
                return {
                    address: () => ({ port: 1234 }),
                    on: vi.fn(),
                    once: vi.fn(),
                    emit: vi.fn(),
                };
            });
            // Avoid process.exit if possible, or mock it if main might fail
            const exitSpy = vi
                .spyOn(process, 'exit')
                .mockImplementation(() => undefined);
            await main();
            expect(listenSpy).toHaveBeenCalledWith(expect.any(Number), 'localhost', expect.any(Function));
            listenSpy.mockRestore();
            exitSpy.mockRestore();
        });
    });
});
//# sourceMappingURL=app.test.js.map