import assert from "node:assert/strict";
import { mkdtemp, rm } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { describe, it } from "node:test";
import { AgentClient } from "./agent-client.js";
import { McpServer } from "./mcp-server.js";
import { MCP_TOOLS } from "./mcp-tools.js";
import { MemoryCapacityError, MemoryStore, } from "./memory-store.js";
import { MessageBuffer } from "./message-buffer.js";
function createServerWithUnread(unreadMessages, overrides = {}) {
    const server = new McpServer();
    Reflect.set(server, "messageBuffer", new MessageBuffer());
    const agentClientStub = {
        getStatusReporter: () => null,
        getMyTasks: async () => [],
        getState: () => ({
            agentId: "agent-self",
            displayName: "Self Agent",
            isConnected: true,
            isRegistered: true,
        }),
        fetchUnreadMessages: async () => unreadMessages,
        ...overrides,
    };
    Reflect.set(server, "agentClient", agentClientStub);
    return server;
}
async function withMemoryServer(callback) {
    const dir = await mkdtemp(path.join(os.tmpdir(), "aws-mcp-memory-"));
    const server = new McpServer();
    Reflect.set(server, "memoryStore", new MemoryStore(path.join(dir, "instance-memory")));
    Reflect.set(server, "roleMemoryStore", new MemoryStore(path.join(dir, "role-memory")));
    try {
        return await callback(server);
    }
    finally {
        await rm(dir, { recursive: true, force: true });
    }
}
async function withScopedMemoryServers(callback) {
    const dir = await mkdtemp(path.join(os.tmpdir(), "aws-mcp-scoped-memory-"));
    const firstServer = new McpServer();
    const secondServer = new McpServer();
    Reflect.set(firstServer, "memoryStore", new MemoryStore(path.join(dir, "agent-one-memory")));
    Reflect.set(secondServer, "memoryStore", new MemoryStore(path.join(dir, "agent-two-memory")));
    Reflect.set(firstServer, "roleMemoryStore", new MemoryStore(path.join(dir, "shared-role-memory")));
    Reflect.set(secondServer, "roleMemoryStore", new MemoryStore(path.join(dir, "shared-role-memory")));
    try {
        return await callback(firstServer, secondServer);
    }
    finally {
        await rm(dir, { recursive: true, force: true });
    }
}
function getHandleToolCall(server) {
    return Reflect.get(server, "handleToolCall");
}
function attachMemoryStatsReporter(server, reportedStats) {
    const existingClient = Reflect.get(server, "agentClient");
    Reflect.set(server, "agentClient", {
        ...existingClient,
        getStatusReporter: () => null,
        reportMemoryStats: async (memoryStats) => {
            reportedStats.push(memoryStats);
        },
    });
}
function withTemporaryEnv(values, callback) {
    const previousValues = new Map();
    for (const [key, value] of Object.entries(values)) {
        previousValues.set(key, process.env[key]);
        if (value === undefined) {
            delete process.env[key];
        }
        else {
            process.env[key] = value;
        }
    }
    try {
        return callback();
    }
    finally {
        for (const [key, value] of previousValues.entries()) {
            if (value === undefined) {
                delete process.env[key];
            }
            else {
                process.env[key] = value;
            }
        }
    }
}
describe("MCP task tool exposure", () => {
    it("should not expose start_task because task claiming is silent", () => {
        assert.equal(MCP_TOOLS.some((tool) => tool.name === "start_task"), false);
    });
});
describe("McpServer unread backlog hydration", () => {
    it("poll_message should return unread direct and group messages fetched after restart", async () => {
        const directMessage = {
            msgId: "dm-offline-1",
            senderId: "agent-b",
            senderName: "Agent B",
            content: "offline dm",
            requireReply: false,
            timestamp: new Date().toISOString(),
        };
        const groupMessage = {
            msgId: 101,
            senderId: "agent-c",
            senderName: "Agent C",
            content: "offline group",
            timestamp: new Date().toISOString(),
            projectName: "demo",
        };
        const server = createServerWithUnread({
            directMessages: [directMessage],
            groupMessages: [groupMessage],
        });
        const handleToolCall = Reflect.get(server, "handleToolCall");
        const result = await handleToolCall.call(server, "poll_message", {});
        assert.deepEqual(result, {
            directMessages: [directMessage],
            groupMessages: [groupMessage],
            fromBuffer: true,
        });
    });
    it("get_message should return unread messages fetched when local buffer is empty", async () => {
        const directMessage = {
            msgId: "dm-offline-2",
            senderId: "agent-d",
            senderName: "Agent D",
            content: "wake up dm",
            requireReply: false,
            timestamp: new Date().toISOString(),
        };
        const server = createServerWithUnread({
            directMessages: [directMessage],
            groupMessages: [],
        });
        const handleToolCall = Reflect.get(server, "handleToolCall");
        const result = await handleToolCall.call(server, "get_message", {});
        assert.deepEqual(result, {
            directMessages: [directMessage],
            groupMessages: [],
            fromBuffer: false,
        });
    });
    it("poll_message should keep blocking through empty polls until messages arrive", async () => {
        const previousBlockWait = process.env.AWS_POLL_MESSAGE_BLOCK_WAIT_MS;
        const previousRetryDelay = process.env.AWS_POLL_MESSAGE_EMPTY_RETRY_DELAY_MS;
        process.env.AWS_POLL_MESSAGE_BLOCK_WAIT_MS = "20";
        process.env.AWS_POLL_MESSAGE_EMPTY_RETRY_DELAY_MS = "1";
        try {
            const directMessage = {
                msgId: "dm-delayed-1",
                senderId: "agent-delayed",
                senderName: "Agent Delayed",
                content: "delayed dm",
                requireReply: false,
                timestamp: new Date().toISOString(),
            };
            const fetchOptions = [];
            let fetchCount = 0;
            const server = createServerWithUnread({ directMessages: [], groupMessages: [] }, {
                fetchUnreadMessages: async (options) => {
                    fetchOptions.push(options ?? {});
                    fetchCount += 1;
                    if (fetchCount <= 2) {
                        return { directMessages: [], groupMessages: [] };
                    }
                    return { directMessages: [directMessage], groupMessages: [] };
                },
            });
            const handleToolCall = getHandleToolCall(server);
            const result = await handleToolCall.call(server, "poll_message", {});
            assert.deepEqual(result, {
                directMessages: [directMessage],
                groupMessages: [],
                fromBuffer: true,
            });
            assert.equal(fetchOptions.length, 3);
            assert.equal(fetchOptions[0].blockIfEmpty, false);
            assert.equal(fetchOptions[0].includePipelineTasks, true);
            assert.equal(fetchOptions[1].blockIfEmpty, true);
            assert.ok(typeof fetchOptions[1].blockTimeoutMs === "number");
            assert.equal(fetchOptions[1].blockTimeoutMs, 20);
            assert.equal(fetchOptions[2].blockTimeoutMs, 20);
        }
        finally {
            if (previousBlockWait === undefined) {
                delete process.env.AWS_POLL_MESSAGE_BLOCK_WAIT_MS;
            }
            else {
                process.env.AWS_POLL_MESSAGE_BLOCK_WAIT_MS = previousBlockWait;
            }
            if (previousRetryDelay === undefined) {
                delete process.env.AWS_POLL_MESSAGE_EMPTY_RETRY_DELAY_MS;
            }
            else {
                process.env.AWS_POLL_MESSAGE_EMPTY_RETRY_DELAY_MS = previousRetryDelay;
            }
        }
    });
    it("poll_message should recover from client-side long poll timeout and keep waiting", async () => {
        const previousBlockWait = process.env.AWS_POLL_MESSAGE_BLOCK_WAIT_MS;
        const previousRetryDelay = process.env.AWS_POLL_MESSAGE_ERROR_RETRY_DELAY_MS;
        process.env.AWS_POLL_MESSAGE_BLOCK_WAIT_MS = "20";
        process.env.AWS_POLL_MESSAGE_ERROR_RETRY_DELAY_MS = "1";
        try {
            const directMessage = {
                msgId: "dm-timeout-recovered-1",
                senderId: "agent-timeout",
                senderName: "Agent Timeout",
                content: "recovered after timeout",
                requireReply: false,
                timestamp: new Date().toISOString(),
            };
            let fetchCount = 0;
            const server = createServerWithUnread({ directMessages: [], groupMessages: [] }, {
                fetchUnreadMessages: async () => {
                    fetchCount += 1;
                    if (fetchCount === 2) {
                        throw new Error("MCP tool poll_message timed out after 120000ms");
                    }
                    if (fetchCount === 3) {
                        return { directMessages: [directMessage], groupMessages: [] };
                    }
                    return { directMessages: [], groupMessages: [] };
                },
            });
            const handleToolCall = getHandleToolCall(server);
            const result = await handleToolCall.call(server, "poll_message", {});
            assert.deepEqual(result, {
                directMessages: [directMessage],
                groupMessages: [],
                fromBuffer: true,
            });
            assert.equal(fetchCount, 3);
        }
        finally {
            if (previousBlockWait === undefined) {
                delete process.env.AWS_POLL_MESSAGE_BLOCK_WAIT_MS;
            }
            else {
                process.env.AWS_POLL_MESSAGE_BLOCK_WAIT_MS = previousBlockWait;
            }
            if (previousRetryDelay === undefined) {
                delete process.env.AWS_POLL_MESSAGE_ERROR_RETRY_DELAY_MS;
            }
            else {
                process.env.AWS_POLL_MESSAGE_ERROR_RETRY_DELAY_MS = previousRetryDelay;
            }
        }
    });
    it("get_message should still hydrate unread group messages after profile push is received", async () => {
        const groupMessage = {
            msgId: 301,
            senderId: "agent-e",
            senderName: "Agent E",
            content: "offline group after profile",
            timestamp: new Date().toISOString(),
            projectName: "demo",
            roomId: "inner_group:g-1",
        };
        const server = createServerWithUnread({
            directMessages: [],
            groupMessages: [groupMessage],
        });
        const messageBuffer = Reflect.get(server, "messageBuffer");
        messageBuffer.setProfileMessage({
            id: "agent-1",
            displayName: "Agent One",
            roleName: "Backend Agent",
            prompt: "prompt",
            isOnline: true,
            onlineSince: new Date().toISOString(),
            createdAt: new Date().toISOString(),
            projectName: "demo",
        });
        const handleToolCall = Reflect.get(server, "handleToolCall");
        const result = (await handleToolCall.call(server, "get_message", {}));
        assert.equal(result.fromBuffer, false);
        assert.deepEqual(result.directMessages, []);
        assert.deepEqual(result.groupMessages, [groupMessage]);
    });
    it("get_message should filter out self-sent unread group messages fetched from server", async () => {
        const selfGroupMessage = {
            msgId: 401,
            senderId: "agent-self",
            senderName: "Self Agent",
            content: "my own group message",
            timestamp: new Date().toISOString(),
            projectName: "demo",
            roomId: "project:demo",
        };
        const otherGroupMessage = {
            msgId: 402,
            senderId: "agent-other",
            senderName: "Other Agent",
            content: "other group message",
            timestamp: new Date().toISOString(),
            projectName: "demo",
            roomId: "project:demo",
        };
        const server = createServerWithUnread({
            directMessages: [],
            groupMessages: [selfGroupMessage, otherGroupMessage],
        });
        Reflect.set(server, "agentClient", {
            fetchUnreadMessages: async () => ({
                directMessages: [],
                groupMessages: [selfGroupMessage, otherGroupMessage],
            }),
            getState: () => ({
                agentId: "agent-self",
                displayName: "Self Agent",
                isConnected: true,
                isRegistered: true,
            }),
            getMyTasks: async () => [],
            getStatusReporter: () => null,
        });
        const handleToolCall = Reflect.get(server, "handleToolCall");
        const result = await handleToolCall.call(server, "get_message", {});
        assert.deepEqual(result, {
            directMessages: [],
            groupMessages: [otherGroupMessage],
            fromBuffer: false,
        });
    });
    it("poll_message should filter out self-sent unread messages fetched from server", async () => {
        const selfDirectMessage = {
            msgId: "dm-self-1",
            senderId: "agent-self",
            senderName: "Self Agent",
            receiverId: "agent-self",
            content: "my own direct message",
            requireReply: false,
            timestamp: new Date().toISOString(),
        };
        const otherDirectMessage = {
            msgId: "dm-other-1",
            senderId: "agent-other",
            senderName: "Other Agent",
            receiverId: "agent-self",
            content: "other direct message",
            requireReply: false,
            timestamp: new Date().toISOString(),
        };
        const selfGroupMessage = {
            msgId: 403,
            senderId: "agent-self",
            senderName: "Self Agent",
            content: "my own group message",
            timestamp: new Date().toISOString(),
            projectName: "demo",
            roomId: "project:demo",
        };
        const otherGroupMessage = {
            msgId: 404,
            senderId: "agent-other",
            senderName: "Other Agent",
            content: "other group message",
            timestamp: new Date().toISOString(),
            projectName: "demo",
            roomId: "project:demo",
        };
        const server = createServerWithUnread({
            directMessages: [selfDirectMessage, otherDirectMessage],
            groupMessages: [selfGroupMessage, otherGroupMessage],
        });
        const handleToolCall = getHandleToolCall(server);
        const result = (await handleToolCall.call(server, "poll_message", {}));
        assert.deepEqual(result.directMessages, [otherDirectMessage]);
        assert.deepEqual(result.groupMessages, [otherGroupMessage]);
        assert.equal(result.fromBuffer, true);
    });
    it("get_message should ignore malformed unread group entries instead of crashing", async () => {
        const validGroupMessage = {
            msgId: 501,
            senderId: "agent-other",
            senderName: "Other Agent",
            content: "valid group message",
            timestamp: new Date().toISOString(),
            projectName: "demo",
            roomId: "project:demo",
        };
        const server = createServerWithUnread({
            directMessages: [],
            groupMessages: [validGroupMessage],
        });
        Reflect.set(server, "agentClient", {
            fetchUnreadMessages: async () => ({
                directMessages: [],
                groupMessages: [undefined, validGroupMessage],
            }),
            getState: () => ({
                agentId: "agent-self",
                displayName: "Self Agent",
                isConnected: true,
                isRegistered: true,
            }),
            getMyTasks: async () => [],
            getStatusReporter: () => null,
        });
        const handleToolCall = getHandleToolCall(server);
        const result = await handleToolCall.call(server, "get_message", {});
        assert.deepEqual(result, {
            directMessages: [],
            groupMessages: [validGroupMessage],
            fromBuffer: false,
        });
    });
    it("get_message should pass through backend task notification without local my_task injection", async () => {
        const taskNotification = {
            msgId: "task-notify-backend-1",
            senderId: "MCP-System",
            senderName: "系统",
            receiverId: "agent-self",
            content: "你有任务需要关注！1 个待处理任务：实现接口。",
            requireReply: false,
            prompt: "后端动态任务提醒",
            timestamp: new Date().toISOString(),
        };
        let myTaskCalls = 0;
        const server = createServerWithUnread({
            directMessages: [taskNotification],
            groupMessages: [],
        }, {
            getMyTasks: async () => {
                myTaskCalls += 1;
                return [];
            },
        });
        const handleToolCall = getHandleToolCall(server);
        const result = (await handleToolCall.call(server, "get_message", {}));
        assert.deepEqual(result.directMessages, [taskNotification]);
        assert.deepEqual(result.groupMessages, []);
        assert.equal(result.fromBuffer, false);
        assert.equal(myTaskCalls, 0);
    });
    it("poll_message should attempt non-blocking backend task claim before local return", async () => {
        const taskNotification = {
            msgId: "task-notify-backend-2",
            senderId: "MCP-System",
            senderName: "系统",
            receiverId: "agent-self",
            content: "你有任务需要关注！1 个处理中任务：需要提交。",
            requireReply: false,
            prompt: "后端动态任务提醒",
            timestamp: new Date().toISOString(),
        };
        const fetchOptions = [];
        const server = createServerWithUnread({
            directMessages: [],
            groupMessages: [],
        }, {
            fetchUnreadMessages: async (options) => {
                fetchOptions.push(options);
                return { directMessages: [taskNotification], groupMessages: [] };
            },
        });
        const handleToolCall = getHandleToolCall(server);
        const result = (await handleToolCall.call(server, "poll_message", {}));
        assert.deepEqual(result.directMessages, [taskNotification]);
        assert.deepEqual(result.groupMessages, []);
        assert.equal(result.fromBuffer, true);
        assert.equal(fetchOptions.length, 1);
        assert.equal(fetchOptions[0]?.blockIfEmpty, false);
        assert.equal(fetchOptions[0]?.includePipelineTasks, true);
        assert.equal(fetchOptions[0]?.blockTimeoutMs, undefined);
    });
    it("poll_message should merge existing local cache with newly claimed task notification", async () => {
        const cachedMessage = {
            msgId: "cached-dm-before-task",
            senderId: "agent-cached",
            senderName: "Cached Agent",
            receiverId: "agent-self",
            content: "cached message already in local buffer",
            requireReply: false,
            timestamp: new Date().toISOString(),
        };
        const taskNotification = {
            msgId: "task-notify-claimed-with-cache",
            senderId: "MCP-System",
            senderName: "系统",
            receiverId: "agent-self",
            content: "你有任务需要关注！已通过 poll_message 静默抢占。",
            requireReply: false,
            prompt: "后端动态任务提醒",
            timestamp: new Date().toISOString(),
        };
        const fetchOptions = [];
        const server = createServerWithUnread({
            directMessages: [],
            groupMessages: [],
        }, {
            fetchUnreadMessages: async (options) => {
                fetchOptions.push(options);
                return { directMessages: [taskNotification], groupMessages: [] };
            },
        });
        const messageBuffer = Reflect.get(server, "messageBuffer");
        messageBuffer.push(cachedMessage);
        const handleToolCall = getHandleToolCall(server);
        const result = (await handleToolCall.call(server, "poll_message", {}));
        assert.deepEqual(result.directMessages, [cachedMessage, taskNotification]);
        assert.deepEqual(result.groupMessages, []);
        assert.equal(result.fromBuffer, true);
        assert.equal(fetchOptions.length, 1);
        assert.equal(fetchOptions[0]?.blockIfEmpty, false);
        assert.equal(fetchOptions[0]?.includePipelineTasks, true);
    });
    it("poll_message should keep waiting when backend long polling returns empty", async () => {
        const delayedMessage = {
            msgId: "dm-after-empty-poll",
            senderId: "agent-later",
            senderName: "Later Agent",
            receiverId: "agent-self",
            content: "arrived after empty backend poll",
            requireReply: false,
            timestamp: new Date().toISOString(),
        };
        let fetchCalls = 0;
        const server = createServerWithUnread({
            directMessages: [],
            groupMessages: [],
        }, {
            fetchUnreadMessages: async () => {
                fetchCalls += 1;
                return fetchCalls <= 2
                    ? { directMessages: [], groupMessages: [] }
                    : { directMessages: [delayedMessage], groupMessages: [] };
            },
        });
        const handleToolCall = getHandleToolCall(server);
        const result = (await handleToolCall.call(server, "poll_message", {}));
        assert.equal(fetchCalls, 3);
        assert.deepEqual(result.directMessages, [delayedMessage]);
        assert.deepEqual(result.groupMessages, []);
        assert.equal(result.fromBuffer, true);
    });
    it("poll_message should use error backoff when unread hydration fails", async () => {
        const previousErrorDelay = process.env.AWS_POLL_MESSAGE_ERROR_RETRY_DELAY_MS;
        process.env.AWS_POLL_MESSAGE_ERROR_RETRY_DELAY_MS = "25";
        try {
            const delayedMessage = {
                msgId: "dm-after-hydration-error",
                senderId: "agent-later",
                senderName: "Later Agent",
                receiverId: "agent-self",
                content: "arrived after failed backend poll",
                requireReply: false,
                timestamp: new Date().toISOString(),
            };
            const sleepDelays = [];
            let fetchCalls = 0;
            const server = createServerWithUnread({
                directMessages: [],
                groupMessages: [],
            }, {
                fetchUnreadMessages: async () => {
                    fetchCalls += 1;
                    if (fetchCalls === 1) {
                        return { directMessages: [], groupMessages: [] };
                    }
                    if (fetchCalls === 2) {
                        throw new Error("backend unavailable");
                    }
                    return { directMessages: [delayedMessage], groupMessages: [] };
                },
            });
            Reflect.set(server, "sleep", async (delayMs) => {
                sleepDelays.push(delayMs);
            });
            const handleToolCall = getHandleToolCall(server);
            const result = (await handleToolCall.call(server, "poll_message", {}));
            assert.equal(fetchCalls, 3);
            assert.deepEqual(sleepDelays, [25]);
            assert.deepEqual(result.directMessages, [delayedMessage]);
            assert.deepEqual(result.groupMessages, []);
            assert.equal(result.fromBuffer, true);
        }
        finally {
            if (previousErrorDelay === undefined) {
                delete process.env.AWS_POLL_MESSAGE_ERROR_RETRY_DELAY_MS;
            }
            else {
                process.env.AWS_POLL_MESSAGE_ERROR_RETRY_DELAY_MS = previousErrorDelay;
            }
        }
    });
    it("poll_message should clamp block wait below HTTP request timeout", async () => {
        const previousBlockWait = process.env.AWS_POLL_MESSAGE_BLOCK_WAIT_MS;
        const previousHttpTimeout = process.env.AWS_MCP_HTTP_REQUEST_TIMEOUT_MS;
        process.env.AWS_POLL_MESSAGE_BLOCK_WAIT_MS = "120000";
        process.env.AWS_MCP_HTTP_REQUEST_TIMEOUT_MS = "120000";
        try {
            const delayedMessage = {
                msgId: "dm-after-clamped-poll",
                senderId: "agent-later",
                senderName: "Later Agent",
                receiverId: "agent-self",
                content: "arrived after clamped backend poll",
                requireReply: false,
                timestamp: new Date().toISOString(),
            };
            const fetchOptions = [];
            const server = createServerWithUnread({
                directMessages: [],
                groupMessages: [],
            }, {
                fetchUnreadMessages: async (options) => {
                    fetchOptions.push(options ?? {});
                    return fetchOptions.length === 1
                        ? { directMessages: [], groupMessages: [] }
                        : fetchOptions.length === 2
                            ? { directMessages: [delayedMessage], groupMessages: [] }
                            : { directMessages: [], groupMessages: [] };
                },
            });
            const handleToolCall = getHandleToolCall(server);
            const result = (await handleToolCall.call(server, "poll_message", {}));
            assert.deepEqual(result.directMessages, [delayedMessage]);
            assert.equal(fetchOptions[0].blockIfEmpty, false);
            assert.equal(fetchOptions[1].blockIfEmpty, true);
            assert.equal(fetchOptions[1].blockTimeoutMs, 119000);
        }
        finally {
            if (previousBlockWait === undefined) {
                delete process.env.AWS_POLL_MESSAGE_BLOCK_WAIT_MS;
            }
            else {
                process.env.AWS_POLL_MESSAGE_BLOCK_WAIT_MS = previousBlockWait;
            }
            if (previousHttpTimeout === undefined) {
                delete process.env.AWS_MCP_HTTP_REQUEST_TIMEOUT_MS;
            }
            else {
                process.env.AWS_MCP_HTTP_REQUEST_TIMEOUT_MS = previousHttpTimeout;
            }
        }
    });
    it("poll_message should restore waiting status when task notification is returned", async () => {
        const statusEvents = [];
        const server = createServerWithUnread({
            directMessages: [],
            groupMessages: [],
        }, {
            fetchUnreadMessages: async () => ({
                directMessages: [
                    {
                        msgId: "task-notify-thinking",
                        senderId: "MCP-System",
                        senderName: "系统",
                        receiverId: "agent-self",
                        content: "你有任务需要关注！处理待办流程。",
                        requireReply: false,
                        timestamp: new Date().toISOString(),
                    },
                ],
                groupMessages: [],
            }),
            getStatusReporter: () => ({
                reportToolUse: () => statusEvents.push("tool_using"),
                reportToolResult: () => statusEvents.push("tool_result"),
                reportThinking: () => statusEvents.push("thinking"),
                reportWaitingInput: () => statusEvents.push("waiting_input"),
                reportError: () => statusEvents.push("error"),
                reportToolError: () => statusEvents.push("tool_error"),
            }),
        });
        const handleToolCall = getHandleToolCall(server);
        const result = (await handleToolCall.call(server, "poll_message", {}));
        assert.equal(result.directMessages.length, 1);
        assert.deepEqual(statusEvents, ["tool_using", "tool_result", "thinking"]);
    });
    it("get_message should restore thinking status when task notification is returned", async () => {
        const statusEvents = [];
        const server = createServerWithUnread({
            directMessages: [],
            groupMessages: [],
        }, {
            fetchUnreadMessages: async () => ({
                directMessages: [
                    {
                        msgId: "task-notify-get-message",
                        senderId: "MCP-System",
                        senderName: "系统",
                        receiverId: "agent-self",
                        content: "你有任务需要关注！继续流程任务。",
                        requireReply: false,
                        timestamp: new Date().toISOString(),
                    },
                ],
                groupMessages: [],
            }),
            getStatusReporter: () => ({
                reportToolUse: () => statusEvents.push("tool_using"),
                reportToolResult: () => statusEvents.push("tool_result"),
                reportThinking: () => statusEvents.push("thinking"),
                reportWaitingInput: () => statusEvents.push("waiting_input"),
                reportError: () => statusEvents.push("error"),
                reportToolError: () => statusEvents.push("tool_error"),
            }),
        });
        const handleToolCall = getHandleToolCall(server);
        const result = (await handleToolCall.call(server, "get_message", {}));
        assert.equal(result.directMessages.length, 1);
        assert.deepEqual(statusEvents, ["tool_using", "tool_result", "thinking"]);
    });
    it("poll_message should restore thinking status when unread messages are returned", async () => {
        const statusEvents = [];
        const directMessage = {
            msgId: "dm-status-thinking",
            senderId: "agent-status-source",
            senderName: "Status Source",
            content: "work message",
            requireReply: false,
            timestamp: new Date().toISOString(),
        };
        const server = createServerWithUnread({
            directMessages: [directMessage],
            groupMessages: [],
        }, {
            getStatusReporter: () => ({
                reportToolUse: () => statusEvents.push("tool_using"),
                reportToolResult: () => statusEvents.push("tool_result"),
                reportThinking: () => statusEvents.push("thinking"),
                reportWaitingInput: () => statusEvents.push("waiting_input"),
                reportError: () => statusEvents.push("error"),
                reportToolError: () => statusEvents.push("tool_error"),
            }),
        });
        const handleToolCall = getHandleToolCall(server);
        const result = (await handleToolCall.call(server, "poll_message", {}));
        assert.deepEqual(result.directMessages, [directMessage]);
        assert.deepEqual(statusEvents, ["tool_using", "tool_result", "thinking"]);
    });
    it("get_message should restore thinking status when unread messages are returned", async () => {
        const statusEvents = [];
        const directMessage = {
            msgId: "dm-get-status-waiting",
            senderId: "agent-get-status-source",
            senderName: "Get Status Source",
            content: "private work message",
            requireReply: false,
            timestamp: new Date().toISOString(),
        };
        const server = createServerWithUnread({
            directMessages: [directMessage],
            groupMessages: [],
        }, {
            getStatusReporter: () => ({
                reportToolUse: () => statusEvents.push("tool_using"),
                reportToolResult: () => statusEvents.push("tool_result"),
                reportThinking: () => statusEvents.push("thinking"),
                reportWaitingInput: () => statusEvents.push("waiting_input"),
                reportError: () => statusEvents.push("error"),
                reportToolError: () => statusEvents.push("tool_error"),
            }),
        });
        const handleToolCall = getHandleToolCall(server);
        const result = (await handleToolCall.call(server, "get_message", {}));
        assert.deepEqual(result.directMessages, [directMessage]);
        assert.deepEqual(statusEvents, [
            "tool_using",
            "tool_result",
            "thinking",
        ]);
    });
    it("get_message should restore thinking status when no messages or tasks are returned", async () => {
        const statusEvents = [];
        const server = createServerWithUnread({
            directMessages: [],
            groupMessages: [],
        }, {
            getStatusReporter: () => ({
                reportToolUse: () => statusEvents.push("tool_using"),
                reportToolResult: () => statusEvents.push("tool_result"),
                reportThinking: () => statusEvents.push("thinking"),
                reportWaitingInput: () => statusEvents.push("waiting_input"),
                reportError: () => statusEvents.push("error"),
                reportToolError: () => statusEvents.push("tool_error"),
            }),
        });
        const handleToolCall = getHandleToolCall(server);
        const result = (await handleToolCall.call(server, "get_message", {}));
        assert.deepEqual(result.directMessages, []);
        assert.deepEqual(result.groupMessages, []);
        assert.deepEqual(statusEvents, [
            "tool_using",
            "tool_result",
            "thinking",
        ]);
    });
});
describe("McpServer memory store paths", () => {
    it("uses project permanent memory directory by default", async () => {
        const dir = await mkdtemp(path.join(os.tmpdir(), "aws-mcp-memory-paths-"));
        try {
            const paths = withTemporaryEnv({
                AWS_WORKSPACE_PATH: dir,
                AWS_ROLE_NAME: "Backend/Engineer:Lead",
                AWS_MEMORY_STORE_PATH: undefined,
                AWS_ROLE_MEMORY_STORE_PATH: undefined,
                AWS_INSTANCE_MEMORY_STORE_PATH: undefined,
                AWS_AGENT_ID: undefined,
            }, () => Reflect.get(McpServer, "resolveMemoryStorePaths").call(McpServer));
            assert.equal(paths.rolePath, path.join(dir, ".agentswork", "memory", "memory", "Backend_Engineer_Lead"));
            assert.equal(paths.instancePath, path.join(dir, ".agentswork", "memory", "server-managed"));
        }
        finally {
            await rm(dir, { recursive: true, force: true });
        }
    });
    it("falls back to a default role memory directory when role name is blank", async () => {
        const dir = await mkdtemp(path.join(os.tmpdir(), "aws-mcp-memory-paths-"));
        try {
            const paths = withTemporaryEnv({
                AWS_WORKSPACE_PATH: dir,
                AWS_ROLE_NAME: "  ",
                AWS_MEMORY_STORE_PATH: undefined,
                AWS_ROLE_MEMORY_STORE_PATH: undefined,
                AWS_INSTANCE_MEMORY_STORE_PATH: undefined,
                AWS_AGENT_ID: undefined,
            }, () => Reflect.get(McpServer, "resolveMemoryStorePaths").call(McpServer));
            assert.equal(paths.rolePath, path.join(dir, ".agentswork", "memory", "memory", "default-role"));
        }
        finally {
            await rm(dir, { recursive: true, force: true });
        }
    });
});
describe("McpServer memory tools", () => {
    it("exposes memory tools in MCP tool catalog", () => {
        const toolNames = MCP_TOOLS.map((tool) => tool.name);
        assert.ok(toolNames.includes("create_memory"));
        assert.ok(toolNames.includes("read_memory"));
        assert.ok(toolNames.includes("update_memory"));
        assert.ok(toolNames.includes("delete_memory"));
        assert.ok(toolNames.includes("search_memory"));
        assert.ok(toolNames.includes("list_memories"));
        assert.ok(toolNames.includes("get_memory_stats"));
        assert.ok(toolNames.includes("remove_task_list_memory"));
        assert.ok(toolNames.includes("archive_recently_memory"));
    });
    it("creates, reads, updates, searches, and deletes memory through MCP dispatch", async () => {
        await withMemoryServer(async (server) => {
            const handleToolCall = getHandleToolCall(server);
            const created = await handleToolCall.call(server, "create_memory", {
                memory_type: "memory",
                uri: "Core://Agent/Profile/",
                content: "prefers concise backend summaries",
            });
            assert.equal(created.uri, "core://Agent/Profile");
            assert.equal(created.content, undefined);
            const read = await handleToolCall.call(server, "read_memory", {
                uri: "core://Agent/Profile",
                memory_type: "memory",
            });
            assert.equal(read.content, "prefers concise backend summaries");
            const updated = await handleToolCall.call(server, "update_memory", {
                uri: "core://Agent/Profile",
                memory_type: "memory",
                old_string: "concise",
                new_string: "structured",
                append: "\nuses MCP memory tools",
            });
            assert.equal(updated.content, undefined);
            assert.equal(updated.version, 2);
            const readUpdated = await handleToolCall.call(server, "read_memory", {
                uri: "core://Agent/Profile",
                memory_type: "memory",
            });
            assert.match(readUpdated.content, /structured backend/);
            assert.match(readUpdated.content, /MCP memory tools/);
            const searchResult = await handleToolCall.call(server, "search_memory", {
                query: "backend",
                limit: 5,
            });
            assert.equal(searchResult.results.length, 1);
            const deleted = await handleToolCall.call(server, "delete_memory", {
                uri: "core://Agent/Profile",
                memory_type: "memory",
            });
            assert.deepEqual(deleted, { deleted: true });
        });
    });
    it("lists task and recent memories while protecting long-term memory from list_memories", async () => {
        await withMemoryServer(async (server) => {
            const handleToolCall = getHandleToolCall(server);
            await handleToolCall.call(server, "create_memory", {
                memory_type: "task_list",
                uri: "task://todo/current",
                content: "finish memory endpoint tests",
            });
            await handleToolCall.call(server, "create_memory", {
                memory_type: "recently_memory",
                uri: "recent://discussion/memory-tools",
                content: "memory tools should stay local for now",
            });
            const taskList = await handleToolCall.call(server, "list_memories", {
                memory_type: "task_list",
            });
            assert.equal(taskList.length, 1);
            const recentList = await handleToolCall.call(server, "list_memories", {
                memory_type: "recently_memory",
            });
            assert.equal(recentList.length, 1);
            await assert.rejects(() => handleToolCall.call(server, "list_memories", {
                memory_type: "memory",
            }), /Invalid memory_type/);
        });
    });
    it("scopes long-term memory to role and listable memory to agent instance", async () => {
        await withScopedMemoryServers(async (firstServer, secondServer) => {
            const firstHandleToolCall = getHandleToolCall(firstServer);
            const secondHandleToolCall = getHandleToolCall(secondServer);
            await firstHandleToolCall.call(firstServer, "create_memory", {
                memory_type: "memory",
                uri: "core://role/shared-profile",
                content: "shared role knowledge",
            });
            await firstHandleToolCall.call(firstServer, "create_memory", {
                memory_type: "recently_memory",
                uri: "recent://instance/private-context",
                content: "agent one private context",
            });
            const roleMemory = await secondHandleToolCall.call(secondServer, "read_memory", {
                uri: "core://role/shared-profile",
                memory_type: "memory",
            });
            const recentMemory = await secondHandleToolCall.call(secondServer, "read_memory", {
                uri: "recent://instance/private-context",
                memory_type: "recently_memory",
            });
            assert.equal(roleMemory.content, "shared role knowledge");
            assert.equal(recentMemory, null);
        });
    });
    it("supports task-list removal and recent-memory archive semantics", async () => {
        await withMemoryServer(async (server) => {
            const handleToolCall = getHandleToolCall(server);
            await handleToolCall.call(server, "create_memory", {
                memory_type: "task_list",
                uri: "task://todo/remove-me",
                content: "temporary task",
            });
            await handleToolCall.call(server, "create_memory", {
                memory_type: "recently_memory",
                uri: "recent://note/archive-me",
                content: "hot context",
            });
            const removed = await handleToolCall.call(server, "remove_task_list_memory", {
                uri: "task://todo/remove-me",
            });
            assert.deepEqual(removed, { removed: true });
            const archived = await handleToolCall.call(server, "archive_recently_memory", {
                uri: "recent://note/archive-me",
            });
            assert.equal(archived.memory_type, "memory");
            const archivedInRoleScope = await handleToolCall.call(server, "read_memory", {
                uri: "recent://note/archive-me",
                memory_type: "memory",
            });
            const removedFromInstanceScope = await handleToolCall.call(server, "read_memory", {
                uri: "recent://note/archive-me",
                memory_type: "recently_memory",
            });
            assert.equal(archivedInRoleScope.content, "hot context");
            assert.equal(removedFromInstanceScope, null);
        });
    });
    it("returns memory stats through MCP dispatch", async () => {
        await withMemoryServer(async (server) => {
            const handleToolCall = getHandleToolCall(server);
            await handleToolCall.call(server, "create_memory", {
                memory_type: "task_list",
                uri: "task://todo/stats",
                content: "temporary task",
            });
            await handleToolCall.call(server, "create_memory", {
                memory_type: "recently_memory",
                uri: "recent://note/stats",
                content: "hot context",
            });
            await handleToolCall.call(server, "create_memory", {
                memory_type: "memory",
                uri: "core://agent/profile",
                content: "long-term profile",
            });
            const stats = await handleToolCall.call(server, "get_memory_stats", {});
            const typedStats = stats;
            assert.equal(typedStats.total, 3);
            assert.deepEqual(typedStats.listable, {
                count: 2,
                limit: 64,
                hardLimit: 128,
                exceeded: false,
                hardExceeded: false,
            });
            assert.equal(typedStats.by_type.task_list.count, 1);
            assert.equal(typedStats.by_type.task_list.domainCount, 1);
            assert.equal(typedStats.by_type.task_list.by_domain.task?.count, 1);
            assert.equal(typedStats.by_type.recently_memory.count, 1);
            assert.equal(typedStats.by_type.recently_memory.domainCount, 1);
            assert.equal(typedStats.by_type.recently_memory.by_domain.recent?.count, 1);
            assert.equal(typedStats.by_type.memory.count, 1);
            assert.equal(typedStats.by_type.memory.domainCount, 1);
            assert.equal(typedStats.by_type.memory.by_domain.core?.count, 1);
        });
    });
    it("reports combined memory stats after long-term memory changes", async () => {
        await withMemoryServer(async (server) => {
            const handleToolCall = getHandleToolCall(server);
            const reportedStats = [];
            attachMemoryStatsReporter(server, reportedStats);
            await handleToolCall.call(server, "create_memory", {
                memory_type: "memory",
                uri: "project://test-aws/permanent-memory-check",
                content: "dashboard should see this permanent memory count",
            });
            assert.equal(reportedStats.length, 1);
            assert.equal(reportedStats[0]?.by_type.memory.count, 1);
            assert.equal(reportedStats[0]?.by_type.memory.by_domain.project?.count, 1);
        });
    });
    it("reports combined memory stats after archiving recent memory", async () => {
        await withMemoryServer(async (server) => {
            const handleToolCall = getHandleToolCall(server);
            const reportedStats = [];
            attachMemoryStatsReporter(server, reportedStats);
            await handleToolCall.call(server, "create_memory", {
                memory_type: "recently_memory",
                uri: "project://test-aws/hot-note",
                content: "hot note that will become permanent",
            });
            await handleToolCall.call(server, "archive_recently_memory", {
                uri: "project://test-aws/hot-note",
            });
            const latestStats = reportedStats.at(-1);
            assert.equal(latestStats?.by_type.recently_memory.count, 0);
            assert.equal(latestStats?.by_type.memory.count, 1);
            assert.equal(latestStats?.by_type.memory.by_domain.project?.count, 1);
        });
    });
    it("reports existing combined memory stats after auto registration", async () => {
        await withMemoryServer(async (server) => {
            const reportedStats = [];
            const roleMemoryStore = Reflect.get(server, "roleMemoryStore");
            await roleMemoryStore.createMemory({
                memory_type: "memory",
                uri: "project://test-aws/existing-permanent-memory",
                content: "existing role memory should be visible on dashboard startup",
            });
            Reflect.set(server, "agentClient", {
                getAgentConfig: () => ({ roleName: "backend", agentId: "agent-1" }),
                register: async () => ({
                    agentId: "agent-1",
                    displayName: "Backend Agent",
                    status: "online",
                }),
                start: () => undefined,
                getStatusReporter: () => null,
                reportMemoryStats: async (memoryStats) => {
                    reportedStats.push(memoryStats);
                },
            });
            const autoRegister = Reflect.get(server, "autoRegister");
            await autoRegister.call(server);
            assert.equal(reportedStats.length, 1);
            assert.equal(reportedStats[0]?.by_type.memory.count, 1);
            assert.equal(reportedStats[0]?.by_type.memory.by_domain.project?.count, 1);
        });
    });
    it("returns memory stats for one requested memory type", async () => {
        await withMemoryServer(async (server) => {
            const handleToolCall = getHandleToolCall(server);
            await handleToolCall.call(server, "create_memory", {
                memory_type: "task_list",
                uri: "task://todo/stats",
                content: "temporary task",
            });
            await handleToolCall.call(server, "create_memory", {
                memory_type: "recently_memory",
                uri: "recent://note/stats",
                content: "hot context",
            });
            await handleToolCall.call(server, "create_memory", {
                memory_type: "memory",
                uri: "core://agent/profile",
                content: "long-term profile",
            });
            const stats = (await handleToolCall.call(server, "get_memory_stats", {
                memory_type: "recently_memory",
            }));
            assert.equal(stats.memory_type, "recently_memory");
            assert.equal(stats.total, 1);
            assert.deepEqual(Object.keys(stats.by_type), ["recently_memory"]);
            assert.equal(stats.by_type.recently_memory?.domainCount, 1);
            assert.equal(stats.by_type.recently_memory?.by_domain.recent?.count, 1);
            const memoryStats = (await handleToolCall.call(server, "get_memory_stats", { memory_type: "memory" }));
            assert.equal(memoryStats.memory_type, "memory");
            assert.equal(memoryStats.total, 1);
            assert.deepEqual(Object.keys(memoryStats.by_type), ["memory"]);
            assert.equal(memoryStats.by_type.memory?.domainCount, 1);
            assert.equal(memoryStats.by_type.memory?.by_domain.core?.count, 1);
        });
    });
    it("keeps structured details on memory capacity errors", async () => {
        await withMemoryServer(async (server) => {
            const handleToolCall = getHandleToolCall(server);
            for (let index = 0; index < 64; index += 1) {
                await handleToolCall.call(server, "create_memory", {
                    memory_type: "recently_memory",
                    uri: `recent://capacity/${index}`,
                    content: `recent ${index}`,
                });
            }
            await assert.rejects(async () => {
                await handleToolCall.call(server, "create_memory", {
                    memory_type: "recently_memory",
                    uri: "recent://capacity/overflow",
                    content: "overflow",
                });
            }, (error) => {
                assert.ok(error instanceof MemoryCapacityError);
                assert.equal(error.details.code, "MEMORY_TYPE_LIMIT_REACHED");
                assert.equal(error.details.memory_type, "recently_memory");
                assert.equal(error.details.domain, "recent");
                assert.equal(error.details.current, 64);
                assert.equal(error.details.limit, 64);
                assert.ok(error.details.recommended_actions.includes("archive_recently_memory({ uri })"));
                return true;
            });
        });
    });
    it("poll_message injects a cleanup notice when listable memory exceeds soft limits", async () => {
        await withMemoryServer(async (server) => {
            const handleToolCall = getHandleToolCall(server);
            Reflect.set(server, "agentClient", {
                fetchUnreadMessages: async () => ({
                    directMessages: [],
                    groupMessages: [],
                }),
                getState: () => ({ agentId: "agent-self" }),
                getStatusReporter: () => null,
            });
            for (let index = 0; index < 33; index += 1) {
                await handleToolCall.call(server, "create_memory", {
                    memory_type: "recently_memory",
                    uri: `recent://cleanup/${index}`,
                    content: `recent ${index}`,
                });
            }
            const result = (await handleToolCall.call(server, "poll_message", {}));
            assert.equal(result.groupMessages.length, 0);
            assert.equal(result.directMessages.length, 1);
            assert.match(result.directMessages[0]?.msgId, /^memory-cleanup-/);
            assert.equal(result.directMessages[0]?.senderId, "MCP-System");
            assert.equal(result.directMessages[0]?.receiverId, "agent-self");
            assert.match(result.directMessages[0]?.content ?? "", /本地 memory 已超过建议容量/);
            assert.match(result.directMessages[0]?.content ?? "", /recently_memory\/recent 类型下当前 33 条 memory/);
            assert.match(result.directMessages[0]?.content ?? "", /get_memory_stats/);
            assert.match(result.directMessages[0]?.prompt ?? "", /无需持久化/);
        });
    });
    it("poll_message should claim backend tasks before returning a cleanup notice", async () => {
        await withMemoryServer(async (server) => {
            const handleToolCall = getHandleToolCall(server);
            const taskNotification = {
                msgId: "task-notify-before-cleanup",
                senderId: "MCP-System",
                senderName: "系统",
                receiverId: "agent-self",
                content: "你有任务需要关注！cleanup 前已完成静默抢占。",
                requireReply: false,
                timestamp: new Date().toISOString(),
            };
            const fetchOptions = [];
            Reflect.set(server, "agentClient", {
                fetchUnreadMessages: async (options) => {
                    fetchOptions.push(options ?? {});
                    return {
                        directMessages: [taskNotification],
                        groupMessages: [],
                    };
                },
                getState: () => ({ agentId: "agent-self" }),
                getStatusReporter: () => null,
            });
            for (let index = 0; index < 33; index += 1) {
                await handleToolCall.call(server, "create_memory", {
                    memory_type: "recently_memory",
                    uri: `recent://cleanup-claim/${index}`,
                    content: `recent ${index}`,
                });
            }
            const result = (await handleToolCall.call(server, "poll_message", {}));
            assert.equal(fetchOptions.length, 1);
            assert.equal(fetchOptions[0]?.blockIfEmpty, false);
            assert.equal(fetchOptions[0]?.includePipelineTasks, true);
            assert.equal(result.groupMessages.length, 0);
            assert.equal(result.directMessages.length, 2);
            assert.match(result.directMessages[0]?.msgId, /^memory-cleanup-/);
            assert.equal(result.directMessages[1]?.msgId, taskNotification.msgId);
        });
    });
    it("get_message injects a cleanup notice when listable memory exceeds soft limits", async () => {
        await withMemoryServer(async (server) => {
            const handleToolCall = getHandleToolCall(server);
            Reflect.set(server, "agentClient", {
                fetchUnreadMessages: async () => ({
                    directMessages: [],
                    groupMessages: [],
                }),
                getState: () => ({ agentId: "agent-self" }),
                getStatusReporter: () => null,
            });
            for (let index = 0; index < 33; index += 1) {
                await handleToolCall.call(server, "create_memory", {
                    memory_type: "recently_memory",
                    uri: `recent://cleanup-get/${index}`,
                    content: `recent ${index}`,
                });
            }
            const result = (await handleToolCall.call(server, "get_message", {}));
            assert.equal(result.fromBuffer, true);
            assert.equal(result.groupMessages.length, 0);
            assert.equal(result.directMessages.length, 1);
            assert.match(result.directMessages[0]?.msgId, /^memory-cleanup-/);
            assert.equal(result.directMessages[0]?.senderId, "MCP-System");
            assert.equal(result.directMessages[0]?.receiverId, "agent-self");
            assert.match(result.directMessages[0]?.content ?? "", /本地 memory 已超过建议容量/);
            assert.match(result.directMessages[0]?.content ?? "", /recently_memory\/recent 类型下当前 33 条 memory/);
            assert.match(result.directMessages[0]?.prompt ?? "", /无需持久化/);
        });
    });
    it("rejects incomplete memory update arguments", async () => {
        await withMemoryServer(async (server) => {
            const handleToolCall = getHandleToolCall(server);
            await handleToolCall.call(server, "create_memory", {
                memory_type: "memory",
                uri: "core://agent/incomplete-update",
                content: "original",
            });
            await assert.rejects(() => handleToolCall.call(server, "update_memory", {
                uri: "core://agent/incomplete-update",
                old_string: "original",
            }), /old_string and new_string must be provided together/);
        });
    });
    it("redacts memory content from status reporting", async () => {
        await withMemoryServer(async (server) => {
            const reported = [];
            const statusReporter = {
                reportToolUse: (name, args) => reported.push({ name, args }),
                reportToolResult: () => undefined,
                reportToolError: () => undefined,
                reportThinking: () => undefined,
                reportWaitingInput: () => undefined,
                reportError: () => undefined,
            };
            Reflect.set(server, "agentClient", {
                getStatusReporter: () => statusReporter,
            });
            const handleToolCall = getHandleToolCall(server);
            await handleToolCall.call(server, "create_memory", {
                memory_type: "memory",
                uri: "core://agent/private",
                content: "secret local memory content",
            });
            await handleToolCall.call(server, "update_memory", {
                uri: "core://agent/private",
                old_string: "secret",
                new_string: "private",
                append: "\nnever report this append",
            });
            await handleToolCall.call(server, "search_memory", {
                query: "private search phrase",
                limit: 3,
            });
            assert.deepEqual(reported[0], {
                name: "create_memory",
                args: { uri: "core://agent/private", memory_type: "memory" },
            });
            assert.deepEqual(reported[1], {
                name: "update_memory",
                args: { uri: "core://agent/private" },
            });
            assert.deepEqual(reported[2], {
                name: "search_memory",
                args: { limit: 3, queryProvided: true },
            });
        });
    });
    it("keeps normal non-memory status reporting unchanged", async () => {
        const server = new McpServer();
        const reported = [];
        const statusReporter = {
            reportToolUse: (name, args) => reported.push({ name, args }),
            reportToolResult: () => undefined,
            reportToolError: () => undefined,
            reportThinking: () => undefined,
            reportWaitingInput: () => undefined,
            reportError: () => undefined,
        };
        Reflect.set(server, "agentClient", {
            sendGroupMessage: async () => ({
                msgId: 1,
                timestamp: new Date().toISOString(),
            }),
            getStatusReporter: () => statusReporter,
        });
        const handleToolCall = getHandleToolCall(server);
        await handleToolCall.call(server, "send_group", {
            content: "normal message",
            roomId: "project:demo",
        });
        assert.deepEqual(reported, [
            {
                name: "send_group",
                args: { content: "normal message", roomId: "project:demo" },
            },
        ]);
    });
    it("rejects invalid memory type before writing", async () => {
        await withMemoryServer(async (server) => {
            const handleToolCall = getHandleToolCall(server);
            await assert.rejects(() => handleToolCall.call(server, "create_memory", {
                memory_type: "unknown",
                uri: "core://agent/invalid-type",
                content: "should not write",
            }), /Invalid memory_type/);
        });
    });
    it("rejects duplicate create and returns deleted false for missing memory", async () => {
        await withMemoryServer(async (server) => {
            const handleToolCall = getHandleToolCall(server);
            await handleToolCall.call(server, "create_memory", {
                memory_type: "memory",
                uri: "core://agent/duplicate",
                content: "first",
            });
            await assert.rejects(() => handleToolCall.call(server, "create_memory", {
                memory_type: "memory",
                uri: "core://agent/duplicate",
                content: "second",
            }), /Memory already exists/);
            const missingDelete = await handleToolCall.call(server, "delete_memory", {
                uri: "core://agent/not-found",
            });
            assert.deepEqual(missingDelete, { deleted: false });
        });
    });
    it("rejects empty append-only updates as no-op", async () => {
        await withMemoryServer(async (server) => {
            const handleToolCall = getHandleToolCall(server);
            await handleToolCall.call(server, "create_memory", {
                memory_type: "memory",
                uri: "core://agent/noop",
                content: "original",
            });
            await assert.rejects(() => handleToolCall.call(server, "update_memory", {
                uri: "core://agent/noop",
                append: "",
            }), /Provide either old_string\/new_string or append/);
        });
    });
});
describe("McpServer group discovery tool", () => {
    it("get_group_rooms should proxy discoverable groups from agent client", async () => {
        const server = new McpServer();
        Reflect.set(server, "agentClient", {
            discoverGroupRooms: async () => ({
                groups: [
                    {
                        id: "project:demo",
                        projectName: "demo",
                        roomType: "project",
                        scopeRefId: "demo",
                        name: "demo 公共群聊",
                        memberUserIds: ["agent-1"],
                    },
                    {
                        id: "inner_group:g-1",
                        projectName: "demo",
                        roomType: "inner_group",
                        scopeRefId: "g-1",
                        name: "后端小组",
                        memberUserIds: ["agent-1", "agent-2"],
                    },
                ],
            }),
            getStatusReporter: () => null,
        });
        const handleToolCall = Reflect.get(server, "handleToolCall");
        const result = await handleToolCall.call(server, "get_group_rooms", {});
        assert.deepEqual(result, {
            groups: [
                {
                    id: "project:demo",
                    projectName: "demo",
                    roomType: "project",
                    scopeRefId: "demo",
                    name: "demo 公共群聊",
                    memberUserIds: ["agent-1"],
                },
                {
                    id: "inner_group:g-1",
                    projectName: "demo",
                    roomType: "inner_group",
                    scopeRefId: "g-1",
                    name: "后端小组",
                    memberUserIds: ["agent-1", "agent-2"],
                },
            ],
        });
    });
    it("send_group should forward roomId to agent client", async () => {
        const server = new McpServer();
        let capturedArgs = null;
        Reflect.set(server, "agentClient", {
            sendGroupMessage: async (content, projectName, roomId) => {
                capturedArgs = { content, projectName, roomId };
                return {
                    msgId: 1,
                    timestamp: new Date().toISOString(),
                };
            },
            getStatusReporter: () => null,
        });
        const handleToolCall = Reflect.get(server, "handleToolCall");
        await handleToolCall.call(server, "send_group", {
            content: "hello room",
            projectName: "demo",
            roomId: "inner_group:g-1",
        });
        assert.deepEqual(capturedArgs, {
            content: "hello room",
            projectName: "demo",
            roomId: "inner_group:g-1",
        });
    });
    it("hire_colleague should forward role creation payload to agent client", async () => {
        const server = new McpServer();
        let capturedArgs = null;
        const hiredResult = {
            id: "agent-new",
            displayName: "后端开发同事-AB12",
            roleName: "后端开发同事",
            projectName: "demo",
            workspacePath: "D:/workspace/demo",
            runtimeBridgeBaseUrl: "http://127.0.0.1:18081",
            runtimeStatus: "stopped",
        };
        Reflect.set(server, "agentClient", {
            hireColleague: async (args) => {
                capturedArgs = args;
                return hiredResult;
            },
            getStatusReporter: () => null,
        });
        const handleToolCall = Reflect.get(server, "handleToolCall");
        const result = await handleToolCall.call(server, "hire_colleague", {
            projectName: "demo",
            roleName: "后端开发同事",
            displayName: "后端开发同事-AB12",
            prompt: "负责后端接口开发",
            workspacePath: "D:/workspace/demo",
            runtimeSkillEnabled: true,
            runtimeMcpEnabled: true,
        });
        assert.deepEqual(capturedArgs, {
            projectName: "demo",
            roleName: "后端开发同事",
            displayName: "后端开发同事-AB12",
            prompt: "负责后端接口开发",
            workspacePath: "D:/workspace/demo",
            runtimeSkillEnabled: true,
            runtimeMcpEnabled: true,
        });
        assert.deepEqual(result, hiredResult);
    });
    it("hire_colleague should validate required workspacePath", async () => {
        const server = new McpServer();
        const handleToolCall = Reflect.get(server, "handleToolCall");
        await assert.rejects(async () => {
            await handleToolCall.call(server, "hire_colleague", {
                roleName: "后端开发同事",
            });
        }, /workspacePath is required/);
    });
    it("hire_colleagues_batch should forward batch payload to agent client", async () => {
        const server = new McpServer();
        let capturedArgs = null;
        const batchResult = {
            batchId: "hire-batch-001",
            projectName: "demo",
            summary: {
                total: 2,
                approved: 1,
                rejected: 1,
                created: 1,
                failed: 0,
            },
            results: [
                {
                    itemId: "item-1",
                    decision: "APPROVED",
                    status: "CREATED",
                    createdAgent: {
                        id: "agent-new",
                        displayName: "后端开发同事-AB12",
                        roleName: "后端开发同事",
                    },
                },
                {
                    itemId: "item-2",
                    decision: "REJECTED",
                    status: "REJECTED",
                    reason: "当前阶段暂不需要",
                },
            ],
        };
        Reflect.set(server, "agentClient", {
            hireColleaguesBatch: async (args) => {
                capturedArgs = args;
                return batchResult;
            },
            getStatusReporter: () => null,
        });
        const handleToolCall = Reflect.get(server, "handleToolCall");
        const result = await handleToolCall.call(server, "hire_colleagues_batch", {
            projectName: "demo",
            items: [
                {
                    roleName: "后端开发同事",
                    displayName: "后端开发同事-AB12",
                    prompt: "负责后端接口开发",
                    workspacePath: "D:/workspace/demo/backend",
                },
                {
                    newRoleName: "测试开发同事",
                    displayName: "测试开发同事-CD34",
                    prompt: "负责接口测试",
                    workspacePath: "D:/workspace/demo/test",
                },
            ],
        });
        assert.deepEqual(capturedArgs, {
            projectName: "demo",
            items: [
                {
                    roleName: "后端开发同事",
                    displayName: "后端开发同事-AB12",
                    prompt: "负责后端接口开发",
                    workspacePath: "D:/workspace/demo/backend",
                },
                {
                    newRoleName: "测试开发同事",
                    displayName: "测试开发同事-CD34",
                    prompt: "负责接口测试",
                    workspacePath: "D:/workspace/demo/test",
                },
            ],
        });
        assert.deepEqual(result, batchResult);
    });
    it("hire_colleagues_batch should validate required items", async () => {
        const server = new McpServer();
        const handleToolCall = Reflect.get(server, "handleToolCall");
        await assert.rejects(async () => {
            await handleToolCall.call(server, "hire_colleagues_batch", {
                projectName: "demo",
                items: [],
            });
        }, /items is required/);
    });
    it("hire_colleagues_batch should validate every item workspacePath", async () => {
        const server = new McpServer();
        const handleToolCall = Reflect.get(server, "handleToolCall");
        await assert.rejects(async () => {
            await handleToolCall.call(server, "hire_colleagues_batch", {
                projectName: "demo",
                items: [
                    {
                        roleName: "后端开发同事",
                        workspacePath: "D:/workspace/demo/backend",
                    },
                    {
                        roleName: "测试开发同事",
                    },
                ],
            });
        }, /items\[1\].workspacePath is required/);
    });
});
describe("McpServer incoming group message filtering", () => {
    it("should ignore self-sent group messages pushed from websocket events", async () => {
        const server = new McpServer();
        Reflect.set(server, "messageBuffer", new MessageBuffer());
        Reflect.set(server, "agentClient", {
            getState: () => ({
                agentId: "agent-self",
                displayName: "Self Agent",
                isConnected: true,
                isRegistered: true,
            }),
            getStatusReporter: () => null,
        });
        const selfGroupMessage = {
            msgId: 501,
            senderId: "agent-self",
            senderName: "Self Agent",
            content: "loopback group message",
            timestamp: new Date().toISOString(),
            projectName: "demo",
            roomId: "project:demo",
        };
        const handleIncomingMessage = Reflect.get(server, "handleIncomingMessage");
        await handleIncomingMessage.call(server, selfGroupMessage);
        const messageBuffer = Reflect.get(server, "messageBuffer");
        const drained = messageBuffer.get();
        assert.deepEqual(drained, {
            directMessages: [],
            groupMessages: [],
        });
    });
    it("should ignore persisted pipeline task assignment direct messages from websocket events", async () => {
        const server = new McpServer();
        Reflect.set(server, "messageBuffer", new MessageBuffer());
        Reflect.set(server, "agentClient", {
            getState: () => ({
                agentId: "agent-loser",
                displayName: "Loser Agent",
                isConnected: true,
                isRegistered: true,
            }),
            getStatusReporter: () => null,
        });
        const taskAssignedMessage = {
            msgId: "task-assigned-1",
            senderId: "MCP-System",
            senderName: "MCP System",
            receiverId: "agent-loser",
            content: "🔔 [系统通知] 您有 1 个新的流程线任务正在等待处理。\n{\"type\":\"pipeline_task_assigned\",\"nodeId\":\"node-1\"}",
            requireReply: false,
            timestamp: new Date().toISOString(),
        };
        const handleIncomingMessage = Reflect.get(server, "handleIncomingMessage");
        await handleIncomingMessage.call(server, taskAssignedMessage);
        const messageBuffer = Reflect.get(server, "messageBuffer");
        const drained = messageBuffer.get();
        assert.deepEqual(drained, {
            directMessages: [],
            groupMessages: [],
        });
    });
});
describe("McpServer unread direct message filtering", () => {
    it("should not return persisted pipeline task assignment messages from local buffer", async () => {
        const taskAssignedMessage = {
            msgId: "task-assigned-2",
            senderId: "MCP-System",
            senderName: "MCP System",
            receiverId: "agent-loser",
            content: "🔔 [系统通知] 您有 1 个新的流程线任务正在等待处理。\n{\"type\":\"pipeline_task_assigned\",\"nodeId\":\"node-1\"}",
            requireReply: false,
            prompt: "pipeline_task_assigned wake signal",
            timestamp: new Date().toISOString(),
        };
        const normalMessage = {
            msgId: "normal-1",
            senderId: "human-1",
            senderName: "Human",
            receiverId: "agent-loser",
            content: "normal direct message",
            requireReply: false,
            timestamp: new Date().toISOString(),
        };
        const server = createServerWithUnread({
            directMessages: [taskAssignedMessage, normalMessage],
            groupMessages: [],
        });
        const handleToolCall = Reflect.get(server, "handleToolCall");
        const result = (await handleToolCall.call(server, "get_message", {}));
        assert.equal(result.directMessages.length, 1);
        assert.equal(result.directMessages[0]?.msgId, "normal-1");
        assert.equal(result.groupMessages.length, 0);
    });
});
describe("AgentClient group room aggregation", () => {
    it("fetchUnreadMessages should aggregate unread messages from all discoverable rooms", async () => {
        const client = new AgentClient();
        Reflect.set(client, "agentId", "agent-1");
        Reflect.set(client, "projectName", "demo");
        const calls = [];
        Reflect.set(client, "httpClient", {
            callTool: async (toolName, args) => {
                calls.push({ toolName, args });
                if (toolName === "get_dm_messages") {
                    return { messages: [] };
                }
                if (toolName === "discover_group_rooms") {
                    return {
                        groups: [
                            {
                                id: "project:demo",
                                projectName: "demo",
                                roomType: "project",
                                scopeRefId: "demo",
                                name: "demo 公共群聊",
                                memberUserIds: ["agent-1"],
                            },
                            {
                                id: "inner_group:g-1",
                                projectName: "demo",
                                roomType: "inner_group",
                                scopeRefId: "g-1",
                                name: "后端小组",
                                memberUserIds: ["agent-1", "agent-2"],
                            },
                        ],
                    };
                }
                if (toolName === "get_group_messages") {
                    if (args.roomId === "project:demo") {
                        return {
                            messages: [
                                {
                                    msgId: 1,
                                    senderId: "agent-a",
                                    senderName: "Agent A",
                                    projectName: "demo",
                                    roomId: "project:demo",
                                    content: "project message",
                                    timestamp: new Date().toISOString(),
                                },
                            ],
                            currentReadPos: 1,
                            hasMore: false,
                        };
                    }
                    if (args.roomId === "inner_group:g-1") {
                        return {
                            messages: [
                                {
                                    msgId: 2,
                                    senderId: "agent-b",
                                    senderName: "Agent B",
                                    projectName: "demo",
                                    roomId: "inner_group:g-1",
                                    content: "inner group message",
                                    timestamp: new Date().toISOString(),
                                },
                            ],
                            currentReadPos: 2,
                            hasMore: false,
                        };
                    }
                }
                throw new Error(`unexpected tool call: ${toolName}`);
            },
        });
        const result = await client.fetchUnreadMessages();
        assert.equal(result.directMessages.length, 0);
        assert.deepEqual(result.groupMessages.map((message) => ({
            roomId: message.roomId,
            content: message.content,
        })), [
            { roomId: "project:demo", content: "project message" },
            { roomId: "inner_group:g-1", content: "inner group message" },
        ]);
        assert.equal(calls.filter((call) => call.toolName === "get_group_messages").length, 2);
    });
    it("getGroupHistory should aggregate history from all discoverable rooms when roomId is omitted", async () => {
        const client = new AgentClient();
        Reflect.set(client, "agentId", "agent-1");
        Reflect.set(client, "projectName", "demo");
        Reflect.set(client, "httpClient", {
            callTool: async (toolName, args) => {
                if (toolName === "discover_group_rooms") {
                    return {
                        groups: [
                            {
                                id: "project:demo",
                                projectName: "demo",
                                roomType: "project",
                                scopeRefId: "demo",
                                name: "demo 公共群聊",
                                memberUserIds: ["agent-1"],
                            },
                            {
                                id: "inner_group:g-1",
                                projectName: "demo",
                                roomType: "inner_group",
                                scopeRefId: "g-1",
                                name: "后端小组",
                                memberUserIds: ["agent-1", "agent-2"],
                            },
                        ],
                    };
                }
                if (toolName === "get_group_history") {
                    if (args.roomId === "project:demo") {
                        return {
                            messages: [
                                {
                                    msgId: 1,
                                    senderId: "agent-a",
                                    senderName: "Agent A",
                                    projectName: "demo",
                                    roomId: "project:demo",
                                    content: "project history",
                                    timestamp: "2026-03-25T10:00:00.000Z",
                                },
                            ],
                            count: 1,
                        };
                    }
                    if (args.roomId === "inner_group:g-1") {
                        return {
                            messages: [
                                {
                                    msgId: 2,
                                    senderId: "agent-b",
                                    senderName: "Agent B",
                                    projectName: "demo",
                                    roomId: "inner_group:g-1",
                                    content: "group history",
                                    timestamp: "2026-03-25T11:00:00.000Z",
                                },
                            ],
                            count: 1,
                        };
                    }
                }
                throw new Error(`unexpected tool call: ${toolName}`);
            },
        });
        const result = await client.getGroupHistory(50);
        assert.deepEqual(result.messages.map((message) => ({
            roomId: message.roomId,
            content: message.content,
        })), [
            { roomId: "project:demo", content: "project history" },
            { roomId: "inner_group:g-1", content: "group history" },
        ]);
        assert.equal(result.count, 2);
    });
});
//# sourceMappingURL=mcp-server.test.js.map