import { EventEmitter } from "node:events";
import { describe, expect, it, vi } from "vitest";
import { AcodeSdkAdapter, isFailedAcodeTurnComplete, } from "./AcodeSdkAdapter.js";
class FailedTurnRuntime extends EventEmitter {
    constructor(config) {
        super();
        this.config = config;
    }
    async start() { }
    async submit(_message) {
        this.emit("event", {
            type: "error",
            sessionId: this.config.sessionId,
            timestamp: new Date(0).toISOString(),
            data: { message: "provider down" },
        });
        this.emit("event", {
            type: "turn_complete",
            sessionId: this.config.sessionId,
            timestamp: new Date(0).toISOString(),
            data: { failed: true, usage: { inputTokens: 1, outputTokens: 0 } },
        });
    }
    async abort() { }
    async stop() { }
}
class McpStatusRuntime extends EventEmitter {
    constructor(config) {
        super();
        this.config = config;
    }
    async start() { }
    async submit(_message) {
        this.emit("event", {
            type: "mcp_status",
            sessionId: this.config.sessionId,
            timestamp: new Date(0).toISOString(),
            data: {
                action: "call_tool",
                server: "aws-mcp",
                tool: "create_memory",
                toolUseId: "tool-call-1",
            },
        });
    }
    async abort() { }
    async stop() { }
}
class BuiltinReadStatusRuntime extends EventEmitter {
    constructor(config) {
        super();
        this.config = config;
    }
    async start() { }
    async submit(_message) {
        this.emit("event", {
            type: "mcp_status",
            sessionId: this.config.sessionId,
            timestamp: new Date(0).toISOString(),
            data: {
                action: "call_tool",
                server: "builtin",
                tool: "read_file",
                exposedName: "builtin/read_file",
                toolUseId: "tool-read-1",
                arguments: { path: "src/views/App.vue" },
            },
        });
    }
    async abort() { }
    async stop() { }
}
class McpResultRuntime extends EventEmitter {
    constructor(config) {
        super();
        this.config = config;
    }
    async start() { }
    async submit(_message) {
        this.emit("event", {
            type: "mcp_status",
            sessionId: this.config.sessionId,
            timestamp: new Date(0).toISOString(),
            data: {
                action: "call_tool",
                server: "aws-mcp",
                tool: "create_memory",
                toolUseId: "tool-call-1",
            },
        });
        this.emit("event", {
            type: "mcp_status",
            sessionId: this.config.sessionId,
            timestamp: new Date(0).toISOString(),
            data: {
                action: "tool_result",
                server: "aws-mcp",
                tool: "create_memory",
                toolUseId: "tool-call-1",
                result: '{"uri":"core://agent/profile"}',
            },
        });
    }
    async abort() { }
    async stop() { }
}
class PollMessageStatusRuntime extends EventEmitter {
    constructor(config) {
        super();
        this.config = config;
    }
    async start() { }
    async submit(_message) {
        this.emit("event", {
            type: "mcp_status",
            sessionId: this.config.sessionId,
            timestamp: new Date(0).toISOString(),
            data: { action: "call_tool", server: "aws-mcp", tool: "poll_message" },
        });
    }
    async abort() { }
    async stop() { }
}
class EmptyPollResultRuntime extends EventEmitter {
    constructor(config) {
        super();
        this.config = config;
    }
    async start() { }
    async submit(_message) {
        this.emit("event", {
            type: "mcp_status",
            sessionId: this.config.sessionId,
            timestamp: new Date(0).toISOString(),
            data: { action: "call_tool", server: "aws-mcp", tool: "poll_message" },
        });
        this.emit("event", {
            type: "mcp_status",
            sessionId: this.config.sessionId,
            timestamp: new Date(0).toISOString(),
            data: {
                action: "tool_result",
                server: "aws-mcp",
                tool: "poll_message",
                result: JSON.stringify({ directMessages: [], groupMessages: [] }),
            },
        });
    }
    async abort() { }
    async stop() { }
}
class PollResultWithMessageRuntime extends EventEmitter {
    constructor(config) {
        super();
        this.config = config;
    }
    async start() { }
    async submit(_message) {
        this.emit("event", {
            type: "mcp_status",
            sessionId: this.config.sessionId,
            timestamp: new Date(0).toISOString(),
            data: { action: "call_tool", server: "aws-mcp", tool: "poll_message" },
        });
        this.emit("event", {
            type: "mcp_status",
            sessionId: this.config.sessionId,
            timestamp: new Date(0).toISOString(),
            data: {
                action: "tool_result",
                server: "aws-mcp",
                tool: "poll_message",
                result: JSON.stringify({
                    directMessages: [{ senderName: "User", content: "ping" }],
                    groupMessages: [],
                }),
            },
        });
    }
    async abort() { }
    async stop() { }
}
class CommandStatusRuntime extends EventEmitter {
    constructor(config) {
        super();
        this.config = config;
    }
    async start() { }
    async submit(_message) {
        this.emit("event", {
            type: "command_status",
            sessionId: this.config.sessionId,
            timestamp: new Date(0).toISOString(),
            data: { action: "slash_command", command: "review" },
        });
    }
    async abort() { }
    async stop() { }
}
class AiRequestRuntime extends EventEmitter {
    constructor(config) {
        super();
        this.config = config;
    }
    async start() { }
    async submit(_message) {
        this.emit("event", {
            type: "ai_request",
            sessionId: this.config.sessionId,
            timestamp: new Date(0).toISOString(),
            data: {
                model: "test-model",
                endpoint: "https://example.invalid/v1/chat/completions",
                requestMethod: "POST",
                requestUrl: "https://example.invalid/v1/chat/completions",
                requestHeaders: {
                    "Content-Type": "application/json",
                    Authorization: "Bearer ***",
                },
                stream: true,
                messageCount: 3,
                toolCount: 2,
                lastUserMessagePreview: "inspect status",
                estimatedContextTokens: 6800,
                requestBody: '{"model":"test-model"}',
            },
        });
    }
    async abort() { }
    async stop() { }
}
class ThinkingDeltaRuntime extends EventEmitter {
    constructor(config) {
        super();
        this.config = config;
    }
    async start() { }
    async submit(_message) {
        this.emit("event", {
            type: "thinking_delta",
            sessionId: this.config.sessionId,
            timestamp: new Date(0).toISOString(),
            data: { text: "reasoning chunk" },
        });
    }
    async abort() { }
    async stop() { }
}
class AssistantDeltaRuntime extends EventEmitter {
    constructor(config) {
        super();
        this.config = config;
    }
    async start() { }
    async submit(_message) {
        this.emit("event", {
            type: "assistant_delta",
            sessionId: this.config.sessionId,
            timestamp: new Date(0).toISOString(),
            data: { text: "answer chunk" },
        });
    }
    async abort() { }
    async stop() { }
}
class CommandOutputRuntime extends EventEmitter {
    constructor(config) {
        super();
        this.config = config;
    }
    async start() { }
    async submit(_message) {
        this.emit("event", {
            type: "command_output",
            sessionId: this.config.sessionId,
            timestamp: new Date(0).toISOString(),
            data: {
                text: "live command output",
                stream: "stdout",
                sequence: 2,
                cwd: "/repo",
                commandLineCount: 1,
                commandFilePath: "/repo/.agentswork/acode/cmd-dist/run.cmd",
            },
        });
    }
    async abort() { }
    async stop() { }
}
class ResponseBodyTurnCompleteRuntime extends EventEmitter {
    constructor(config) {
        super();
        this.config = config;
    }
    async start() { }
    async submit(_message) {
        this.emit("event", {
            type: "turn_complete",
            sessionId: this.config.sessionId,
            timestamp: new Date(0).toISOString(),
            data: {
                usage: { inputTokens: 9, outputTokens: 3 },
                responseBody: '{"choices":[{"message":{"content":"done"}}]}',
                finishReason: "stop",
            },
        });
    }
    async abort() { }
    async stop() { }
}
class ProviderUsageTurnCompleteRuntime extends EventEmitter {
    constructor(config) {
        super();
        this.config = config;
    }
    async start() { }
    async submit(_message) {
        this.emit("event", {
            type: "turn_complete",
            sessionId: this.config.sessionId,
            timestamp: new Date(0).toISOString(),
            data: {
                usage: { prompt_tokens: 31, completion_tokens: 58 },
            },
        });
    }
    async abort() { }
    async stop() { }
}
class ResettableRuntime extends EventEmitter {
    constructor(config) {
        super();
        this.config = config;
    }
    async start() { }
    async submit(_message) {
        this.emit("event", {
            type: "assistant_delta",
            sessionId: this.config.sessionId,
            timestamp: new Date(0).toISOString(),
            data: { text: "done" },
        });
        this.emit("event", {
            type: "turn_complete",
            sessionId: this.config.sessionId,
            timestamp: new Date(0).toISOString(),
            data: { usage: { inputTokens: 7, outputTokens: 3 } },
        });
    }
    async resetContext() {
        ResettableRuntime.resetCalls += 1;
    }
    async abort() { }
    async stop() {
        ResettableRuntime.stopCalls += 1;
    }
}
ResettableRuntime.resetCalls = 0;
ResettableRuntime.stopCalls = 0;
class ThinkingThenAssistantDeltaRuntime extends EventEmitter {
    constructor(config) {
        super();
        this.config = config;
    }
    async start() { }
    async submit(_message) {
        this.emit("event", {
            type: "thinking_delta",
            sessionId: this.config.sessionId,
            timestamp: new Date(0).toISOString(),
            data: { text: "thinking chunk" },
        });
        this.emit("event", {
            type: "assistant_delta",
            sessionId: this.config.sessionId,
            timestamp: new Date(1).toISOString(),
            data: { text: "answer chunk" },
        });
    }
    async abort() { }
    async stop() { }
}
class McpThenSessionCompleteRuntime extends EventEmitter {
    constructor(config) {
        super();
        this.config = config;
    }
    async start() { }
    async submit(_message) {
        this.emit("event", {
            type: "mcp_status",
            sessionId: this.config.sessionId,
            timestamp: new Date(0).toISOString(),
            data: { action: "call_tool", server: "aws-mcp", tool: "send_private" },
        });
        this.emit("event", {
            type: "session_complete",
            sessionId: this.config.sessionId,
            timestamp: new Date(0).toISOString(),
            data: { exitCode: 0 },
        });
    }
    async abort() { }
    async stop() { }
}
class DeferredInitialPromptRuntime extends EventEmitter {
    constructor(config) {
        super();
        DeferredInitialPromptRuntime.receivedInitialPrompt = config.initialPrompt;
    }
    async start() { }
    async submit(_message) {
        DeferredInitialPromptRuntime.submitCalls += 1;
        await new Promise(() => undefined);
    }
    async abort() { }
    async stop() { }
}
DeferredInitialPromptRuntime.submitCalls = 0;
class AutoPollRuntime extends EventEmitter {
    constructor(config) {
        super();
        this.config = config;
    }
    async start() { }
    async submit(message) {
        AutoPollRuntime.submitMessages.push(message);
        if (AutoPollRuntime.submitMessages.length === 1) {
            this.emit("event", {
                type: "turn_complete",
                sessionId: this.config.sessionId,
                timestamp: new Date(0).toISOString(),
                data: { usage: { inputTokens: 1, outputTokens: 1 } },
            });
        }
    }
    async abort() { }
    async stop() { }
}
AutoPollRuntime.submitMessages = [];
class NonPollingAutoPollRuntime extends EventEmitter {
    constructor(config) {
        super();
        this.config = config;
    }
    async start() { }
    async submit(message) {
        NonPollingAutoPollRuntime.submitMessages.push(message);
        this.emit("event", {
            type: "assistant_delta",
            sessionId: this.config.sessionId,
            timestamp: new Date(0).toISOString(),
            data: { text: "I cannot call poll_message." },
        });
        this.emit("event", {
            type: "turn_complete",
            sessionId: this.config.sessionId,
            timestamp: new Date(0).toISOString(),
            data: { usage: { inputTokens: 1, outputTokens: 1 } },
        });
    }
    async abort() { }
    async stop() { }
}
NonPollingAutoPollRuntime.submitMessages = [];
class PollingAutoPollRuntime extends EventEmitter {
    constructor(config) {
        super();
        this.config = config;
    }
    async start() { }
    async submit(message) {
        PollingAutoPollRuntime.submitMessages.push(message);
        this.emit("event", {
            type: "mcp_status",
            sessionId: this.config.sessionId,
            timestamp: new Date(0).toISOString(),
            data: { action: "call_tool", server: "aws-mcp", tool: "poll_message" },
        });
        this.emit("event", {
            type: "turn_complete",
            sessionId: this.config.sessionId,
            timestamp: new Date(0).toISOString(),
            data: { usage: { inputTokens: 1, outputTokens: 1 } },
        });
    }
    async abort() { }
    async stop() { }
}
PollingAutoPollRuntime.submitMessages = [];
class TerminalAfterTurnRuntime extends EventEmitter {
    constructor(config) {
        super();
        this.config = config;
    }
    async start() { }
    async submit(message) {
        TerminalAfterTurnRuntime.submitMessages.push(message);
        this.emit("event", {
            type: "turn_complete",
            sessionId: this.config.sessionId,
            timestamp: new Date(0).toISOString(),
            data: { usage: { inputTokens: 1, outputTokens: 1 } },
        });
        this.emit("event", {
            type: "session_complete",
            sessionId: this.config.sessionId,
            timestamp: new Date(0).toISOString(),
            data: { exitCode: 0 },
        });
    }
    async abort() { }
    async stop() { }
}
TerminalAfterTurnRuntime.submitMessages = [];
class FailingAutoPollRuntime extends EventEmitter {
    constructor(config) {
        super();
        this.config = config;
    }
    async start() { }
    async submit(message) {
        FailingAutoPollRuntime.submitMessages.push(message);
        this.emit("event", {
            type: "error",
            sessionId: this.config.sessionId,
            timestamp: new Date(0).toISOString(),
            data: { message: "mcp tool timeout" },
        });
        this.emit("event", {
            type: "turn_complete",
            sessionId: this.config.sessionId,
            timestamp: new Date(0).toISOString(),
            data: { failed: true, usage: { inputTokens: 1, outputTokens: 0 } },
        });
    }
    async abort() { }
    async stop() { }
}
FailingAutoPollRuntime.submitMessages = [];
class ResolvedWithoutTerminalEventRuntime extends EventEmitter {
    constructor(config) {
        super();
        this.config = config;
    }
    async start() { }
    async submit(message) {
        ResolvedWithoutTerminalEventRuntime.submitMessages.push(message);
        this.emit("event", {
            type: "command_status",
            sessionId: this.config.sessionId,
            timestamp: new Date(0).toISOString(),
            data: { action: "slash_command", command: "review" },
        });
    }
    async abort() { }
    async stop() { }
}
ResolvedWithoutTerminalEventRuntime.submitMessages = [];
class BareErrorRuntime extends EventEmitter {
    constructor(config) {
        super();
        this.config = config;
    }
    async start() { }
    async submit(message) {
        BareErrorRuntime.submitMessages.push(message);
        this.emit("event", {
            type: "error",
            sessionId: this.config.sessionId,
            timestamp: new Date(0).toISOString(),
            data: { message: "provider emitted bare error" },
        });
    }
    async abort() { }
    async stop() { }
}
BareErrorRuntime.submitMessages = [];
class RejectingSubmitRuntime extends EventEmitter {
    constructor(_config) {
        super();
    }
    async start() { }
    async submit(message) {
        RejectingSubmitRuntime.submitMessages.push(message);
        throw new Error("submit rejected before turn_complete");
    }
    async abort() { }
    async stop() { }
}
RejectingSubmitRuntime.submitMessages = [];
class StartFailureRuntime extends EventEmitter {
    constructor(_config) {
        super();
    }
    async start() {
        throw new Error("runtime start failed");
    }
    async submit(_message) { }
    async abort() { }
    async stop() { }
}
class FailingPollThenSuccessfulUserRuntime extends EventEmitter {
    constructor(config) {
        super();
        this.config = config;
    }
    async start() { }
    async submit(message) {
        FailingPollThenSuccessfulUserRuntime.submitMessages.push(message);
        if (message.includes("poll_message")) {
            this.emit("event", {
                type: "error",
                sessionId: this.config.sessionId,
                timestamp: new Date(0).toISOString(),
                data: { message: "poll failed once" },
            });
            this.emit("event", {
                type: "turn_complete",
                sessionId: this.config.sessionId,
                timestamp: new Date(0).toISOString(),
                data: { failed: true, usage: { inputTokens: 1, outputTokens: 0 } },
            });
            return;
        }
        this.emit("event", {
            type: "turn_complete",
            sessionId: this.config.sessionId,
            timestamp: new Date(0).toISOString(),
            data: { usage: { inputTokens: 1, outputTokens: 1 } },
        });
        this.emit("event", {
            type: "session_complete",
            sessionId: this.config.sessionId,
            timestamp: new Date(0).toISOString(),
            data: { exitCode: 0 },
        });
    }
    async abort() { }
    async stop() { }
}
FailingPollThenSuccessfulUserRuntime.submitMessages = [];
class StartupMcpStatusRuntime extends EventEmitter {
    constructor(config) {
        super();
        this.config = config;
    }
    async start() {
        // Mirrors the startup mcp_status event emitted by ACode's
        // connectMcpServers before any user interaction. The R3 check in
        // AcodeSdkAdapter validates the `tools` array (the tools the LLM
        // can actually call) rather than the `servers` config keys, so
        // the runtime must emit the tools form here to exercise that path.
        this.emit("event", {
            type: "mcp_status",
            sessionId: this.config.sessionId,
            timestamp: new Date(0).toISOString(),
            data: {
                tools: StartupMcpStatusRuntime.includeAwsMcp
                    ? [{ name: "aws-mcp__poll_message", source: "aws-mcp.poll_message" }]
                    : [{ name: "other-mcp__other_tool", source: "other-mcp.other_tool" }],
            },
        });
    }
    async submit(_message) { }
    async abort() { }
    async stop() { }
}
// When true (default) the emitted startup mcp_status event includes a
// tool named 'aws-mcp__poll_message', mirroring the happy-path runtime
// configuration. When false, the tools list intentionally omits
// poll_message so the R3 check is forced to take the warning path.
StartupMcpStatusRuntime.includeAwsMcp = true;
class DifferentlyNamedMcpRuntime extends EventEmitter {
    // Reproduces the bug-report scenario: the user's MCP is configured
    // under a non-default name ('aws-mcp-server') and the runtime
    // reports that name in the legacy `servers` field, but the LLM-
    // visible tools are still prefixed with the canonical 'aws-mcp__'.
    // The R3 check must validate against the tools (not the server
    // names) so this configuration is accepted as healthy.
    constructor(config) {
        super();
        this.config = config;
    }
    async start() {
        this.emit("event", {
            type: "mcp_status",
            sessionId: this.config.sessionId,
            timestamp: new Date(0).toISOString(),
            data: {
                servers: ["aws-mcp-server"],
                tools: [
                    { name: "aws-mcp__poll_message", source: "aws-mcp.poll_message" },
                    {
                        name: "aws-mcp__submit_response",
                        source: "aws-mcp.submit_response",
                    },
                ],
            },
        });
    }
    async submit(_message) { }
    async abort() { }
    async stop() { }
}
class FakePollMessageToolRuntime extends EventEmitter {
    constructor(config) {
        super();
        this.config = config;
    }
    async start() {
        this.emit("event", {
            type: "mcp_status",
            sessionId: this.config.sessionId,
            timestamp: new Date(0).toISOString(),
            data: {
                tools: [
                    {
                        name: "other-mcp__not_poll_message",
                        source: "other-mcp.not_poll_message",
                    },
                ],
            },
        });
    }
    async submit(_message) { }
    async abort() { }
    async stop() { }
}
class DeferredInitialThenRegularIdleRuntime extends EventEmitter {
    constructor(config) {
        super();
        this.config = config;
    }
    async start() { }
    async submit(message) {
        DeferredInitialThenRegularIdleRuntime.submitMessages.push(message);
        if (DeferredInitialThenRegularIdleRuntime.submitMessages.length !== 1) {
            return;
        }
        await new Promise((resolve) => {
            DeferredInitialThenRegularIdleRuntime.completeInitialTurn = () => {
                this.emit("event", {
                    type: "turn_complete",
                    sessionId: this.config.sessionId,
                    timestamp: new Date(0).toISOString(),
                    data: { usage: { inputTokens: 1, outputTokens: 1 } },
                });
                DeferredInitialThenRegularIdleRuntime.completeInitialTurn = undefined;
                resolve();
            };
        });
    }
    async abort() { }
    async stop() { }
}
DeferredInitialThenRegularIdleRuntime.submitMessages = [];
describe("AcodeSdkAdapter turn completion state", () => {
    it("keeps error status for failed turn_complete events", () => {
        expect(isFailedAcodeTurnComplete({ failed: true })).toBe(true);
        expect(isFailedAcodeTurnComplete({ failed: false })).toBe(false);
        expect(isFailedAcodeTurnComplete({ usage: { inputTokens: 1, outputTokens: 2 } })).toBe(false);
    });
    it("recovers failed turn_complete to waiting_input so the agent never stays stuck in error (R4)", async () => {
        const adapter = new AcodeSdkAdapter();
        adapter.setRuntimeConstructorForTest(FailedTurnRuntime);
        const events = [];
        const statuses = [];
        adapter.on("event", (event) => events.push(event));
        adapter.on("status-change", (_sessionId, status) => statuses.push(status));
        await adapter.startSession("session-failed", {
            command: "acode",
            workingDirectory: process.cwd(),
            autoAccept: true,
            extraMcpServers: {},
            envOverrides: {},
        });
        await adapter.sendMessage("session-failed", "fail please");
        expect(events.map((event) => event.type)).toEqual(["error"]);
        // R4: the session briefly enters 'error' but then recovers to 'waiting_input'
        // so the agent can retry via the idle poll loop instead of being stuck forever.
        expect(statuses).toEqual([
            "starting",
            "waiting_input",
            "thinking",
            "error",
            "waiting_input",
        ]);
        expect(adapter.getSessionStatus("session-failed")).toBe("waiting_input");
    });
    it("forwards ACode turn_complete response metadata", async () => {
        const adapter = new AcodeSdkAdapter();
        adapter.setRuntimeConstructorForTest(ResponseBodyTurnCompleteRuntime);
        const events = [];
        const statuses = [];
        adapter.on("event", (event) => events.push(event));
        adapter.on("status-change", (_sessionId, status, actionInfo) => {
            statuses.push({ status, usage: actionInfo?.usage });
        });
        await adapter.startSession("session-response-body", {
            command: "acode",
            workingDirectory: process.cwd(),
            autoAccept: true,
            extraMcpServers: {},
            envOverrides: {},
        });
        await adapter.sendMessage("session-response-body", "answer");
        expect(events).toHaveLength(1);
        expect(events[0]).toMatchObject({
            type: "turn_complete",
            data: {
                usage: { inputTokens: 9, outputTokens: 3 },
                responseBody: '{"choices":[{"message":{"content":"done"}}]}',
                finishReason: "stop",
            },
        });
    });
    it("normalizes provider-native ACode usage fields for context display", async () => {
        const adapter = new AcodeSdkAdapter();
        adapter.setRuntimeConstructorForTest(ProviderUsageTurnCompleteRuntime);
        const events = [];
        const statuses = [];
        adapter.on("event", (event) => events.push(event));
        adapter.on("status-change", (_sessionId, status, actionInfo) => {
            statuses.push({ status, usage: actionInfo?.usage });
        });
        await adapter.startSession("session-provider-usage", {
            command: "acode",
            workingDirectory: process.cwd(),
            autoAccept: true,
            extraMcpServers: {},
            envOverrides: {},
        });
        await adapter.sendMessage("session-provider-usage", "answer");
        expect(events).toHaveLength(1);
        expect(events[0]).toMatchObject({
            type: "turn_complete",
            data: { usage: { inputTokens: 31, outputTokens: 58 } },
        });
        expect(statuses.at(-1)).toEqual({
            status: "waiting_input",
            usage: { inputTokens: 31, outputTokens: 58 },
        });
    });
    it("resets ACode context in place without terminating the SDK session", async () => {
        vi.useFakeTimers();
        ResettableRuntime.resetCalls = 0;
        ResettableRuntime.stopCalls = 0;
        const adapter = new AcodeSdkAdapter();
        adapter.setRuntimeConstructorForTest(ResettableRuntime);
        const statuses = [];
        adapter.on("status-change", (_sessionId, status, actionInfo) => {
            statuses.push({ status, usage: actionInfo?.usage });
        });
        try {
            await adapter.startSession("session-reset-context", {
                command: "acode",
                workingDirectory: process.cwd(),
                autoAccept: true,
                extraMcpServers: {},
                envOverrides: {},
            });
            await adapter.sendMessage("session-reset-context", "finish work");
            expect(adapter.getConversation("session-reset-context")).toHaveLength(2);
            expect(statuses.at(-1)).toEqual({
                status: "waiting_input",
                usage: { inputTokens: 7, outputTokens: 3 },
            });
            await adapter.resetContext("session-reset-context");
            expect(ResettableRuntime.resetCalls).toBe(1);
            expect(ResettableRuntime.stopCalls).toBe(0);
            expect(adapter.hasSession("session-reset-context")).toBe(true);
            expect(adapter.getSessionStatus("session-reset-context")).toBe("waiting_input");
            expect(adapter.getConversation("session-reset-context")).toEqual([]);
            expect(statuses.at(-1)).toEqual({
                status: "waiting_input",
                usage: { inputTokens: 0, outputTokens: 0 },
            });
        }
        finally {
            vi.useRealTimers();
            adapter.cleanup();
        }
    });
    it("maps ACode MCP status events to tool_using instead of thinking", async () => {
        const adapter = new AcodeSdkAdapter();
        adapter.setRuntimeConstructorForTest(McpStatusRuntime);
        const events = [];
        const statuses = [];
        adapter.on("event", (event) => events.push(event));
        adapter.on("status-change", (_sessionId, status, actionInfo) => {
            statuses.push({
                status,
                actionType: actionInfo?.actionType,
                actionDetail: actionInfo?.actionDetail,
            });
        });
        await adapter.startSession("session-mcp", {
            command: "acode",
            workingDirectory: process.cwd(),
            autoAccept: true,
            extraMcpServers: {},
            envOverrides: {},
        });
        await adapter.sendMessage("session-mcp", "call mcp");
        expect(events.map((event) => event.type)).toEqual(["tool_use_start"]);
        expect(events[0]?.data).toMatchObject({
            actionType: "mcp",
            actionLabel: "调用MCP",
            actionDetail: "aws-mcp/create_memory",
            actionId: "tool-call-1",
            toolUseId: "tool-call-1",
        });
        expect(statuses.at(-2)).toEqual({
            status: "tool_using",
            actionType: "mcp",
            actionDetail: "aws-mcp/create_memory",
        });
        expect(statuses.at(-1)).toEqual({
            status: "waiting_input",
            actionType: undefined,
            actionDetail: undefined,
        });
        expect(adapter.getSessionStatus("session-mcp")).toBe("waiting_input");
    });
    it("preserves built-in file tool arguments in ACode provider events and statuses", async () => {
        const adapter = new AcodeSdkAdapter();
        adapter.setRuntimeConstructorForTest(BuiltinReadStatusRuntime);
        const events = [];
        const statuses = [];
        adapter.on("event", (event) => events.push(event));
        adapter.on("status-change", (_sessionId, status, actionInfo) => {
            statuses.push({
                status,
                actionType: actionInfo?.actionType,
                actionDetail: actionInfo?.actionDetail,
            });
        });
        await adapter.startSession("session-builtin-read", {
            command: "acode",
            workingDirectory: process.cwd(),
            autoAccept: true,
            extraMcpServers: {},
            envOverrides: {},
        });
        await adapter.sendMessage("session-builtin-read", "read file");
        expect(events.map((event) => event.type)).toEqual(["tool_use_start"]);
        expect(events[0]?.data).toMatchObject({
            actionType: "read_file",
            actionLabel: "读取文件",
            actionDetail: "src/views/App.vue",
            actionId: "tool-read-1",
            toolUseId: "tool-read-1",
            toolInput: { path: "src/views/App.vue" },
        });
        expect(statuses.at(-2)).toEqual({
            status: "tool_using",
            actionType: "read_file",
            actionDetail: "src/views/App.vue",
        });
    });
    it("clears active MCP tool status when ACode reports a tool result", async () => {
        const adapter = new AcodeSdkAdapter();
        adapter.setRuntimeConstructorForTest(McpResultRuntime);
        const events = [];
        const statuses = [];
        adapter.on("event", (event) => events.push(event));
        adapter.on("status-change", (_sessionId, status, actionInfo) => {
            statuses.push({
                status,
                actionType: actionInfo?.actionType,
                actionDetail: actionInfo?.actionDetail,
            });
        });
        await adapter.startSession("session-mcp-result", {
            command: "acode",
            workingDirectory: process.cwd(),
            autoAccept: true,
            extraMcpServers: {},
            envOverrides: {},
        });
        await adapter.sendMessage("session-mcp-result", "remember profile");
        expect(events.map((event) => event.type)).toEqual([
            "tool_use_start",
            "tool_use_end",
        ]);
        expect(events[1]?.data).toMatchObject({
            actionType: "mcp",
            actionLabel: "调用MCP",
            actionDetail: "aws-mcp/create_memory",
            actionId: "tool-call-1",
            toolUseId: "tool-call-1",
            toolResult: '{"uri":"core://agent/profile"}',
        });
        expect(statuses.at(-2)).toEqual({
            status: "thinking",
            actionType: undefined,
            actionDetail: undefined,
        });
        expect(statuses.at(-1)).toEqual({
            status: "waiting_input",
            actionType: undefined,
            actionDetail: undefined,
        });
        expect(adapter.getSessionStatus("session-mcp-result")).toBe("waiting_input");
    });
    it("maps ACode poll_message MCP status events to waiting_input idle state", async () => {
        const adapter = new AcodeSdkAdapter();
        adapter.setRuntimeConstructorForTest(PollMessageStatusRuntime);
        const events = [];
        const statuses = [];
        adapter.on("event", (event) => events.push(event));
        adapter.on("status-change", (_sessionId, status, actionInfo) => {
            statuses.push({
                status,
                actionType: actionInfo?.actionType,
                actionDetail: actionInfo?.actionDetail,
            });
        });
        await adapter.startSession("session-poll-message", {
            command: "acode",
            workingDirectory: process.cwd(),
            autoAccept: true,
            extraMcpServers: {},
            envOverrides: {},
        });
        await adapter.sendMessage("session-poll-message", "listen");
        expect(events.map((event) => event.type)).toEqual(["tool_use_start"]);
        expect(events[0]?.data).toMatchObject({
            actionType: "idle",
            actionLabel: "监听消息",
            actionDetail: "aws-mcp/poll_message",
        });
        expect(statuses.at(-1)).toEqual({
            status: "waiting_input",
            actionType: "idle",
            actionDetail: "aws-mcp/poll_message",
        });
        expect(adapter.getSessionStatus("session-poll-message")).toBe("waiting_input");
    });
    it("keeps empty poll_message results in waiting_input instead of showing stuck thinking", async () => {
        const adapter = new AcodeSdkAdapter();
        adapter.setRuntimeConstructorForTest(EmptyPollResultRuntime);
        const events = [];
        const statuses = [];
        adapter.on("event", (event) => events.push(event));
        adapter.on("status-change", (_sessionId, status) => statuses.push(status));
        await adapter.startSession("session-empty-poll-result", {
            command: "acode",
            workingDirectory: process.cwd(),
            autoAccept: true,
            extraMcpServers: {},
            envOverrides: {},
        });
        await adapter.sendMessage("session-empty-poll-result", "listen");
        expect(events.map((event) => event.type)).toEqual([
            "tool_use_start",
            "tool_use_end",
        ]);
        expect(statuses).toEqual([
            "starting",
            "waiting_input",
            "thinking",
            "waiting_input",
            "waiting_input",
        ]);
        expect(adapter.getSessionStatus("session-empty-poll-result")).toBe("waiting_input");
    });
    it("shows thinking after poll_message returns actual messages to process", async () => {
        const adapter = new AcodeSdkAdapter();
        adapter.setRuntimeConstructorForTest(PollResultWithMessageRuntime);
        const events = [];
        const statuses = [];
        adapter.on("event", (event) => events.push(event));
        adapter.on("status-change", (_sessionId, status) => statuses.push(status));
        await adapter.startSession("session-poll-result-with-message", {
            command: "acode",
            workingDirectory: process.cwd(),
            autoAccept: true,
            extraMcpServers: {},
            envOverrides: {},
        });
        await adapter.sendMessage("session-poll-result-with-message", "listen");
        expect(events.map((event) => event.type)).toEqual([
            "tool_use_start",
            "tool_use_end",
        ]);
        expect(statuses).toEqual([
            "starting",
            "waiting_input",
            "thinking",
            "waiting_input",
            "thinking",
            "waiting_input",
        ]);
        expect(adapter.getSessionStatus("session-poll-result-with-message")).toBe("waiting_input");
    });
    it("forwards ACode command status as thinking without changing session status", async () => {
        const adapter = new AcodeSdkAdapter();
        adapter.setRuntimeConstructorForTest(CommandStatusRuntime);
        const events = [];
        const statuses = [];
        adapter.on("event", (event) => events.push(event));
        adapter.on("status-change", (_sessionId, status) => statuses.push(status));
        await adapter.startSession("session-command-status", {
            command: "acode",
            workingDirectory: process.cwd(),
            autoAccept: true,
            extraMcpServers: {},
            envOverrides: {},
        });
        await adapter.sendMessage("session-command-status", "/review src");
        expect(events).toHaveLength(1);
        expect(events[0]?.type).toBe("thinking");
        expect(String(events[0]?.data.text)).toContain("slash_command");
        expect(statuses).toEqual([
            "starting",
            "waiting_input",
            "thinking",
            "waiting_input",
        ]);
        expect(adapter.getSessionStatus("session-command-status")).toBe("waiting_input");
    });
    it("forwards ACode AI request summaries as provider events", async () => {
        const adapter = new AcodeSdkAdapter();
        adapter.setRuntimeConstructorForTest(AiRequestRuntime);
        const events = [];
        const statuses = [];
        adapter.on("event", (event) => events.push(event));
        adapter.on("status-change", (_sessionId, status, actionInfo) => {
            statuses.push({ status, usage: actionInfo?.usage });
        });
        await adapter.startSession("session-ai-request", {
            command: "acode",
            workingDirectory: process.cwd(),
            autoAccept: true,
            extraMcpServers: {},
            envOverrides: {},
        });
        await adapter.sendMessage("session-ai-request", "inspect status");
        expect(events).toHaveLength(1);
        expect(events[0]).toMatchObject({
            type: "ai_request",
            data: {
                model: "test-model",
                endpoint: "https://example.invalid/v1/chat/completions",
                requestMethod: "POST",
                requestUrl: "https://example.invalid/v1/chat/completions",
                requestHeaders: {
                    "Content-Type": "application/json",
                    Authorization: "Bearer ***",
                },
                stream: true,
                messageCount: 3,
                toolCount: 2,
                lastUserMessagePreview: "inspect status",
                requestBody: '{"model":"test-model"}',
            },
        });
        expect(statuses).toContainEqual({
            status: "thinking",
            usage: { inputTokens: 6800, outputTokens: 0 },
        });
    });
    it("forwards ACode thinking deltas as realtime thinking provider events", async () => {
        const adapter = new AcodeSdkAdapter();
        adapter.setRuntimeConstructorForTest(ThinkingDeltaRuntime);
        const events = [];
        adapter.on("event", (event) => events.push(event));
        await adapter.startSession("session-thinking-delta", {
            command: "acode",
            workingDirectory: process.cwd(),
            autoAccept: true,
            extraMcpServers: {},
            envOverrides: {},
        });
        await adapter.sendMessage("session-thinking-delta", "think aloud");
        expect(events).toHaveLength(1);
        expect(events[0]).toMatchObject({
            type: "thinking",
            data: { text: "reasoning chunk" },
        });
    });
    it("forwards ACode assistant deltas as realtime text provider events", async () => {
        const adapter = new AcodeSdkAdapter();
        adapter.setRuntimeConstructorForTest(AssistantDeltaRuntime);
        const events = [];
        adapter.on("event", (event) => events.push(event));
        await adapter.startSession("session-assistant-delta", {
            command: "acode",
            workingDirectory: process.cwd(),
            autoAccept: true,
            extraMcpServers: {},
            envOverrides: {},
        });
        await adapter.sendMessage("session-assistant-delta", "answer now");
        expect(events).toHaveLength(1);
        expect(events[0]).toMatchObject({
            type: "text_delta",
            data: { text: "answer chunk" },
        });
    });
    it("forwards ACode command output as ordered realtime provider events", async () => {
        const adapter = new AcodeSdkAdapter();
        adapter.setRuntimeConstructorForTest(CommandOutputRuntime);
        const events = [];
        adapter.on("event", (event) => events.push(event));
        await adapter.startSession("session-command-output", {
            command: "acode",
            workingDirectory: process.cwd(),
            autoAccept: true,
            extraMcpServers: {},
            envOverrides: {},
        });
        await adapter.sendMessage("session-command-output", "run command");
        expect(events).toHaveLength(1);
        expect(events[0]).toMatchObject({
            type: "command_output",
            data: {
                text: "live command output",
                stream: "stdout",
                sequence: 2,
                cwd: "/repo",
                commandLineCount: 1,
                commandFilePath: "/repo/.agentswork/acode/cmd-dist/run.cmd",
            },
        });
    });
    it("preserves realtime thinking then answer delta order", async () => {
        const adapter = new AcodeSdkAdapter();
        adapter.setRuntimeConstructorForTest(ThinkingThenAssistantDeltaRuntime);
        const events = [];
        adapter.on("event", (event) => events.push(event));
        await adapter.startSession("session-thinking-answer-order", {
            command: "acode",
            workingDirectory: process.cwd(),
            autoAccept: true,
            extraMcpServers: {},
            envOverrides: {},
        });
        await adapter.sendMessage("session-thinking-answer-order", "think then answer");
        expect(events.map((event) => event.type)).toEqual([
            "thinking",
            "text_delta",
        ]);
        expect(events.map((event) => event.data.text)).toEqual([
            "thinking chunk",
            "answer chunk",
        ]);
    });
    it("recovers to waiting_input when ACode submit resolves without a terminal turn event", async () => {
        ResolvedWithoutTerminalEventRuntime.submitMessages = [];
        const adapter = new AcodeSdkAdapter();
        adapter.setRuntimeConstructorForTest(ResolvedWithoutTerminalEventRuntime);
        const statuses = [];
        adapter.on("status-change", (_sessionId, status) => statuses.push(status));
        await adapter.startSession("session-resolved-no-terminal", {
            command: "acode",
            workingDirectory: process.cwd(),
            autoAccept: true,
            extraMcpServers: {},
            envOverrides: {},
        });
        await adapter.sendMessage("session-resolved-no-terminal", "/review src");
        expect(ResolvedWithoutTerminalEventRuntime.submitMessages).toEqual([
            "/review src",
        ]);
        expect(statuses).toEqual([
            "starting",
            "waiting_input",
            "thinking",
            "waiting_input",
        ]);
        expect(adapter.getSessionStatus("session-resolved-no-terminal")).toBe("waiting_input");
    });
    it("forwards session_complete status after MCP calls so UI can clear active tool state", async () => {
        const adapter = new AcodeSdkAdapter();
        adapter.setRuntimeConstructorForTest(McpThenSessionCompleteRuntime);
        const events = [];
        const statuses = [];
        adapter.on("event", (event) => events.push(event));
        adapter.on("status-change", (_sessionId, status, actionInfo) => {
            statuses.push({
                status,
                actionType: actionInfo?.actionType,
                actionDetail: actionInfo?.actionDetail,
            });
        });
        await adapter.startSession("session-mcp-complete", {
            command: "acode",
            workingDirectory: process.cwd(),
            autoAccept: true,
            extraMcpServers: {},
            envOverrides: {},
        });
        await adapter.sendMessage("session-mcp-complete", "send private");
        expect(events.map((event) => event.type)).toEqual([
            "tool_use_start",
            "session_complete",
        ]);
        expect(statuses.at(-2)).toEqual({
            status: "tool_using",
            actionType: "mcp",
            actionDetail: "aws-mcp/send_private",
        });
        expect(statuses.at(-1)).toEqual({
            status: "completed",
            actionType: undefined,
            actionDetail: undefined,
        });
        expect(adapter.getSessionStatus("session-mcp-complete")).toBe("completed");
    });
    it("does not block launch completion while the initial prompt runs in the background", async () => {
        DeferredInitialPromptRuntime.submitCalls = 0;
        DeferredInitialPromptRuntime.receivedInitialPrompt = undefined;
        const adapter = new AcodeSdkAdapter();
        adapter.setRuntimeConstructorForTest(DeferredInitialPromptRuntime);
        const statuses = [];
        adapter.on("status-change", (_sessionId, status) => statuses.push(status));
        await adapter.startSession("session-initial-prompt", {
            command: "acode",
            workingDirectory: process.cwd(),
            autoAccept: true,
            initialPrompt: "start working loop",
            extraMcpServers: {},
            envOverrides: {},
        });
        expect(DeferredInitialPromptRuntime.receivedInitialPrompt).toBeUndefined();
        expect(DeferredInitialPromptRuntime.submitCalls).toBe(1);
        expect(statuses).toEqual(["starting", "waiting_input", "thinking"]);
        expect(adapter.getSessionStatus("session-initial-prompt")).toBe("thinking");
    });
    it("runs regular idle commands configured while ACode is still busy", async () => {
        vi.useFakeTimers();
        DeferredInitialThenRegularIdleRuntime.submitMessages = [];
        DeferredInitialThenRegularIdleRuntime.completeInitialTurn = undefined;
        const adapter = new AcodeSdkAdapter();
        adapter.setRuntimeConstructorForTest(DeferredInitialThenRegularIdleRuntime);
        try {
            await adapter.startSession("session-regular-idle-busy-config", {
                command: "acode",
                workingDirectory: process.cwd(),
                autoAccept: true,
                initialPrompt: "bootstrap current task",
                extraMcpServers: {},
                envOverrides: {},
            });
            expect(DeferredInitialThenRegularIdleRuntime.submitMessages).toEqual([
                "bootstrap current task",
            ]);
            expect(adapter.getSessionStatus("session-regular-idle-busy-config")).toBe("thinking");
            adapter.setIdleCommands("session-regular-idle-busy-config", {
                idleInputCommand: "continue after idle",
                nonInputCommand: "",
            });
            const completeInitialTurn = DeferredInitialThenRegularIdleRuntime.completeInitialTurn;
            expect(completeInitialTurn).toBeDefined();
            completeInitialTurn?.();
            await vi.advanceTimersByTimeAsync(1500);
            expect(DeferredInitialThenRegularIdleRuntime.submitMessages).toEqual([
                "bootstrap current task",
                "continue after idle",
            ]);
        }
        finally {
            vi.useRealTimers();
            adapter.cleanup();
        }
    });
    it("continues the message loop by auto-submitting poll_message after a completed turn", async () => {
        vi.useFakeTimers();
        AutoPollRuntime.submitMessages = [];
        const adapter = new AcodeSdkAdapter();
        adapter.setRuntimeConstructorForTest(AutoPollRuntime);
        const statuses = [];
        adapter.on("status-change", (_sessionId, status) => statuses.push(status));
        try {
            await adapter.startSession("session-auto-poll", {
                command: "acode",
                workingDirectory: process.cwd(),
                autoAccept: true,
                extraMcpServers: {},
                envOverrides: {},
            });
            await adapter.sendMessage("session-auto-poll", "handle current work");
            await vi.advanceTimersByTimeAsync(250);
            expect(AutoPollRuntime.submitMessages).toHaveLength(2);
            expect(AutoPollRuntime.submitMessages[0]).toBe("handle current work");
            expect(AutoPollRuntime.submitMessages[1]).toContain("poll_message");
            expect(statuses).toEqual([
                "starting",
                "waiting_input",
                "thinking",
                "waiting_input",
            ]);
            expect(adapter.getSessionStatus("session-auto-poll")).toBe("waiting_input");
        }
        finally {
            vi.useRealTimers();
            adapter.cleanup();
        }
    });
    it("keeps waiting_input while dispatching an auto-poll prompt", async () => {
        vi.useFakeTimers();
        DeferredInitialPromptRuntime.submitCalls = 0;
        const adapter = new AcodeSdkAdapter();
        adapter.setRuntimeConstructorForTest(DeferredInitialPromptRuntime);
        const statuses = [];
        adapter.on("status-change", (_sessionId, status) => statuses.push(status));
        try {
            await adapter.startSession("session-auto-poll-pending", {
                command: "acode",
                workingDirectory: process.cwd(),
                autoAccept: true,
                extraMcpServers: {},
                envOverrides: {},
            });
            await vi.advanceTimersByTimeAsync(250);
            expect(DeferredInitialPromptRuntime.submitCalls).toBe(1);
            expect(adapter.getSessionStatus("session-auto-poll-pending")).toBe("waiting_input");
            expect(statuses).toEqual(["starting", "waiting_input"]);
        }
        finally {
            vi.useRealTimers();
            adapter.cleanup();
        }
    });
    it("stops re-arming auto-poll when the model answers without calling poll_message", async () => {
        vi.useFakeTimers();
        NonPollingAutoPollRuntime.submitMessages = [];
        const adapter = new AcodeSdkAdapter();
        adapter.setRuntimeConstructorForTest(NonPollingAutoPollRuntime);
        try {
            await adapter.startSession("session-non-polling-auto-poll", {
                command: "acode",
                workingDirectory: process.cwd(),
                autoAccept: true,
                extraMcpServers: {},
                envOverrides: {},
            });
            await vi.advanceTimersByTimeAsync(250);
            expect(NonPollingAutoPollRuntime.submitMessages).toHaveLength(1);
            expect(NonPollingAutoPollRuntime.submitMessages[0]).toContain("poll_message");
            await vi.advanceTimersByTimeAsync(5_000);
            expect(NonPollingAutoPollRuntime.submitMessages).toHaveLength(1);
            expect(adapter.getSessionStatus("session-non-polling-auto-poll")).toBe("waiting_input");
        }
        finally {
            vi.useRealTimers();
            adapter.cleanup();
        }
    });
    it("keeps re-arming auto-poll when the model actually calls poll_message", async () => {
        vi.useFakeTimers();
        PollingAutoPollRuntime.submitMessages = [];
        const adapter = new AcodeSdkAdapter();
        adapter.setRuntimeConstructorForTest(PollingAutoPollRuntime);
        try {
            await adapter.startSession("session-polling-auto-poll", {
                command: "acode",
                workingDirectory: process.cwd(),
                autoAccept: true,
                extraMcpServers: {},
                envOverrides: {},
            });
            await vi.advanceTimersByTimeAsync(250);
            expect(PollingAutoPollRuntime.submitMessages).toHaveLength(1);
            await vi.advanceTimersByTimeAsync(250);
            expect(PollingAutoPollRuntime.submitMessages).toHaveLength(2);
        }
        finally {
            vi.useRealTimers();
            adapter.cleanup();
        }
    });
    it("keeps re-arming auto-poll when a poll turn completes without assistant text", async () => {
        vi.useFakeTimers();
        AutoPollRuntime.submitMessages = [];
        const adapter = new AcodeSdkAdapter();
        adapter.setRuntimeConstructorForTest(AutoPollRuntime);
        try {
            await adapter.startSession("session-empty-auto-poll-turn", {
                command: "acode",
                workingDirectory: process.cwd(),
                autoAccept: true,
                extraMcpServers: {},
                envOverrides: {},
            });
            await vi.advanceTimersByTimeAsync(250);
            expect(AutoPollRuntime.submitMessages).toHaveLength(1);
            await vi.advanceTimersByTimeAsync(250);
            expect(AutoPollRuntime.submitMessages).toHaveLength(2);
            expect(AutoPollRuntime.submitMessages[1]).toContain("poll_message");
        }
        finally {
            vi.useRealTimers();
            adapter.cleanup();
        }
    });
    it("cancels a pending auto-poll when a user message arrives", async () => {
        vi.useFakeTimers();
        AutoPollRuntime.submitMessages = [];
        const adapter = new AcodeSdkAdapter();
        adapter.setRuntimeConstructorForTest(AutoPollRuntime);
        try {
            await adapter.startSession("session-cancel-auto-poll", {
                command: "acode",
                workingDirectory: process.cwd(),
                autoAccept: true,
                extraMcpServers: {},
                envOverrides: {},
            });
            await adapter.sendMessage("session-cancel-auto-poll", "handle current work");
            await adapter.sendMessage("session-cancel-auto-poll", "user follow up");
            await vi.advanceTimersByTimeAsync(250);
            expect(AutoPollRuntime.submitMessages).toEqual([
                "handle current work",
                "user follow up",
                expect.stringContaining("poll_message"),
            ]);
            expect(adapter.getSessionStatus("session-cancel-auto-poll")).toBe("waiting_input");
        }
        finally {
            vi.useRealTimers();
            adapter.cleanup();
        }
    });
    it("does not schedule auto-poll after terminal session status", async () => {
        vi.useFakeTimers();
        TerminalAfterTurnRuntime.submitMessages = [];
        const adapter = new AcodeSdkAdapter();
        adapter.setRuntimeConstructorForTest(TerminalAfterTurnRuntime);
        try {
            await adapter.startSession("session-terminal-auto-poll", {
                command: "acode",
                workingDirectory: process.cwd(),
                autoAccept: true,
                extraMcpServers: {},
                envOverrides: {},
            });
            await adapter.sendMessage("session-terminal-auto-poll", "complete");
            await vi.advanceTimersByTimeAsync(250);
            expect(TerminalAfterTurnRuntime.submitMessages).toEqual(["complete"]);
            expect(adapter.getSessionStatus("session-terminal-auto-poll")).toBe("completed");
        }
        finally {
            vi.useRealTimers();
            adapter.cleanup();
        }
    });
    it("arms auto-poll when session is started without an initial prompt (R1)", async () => {
        vi.useFakeTimers();
        AutoPollRuntime.submitMessages = [];
        const adapter = new AcodeSdkAdapter();
        adapter.setRuntimeConstructorForTest(AutoPollRuntime);
        const statuses = [];
        adapter.on("status-change", (_sessionId, status) => statuses.push(status));
        try {
            // No initialPrompt: previously the session sat at 'waiting_input'
            // forever because no auto-poll timer was armed and no setIdleCommands
            // was called yet. R1 fix arms the auto-poll on startSession instead.
            await adapter.startSession("session-r1-no-initial", {
                command: "acode",
                workingDirectory: process.cwd(),
                autoAccept: true,
                extraMcpServers: {},
                envOverrides: {},
            });
            await vi.advanceTimersByTimeAsync(250);
            // Status sequence: starting -> waiting_input (startSession) ->
            // waiting_input (poll turn completes and re-arms the loop). The
            // auto-poll prompt stays in waiting_input so a slow or missing
            // poll_message tool event cannot leave the dashboard on thinking.
            expect(AutoPollRuntime.submitMessages).toHaveLength(1);
            expect(AutoPollRuntime.submitMessages[0]).toContain("poll_message");
            expect(statuses).toEqual(["starting", "waiting_input", "waiting_input"]);
            expect(adapter.getSessionStatus("session-r1-no-initial")).toBe("waiting_input");
        }
        finally {
            vi.useRealTimers();
            adapter.cleanup();
        }
    });
    it("recovers a polling-driven session to waiting_input after a failed turn and re-arms with a longer backoff (R2)", async () => {
        vi.useFakeTimers();
        FailingAutoPollRuntime.submitMessages = [];
        const adapter = new AcodeSdkAdapter();
        adapter.setRuntimeConstructorForTest(FailingAutoPollRuntime);
        const statuses = [];
        adapter.on("status-change", (_sessionId, status) => statuses.push(status));
        try {
            await adapter.startSession("session-r2-recover-failed", {
                command: "acode",
                workingDirectory: process.cwd(),
                autoAccept: true,
                extraMcpServers: {},
                envOverrides: {},
            });
            // R1 arms the auto-poll; advance the normal 250ms delay to fire it.
            await vi.advanceTimersByTimeAsync(250);
            expect(FailingAutoPollRuntime.submitMessages).toHaveLength(1);
            // R2 fix runs synchronously during the failing submit: the
            // 'error' event sets status to 'error', then the 'turn_complete'
            // (failed) event triggers the recovery path which flips status
            // back to 'waiting_input' and schedules a 2s delayed re-arm.
            expect(adapter.getSessionStatus("session-r2-recover-failed")).toBe("waiting_input");
            expect(statuses).toEqual([
                "starting",
                "waiting_input",
                "error",
                "waiting_input",
            ]);
            expect(statuses.at(-1)).toBe("waiting_input");
            // 250ms after recovery is too short for the 2s backoff.
            await vi.advanceTimersByTimeAsync(250);
            expect(FailingAutoPollRuntime.submitMessages).toHaveLength(1);
            // After the 2s backoff elapses, the recovery timer fires and submits
            // a new poll prompt, restoring the message loop.
            await vi.advanceTimersByTimeAsync(2000);
            expect(FailingAutoPollRuntime.submitMessages.length).toBeGreaterThanOrEqual(2);
            expect(FailingAutoPollRuntime.submitMessages[1]).toContain("poll_message");
        }
        finally {
            vi.useRealTimers();
            adapter.cleanup();
        }
    });
    it("recovers from a bare ACode error event so auto-poll does not get stuck terminal", async () => {
        vi.useFakeTimers();
        BareErrorRuntime.submitMessages = [];
        const adapter = new AcodeSdkAdapter();
        adapter.setRuntimeConstructorForTest(BareErrorRuntime);
        const statuses = [];
        adapter.on("status-change", (_sessionId, status) => statuses.push(status));
        try {
            await adapter.startSession("session-bare-error", {
                command: "acode",
                workingDirectory: process.cwd(),
                autoAccept: true,
                extraMcpServers: {},
                envOverrides: {},
            });
            await vi.advanceTimersByTimeAsync(250);
            expect(BareErrorRuntime.submitMessages).toHaveLength(1);
            expect(adapter.getSessionStatus("session-bare-error")).toBe("waiting_input");
            expect(statuses).toEqual([
                "starting",
                "waiting_input",
                "error",
                "waiting_input",
            ]);
            await vi.advanceTimersByTimeAsync(1999);
            expect(BareErrorRuntime.submitMessages).toHaveLength(1);
            await vi.advanceTimersByTimeAsync(1);
            expect(BareErrorRuntime.submitMessages).toHaveLength(2);
            expect(BareErrorRuntime.submitMessages[1]).toContain("poll_message");
        }
        finally {
            vi.useRealTimers();
            adapter.cleanup();
        }
    });
    it("recovers from direct runtime submit rejection during user turns", async () => {
        vi.useFakeTimers();
        RejectingSubmitRuntime.submitMessages = [];
        const adapter = new AcodeSdkAdapter();
        adapter.setRuntimeConstructorForTest(RejectingSubmitRuntime);
        const statuses = [];
        adapter.on("status-change", (_sessionId, status) => statuses.push(status));
        try {
            await adapter.startSession("session-user-submit-reject", {
                command: "acode",
                workingDirectory: process.cwd(),
                autoAccept: true,
                extraMcpServers: {},
                envOverrides: {},
            });
            await adapter.sendMessage("session-user-submit-reject", "user work");
            expect(RejectingSubmitRuntime.submitMessages).toEqual(["user work"]);
            expect(adapter.getSessionStatus("session-user-submit-reject")).toBe("waiting_input");
            expect(statuses).toEqual([
                "starting",
                "waiting_input",
                "thinking",
                "error",
                "waiting_input",
            ]);
            await vi.advanceTimersByTimeAsync(2000);
            expect(RejectingSubmitRuntime.submitMessages[1]).toContain("poll_message");
        }
        finally {
            vi.useRealTimers();
            adapter.cleanup();
        }
    });
    it("does not let stale failed-turn recovery poll after a user follow-up arrives", async () => {
        vi.useFakeTimers();
        FailingPollThenSuccessfulUserRuntime.submitMessages = [];
        const adapter = new AcodeSdkAdapter();
        adapter.setRuntimeConstructorForTest(FailingPollThenSuccessfulUserRuntime);
        try {
            await adapter.startSession("session-stale-recovery-user", {
                command: "acode",
                workingDirectory: process.cwd(),
                autoAccept: true,
                extraMcpServers: {},
                envOverrides: {},
            });
            await vi.advanceTimersByTimeAsync(250);
            expect(FailingPollThenSuccessfulUserRuntime.submitMessages).toHaveLength(1);
            await adapter.sendMessage("session-stale-recovery-user", "human follow up");
            await vi.advanceTimersByTimeAsync(2000);
            expect(FailingPollThenSuccessfulUserRuntime.submitMessages).toEqual([
                expect.stringContaining("poll_message"),
                "human follow up",
            ]);
            expect(adapter.getSessionStatus("session-stale-recovery-user")).toBe("completed");
        }
        finally {
            vi.useRealTimers();
            adapter.cleanup();
        }
    });
    it("cleans up sessions when ACode runtime fails during startup", async () => {
        const adapter = new AcodeSdkAdapter();
        adapter.setRuntimeConstructorForTest(StartFailureRuntime);
        const statuses = [];
        adapter.on("status-change", (_sessionId, status) => statuses.push(status));
        await expect(adapter.startSession("session-start-failure", {
            command: "acode",
            workingDirectory: process.cwd(),
            autoAccept: true,
            extraMcpServers: {},
            envOverrides: {},
        })).rejects.toThrow("runtime start failed");
        expect(adapter.hasSession("session-start-failure")).toBe(false);
        expect(adapter.getSessionStatus("session-start-failure")).toBeUndefined();
        expect(statuses).toEqual(["starting", "error"]);
    });
    it("logs a warning but does not emit a fatal error event when poll_message is missing (R3)", async () => {
        StartupMcpStatusRuntime.includeAwsMcp = false;
        const adapter = new AcodeSdkAdapter();
        adapter.setRuntimeConstructorForTest(StartupMcpStatusRuntime);
        const events = [];
        const consoleWarn = vi
            .spyOn(console, "warn")
            .mockImplementation(() => undefined);
        adapter.on("event", (event) => events.push(event));
        try {
            await adapter.startSession("session-r3-no-poll-message", {
                command: "acode",
                workingDirectory: process.cwd(),
                autoAccept: true,
                extraMcpServers: {},
                envOverrides: {},
            });
            // R3 fix: a missing poll_message tool is a configuration smell
            // (the LLM will fail when it tries to call it), but it must NOT
            // be promoted to a session-level 'error' event. Doing so would
            // lock the dashboard timeline into a permanent '发生错误' state
            // even though the LLM itself never reports a real error.
            expect(events.find((event) => event.type === "error")).toBeUndefined();
            expect(consoleWarn).toHaveBeenCalled();
            const warnText = String(consoleWarn.mock.calls[0]?.[0] ?? "");
            expect(warnText).toContain("poll_message");
        }
        finally {
            consoleWarn.mockRestore();
            adapter.cleanup();
        }
    });
    it("does not log a warning or emit an error event when poll_message is exposed (R3 negative)", async () => {
        StartupMcpStatusRuntime.includeAwsMcp = true;
        const adapter = new AcodeSdkAdapter();
        adapter.setRuntimeConstructorForTest(StartupMcpStatusRuntime);
        const events = [];
        const consoleWarn = vi
            .spyOn(console, "warn")
            .mockImplementation(() => undefined);
        adapter.on("event", (event) => events.push(event));
        try {
            await adapter.startSession("session-r3-has-poll-message", {
                command: "acode",
                workingDirectory: process.cwd(),
                autoAccept: true,
                extraMcpServers: {},
                envOverrides: {},
            });
            expect(events.find((event) => event.type === "error")).toBeUndefined();
            expect(consoleWarn).not.toHaveBeenCalled();
        }
        finally {
            consoleWarn.mockRestore();
            adapter.cleanup();
        }
    });
    it("does not treat tools that only contain poll_message as a real listener", async () => {
        const adapter = new AcodeSdkAdapter();
        adapter.setRuntimeConstructorForTest(FakePollMessageToolRuntime);
        const events = [];
        const consoleWarn = vi
            .spyOn(console, "warn")
            .mockImplementation(() => undefined);
        adapter.on("event", (event) => events.push(event));
        try {
            await adapter.startSession("session-r3-fake-poll-message", {
                command: "acode",
                workingDirectory: process.cwd(),
                autoAccept: true,
                extraMcpServers: {},
                envOverrides: {},
            });
            expect(events.find((event) => event.type === "error")).toBeUndefined();
            expect(consoleWarn).toHaveBeenCalled();
            expect(String(consoleWarn.mock.calls[0]?.[0] ?? "")).toContain("poll_message");
        }
        finally {
            consoleWarn.mockRestore();
            adapter.cleanup();
        }
    });
    it("does not log a warning when the MCP server is named differently but still exposes poll_message (R3 user-scenario)", async () => {
        // Reproduces the production scenario from the bug report: the user
        // configures the AWS MCP under a non-default name (e.g. 'aws-mcp-server'
        // or 'agent-mcp'), but aws-mcp's tools are still exposed to the LLM
        // under names like 'aws-mcp__poll_message'. The old R3 check matched
        // the literal string 'aws-mcp' against the user-configured server
        // names, which produced a false-positive 'error' event and locked
        // the dashboard into a permanent '发生错误' state.
        const adapter = new AcodeSdkAdapter();
        adapter.setRuntimeConstructorForTest(DifferentlyNamedMcpRuntime);
        const events = [];
        const consoleWarn = vi
            .spyOn(console, "warn")
            .mockImplementation(() => undefined);
        adapter.on("event", (event) => events.push(event));
        try {
            await adapter.startSession("session-r3-differently-named-mcp", {
                command: "acode",
                workingDirectory: process.cwd(),
                autoAccept: true,
                extraMcpServers: {},
                envOverrides: {},
            });
            expect(events.find((event) => event.type === "error")).toBeUndefined();
            expect(consoleWarn).not.toHaveBeenCalled();
        }
        finally {
            consoleWarn.mockRestore();
            adapter.cleanup();
        }
    });
});
