import { randomUUID } from "node:crypto";
import {
  ARCHESTRA_MCP_CATALOG_ID,
  getArchestraMcpCatalogName,
  getArchestraToolFullName,
  MCP_SERVER_TOOL_NAME_SEPARATOR,
  TOOL_QUERY_KNOWLEDGE_SOURCES_FULL_NAME,
  TOOL_QUERY_KNOWLEDGE_SOURCES_SHORT_NAME,
  TOOL_RUN_TOOL_FULL_NAME,
  TOOL_SEARCH_TOOLS_FULL_NAME,
  TOOL_TODO_WRITE_FULL_NAME,
  TOOL_TODO_WRITE_SHORT_NAME,
} from "@archestra/shared";
import { and, eq, sql } from "drizzle-orm";
import { beforeEach, vi } from "vitest";
import { archestraMcpBranding } from "@/archestra-mcp-server";
import { getArchestraMcpCatalogMetadata } from "@/archestra-mcp-server/metadata";
import config from "@/config";
import db, { schema } from "@/database";
import { describe, expect, test } from "@/test";
import AgentToolModel from "./agent-tool";
import OrganizationModel from "./organization";
import TeamModel from "./team";
import ToolModel, { parseArchestraBuiltInName } from "./tool";
import ToolInvocationPolicyModel from "./tool-invocation-policy";
import TrustedDataPolicyModel from "./trusted-data-policy";

// these suites assert exact assigned-tool sets after agent creation; pin the
// sandbox runtime off so its tools do not leak into the default-assignment
// counts (relying on the worker-pristine baseline for this let a rare
// cross-file ordering surface sandbox tools). App tools are seeded and
// auto-assigned to every agent; their assignment is covered in
// tool-archestra-assignment.test.ts.
beforeEach(() => {
  (config.skillsSandbox as { enabled: boolean }).enabled = false;
});

describe("ToolModel", () => {
  describe("slugifyName", () => {
    test("creates valid tool name from simple server and tool names", () => {
      const result = ToolModel.slugifyName("github", "list_repos");
      expect(result).toBe(`github${MCP_SERVER_TOOL_NAME_SEPARATOR}list_repos`);
    });

    test("converts to lowercase", () => {
      const result = ToolModel.slugifyName("GitHub", "ListRepos");
      expect(result).toBe(`github${MCP_SERVER_TOOL_NAME_SEPARATOR}listrepos`);
    });

    test("replaces spaces with underscores", () => {
      const result = ToolModel.slugifyName("My Server", "list all repos");
      expect(result).toBe(
        `my_server${MCP_SERVER_TOOL_NAME_SEPARATOR}list_all_repos`,
      );
    });

    test("removes brackets from server name", () => {
      const result = ToolModel.slugifyName(
        "[AI SRE Demo] Kubernetes MCP Server",
        "list_namespaces",
      );
      expect(result).toBe(
        `ai_sre_demo_kubernetes_mcp_server${MCP_SERVER_TOOL_NAME_SEPARATOR}list_namespaces`,
      );
    });

    test("removes parentheses from server name", () => {
      const result = ToolModel.slugifyName("Server (Production)", "get_status");
      expect(result).toBe(
        `server_production${MCP_SERVER_TOOL_NAME_SEPARATOR}get_status`,
      );
    });

    test("removes special characters while preserving hyphens", () => {
      const result = ToolModel.slugifyName("my-server!@#$%", "tool-name");
      expect(result).toBe(
        `my-server${MCP_SERVER_TOOL_NAME_SEPARATOR}tool-name`,
      );
    });

    test("collapses multiple consecutive spaces into single underscore", () => {
      const result = ToolModel.slugifyName("My   Server", "list    repos");
      // Multiple spaces become a single underscore for cleaner names
      expect(result).toBe(
        `my_server${MCP_SERVER_TOOL_NAME_SEPARATOR}list_repos`,
      );
    });

    test("handles tabs and newlines as whitespace", () => {
      const result = ToolModel.slugifyName("My\tServer", "list\nrepos");
      expect(result).toBe(
        `my_server${MCP_SERVER_TOOL_NAME_SEPARATOR}list_repos`,
      );
    });

    test("preserves numbers in names", () => {
      const result = ToolModel.slugifyName("Server123", "tool456");
      expect(result).toBe(`server123${MCP_SERVER_TOOL_NAME_SEPARATOR}tool456`);
    });

    test("handles empty tool name", () => {
      const result = ToolModel.slugifyName("server", "");
      expect(result).toBe(`server${MCP_SERVER_TOOL_NAME_SEPARATOR}`);
    });

    test("produces names matching LLM provider pattern", () => {
      // Anthropic pattern: ^[a-zA-Z0-9_-]{1,128}$
      const pattern = /^[a-zA-Z0-9_-]+$/;

      const testCases = [
        ["[AI SRE Demo] Kubernetes MCP Server", "list_namespaces"],
        ["Server (v2.0)", "get_data"],
        ["My Server!", "tool@name"],
        ["Test & Demo", "run#test"],
        ["Unicode: 日本語", "tool"],
      ];

      for (const [serverName, toolName] of testCases) {
        const result = ToolModel.slugifyName(serverName, toolName);
        expect(result).toMatch(pattern);
      }
    });
  });

  describe("unslugifyName", () => {
    test("extracts tool name from slugified name", () => {
      const slugified = `server${MCP_SERVER_TOOL_NAME_SEPARATOR}list_repos`;
      const result = ToolModel.unslugifyName(slugified);
      expect(result).toBe("list_repos");
    });

    test("handles server names containing separator (e.g. upstash__context7)", () => {
      const result = ToolModel.unslugifyName(
        "upstash__context7__resolve-library-id",
      );
      expect(result).toBe("resolve-library-id");
    });

    test("returns original name if no separator found", () => {
      const result = ToolModel.unslugifyName("simple_tool_name");
      expect(result).toBe("simple_tool_name");
    });

    test("handles empty string after separator", () => {
      const slugified = `server${MCP_SERVER_TOOL_NAME_SEPARATOR}`;
      const result = ToolModel.unslugifyName(slugified);
      expect(result).toBe("");
    });
  });

  describe("Access Control", () => {
    test("admin can see all tools", async ({
      makeAdmin,
      makeAgent,
      makeTool,
      makeAgentTool,
    }) => {
      const admin = await makeAdmin();
      const agent1 = await makeAgent({ name: "Agent1" });
      const agent2 = await makeAgent({ name: "Agent2" });

      const tool1 = await makeTool({
        name: "tool1",
        description: "Tool 1",
      });
      await makeAgentTool(agent1.id, tool1.id);

      const tool2 = await makeTool({
        name: "tool2",
        description: "Tool 2",
        parameters: {},
      });
      await makeAgentTool(agent2.id, tool2.id);

      const tools = await ToolModel.findAll(admin.id, true);
      // Expects exactly 2 proxy-discovered tools (Archestra tools are no longer auto-assigned)
      expect(tools.length).toBe(2);
    });

    test("non-admin only sees MCP tools, not proxy tools", async ({
      makeUser,
      makeAdmin,
      makeOrganization,
      makeTeam,
      makeAgent,
      makeTool,
      makeAgentTool,
      makeInternalMcpCatalog,
    }) => {
      const user1 = await makeUser();
      const user2 = await makeUser();
      const admin = await makeAdmin();
      const org = await makeOrganization();

      // Create teams and add users
      const team1 = await makeTeam(org.id, admin.id, { name: "Team 1" });
      await TeamModel.addMember(team1.id, user1.id);

      const team2 = await makeTeam(org.id, admin.id, { name: "Team 2" });
      await TeamModel.addMember(team2.id, user2.id);

      // Create agents with team assignments
      const agent1 = await makeAgent({ name: "Agent1", teams: [team1.id] });
      const agent2 = await makeAgent({ name: "Agent2", teams: [team2.id] });

      const catalog = await makeInternalMcpCatalog();

      // Proxy tools (no catalogId) — not visible to non-admins
      const proxyTool1 = await makeTool({
        name: "tool1",
        description: "Tool 1",
        parameters: {},
      });
      await makeAgentTool(agent1.id, proxyTool1.id);

      const proxyTool2 = await makeTool({
        name: "tool2",
        description: "Tool 2",
        parameters: {},
      });
      await makeAgentTool(agent2.id, proxyTool2.id);

      // MCP tool (catalogId set) — visible to non-admins
      const mcpTool = await makeTool({
        name: "mcp-tool",
        description: "MCP Tool",
        catalogId: catalog.id,
      });
      await makeAgentTool(agent1.id, mcpTool.id);

      // Non-admin user only sees MCP tools, not proxy tools
      const tools = await ToolModel.findAll(user1.id, false);
      expect(tools).toHaveLength(1);
      expect(tools[0].id).toBe(mcpTool.id);
    });

    test("member with no access sees only MCP tools", async ({
      makeUser,
      makeAgent,
      makeTool,
      makeAgentTool,
    }) => {
      const user = await makeUser();
      const agent1 = await makeAgent({ name: "Agent1" });

      // Proxy tool — not visible to non-admins
      const tool1 = await makeTool({
        name: "tool1",
        description: "Tool 1",
      });
      await makeAgentTool(agent1.id, tool1.id);

      const tools = await ToolModel.findAll(user.id, false);
      expect(tools).toHaveLength(0);
    });

    test("findById returns tool for admin", async ({
      makeAdmin,
      makeAgent,
      makeTool,
      makeAgentTool,
    }) => {
      const admin = await makeAdmin();
      const agent = await makeAgent();

      const tool = await makeTool({
        name: "test-tool",
        description: "Test Tool",
        parameters: {},
      });
      await makeAgentTool(agent.id, tool.id);

      const found = await ToolModel.findById(tool.id, admin.id, true);
      expect(found).not.toBeNull();
      expect(found?.id).toBe(tool.id);
    });

    test("findById returns tool for user with agent access", async ({
      makeUser,
      makeAdmin,
      makeOrganization,
      makeTeam,
      makeAgent,
      makeTool,
      makeAgentTool,
    }) => {
      const user = await makeUser();
      const admin = await makeAdmin();
      const org = await makeOrganization();

      // Create team and add user
      const team = await makeTeam(org.id, admin.id);
      await TeamModel.addMember(team.id, user.id);

      const agent = await makeAgent({ teams: [team.id] });

      const tool = await makeTool({
        name: "test-tool",
        description: "Test Tool",
        parameters: {},
      });
      await makeAgentTool(agent.id, tool.id);

      // Proxy tools with agentId=null are visible to all (same as MCP tools)
      const found = await ToolModel.findById(tool.id, user.id, false);
      expect(found).not.toBeNull();
      expect(found?.id).toBe(tool.id);
    });

    test("findByName returns tool for admin", async ({
      makeAdmin,
      makeAgent,
      makeTool,
      makeAgentTool,
    }) => {
      const admin = await makeAdmin();
      const agent = await makeAgent();

      const tool = await makeTool({
        name: "unique-tool",
        description: "Unique Tool",
        parameters: {},
      });
      await makeAgentTool(agent.id, tool.id);

      const found = await ToolModel.findByName("unique-tool", admin.id, true);
      expect(found).not.toBeNull();
      expect(found?.name).toBe("unique-tool");
    });

    test("findByName returns tool for user with agent access", async ({
      makeUser,
      makeAdmin,
      makeOrganization,
      makeTeam,
      makeAgent,
      makeTool,
      makeAgentTool,
    }) => {
      const user = await makeUser();
      const admin = await makeAdmin();
      const org = await makeOrganization();

      // Create team and add user
      const team = await makeTeam(org.id, admin.id);
      await TeamModel.addMember(team.id, user.id);

      const agent = await makeAgent({ teams: [team.id] });

      const tool = await makeTool({
        name: "user-tool",
        description: "User Tool",
        parameters: {},
      });
      await makeAgentTool(agent.id, tool.id);

      const found = await ToolModel.findByName("user-tool", user.id, false);
      expect(found).not.toBeNull();
      expect(found?.name).toBe("user-tool");
    });
  });

  describe("getMcpToolsAssignedToAgent", () => {
    test("returns empty array when no tools provided", async ({
      makeAgent,
      makeUser,
    }) => {
      const _user = await makeUser();
      const agent = await makeAgent();

      const result = await ToolModel.getMcpToolsAssignedToAgent([], agent.id);
      expect(result).toEqual([]);
    });

    test("returns empty array when no MCP tools assigned to agent", async ({
      makeAgent,
      makeUser,
      makeTool,
      makeAgentTool,
    }) => {
      const _user = await makeUser();
      const agent = await makeAgent();

      // Create a proxy-sniffed tool (no catalogId) and assign via junction
      const proxyTool = await makeTool({
        name: "proxy_tool",
        description: "Proxy Tool",
        parameters: {},
      });
      await makeAgentTool(agent.id, proxyTool.id);

      const result = await ToolModel.getMcpToolsAssignedToAgent(
        ["proxy_tool", "non_existent"],
        agent.id,
      );
      expect(result).toEqual([]);
    });

    test("returns MCP tools with server metadata for assigned tools", async ({
      makeUser,
      makeAgent,
      makeInternalMcpCatalog,
      makeMcpServer,
      makeTool,
    }) => {
      const user = await makeUser();
      const agent = await makeAgent();

      const catalogItem = await makeInternalMcpCatalog({
        name: "github-mcp-server",
        serverUrl: "https://api.githubcopilot.com/mcp/",
      });

      // Create an MCP server with GitHub metadata
      await makeMcpServer({
        name: "test-github-server",
        catalogId: catalogItem.id,
        ownerId: user.id,
      });

      // Create an MCP tool
      const mcpTool = await makeTool({
        name: "github_mcp_server__list_issues",
        description: "List GitHub issues",
        parameters: {
          type: "object",
          properties: {
            repo: { type: "string" },
            count: { type: "number" },
          },
        },
        catalogId: catalogItem.id,
      });

      // Assign tool to agent
      await AgentToolModel.create(agent.id, mcpTool.id);

      const result = await ToolModel.getMcpToolsAssignedToAgent(
        ["github_mcp_server__list_issues"],
        agent.id,
      );

      expect(result).toHaveLength(1);
      expect(result[0]).toEqual({
        toolName: "github_mcp_server__list_issues",
        mcpServerId: null,
        catalogId: catalogItem.id,
        catalogName: "github-mcp-server",
        credentialResolutionMode: "static",
        meta: null,
      });
    });

    test("filters to only requested tool names", async ({
      makeUser,
      makeAgent,
      makeInternalMcpCatalog,
      makeMcpServer,
      makeTool,
    }) => {
      const user = await makeUser();
      const agent = await makeAgent();

      const catalogItem = await makeInternalMcpCatalog({
        name: "github-mcp-server",
        serverUrl: "https://api.githubcopilot.com/mcp/",
      });

      // Create an MCP server
      await makeMcpServer({
        name: "test-server",
        catalogId: catalogItem.id,
        ownerId: user.id,
      });

      // Create multiple MCP tools
      const tool1 = await makeTool({
        name: "tool_one",
        description: "First tool",
        parameters: {},
        catalogId: catalogItem.id,
      });

      const tool2 = await makeTool({
        name: "tool_two",
        description: "Second tool",
        parameters: {},
        catalogId: catalogItem.id,
      });

      // Assign both tools to agent
      await AgentToolModel.create(agent.id, tool1.id);
      await AgentToolModel.create(agent.id, tool2.id);

      // Request only one tool
      const result = await ToolModel.getMcpToolsAssignedToAgent(
        ["tool_one"],
        agent.id,
      );

      expect(result).toHaveLength(1);
      expect(result[0].toolName).toBe("tool_one");
    });

    test("returns empty array when tools exist but not assigned to agent", async ({
      makeUser,
      makeAgent,
      makeInternalMcpCatalog,
      makeMcpServer,
      makeTool,
    }) => {
      const user = await makeUser();
      const agent1 = await makeAgent({ name: "Agent1" });
      const agent2 = await makeAgent({ name: "Agent2" });

      // Create an MCP server and tool
      const catalogItem = await makeInternalMcpCatalog({
        name: "github-mcp-server",
        serverUrl: "https://api.githubcopilot.com/mcp/",
      });
      await makeMcpServer({
        name: "test-server",
        catalogId: catalogItem.id,
        ownerId: user.id,
      });

      const mcpTool = await makeTool({
        name: "exclusive_tool",
        description: "Exclusive tool",
        parameters: {},
        catalogId: catalogItem.id,
      });

      // Assign tool to agent1 only
      await AgentToolModel.create(agent1.id, mcpTool.id);

      // Request tool for agent2 (should return empty)
      const result = await ToolModel.getMcpToolsAssignedToAgent(
        ["exclusive_tool"],
        agent2.id,
      );

      expect(result).toEqual([]);
    });

    test("excludes proxy-sniffed tools (tools without catalogId)", async ({
      makeUser,
      makeAgent,
      makeInternalMcpCatalog,
      makeMcpServer,
      makeTool,
      makeAgentTool,
    }) => {
      const user = await makeUser();
      const agent = await makeAgent();

      // Create an MCP server
      const catalogItem = await makeInternalMcpCatalog({
        name: "github-mcp-server",
        serverUrl: "https://api.githubcopilot.com/mcp/",
      });
      await makeMcpServer({
        name: "test-server",
        catalogId: catalogItem.id,
        ownerId: user.id,
      });

      // Create a shared proxy tool (agentId=null, catalogId=null)
      const proxyTool = await makeTool({
        name: "proxy_tool",
        description: "Proxy Tool",
        parameters: {},
      });
      await makeAgentTool(agent.id, proxyTool.id);

      // Create an MCP tool (linked via catalogId)
      const mcpTool = await makeTool({
        name: "mcp_tool",
        description: "MCP Tool",
        parameters: {},
        catalogId: catalogItem.id,
      });

      // Assign MCP tool to agent
      await AgentToolModel.create(agent.id, mcpTool.id);

      const result = await ToolModel.getMcpToolsAssignedToAgent(
        ["proxy_tool", "mcp_tool"],
        agent.id,
      );

      // Should only return the MCP tool, not the proxy-sniffed tool
      expect(result).toHaveLength(1);
      expect(result[0].toolName).toBe("mcp_tool");
    });

    test("handles multiple MCP tools with different servers", async ({
      makeUser,
      makeAgent,
      makeInternalMcpCatalog,
      makeMcpServer,
      makeTool,
    }) => {
      const user = await makeUser();
      const agent = await makeAgent();

      // Create two MCP servers
      const catalogItem = await makeInternalMcpCatalog({
        name: "github-mcp-server",
        serverUrl: "https://api.githubcopilot.com/mcp/",
      });
      await makeMcpServer({
        name: "github-server",
        catalogId: catalogItem.id,
        ownerId: user.id,
      });

      const catalogItem2 = await makeInternalMcpCatalog({
        name: "other-mcp-server",
        serverUrl: "https://api.othercopilot.com/mcp/",
      });
      await makeMcpServer({
        name: "other-server",
        catalogId: catalogItem2.id,
      });

      // Create tools for each server
      const githubTool = await makeTool({
        name: "github_list_issues",
        description: "List GitHub issues",
        parameters: {},
        catalogId: catalogItem.id,
      });

      const otherTool = await makeTool({
        name: "other_tool",
        description: "Other tool",
        parameters: {},
        catalogId: catalogItem2.id,
      });

      // Assign both tools to agent
      await AgentToolModel.create(agent.id, githubTool.id);
      await AgentToolModel.create(agent.id, otherTool.id);

      const result = await ToolModel.getMcpToolsAssignedToAgent(
        ["github_list_issues", "other_tool"],
        agent.id,
      );

      expect(result).toHaveLength(2);
    });

    test("orders the healthy-connection candidate first when two assigned tools share a name across different catalog items", async ({
      makeUser,
      makeAgent,
      makeInternalMcpCatalog,
      makeMcpServer,
    }) => {
      const user = await makeUser();
      const agent = await makeAgent();

      // Two different catalog items whose installs happen to share a
      // display name, producing two tool rows with the identical slugified
      // name — the collision a caller cannot see in the tool name alone.
      // Created directly via the model (not the makeTool fixture, whose
      // findByName lookup can't disambiguate same-named tools either).
      const brokenCatalog = await makeInternalMcpCatalog({
        name: "weather-fixture-broken",
        serverUrl: "https://weather.example.com/mcp",
      });
      await makeMcpServer({
        name: "Weather Fixture",
        catalogId: brokenCatalog.id,
        ownerId: user.id,
        localInstallationStatus: "success",
        oauthRefreshError: "refresh_failed",
      });
      const brokenTool = await ToolModel.createToolIfNotExists({
        name: "weather_fixture__get_weather",
        description: "broken connection",
        parameters: {},
        catalogId: brokenCatalog.id,
      });

      const healthyCatalog = await makeInternalMcpCatalog({
        name: "weather-fixture-healthy",
        serverUrl: "https://weather.example.com/mcp",
      });
      await makeMcpServer({
        name: "Weather Fixture",
        catalogId: healthyCatalog.id,
        ownerId: user.id,
        localInstallationStatus: "success",
      });
      const healthyTool = await ToolModel.createToolIfNotExists({
        name: "weather_fixture__get_weather",
        description: "healthy connection",
        parameters: {},
        catalogId: healthyCatalog.id,
      });

      // Assign the broken one first so a naive "first row returned" pick
      // would resolve to it.
      await AgentToolModel.create(agent.id, brokenTool.id);
      await AgentToolModel.create(agent.id, healthyTool.id);

      const result = await ToolModel.getMcpToolsAssignedToAgent(
        ["weather_fixture__get_weather"],
        agent.id,
      );

      expect(result).toHaveLength(2);
      expect(result[0].catalogId).toBe(healthyCatalog.id);
    });

    test("does not rank a static pin to a broken server as healthy just because its catalog has an unrelated working install", async ({
      makeUser,
      makeAgent,
      makeInternalMcpCatalog,
      makeMcpServer,
    }) => {
      const user = await makeUser();
      const agent = await makeAgent();

      // A catalog item with two installs: the one this agent's assignment
      // is pinned to is broken, but a different, unrelated install of the
      // same catalog item is healthy. A catalog-wide health check would
      // wrongly call the pinned assignment healthy.
      const pinnedCatalog = await makeInternalMcpCatalog({
        name: "weather-fixture-pinned",
        serverUrl: "https://weather.example.com/mcp",
      });
      const brokenPinnedServer = await makeMcpServer({
        name: "Weather Fixture",
        catalogId: pinnedCatalog.id,
        ownerId: user.id,
        localInstallationStatus: "success",
        oauthRefreshError: "refresh_failed",
      });
      await makeMcpServer({
        name: "Weather Fixture (unrelated install)",
        catalogId: pinnedCatalog.id,
        localInstallationStatus: "success",
      });
      // Explicit, deterministically ordered ids: without the fix, a
      // catalog-wide health tie falls back to sorting by id, which would
      // otherwise pick either row depending on random UUID generation.
      // Pinning the broken row's id below the healthy row's id means a
      // health tie would deterministically (wrongly) prefer it.
      const pinnedTool = await ToolModel.createToolIfNotExists({
        id: "00000000-0000-4000-8000-000000000001",
        name: "weather_fixture__get_weather",
        description: "statically pinned to the broken install",
        parameters: {},
        catalogId: pinnedCatalog.id,
      });

      const dynamicCatalog = await makeInternalMcpCatalog({
        name: "weather-fixture-dynamic",
        serverUrl: "https://weather.example.com/mcp",
      });
      await makeMcpServer({
        name: "Weather Fixture",
        catalogId: dynamicCatalog.id,
        ownerId: user.id,
        localInstallationStatus: "success",
      });
      const dynamicTool = await ToolModel.createToolIfNotExists({
        id: "00000000-0000-4000-8000-000000000002",
        name: "weather_fixture__get_weather",
        description: "dynamically resolved, genuinely healthy",
        parameters: {},
        catalogId: dynamicCatalog.id,
      });

      await AgentToolModel.create(agent.id, pinnedTool.id, {
        mcpServerId: brokenPinnedServer.id,
        credentialResolutionMode: "static",
      });
      await AgentToolModel.create(agent.id, dynamicTool.id);

      const result = await ToolModel.getMcpToolsAssignedToAgent(
        ["weather_fixture__get_weather"],
        agent.id,
      );

      expect(result).toHaveLength(2);
      expect(result[0].catalogId).toBe(dynamicCatalog.id);
    });
  });

  describe("findByNameForAgent", () => {
    test("returns tool when assigned to the agent", async ({
      makeAgent,
      makeTool,
      makeInternalMcpCatalog,
    }) => {
      const agent = await makeAgent();
      const catalogItem = await makeInternalMcpCatalog({
        name: "test-catalog",
        serverUrl: "https://example.com/mcp/",
      });

      const tool = await makeTool({
        name: "my_tool",
        description: "A test tool",
        parameters: { type: "object", properties: {} },
        catalogId: catalogItem.id,
      });

      await AgentToolModel.create(agent.id, tool.id);

      const result = await ToolModel.findByNameForAgent("my_tool", agent.id);

      expect(result).toEqual(
        expect.objectContaining({ id: tool.id, name: "my_tool" }),
      );
    });

    test("returns null when tool exists but is not assigned to the agent", async ({
      makeAgent,
      makeTool,
      makeInternalMcpCatalog,
    }) => {
      const agent = await makeAgent();
      const catalogItem = await makeInternalMcpCatalog({
        name: "test-catalog",
        serverUrl: "https://example.com/mcp/",
      });

      await makeTool({
        name: "unassigned_tool",
        description: "Not assigned",
        parameters: {},
        catalogId: catalogItem.id,
      });

      const result = await ToolModel.findByNameForAgent(
        "unassigned_tool",
        agent.id,
      );

      expect(result).toBeNull();
    });

    test("returns null when tool does not exist", async ({ makeAgent }) => {
      const agent = await makeAgent();

      const result = await ToolModel.findByNameForAgent(
        "nonexistent_tool",
        agent.id,
      );

      expect(result).toBeNull();
    });

    test("scopes to the correct agent", async ({
      makeAgent,
      makeTool,
      makeInternalMcpCatalog,
    }) => {
      const agent1 = await makeAgent({ name: "Agent1" });
      const agent2 = await makeAgent({ name: "Agent2" });
      const catalogItem = await makeInternalMcpCatalog({
        name: "test-catalog",
        serverUrl: "https://example.com/mcp/",
      });

      const tool = await makeTool({
        name: "scoped_tool",
        description: "Scoped tool",
        parameters: {},
        catalogId: catalogItem.id,
      });

      // Assign only to agent1
      await AgentToolModel.create(agent1.id, tool.id);

      const result1 = await ToolModel.findByNameForAgent(
        "scoped_tool",
        agent1.id,
      );
      const result2 = await ToolModel.findByNameForAgent(
        "scoped_tool",
        agent2.id,
      );

      expect(result1).toEqual(
        expect.objectContaining({ id: tool.id, name: "scoped_tool" }),
      );
      expect(result2).toBeNull();
    });
  });

  describe("assignArchestraToolsToAgent", () => {
    test("assigns Archestra built-in tools to agent in bulk", async ({
      makeAgent,
      seedAndAssignArchestraTools,
    }) => {
      const agent = await makeAgent();

      // Agents should NOT have Archestra tools auto-assigned (they must be explicitly assigned)
      const toolIdsBeforeAssign = await AgentToolModel.findToolIdsByAgent(
        agent.id,
      );
      expect(toolIdsBeforeAssign.length).toBe(0);

      // Explicitly assign Archestra tools
      await seedAndAssignArchestraTools(agent.id);

      // Verify Archestra tools are assigned after explicit assignment
      const mcpTools = await ToolModel.getMcpToolsByAgent(agent.id);
      const archestraToolNames = mcpTools
        .map((tool) => tool.name)
        .filter((name) => name.startsWith("archestra__"));

      expect(archestraToolNames.length).toBeGreaterThan(0);
      expect(archestraToolNames).toContain("archestra__whoami");
    });

    test("is idempotent - does not create duplicates", async ({
      makeAgent,
      seedAndAssignArchestraTools,
    }) => {
      const agent = await makeAgent();

      await seedAndAssignArchestraTools(agent.id);
      const toolIdsAfterFirst = await AgentToolModel.findToolIdsByAgent(
        agent.id,
      );

      await seedAndAssignArchestraTools(agent.id);
      const toolIdsAfterSecond = await AgentToolModel.findToolIdsByAgent(
        agent.id,
      );

      expect(toolIdsAfterSecond.length).toBe(toolIdsAfterFirst.length);
    });
  });

  describe("findByCatalogId", () => {
    test("returns tools with assigned agents for catalog efficiently", async ({
      makeUser,
      makeAgent,
      makeInternalMcpCatalog,
      makeMcpServer,
      makeTool,
    }) => {
      const user = await makeUser();
      const agent1 = await makeAgent({ name: "Agent 1" });
      const agent2 = await makeAgent({ name: "Agent 2" });

      const catalogItem = await makeInternalMcpCatalog({
        name: "shared-catalog",
        serverUrl: "https://api.shared.com/mcp/",
      });

      // Create two servers with the same catalog
      await makeMcpServer({
        name: "server1",
        catalogId: catalogItem.id,
        ownerId: user.id,
      });

      await makeMcpServer({
        name: "server2",
        catalogId: catalogItem.id,
        ownerId: user.id,
      });

      // Create tools for both servers (same catalog)
      const tool1 = await makeTool({
        name: "shared_tool",
        description: "Shared Tool",
        parameters: {},
        catalogId: catalogItem.id,
      });

      const tool2 = await makeTool({
        name: "another_tool",
        description: "Another Tool",
        parameters: {},
        catalogId: catalogItem.id,
      });

      // Assign tools to agents
      await AgentToolModel.create(agent1.id, tool1.id);
      await AgentToolModel.create(agent2.id, tool1.id);
      await AgentToolModel.create(agent1.id, tool2.id);

      const result = await ToolModel.findByCatalogId(catalogItem.id);

      expect(result).toHaveLength(2);

      const sharedToolResult = result.find((t) => t.name === "shared_tool");
      expect(sharedToolResult?.assignedAgentCount).toBe(2);
      expect(sharedToolResult?.assignedAgents.map((a) => a.id)).toContain(
        agent1.id,
      );
      expect(sharedToolResult?.assignedAgents.map((a) => a.id)).toContain(
        agent2.id,
      );

      const anotherToolResult = result.find((t) => t.name === "another_tool");
      expect(anotherToolResult?.assignedAgentCount).toBe(1);
      expect(anotherToolResult?.assignedAgents.map((a) => a.id)).toContain(
        agent1.id,
      );
    });

    test("returns empty array when catalog has no tools", async ({
      makeInternalMcpCatalog,
    }) => {
      const catalogItem = await makeInternalMcpCatalog({
        name: "empty-catalog",
        serverUrl: "https://api.empty.com/mcp/",
      });

      const result = await ToolModel.findByCatalogId(catalogItem.id);
      expect(result).toHaveLength(0);
    });
  });

  describe("getDefaultToolPolicies", () => {
    test("falls back to safe restrictive defaults when no organization exists", async () => {
      // setup.ts intentionally seeds no organization, so getFirst() returns
      // null here. New tools must then fail safe: block on an untrusted context
      // and mark results untrusted, never silently permissive.
      const defaults = await ToolModel.getDefaultToolPolicies();

      expect(defaults).toEqual({
        invocationAction: "block_when_context_is_untrusted",
        resultAction: "mark_as_untrusted",
      });
    });
  });

  describe("bulkCreateToolsIfNotExists", () => {
    test("creates multiple tools for an MCP server in bulk", async ({
      makeInternalMcpCatalog,
      makeMcpServer,
    }) => {
      const catalog = await makeInternalMcpCatalog();
      await makeMcpServer({
        catalogId: catalog.id,
      });

      const toolsToCreate = [
        {
          name: "tool-1",
          description: "First tool",
          parameters: { type: "object", properties: {} },
          catalogId: catalog.id,
        },
        {
          name: "tool-2",
          description: "Second tool",
          parameters: { type: "object", properties: {} },
          catalogId: catalog.id,
        },
        {
          name: "tool-3",
          description: "Third tool",
          parameters: { type: "object", properties: {} },
          catalogId: catalog.id,
        },
      ];

      const createdTools =
        await ToolModel.bulkCreateToolsIfNotExists(toolsToCreate);

      expect(createdTools).toHaveLength(3);
      expect(createdTools.map((t) => t.name)).toContain("tool-1");
      expect(createdTools.map((t) => t.name)).toContain("tool-2");
      expect(createdTools.map((t) => t.name)).toContain("tool-3");

      // Verify all tools have correct catalogId
      createdTools.forEach((tool) => {
        expect(tool.catalogId).toBe(catalog.id);
        expect(tool.agentId).toBeNull();
      });
    });

    test("applies the org's configured default guardrails to new MCP catalog tools", async ({
      makeOrganization,
      makeInternalMcpCatalog,
    }) => {
      // The org's "Default Guardrails for MCP Tools" settings must flow to every
      // newly created catalog tool — not just proxy-discovered ones.
      const org = await makeOrganization({
        defaultDiscoveredToolInvocationPolicy: "require_approval",
        defaultDiscoveredToolResultPolicy: "mark_as_trusted",
      });
      const catalog = await makeInternalMcpCatalog({ organizationId: org.id });

      const [tool] = await ToolModel.bulkCreateToolsIfNotExists([
        {
          name: "catalog-tool-honors-org-default",
          description: "Catalog tool",
          parameters: { type: "object", properties: {} },
          catalogId: catalog.id,
        },
      ]);

      const inv = await db
        .select()
        .from(schema.toolInvocationPoliciesTable)
        .where(eq(schema.toolInvocationPoliciesTable.toolId, tool.id));
      expect(inv).toHaveLength(1);
      expect(inv[0].action).toBe("require_approval");

      const trusted = await db
        .select()
        .from(schema.trustedDataPoliciesTable)
        .where(eq(schema.trustedDataPoliciesTable.toolId, tool.id));
      expect(trusted).toHaveLength(1);
      expect(trusted[0].action).toBe("mark_as_trusted");
    });

    test("returns existing tools when some tools already exist", async ({
      makeInternalMcpCatalog,
      makeMcpServer,
      makeTool,
    }) => {
      const catalog = await makeInternalMcpCatalog();
      await makeMcpServer({
        catalogId: catalog.id,
      });

      // Create one tool manually
      const existingTool = await makeTool({
        name: "tool-1",
        catalogId: catalog.id,
      });

      const toolsToCreate = [
        {
          name: "tool-1", // Already exists
          description: "First tool",
          parameters: { type: "object", properties: {} },
          catalogId: catalog.id,
        },
        {
          name: "tool-2", // New
          description: "Second tool",
          parameters: { type: "object", properties: {} },
          catalogId: catalog.id,
        },
        {
          name: "tool-3", // New
          description: "Third tool",
          parameters: { type: "object", properties: {} },
          catalogId: catalog.id,
        },
      ];

      const createdTools =
        await ToolModel.bulkCreateToolsIfNotExists(toolsToCreate);

      expect(createdTools).toHaveLength(3);
      // Should return the existing tool
      expect(createdTools.find((t) => t.id === existingTool.id)).toBeDefined();
      // Should create new tools
      expect(createdTools.map((t) => t.name)).toContain("tool-2");
      expect(createdTools.map((t) => t.name)).toContain("tool-3");
    });

    test("maintains input order in returned tools", async ({
      makeInternalMcpCatalog,
      makeMcpServer,
    }) => {
      const catalog = await makeInternalMcpCatalog();
      await makeMcpServer({
        catalogId: catalog.id,
      });

      const toolsToCreate = [
        {
          name: "tool-c",
          description: "Tool C",
          parameters: { type: "object", properties: {} },
          catalogId: catalog.id,
        },
        {
          name: "tool-a",
          description: "Tool A",
          parameters: { type: "object", properties: {} },
          catalogId: catalog.id,
        },
        {
          name: "tool-b",
          description: "Tool B",
          parameters: { type: "object", properties: {} },
          catalogId: catalog.id,
        },
      ];

      const createdTools =
        await ToolModel.bulkCreateToolsIfNotExists(toolsToCreate);

      expect(createdTools).toHaveLength(3);
      // Should maintain input order
      expect(createdTools[0].name).toBe("tool-c");
      expect(createdTools[1].name).toBe("tool-a");
      expect(createdTools[2].name).toBe("tool-b");
    });

    test("handles empty tools array", async () => {
      const createdTools = await ToolModel.bulkCreateToolsIfNotExists([]);
      expect(createdTools).toHaveLength(0);
    });

    test("handles conflict during insert and fetches existing tools", async ({
      makeInternalMcpCatalog,
      makeMcpServer,
    }) => {
      const catalog = await makeInternalMcpCatalog();
      await makeMcpServer({
        catalogId: catalog.id,
      });

      const toolsToCreate = [
        {
          name: "conflict-tool",
          description: "Tool that might conflict",
          parameters: { type: "object", properties: {} },
          catalogId: catalog.id,
        },
      ];

      // Create tools in parallel to simulate race condition
      const [result1, result2] = await Promise.all([
        ToolModel.bulkCreateToolsIfNotExists(toolsToCreate),
        ToolModel.bulkCreateToolsIfNotExists(toolsToCreate),
      ]);

      // Both should return the same tool (one created, one fetched)
      expect(result1).toHaveLength(1);
      expect(result2).toHaveLength(1);
      expect(result1[0].name).toBe("conflict-tool");
      expect(result2[0].name).toBe("conflict-tool");
    });

    test("upgrades proxy-discovered tools by setting catalogId (same tool IDs, no duplicates)", async ({
      makeInternalMcpCatalog,
      makeTool,
      makeAgent,
      makeAgentTool,
    }) => {
      const catalog = await makeInternalMcpCatalog();
      const agent = await makeAgent();

      // Create proxy-discovered tools (catalogId=NULL)
      const proxyTool1 = await makeTool({
        name: "proxy-upgrade-1",
        description: "Proxy tool 1",
      });
      const proxyTool2 = await makeTool({
        name: "proxy-upgrade-2",
        description: "Proxy tool 2",
      });

      // Assign proxy tools to agent (simulating proxy discovery)
      await makeAgentTool(agent.id, proxyTool1.id);
      await makeAgentTool(agent.id, proxyTool2.id);

      // Now bulk-create the same tools as MCP tools (simulating MCP server install)
      const result = await ToolModel.bulkCreateToolsIfNotExists([
        {
          name: "proxy-upgrade-1",
          description: "MCP tool 1",
          parameters: {},
          catalogId: catalog.id,
        },
        {
          name: "proxy-upgrade-2",
          description: "MCP tool 2",
          parameters: {},
          catalogId: catalog.id,
        },
      ]);

      // Should return the same tool IDs (upgraded, not duplicated)
      expect(result).toHaveLength(2);
      expect(result.find((t) => t.name === "proxy-upgrade-1")?.id).toBe(
        proxyTool1.id,
      );
      expect(result.find((t) => t.name === "proxy-upgrade-2")?.id).toBe(
        proxyTool2.id,
      );

      // Tools should now have the catalogId set
      for (const tool of result) {
        expect(tool.catalogId).toBe(catalog.id);
      }

      // Agent-tool links should still be intact
      const agentToolIds = await AgentToolModel.findToolIdsByAgent(agent.id);
      expect(agentToolIds).toContain(proxyTool1.id);
      expect(agentToolIds).toContain(proxyTool2.id);
    });

    test("handles mix of proxy tools and genuinely new tools", async ({
      makeInternalMcpCatalog,
      makeTool,
    }) => {
      const catalog = await makeInternalMcpCatalog();

      // Create one proxy-discovered tool
      const proxyTool = await makeTool({
        name: "mixed-proxy-tool",
        description: "Proxy tool",
      });

      // Bulk-create with one proxy tool and one genuinely new tool
      const result = await ToolModel.bulkCreateToolsIfNotExists([
        {
          name: "mixed-proxy-tool",
          description: "MCP tool (was proxy)",
          parameters: {},
          catalogId: catalog.id,
        },
        {
          name: "mixed-new-tool",
          description: "Brand new MCP tool",
          parameters: {},
          catalogId: catalog.id,
        },
      ]);

      expect(result).toHaveLength(2);

      // Proxy tool should be upgraded (same ID)
      const upgradedTool = result.find((t) => t.name === "mixed-proxy-tool");
      expect(upgradedTool?.id).toBe(proxyTool.id);
      expect(upgradedTool?.catalogId).toBe(catalog.id);

      // New tool should be created
      const newTool = result.find((t) => t.name === "mixed-new-tool");
      expect(newTool).toBeDefined();
      expect(newTool?.catalogId).toBe(catalog.id);
    });

    test("does not touch tools that already have a different catalogId", async ({
      makeInternalMcpCatalog,
      makeTool,
    }) => {
      const catalog1 = await makeInternalMcpCatalog({ name: "Catalog 1" });
      const catalog2 = await makeInternalMcpCatalog({ name: "Catalog 2" });

      // Create a tool that already belongs to catalog1
      const existingTool = await makeTool({
        name: "already-owned-tool",
        description: "Owned by catalog1",
        catalogId: catalog1.id,
      });

      // Try to bulk-create same-named tool for catalog2
      const result = await ToolModel.bulkCreateToolsIfNotExists([
        {
          name: "already-owned-tool",
          description: "Should not steal from catalog1",
          parameters: {},
          catalogId: catalog2.id,
        },
      ]);

      // Should create a new tool for catalog2 (not upgrade catalog1's tool)
      // The proxy upgrade only targets catalogId=NULL tools
      expect(result).toHaveLength(1);
      // The original tool should still belong to catalog1
      const originalTool = await ToolModel.findById(existingTool.id);
      expect(originalTool?.catalogId).toBe(catalog1.id);
    });
  });

  describe("createToolIfNotExists - proxy to MCP upgrade", () => {
    test("upgrades existing proxy tool when MCP tool with same name is created", async ({
      makeTool,
      makeAgentTool,
      makeAgent,
      makeInternalMcpCatalog,
    }) => {
      const agent = await makeAgent();
      const catalog = await makeInternalMcpCatalog();

      // Create a shared proxy tool and link to agent
      const proxyTool = await makeTool({
        name: "upgradeable-tool",
        description: "Proxy description",
        parameters: { type: "object" },
      });
      await makeAgentTool(agent.id, proxyTool.id);

      // Now create an MCP tool with the same name — should upgrade the proxy tool
      const mcpTool = await makeTool({
        name: "upgradeable-tool",
        catalogId: catalog.id,
        description: "MCP description",
      });

      // Same row was reused
      expect(mcpTool.id).toBe(proxyTool.id);
      expect(mcpTool.catalogId).toBe(catalog.id);
      expect(mcpTool.description).toBe("MCP description");

      // Agent-tool link still intact
      const agentTools = await ToolModel.getToolsByAgent(agent.id);
      expect(agentTools.some((t) => t.id === proxyTool.id)).toBe(true);
    });

    test("does not upgrade when MCP tool with same catalog already exists", async ({
      makeTool,
      makeInternalMcpCatalog,
    }) => {
      const catalog = await makeInternalMcpCatalog();

      // Create MCP tool directly
      const mcpTool = await makeTool({
        name: "existing-mcp-tool",
        catalogId: catalog.id,
        description: "Original MCP",
      });

      // Creating again with same catalog+name returns existing
      const result = await ToolModel.createToolIfNotExists({
        name: "existing-mcp-tool",
        catalogId: catalog.id,
        description: "Duplicate attempt",
        parameters: {},
      });

      expect(result.id).toBe(mcpTool.id);
      expect(result.description).toBe("Original MCP");
    });
  });

  describe("bulkCreateProxyToolsIfNotExists", () => {
    test("creates multiple shared proxy tools in bulk", async ({
      makeAgent,
    }) => {
      const agent = await makeAgent({ name: "Test Agent" });

      const toolsToCreate = [
        {
          name: "proxy-tool-1",
          description: "First proxy tool",
          parameters: { type: "object", properties: {} },
        },
        {
          name: "proxy-tool-2",
          description: "Second proxy tool",
          parameters: { type: "object", properties: {} },
        },
        {
          name: "proxy-tool-3",
          description: "Third proxy tool",
          parameters: { type: "object", properties: {} },
        },
      ];

      const createdTools = await ToolModel.bulkCreateProxyToolsIfNotExists(
        toolsToCreate,
        agent.id,
      );

      expect(createdTools).toHaveLength(3);
      expect(createdTools.map((t) => t.name)).toContain("proxy-tool-1");
      expect(createdTools.map((t) => t.name)).toContain("proxy-tool-2");
      expect(createdTools.map((t) => t.name)).toContain("proxy-tool-3");

      // Verify all tools are shared (agentId=null) and have null catalogId
      for (const tool of createdTools) {
        expect(tool.agentId).toBeNull();
        expect(tool.catalogId).toBeNull();
      }
    });

    test("returns existing tools when some tools already exist", async ({
      makeAgent,
      makeTool,
      makeAgentTool,
    }) => {
      const agent = await makeAgent({ name: "Test Agent" });

      // Create one shared proxy tool manually and assign to agent
      const existingTool = await makeTool({
        name: "proxy-tool-1",
        description: "Existing tool",
      });
      await makeAgentTool(agent.id, existingTool.id);

      const toolsToCreate = [
        {
          name: "proxy-tool-1", // Already exists
          description: "First proxy tool (updated)",
          parameters: { type: "object", properties: {} },
        },
        {
          name: "proxy-tool-2",
          description: "Second proxy tool",
          parameters: { type: "object", properties: {} },
        },
        {
          name: "proxy-tool-3",
          description: "Third proxy tool",
          parameters: { type: "object", properties: {} },
        },
      ];

      const createdTools = await ToolModel.bulkCreateProxyToolsIfNotExists(
        toolsToCreate,
        agent.id,
      );

      expect(createdTools).toHaveLength(3);
      // Should return the existing tool
      expect(createdTools.find((t) => t.id === existingTool.id)).toBeDefined();
      // Should create new tools
      expect(createdTools.map((t) => t.name)).toContain("proxy-tool-2");
      expect(createdTools.map((t) => t.name)).toContain("proxy-tool-3");
    });

    test("maintains input order in returned tools", async ({ makeAgent }) => {
      const agent = await makeAgent({ name: "Test Agent" });

      const toolsToCreate = [
        {
          name: "proxy-tool-c",
          description: "Tool C",
          parameters: { type: "object", properties: {} },
        },
        {
          name: "proxy-tool-a",
          description: "Tool A",
          parameters: { type: "object", properties: {} },
        },
        {
          name: "proxy-tool-b",
          description: "Tool B",
          parameters: { type: "object", properties: {} },
        },
      ];

      const createdTools = await ToolModel.bulkCreateProxyToolsIfNotExists(
        toolsToCreate,
        agent.id,
      );

      expect(createdTools).toHaveLength(3);
      // Should maintain input order
      expect(createdTools[0].name).toBe("proxy-tool-c");
      expect(createdTools[1].name).toBe("proxy-tool-a");
      expect(createdTools[2].name).toBe("proxy-tool-b");
    });

    test("handles empty tools array", async ({ makeAgent }) => {
      const agent = await makeAgent({ name: "Test Agent" });
      const createdTools = await ToolModel.bulkCreateProxyToolsIfNotExists(
        [],
        agent.id,
      );
      expect(createdTools).toHaveLength(0);
    });

    test("handles conflict during insert and fetches existing tools", async ({
      makeAgent,
    }) => {
      const agent = await makeAgent({ name: "Test Agent" });

      const toolsToCreate = [
        {
          name: "conflict-proxy-tool",
          description: "Tool that might conflict",
          parameters: { type: "object", properties: {} },
        },
      ];

      // Create tools in parallel to simulate race condition
      const [result1, result2] = await Promise.all([
        ToolModel.bulkCreateProxyToolsIfNotExists(toolsToCreate, agent.id),
        ToolModel.bulkCreateProxyToolsIfNotExists(toolsToCreate, agent.id),
      ]);

      // Both should return the same tool (one created, one fetched)
      expect(result1).toHaveLength(1);
      expect(result2).toHaveLength(1);
      expect(result1[0].name).toBe("conflict-proxy-tool");
      expect(result2[0].name).toBe("conflict-proxy-tool");
    });

    test("shares tools between different agents (same tool row reused)", async ({
      makeAgent,
    }) => {
      const agent1 = await makeAgent({ name: "Agent 1" });
      const agent2 = await makeAgent({ name: "Agent 2" });

      // Create same-named tool for agent1
      const result1 = await ToolModel.bulkCreateProxyToolsIfNotExists(
        [{ name: "shared-name-tool", description: "Tool for agent 1" }],
        agent1.id,
      );

      // Create same-named tool for agent2
      const result2 = await ToolModel.bulkCreateProxyToolsIfNotExists(
        [{ name: "shared-name-tool", description: "Tool for agent 2" }],
        agent2.id,
      );

      // Both agents should get the SAME shared tool row (agentId=null)
      expect(result1).toHaveLength(1);
      expect(result2).toHaveLength(1);
      expect(result1[0].id).toBe(result2[0].id);
      expect(result1[0].agentId).toBeNull();
      expect(result2[0].agentId).toBeNull();
    });

    test("handles tools with optional parameters", async ({ makeAgent }) => {
      const agent = await makeAgent({ name: "Test Agent" });

      const toolsToCreate = [
        {
          name: "tool-with-all-fields",
          description: "Has description",
          parameters: { type: "object" },
        },
        {
          name: "tool-without-description",
          // No description
        },
        {
          name: "tool-without-parameters",
          description: "Has description but no parameters",
          // No parameters
        },
      ];

      const createdTools = await ToolModel.bulkCreateProxyToolsIfNotExists(
        toolsToCreate,
        agent.id,
      );

      expect(createdTools).toHaveLength(3);

      const toolWithAll = createdTools.find(
        (t) => t.name === "tool-with-all-fields",
      );
      expect(toolWithAll?.description).toBe("Has description");
      expect(toolWithAll?.parameters).toEqual({ type: "object" });

      const toolWithoutDesc = createdTools.find(
        (t) => t.name === "tool-without-description",
      );
      expect(toolWithoutDesc?.description).toBeNull();

      const toolWithoutParams = createdTools.find(
        (t) => t.name === "tool-without-parameters",
      );
      expect(toolWithoutParams?.description).toBe(
        "Has description but no parameters",
      );
    });

    test("applies the configured invocation and result defaults to new proxy tools", async ({
      makeAgent,
    }) => {
      const agent = await makeAgent({ name: "Test Agent" });

      const [tool] = await ToolModel.bulkCreateProxyToolsIfNotExists(
        [
          {
            name: "proxy-tool-with-default",
            description: "Discovered tool",
            parameters: { type: "object", properties: {} },
          },
        ],
        agent.id,
        {
          invocationAction: "allow_when_context_is_untrusted",
          resultAction: "mark_as_trusted",
        },
      );

      const inv = await db
        .select()
        .from(schema.toolInvocationPoliciesTable)
        .where(eq(schema.toolInvocationPoliciesTable.toolId, tool.id));
      expect(inv).toHaveLength(1);
      expect(inv[0].action).toBe("allow_when_context_is_untrusted");

      const trusted = await db
        .select()
        .from(schema.trustedDataPoliciesTable)
        .where(eq(schema.trustedDataPoliciesTable.toolId, tool.id));
      expect(trusted).toHaveLength(1);
      expect(trusted[0].action).toBe("mark_as_trusted");
    });

    test("falls back to the original hardcoded defaults when no override is provided", async ({
      makeAgent,
    }) => {
      const agent = await makeAgent({ name: "Test Agent" });

      const [tool] = await ToolModel.bulkCreateProxyToolsIfNotExists(
        [
          {
            name: "proxy-tool-no-override",
            description: "Discovered tool",
            parameters: { type: "object", properties: {} },
          },
        ],
        agent.id,
      );

      const inv = await db
        .select()
        .from(schema.toolInvocationPoliciesTable)
        .where(eq(schema.toolInvocationPoliciesTable.toolId, tool.id));
      expect(inv).toHaveLength(1);
      expect(inv[0].action).toBe("block_when_context_is_untrusted");

      const trusted = await db
        .select()
        .from(schema.trustedDataPoliciesTable)
        .where(eq(schema.trustedDataPoliciesTable.toolId, tool.id));
      expect(trusted).toHaveLength(1);
      expect(trusted[0].action).toBe("mark_as_untrusted");
    });
  });

  describe("assignDefaultArchestraToolsToAgent", () => {
    test("assigns all default tools including query_knowledge_sources, but getMcpToolsByAgent filters it out when agent has no knowledge sources", async ({
      makeAgent,
      seedAndAssignArchestraTools,
    }) => {
      // First seed Archestra tools (but don't assign to agent)
      const tempAgent = await makeAgent({ name: "Temp Agent for Seeding" });
      await seedAndAssignArchestraTools(tempAgent.id);

      // Create a new agent WITHOUT a knowledgeBaseId
      const agent = await makeAgent({ name: "Test Agent" });

      // Assign default tools (always includes query_knowledge_sources now)
      await ToolModel.assignDefaultArchestraToolsToAgent(agent.id);

      // Verify the tool was assigned in the junction table
      const assignedToolIds = await AgentToolModel.findToolIdsByAgent(agent.id);
      expect(assignedToolIds.length).toBeGreaterThanOrEqual(2);

      // But getMcpToolsByAgent filters it out because the agent has no knowledge sources
      const mcpTools = await ToolModel.getMcpToolsByAgent(agent.id);
      const toolNames = mcpTools.map((t) => t.name);

      expect(toolNames).toContain(TOOL_TODO_WRITE_FULL_NAME);

      // query_knowledge_sources is filtered out at query time because agent has no knowledge sources
      expect(toolNames).not.toContain(TOOL_QUERY_KNOWLEDGE_SOURCES_FULL_NAME);
    });

    test("includes query_knowledge_sources when agent has a knowledge base assigned", async ({
      makeAgent,
      makeOrganization,
      makeKnowledgeBase,
      seedAndAssignArchestraTools,
    }) => {
      // First seed Archestra tools
      const tempAgent = await makeAgent({ name: "Temp Agent for Seeding" });
      await seedAndAssignArchestraTools(tempAgent.id);

      // Create an organization and knowledge base
      const org = await makeOrganization();
      const kg = await makeKnowledgeBase(org.id);

      // Create a new agent and assign the knowledge base
      const agent = await makeAgent({
        name: "Knowledge Base Enabled Agent",
        organizationId: org.id,
      });
      await db
        .insert(schema.agentKnowledgeBasesTable)
        .values({ agentId: agent.id, knowledgeBaseId: kg.id });

      // Assign default tools
      await ToolModel.assignDefaultArchestraToolsToAgent(agent.id);

      // Get the tools assigned to the agent
      const mcpTools = await ToolModel.getMcpToolsByAgent(agent.id);
      const toolNames = mcpTools.map((t) => t.name);

      // Should have both default tools including query_knowledge_sources
      expect(toolNames).toContain(TOOL_TODO_WRITE_FULL_NAME);
      expect(toolNames).toContain(TOOL_QUERY_KNOWLEDGE_SOURCES_FULL_NAME);
    });

    test("is idempotent - does not create duplicates", async ({
      makeAgent,
      seedAndAssignArchestraTools,
    }) => {
      const tempAgent = await makeAgent({ name: "Temp Agent for Seeding" });
      await seedAndAssignArchestraTools(tempAgent.id);

      const agent = await makeAgent({ name: "Test Agent" });

      await ToolModel.assignDefaultArchestraToolsToAgent(agent.id);
      const toolIdsAfterFirst = await AgentToolModel.findToolIdsByAgent(
        agent.id,
      );

      await ToolModel.assignDefaultArchestraToolsToAgent(agent.id);
      const toolIdsAfterSecond = await AgentToolModel.findToolIdsByAgent(
        agent.id,
      );

      expect(toolIdsAfterSecond.length).toBe(toolIdsAfterFirst.length);
    });

    test("does nothing when tools are not seeded", async ({ makeAgent }) => {
      // Create agent without seeding Archestra tools first
      const agent = await makeAgent({ name: "Agent Without Seeded Tools" });

      // This should not throw, just skip assignment
      await ToolModel.assignDefaultArchestraToolsToAgent(agent.id);

      const toolIds = await AgentToolModel.findToolIdsByAgent(agent.id);
      expect(toolIds).toHaveLength(0);
    });

    test("assigns white-labeled default tools by their branded names", async ({
      makeOrganization,
      makeAgent,
    }) => {
      const org = await makeOrganization();
      await OrganizationModel.patch(org.id, { appName: "Acme Copilot" });

      // Create the agent before the catalog is seeded so create-time
      // auto-assignment (app tools) does not add to the default set under test.
      const agent = await makeAgent({ organizationId: org.id });
      await ToolModel.syncArchestraBuiltInCatalog({
        organization: { appName: "Acme Copilot", iconLogo: null },
      });
      await ToolModel.assignDefaultArchestraToolsToAgent(agent.id);

      const assignedToolIds = await AgentToolModel.findToolIdsByAgent(agent.id);
      const assignedTools = await ToolModel.getByIds(assignedToolIds);

      expect(assignedTools.map((tool) => tool.name).sort()).toEqual(
        (
          [
            TOOL_QUERY_KNOWLEDGE_SOURCES_SHORT_NAME,
            TOOL_TODO_WRITE_SHORT_NAME,
          ] as const
        )
          .map((shortName) =>
            getArchestraToolFullName(shortName, {
              appName: "Acme Copilot",
              fullWhiteLabeling: true,
            }),
          )
          .sort(),
      );
    });
  });

  describe("knowledge base tool visibility", () => {
    test("getMcpToolsByAgent excludes query_knowledge_sources when agent has no knowledge base assigned", async ({
      makeAgent,
      seedAndAssignArchestraTools,
    }) => {
      // Create agent WITHOUT a knowledgeBaseId
      const agent = await makeAgent();
      await seedAndAssignArchestraTools(agent.id);

      const tools = await ToolModel.getMcpToolsByAgent(agent.id);
      const toolNames = tools.map((t) => t.name);

      expect(toolNames).not.toContain(TOOL_QUERY_KNOWLEDGE_SOURCES_FULL_NAME);
      // Other Archestra tools should still be present
      expect(toolNames).toContain(TOOL_TODO_WRITE_FULL_NAME);
    });

    test("getMcpToolsByAgent includes query_knowledge_sources when agent has a knowledge base assigned", async ({
      makeAgent,
      makeOrganization,
      makeKnowledgeBase,
      seedAndAssignArchestraTools,
    }) => {
      // Create an organization and knowledge base
      const org = await makeOrganization();
      const kg = await makeKnowledgeBase(org.id);

      // Create agent and assign the knowledge base
      const agent = await makeAgent({ organizationId: org.id });
      await db
        .insert(schema.agentKnowledgeBasesTable)
        .values({ agentId: agent.id, knowledgeBaseId: kg.id });

      await seedAndAssignArchestraTools(agent.id);

      const tools = await ToolModel.getMcpToolsByAgent(agent.id);
      const toolNames = tools.map((t) => t.name);

      expect(toolNames).toContain(TOOL_QUERY_KNOWLEDGE_SOURCES_FULL_NAME);
    });

    test("getMcpToolsByAgent includes query_knowledge_sources when agent has a directly-assigned connector", async ({
      makeAgent,
      makeOrganization,
      makeKnowledgeBase,
      makeKnowledgeBaseConnector,
      seedAndAssignArchestraTools,
    }) => {
      const org = await makeOrganization();
      // Create a KB + connector so the connector exists in the DB
      const kb = await makeKnowledgeBase(org.id);
      const connector = await makeKnowledgeBaseConnector(kb.id, org.id);

      // Create agent WITHOUT a knowledge base, but with a direct connector assignment
      const agent = await makeAgent({ organizationId: org.id });
      await db
        .insert(schema.agentConnectorAssignmentsTable)
        .values({ agentId: agent.id, connectorId: connector.id });

      await seedAndAssignArchestraTools(agent.id);

      const tools = await ToolModel.getMcpToolsByAgent(agent.id);
      const toolNames = tools.map((t) => t.name);

      // query_knowledge_sources should be injected for direct connector assignments
      expect(toolNames).toContain(TOOL_QUERY_KNOWLEDGE_SOURCES_FULL_NAME);
      // Other default tools should still be present
      expect(toolNames).toContain(TOOL_TODO_WRITE_FULL_NAME);
    });

    test("getMcpToolsByAgent excludes query_knowledge_sources when agent has no knowledge base and no direct connectors", async ({
      makeAgent,
      seedAndAssignArchestraTools,
    }) => {
      // Create agent with neither KB nor direct connector assignments
      const agent = await makeAgent();
      await seedAndAssignArchestraTools(agent.id);

      const tools = await ToolModel.getMcpToolsByAgent(agent.id);
      const toolNames = tools.map((t) => t.name);

      expect(toolNames).not.toContain(TOOL_QUERY_KNOWLEDGE_SOURCES_FULL_NAME);
    });

    test("getMcpToolsByAgent includes query_knowledge_sources when agent has both a knowledge base and a direct connector", async ({
      makeAgent,
      makeOrganization,
      makeKnowledgeBase,
      makeKnowledgeBaseConnector,
      seedAndAssignArchestraTools,
    }) => {
      const org = await makeOrganization();
      const kb = await makeKnowledgeBase(org.id);
      const connector = await makeKnowledgeBaseConnector(kb.id, org.id);

      // Create agent with BOTH a knowledge base AND a direct connector assignment
      const agent = await makeAgent({ organizationId: org.id });
      await db
        .insert(schema.agentKnowledgeBasesTable)
        .values({ agentId: agent.id, knowledgeBaseId: kb.id });
      await db
        .insert(schema.agentConnectorAssignmentsTable)
        .values({ agentId: agent.id, connectorId: connector.id });

      await seedAndAssignArchestraTools(agent.id);

      const tools = await ToolModel.getMcpToolsByAgent(agent.id);
      const toolNames = tools.map((t) => t.name);

      expect(toolNames).toContain(TOOL_QUERY_KNOWLEDGE_SOURCES_FULL_NAME);
      // Should not have duplicates
      const kbToolCount = toolNames.filter(
        (n) => n === TOOL_QUERY_KNOWLEDGE_SOURCES_FULL_NAME,
      ).length;
      expect(kbToolCount).toBe(1);
    });

    test("getMcpToolsByAgent includes query_knowledge_sources with multiple direct connectors", async ({
      makeAgent,
      makeOrganization,
      makeKnowledgeBase,
      makeKnowledgeBaseConnector,
      seedAndAssignArchestraTools,
    }) => {
      const org = await makeOrganization();
      const kb = await makeKnowledgeBase(org.id);
      const connector1 = await makeKnowledgeBaseConnector(kb.id, org.id, {
        connectorType: "jira",
      });
      const connector2 = await makeKnowledgeBaseConnector(kb.id, org.id, {
        connectorType: "confluence",
      });

      // Agent with multiple direct connectors, no KB assignment
      const agent = await makeAgent({ organizationId: org.id });
      await db.insert(schema.agentConnectorAssignmentsTable).values([
        { agentId: agent.id, connectorId: connector1.id },
        { agentId: agent.id, connectorId: connector2.id },
      ]);

      await seedAndAssignArchestraTools(agent.id);

      const tools = await ToolModel.getMcpToolsByAgent(agent.id);
      const toolNames = tools.map((t) => t.name);

      expect(toolNames).toContain(TOOL_QUERY_KNOWLEDGE_SOURCES_FULL_NAME);
    });

    test("getMcpToolsByAgent auto-injects query_knowledge_sources even with no other tools assigned", async ({
      makeAgent,
      makeOrganization,
      makeKnowledgeBase,
      makeKnowledgeBaseConnector,
      seedAndAssignArchestraTools,
    }) => {
      const org = await makeOrganization();
      const kb = await makeKnowledgeBase(org.id);
      const connector = await makeKnowledgeBaseConnector(kb.id, org.id);

      // Agent with a direct connector but NO tools assigned. Created BEFORE
      // the built-ins are seeded so the create path has nothing to
      // auto-assign.
      const agent = await makeAgent({ organizationId: org.id });
      await db
        .insert(schema.agentConnectorAssignmentsTable)
        .values({ agentId: agent.id, connectorId: connector.id });

      // Seed the archestra tools (so query_knowledge_sources exists in DB)
      // but do NOT assign them to the agent
      const tempAgent = await makeAgent({ name: "Temp Agent for Seeding" });
      await seedAndAssignArchestraTools(tempAgent.id);

      const tools = await ToolModel.getMcpToolsByAgent(agent.id);
      const toolNames = tools.map((t) => t.name);

      // query_knowledge_sources should still be auto-injected
      expect(toolNames).toContain(TOOL_QUERY_KNOWLEDGE_SOURCES_FULL_NAME);
      // No other tools should be present since none were assigned
      expect(toolNames).toHaveLength(1);
    });

    test("getMcpToolsByAgent removes query_knowledge_sources after connector unassignment", async ({
      makeAgent,
      makeOrganization,
      makeKnowledgeBase,
      makeKnowledgeBaseConnector,
      seedAndAssignArchestraTools,
    }) => {
      const org = await makeOrganization();
      const kb = await makeKnowledgeBase(org.id);
      const connector = await makeKnowledgeBaseConnector(kb.id, org.id);

      const agent = await makeAgent({ organizationId: org.id });
      await db
        .insert(schema.agentConnectorAssignmentsTable)
        .values({ agentId: agent.id, connectorId: connector.id });
      await seedAndAssignArchestraTools(agent.id);

      // Verify tool is present
      let tools = await ToolModel.getMcpToolsByAgent(agent.id);
      expect(tools.map((t) => t.name)).toContain(
        TOOL_QUERY_KNOWLEDGE_SOURCES_FULL_NAME,
      );

      // Unassign the connector
      await db
        .delete(schema.agentConnectorAssignmentsTable)
        .where(
          and(
            eq(schema.agentConnectorAssignmentsTable.agentId, agent.id),
            eq(schema.agentConnectorAssignmentsTable.connectorId, connector.id),
          ),
        );

      // Tool should no longer appear
      tools = await ToolModel.getMcpToolsByAgent(agent.id);
      expect(tools.map((t) => t.name)).not.toContain(
        TOOL_QUERY_KNOWLEDGE_SOURCES_FULL_NAME,
      );
    });

    test("findByCatalogId excludes built-in tools that are not user-assignable", async ({
      makeAgent,
      seedAndAssignArchestraTools,
    }) => {
      const agent = await makeAgent();
      await seedAndAssignArchestraTools(agent.id);

      const { ARCHESTRA_MCP_CATALOG_ID } = await import("@archestra/shared");
      const tools = await ToolModel.findByCatalogId(ARCHESTRA_MCP_CATALOG_ID);
      const toolNames = tools.map((t) => t.name);

      expect(toolNames).not.toContain(TOOL_QUERY_KNOWLEDGE_SOURCES_FULL_NAME);
      expect(toolNames).not.toContain(TOOL_SEARCH_TOOLS_FULL_NAME);
      expect(toolNames).not.toContain(TOOL_RUN_TOOL_FULL_NAME);
      expect(toolNames).toContain(TOOL_TODO_WRITE_FULL_NAME);
    });

    test("getToolsByAgent and findByCatalogId use branded knowledge-tool filtering after white-label sync", async ({
      makeOrganization,
      makeAgent,
      makeKnowledgeBase,
    }) => {
      const org = await makeOrganization();
      await OrganizationModel.patch(org.id, { appName: "Acme Copilot" });
      await ToolModel.syncArchestraBuiltInCatalog({
        organization: { appName: "Acme Copilot", iconLogo: null },
      });

      const kb = await makeKnowledgeBase(org.id);
      const agent = await makeAgent({ organizationId: org.id });
      await db
        .insert(schema.agentKnowledgeBasesTable)
        .values({ agentId: agent.id, knowledgeBaseId: kb.id });
      await ToolModel.assignArchestraToolsToAgent(
        agent.id,
        ARCHESTRA_MCP_CATALOG_ID,
      );

      const brandedKbToolName = getArchestraToolFullName(
        TOOL_QUERY_KNOWLEDGE_SOURCES_SHORT_NAME,
        {
          appName: "Acme Copilot",
          fullWhiteLabeling: true,
        },
      );

      const visibleTools = await ToolModel.getToolsByAgent(agent.id);
      const visibleMcpTools = await ToolModel.getMcpToolsByAgent(agent.id);
      const catalogTools = await ToolModel.findByCatalogId(
        ARCHESTRA_MCP_CATALOG_ID,
      );

      expect(visibleTools.map((tool) => tool.name)).not.toContain(
        brandedKbToolName,
      );
      expect(visibleMcpTools.map((tool) => tool.name)).toContain(
        brandedKbToolName,
      );
      expect(catalogTools.map((tool) => tool.name)).not.toContain(
        brandedKbToolName,
      );
    });

    test("assignArchestraToolsToAgent always assigns query_knowledge_sources (filtered at query time)", async ({
      makeAgent,
      seedAndAssignArchestraTools,
    }) => {
      // Seed tools first (seeding is independent of visibility filtering)
      const tempAgent = await makeAgent({ name: "Temp Agent for Seeding" });
      await seedAndAssignArchestraTools(tempAgent.id);

      // Create a new agent WITHOUT a knowledgeBaseId and assign all Archestra tools
      const agent = await makeAgent({ name: "Test Agent" });
      const { ARCHESTRA_MCP_CATALOG_ID } = await import("@archestra/shared");
      await ToolModel.assignArchestraToolsToAgent(
        agent.id,
        ARCHESTRA_MCP_CATALOG_ID,
      );

      // Tool is assigned (in junction table) but filtered out by getMcpToolsByAgent
      // since the agent has no knowledge base assigned
      const tools = await ToolModel.getMcpToolsByAgent(agent.id);
      const toolNames = tools.map((t) => t.name);

      expect(toolNames).not.toContain(TOOL_QUERY_KNOWLEDGE_SOURCES_FULL_NAME);
      expect(toolNames).toContain(TOOL_TODO_WRITE_FULL_NAME);
    });
  });

  describe("syncToolsForCatalog", () => {
    test("creates new tools when none exist", async ({
      makeInternalMcpCatalog,
      makeMcpServer,
    }) => {
      const catalog = await makeInternalMcpCatalog();
      await makeMcpServer({ catalogId: catalog.id });

      const toolsToSync = [
        {
          name: "tool-1",
          description: "First tool",
          parameters: { type: "object", properties: {} },
          catalogId: catalog.id,
        },
        {
          name: "tool-2",
          description: "Second tool",
          parameters: { type: "object", properties: {} },
          catalogId: catalog.id,
        },
      ];

      const result = await ToolModel.syncToolsForCatalog(toolsToSync);

      expect(result.created).toHaveLength(2);
      expect(result.updated).toHaveLength(0);
      expect(result.unchanged).toHaveLength(0);
      expect(result.created.map((t) => t.name)).toContain("tool-1");
      expect(result.created.map((t) => t.name)).toContain("tool-2");
    });

    test("updates existing tools when description changes", async ({
      makeInternalMcpCatalog,
      makeMcpServer,
      makeTool,
    }) => {
      const catalog = await makeInternalMcpCatalog();
      await makeMcpServer({ catalogId: catalog.id });

      // Create existing tool
      const existingTool = await makeTool({
        name: "tool-1",
        description: "Original description",
        parameters: { type: "object" },
        catalogId: catalog.id,
      });

      const toolsToSync = [
        {
          name: "tool-1",
          description: "Updated description",
          parameters: { type: "object" },
          catalogId: catalog.id,
        },
      ];

      const result = await ToolModel.syncToolsForCatalog(toolsToSync);

      expect(result.created).toHaveLength(0);
      expect(result.updated).toHaveLength(1);
      expect(result.unchanged).toHaveLength(0);
      expect(result.updated[0].id).toBe(existingTool.id);
      expect(result.updated[0].description).toBe("Updated description");
    });

    test("updates existing tools when parameters change", async ({
      makeInternalMcpCatalog,
      makeMcpServer,
      makeTool,
    }) => {
      const catalog = await makeInternalMcpCatalog();
      await makeMcpServer({ catalogId: catalog.id });

      // Create existing tool
      const existingTool = await makeTool({
        name: "tool-1",
        description: "Tool description",
        parameters: { type: "object", properties: { a: { type: "string" } } },
        catalogId: catalog.id,
      });

      const toolsToSync = [
        {
          name: "tool-1",
          description: "Tool description",
          parameters: {
            type: "object",
            properties: { a: { type: "string" }, b: { type: "number" } },
          },
          catalogId: catalog.id,
        },
      ];

      const result = await ToolModel.syncToolsForCatalog(toolsToSync);

      expect(result.created).toHaveLength(0);
      expect(result.updated).toHaveLength(1);
      expect(result.unchanged).toHaveLength(0);
      expect(result.updated[0].id).toBe(existingTool.id);
    });

    test("leaves tools unchanged when nothing changes", async ({
      makeInternalMcpCatalog,
      makeMcpServer,
      makeTool,
    }) => {
      const catalog = await makeInternalMcpCatalog();
      await makeMcpServer({ catalogId: catalog.id });

      // Create existing tool
      const existingTool = await makeTool({
        name: "tool-1",
        description: "Tool description",
        parameters: { type: "object" },
        catalogId: catalog.id,
      });

      const toolsToSync = [
        {
          name: "tool-1",
          description: "Tool description",
          parameters: { type: "object" },
          catalogId: catalog.id,
        },
      ];

      const result = await ToolModel.syncToolsForCatalog(toolsToSync);

      expect(result.created).toHaveLength(0);
      expect(result.updated).toHaveLength(0);
      expect(result.unchanged).toHaveLength(1);
      expect(result.unchanged[0].id).toBe(existingTool.id);
    });

    test("handles mix of create, update, and unchanged", async ({
      makeInternalMcpCatalog,
      makeMcpServer,
      makeTool,
    }) => {
      const catalog = await makeInternalMcpCatalog();
      await makeMcpServer({ catalogId: catalog.id });

      // Create existing tools
      const unchangedTool = await makeTool({
        name: "tool-unchanged",
        description: "No change",
        parameters: { type: "object" },
        catalogId: catalog.id,
      });

      const updateTool = await makeTool({
        name: "tool-update",
        description: "Old description",
        parameters: { type: "object" },
        catalogId: catalog.id,
      });

      const toolsToSync = [
        {
          name: "tool-unchanged",
          description: "No change",
          parameters: { type: "object" },
          catalogId: catalog.id,
        },
        {
          name: "tool-update",
          description: "New description",
          parameters: { type: "object" },
          catalogId: catalog.id,
        },
        {
          name: "tool-new",
          description: "Brand new tool",
          parameters: { type: "object" },
          catalogId: catalog.id,
        },
      ];

      const result = await ToolModel.syncToolsForCatalog(toolsToSync);

      expect(result.created).toHaveLength(1);
      expect(result.updated).toHaveLength(1);
      expect(result.unchanged).toHaveLength(1);

      expect(result.created[0].name).toBe("tool-new");
      expect(result.updated[0].id).toBe(updateTool.id);
      expect(result.unchanged[0].id).toBe(unchangedTool.id);
    });

    test("preserves tool IDs during update (for policy preservation)", async ({
      makeInternalMcpCatalog,
      makeMcpServer,
      makeTool,
      makeToolPolicy,
      makeAgent,
    }) => {
      const catalog = await makeInternalMcpCatalog();
      await makeMcpServer({ catalogId: catalog.id });
      const agent = await makeAgent();

      // Create existing tool with policy
      const existingTool = await makeTool({
        name: "tool-with-policy",
        description: "Has policy",
        parameters: { type: "object" },
        catalogId: catalog.id,
      });

      // Create a tool invocation policy for this tool
      await makeToolPolicy(existingTool.id, {
        action: "block_always",
        reason: "Test policy",
      });

      // Assign tool to agent
      await AgentToolModel.create(agent.id, existingTool.id);

      // Sync with updated description
      const toolsToSync = [
        {
          name: "tool-with-policy",
          description: "Updated description",
          parameters: { type: "object" },
          catalogId: catalog.id,
        },
      ];

      const result = await ToolModel.syncToolsForCatalog(toolsToSync);

      expect(result.updated).toHaveLength(1);
      expect(result.updated[0].id).toBe(existingTool.id);

      // Verify agent-tool assignment still exists (key verification for policy preservation)
      const agentToolIds = await AgentToolModel.findToolIdsByAgent(agent.id);
      expect(agentToolIds).toContain(existingTool.id);
    });

    test("returns empty arrays for empty input", async () => {
      const result = await ToolModel.syncToolsForCatalog([]);

      expect(result.created).toHaveLength(0);
      expect(result.updated).toHaveLength(0);
      expect(result.unchanged).toHaveLength(0);
      expect(result.deleted).toHaveLength(0);
    });

    test("renames tools when catalog name changes (preserves ID and assignments)", async ({
      makeInternalMcpCatalog,
      makeMcpServer,
      makeTool,
      makeAgent,
    }) => {
      const catalog = await makeInternalMcpCatalog({
        name: "old-catalog-name",
      });
      await makeMcpServer({ catalogId: catalog.id });
      const agent = await makeAgent();

      // Create existing tool with old catalog name prefix
      const existingTool = await makeTool({
        name: "old-catalog-name__query-docs",
        description: "Query docs",
        parameters: { type: "object" },
        catalogId: catalog.id,
      });

      // Assign tool to agent
      await AgentToolModel.create(agent.id, existingTool.id);

      // Sync with new catalog name (simulating catalog rename)
      const toolsToSync = [
        {
          name: "new-catalog-name__query-docs", // Same raw name, different prefix
          description: "Query docs",
          parameters: { type: "object" },
          catalogId: catalog.id,
        },
      ];

      const result = await ToolModel.syncToolsForCatalog(toolsToSync);

      // Should update (rename) the existing tool, not create a new one
      expect(result.created).toHaveLength(0);
      expect(result.updated).toHaveLength(1);
      expect(result.unchanged).toHaveLength(0);
      expect(result.deleted).toHaveLength(0);

      // Verify the tool was renamed but kept the same ID
      expect(result.updated[0].id).toBe(existingTool.id);
      expect(result.updated[0].name).toBe("new-catalog-name__query-docs");

      // Verify agent-tool assignment still exists (uses same tool ID)
      const agentToolIds = await AgentToolModel.findToolIdsByAgent(agent.id);
      expect(agentToolIds).toContain(existingTool.id);
    });

    test("deletes orphaned tools that are no longer returned by MCP server", async ({
      makeInternalMcpCatalog,
      makeMcpServer,
      makeTool,
    }) => {
      const catalog = await makeInternalMcpCatalog();
      await makeMcpServer({ catalogId: catalog.id });

      // Create existing tools
      const tool1 = await makeTool({
        name: "catalog__tool-1",
        description: "Tool 1",
        parameters: { type: "object" },
        catalogId: catalog.id,
      });

      await makeTool({
        name: "catalog__tool-2",
        description: "Tool 2 - will be removed",
        parameters: { type: "object" },
        catalogId: catalog.id,
      });

      // Sync with only one tool (simulating tool-2 being removed from MCP server)
      const toolsToSync = [
        {
          name: "catalog__tool-1",
          description: "Tool 1",
          parameters: { type: "object" },
          catalogId: catalog.id,
        },
      ];

      const result = await ToolModel.syncToolsForCatalog(toolsToSync);

      // tool-1 should be unchanged, tool-2 should be deleted
      expect(result.unchanged).toHaveLength(1);
      expect(result.unchanged[0].id).toBe(tool1.id);
      expect(result.deleted).toHaveLength(1);
      expect(result.deleted[0].name).toBe("catalog__tool-2");
    });

    test("cleans up duplicate tools after catalog rename (legacy duplicates)", async ({
      makeInternalMcpCatalog,
      makeMcpServer,
      makeTool,
    }) => {
      const catalog = await makeInternalMcpCatalog();
      await makeMcpServer({ catalogId: catalog.id });

      // Create legacy tool with old catalog name prefix
      // This simulates a tool that existed before catalog was renamed
      await makeTool({
        name: "old-name__query-docs",
        description: "Old tool with legacy name",
        parameters: { type: "object" },
        catalogId: catalog.id,
      });

      // Sync with the new name (after catalog rename)
      const toolsToSync = [
        {
          name: "new-name__query-docs",
          description: "New tool",
          parameters: { type: "object" },
          catalogId: catalog.id,
          rawToolName: "query-docs",
        },
      ];

      const result = await ToolModel.syncToolsForCatalog(toolsToSync);

      // The old tool should be updated with the new name (matched by rawToolName)
      // Note: If the old tool didn't have rawToolName stored, it would be deleted
      // and the new tool would be created instead
      const survivingTools = [...result.unchanged, ...result.updated];

      // Verify exactly one tool survives with the new name
      expect(survivingTools.length + result.created.length).toBe(1);
      const finalTool = survivingTools[0] || result.created[0];
      expect(finalTool.name).toBe("new-name__query-docs");
    });

    test("creates default policies for newly created tools", async ({
      makeInternalMcpCatalog,
      makeMcpServer,
    }) => {
      const catalog = await makeInternalMcpCatalog();
      await makeMcpServer({ catalogId: catalog.id });

      const toolsToSync = [
        {
          name: "new-tool",
          description: "New tool",
          parameters: { type: "object" },
          catalogId: catalog.id,
        },
      ];

      const result = await ToolModel.syncToolsForCatalog(toolsToSync);

      expect(result.created).toHaveLength(1);

      // Verify the tool was created (default policies are created internally by createDefaultPolicies)
      const createdTool = result.created[0];
      expect(createdTool.id).toBeDefined();
      expect(createdTool.name).toBe("new-tool");
    });

    test("creates new tools with meta field", async ({
      makeInternalMcpCatalog,
      makeMcpServer,
    }) => {
      const catalog = await makeInternalMcpCatalog();
      const mcpServer = await makeMcpServer({ catalogId: catalog.id });

      const meta = {
        _meta: { ui: { resourceUri: "mcp://widget/stats" } },
        annotations: { audience: ["user"] },
      };

      const toolsToSync = [
        {
          name: "tool-with-meta",
          description: "Tool with UI metadata",
          parameters: { type: "object" },
          catalogId: catalog.id,
          mcpServerId: mcpServer.id,
          meta,
        },
      ];

      const result = await ToolModel.syncToolsForCatalog(toolsToSync);

      expect(result.created).toHaveLength(1);
      expect(result.created[0].meta).toEqual(meta);
    });

    test("updates tools when meta changes", async ({
      makeInternalMcpCatalog,
      makeMcpServer,
      makeTool,
    }) => {
      const catalog = await makeInternalMcpCatalog();
      const mcpServer = await makeMcpServer({ catalogId: catalog.id });

      await makeTool({
        name: "tool-meta-update",
        description: "Tool",
        parameters: { type: "object" },
        catalogId: catalog.id,
      });

      const newMeta = {
        _meta: { ui: { resourceUri: "mcp://widget/new-ui" } },
      };

      const toolsToSync = [
        {
          name: "tool-meta-update",
          description: "Tool",
          parameters: { type: "object" },
          catalogId: catalog.id,
          mcpServerId: mcpServer.id,
          meta: newMeta,
        },
      ];

      const result = await ToolModel.syncToolsForCatalog(toolsToSync);

      expect(result.updated).toHaveLength(1);
      expect(result.unchanged).toHaveLength(0);
      expect(result.updated[0].meta).toEqual(newMeta);
    });

    test("treats null and undefined meta as equivalent (unchanged)", async ({
      makeInternalMcpCatalog,
      makeMcpServer,
      makeTool,
    }) => {
      const catalog = await makeInternalMcpCatalog();
      const mcpServer = await makeMcpServer({ catalogId: catalog.id });

      // Tool created without meta (stored as null in DB)
      await makeTool({
        name: "tool-null-meta",
        description: "Tool",
        parameters: { type: "object" },
        catalogId: catalog.id,
      });

      // Sync without providing meta (undefined) — should be unchanged
      const toolsToSync = [
        {
          name: "tool-null-meta",
          description: "Tool",
          parameters: { type: "object" },
          catalogId: catalog.id,
          mcpServerId: mcpServer.id,
        },
      ];

      const result = await ToolModel.syncToolsForCatalog(toolsToSync);

      expect(result.unchanged).toHaveLength(1);
      expect(result.updated).toHaveLength(0);
    });
  });

  describe("getMcpToolsAssignedToAgentBySuffix", () => {
    test("returns empty array when no tools match suffix", async ({
      makeAgent,
      makeUser,
    }) => {
      await makeUser();
      const agent = await makeAgent();

      const result = await ToolModel.getMcpToolsAssignedToAgentBySuffix(
        "nonexistent-tool",
        agent.id,
      );

      expect(result).toEqual([]);
    });

    test("finds tool by raw name suffix", async ({
      makeAgent,
      makeInternalMcpCatalog,
      makeTool,
    }) => {
      const agent = await makeAgent();

      const catalogItem = await makeInternalMcpCatalog({
        name: "system-server",
        serverUrl: "https://example.com/mcp/",
      });

      const tool = await makeTool({
        name: `system${MCP_SERVER_TOOL_NAME_SEPARATOR}refresh-stats`,
        description: "Refresh stats",
        parameters: { type: "object" },
        catalogId: catalogItem.id,
      });

      await AgentToolModel.create(agent.id, tool.id);

      const result = await ToolModel.getMcpToolsAssignedToAgentBySuffix(
        "refresh-stats",
        agent.id,
      );

      expect(result).toHaveLength(1);
      expect(result[0].toolName).toBe(
        `system${MCP_SERVER_TOOL_NAME_SEPARATOR}refresh-stats`,
      );
      expect(result[0].catalogId).toBe(catalogItem.id);
      expect(result[0].catalogName).toBe("system-server");
    });

    test("does not match proxy-sniffed tools without catalogId", async ({
      makeUser,
      makeAgent,
      makeTool,
    }) => {
      await makeUser();
      const agent = await makeAgent();

      // Proxy-sniffed tool has agentId set but no catalogId
      const tool = await makeTool({
        name: `server${MCP_SERVER_TOOL_NAME_SEPARATOR}some-tool`,
        description: "Proxy tool",
        parameters: {},
        agentId: agent.id,
      });

      await AgentToolModel.create(agent.id, tool.id);

      const result = await ToolModel.getMcpToolsAssignedToAgentBySuffix(
        "some-tool",
        agent.id,
      );

      expect(result).toEqual([]);
    });

    test("returns at most one result (limit 1)", async ({
      makeAgent,
      makeInternalMcpCatalog,
      makeTool,
    }) => {
      const agent = await makeAgent();

      const catalog1 = await makeInternalMcpCatalog({
        name: "server-a",
        serverUrl: "https://a.com/mcp/",
      });
      const catalog2 = await makeInternalMcpCatalog({
        name: "server-b",
        serverUrl: "https://b.com/mcp/",
      });

      const tool1 = await makeTool({
        name: `server-a${MCP_SERVER_TOOL_NAME_SEPARATOR}list-items`,
        description: "List items A",
        parameters: {},
        catalogId: catalog1.id,
      });
      const tool2 = await makeTool({
        name: `server-b${MCP_SERVER_TOOL_NAME_SEPARATOR}list-items`,
        description: "List items B",
        parameters: {},
        catalogId: catalog2.id,
      });

      await AgentToolModel.create(agent.id, tool1.id);
      await AgentToolModel.create(agent.id, tool2.id);

      const result = await ToolModel.getMcpToolsAssignedToAgentBySuffix(
        "list-items",
        agent.id,
      );

      expect(result).toHaveLength(1);
    });
  });

  describe("findToolsByUiResourceUri", () => {
    test("returns empty array when agent has no tools", async ({
      makeAgent,
    }) => {
      const agent = await makeAgent();

      const result = await ToolModel.findToolsByUiResourceUri(
        agent.id,
        "mcp://widget/stats",
      );

      expect(result).toEqual([]);
    });

    test("finds tools matching ui/resourceUri in meta", async ({
      makeAgent,
      makeInternalMcpCatalog,
      makeTool,
    }) => {
      const agent = await makeAgent();

      const catalogItem = await makeInternalMcpCatalog({
        name: "widget-server",
        serverUrl: "https://example.com/mcp/",
      });

      const tool = await makeTool({
        name: "widget-server__show-dashboard",
        description: "Show dashboard",
        parameters: {},
        catalogId: catalogItem.id,
      });

      // Update meta directly via syncToolsForCatalog to set the resourceUri
      const resourceUri = "mcp://widget/dashboard";
      await ToolModel.syncToolsForCatalog([
        {
          name: "widget-server__show-dashboard",
          description: "Show dashboard",
          parameters: {},
          catalogId: catalogItem.id,
          meta: { _meta: { ui: { resourceUri } } },
        },
      ]);

      await AgentToolModel.create(agent.id, tool.id);

      const result = await ToolModel.findToolsByUiResourceUri(
        agent.id,
        resourceUri,
      );

      expect(result).toHaveLength(1);
      expect(result[0].tool.id).toBe(tool.id);
    });

    test("does not return tools with non-matching resourceUri", async ({
      makeAgent,
      makeInternalMcpCatalog,
      makeTool,
    }) => {
      const agent = await makeAgent();

      const catalogItem = await makeInternalMcpCatalog({
        name: "widget-server",
        serverUrl: "https://example.com/mcp/",
      });

      const tool = await makeTool({
        name: "widget-server__show-chart",
        description: "Show chart",
        parameters: {},
        catalogId: catalogItem.id,
      });

      await ToolModel.syncToolsForCatalog([
        {
          name: "widget-server__show-chart",
          description: "Show chart",
          parameters: {},
          catalogId: catalogItem.id,
          meta: { _meta: { ui: { resourceUri: "mcp://widget/chart" } } },
        },
      ]);

      await AgentToolModel.create(agent.id, tool.id);

      const result = await ToolModel.findToolsByUiResourceUri(
        agent.id,
        "mcp://widget/completely-different",
      );

      expect(result).toEqual([]);
    });

    test("does not return tools without meta", async ({
      makeAgent,
      makeInternalMcpCatalog,
      makeTool,
    }) => {
      const agent = await makeAgent();

      const catalogItem = await makeInternalMcpCatalog({
        name: "plain-server",
        serverUrl: "https://example.com/mcp/",
      });

      const tool = await makeTool({
        name: "plain-server__plain-tool",
        description: "No UI metadata",
        parameters: {},
        catalogId: catalogItem.id,
      });

      await AgentToolModel.create(agent.id, tool.id);

      const result = await ToolModel.findToolsByUiResourceUri(
        agent.id,
        "mcp://widget/any",
      );

      expect(result).toEqual([]);
    });
  });

  describe("bulkCreateToolsIfNotExists", () => {
    test("stores meta field when creating tools", async ({
      makeInternalMcpCatalog,
      makeMcpServer,
    }) => {
      const catalog = await makeInternalMcpCatalog();
      await makeMcpServer({ catalogId: catalog.id });

      const meta = {
        _meta: { ui: { resourceUri: "mcp://app/view" } },
        annotations: { readOnlyHint: true },
      };

      const result = await ToolModel.bulkCreateToolsIfNotExists([
        {
          name: "bulk-tool-with-meta",
          description: "Tool with meta",
          parameters: { type: "object" },
          catalogId: catalog.id,
          meta,
        },
      ]);

      expect(result).toHaveLength(1);
      expect(result[0].meta).toEqual(meta);
    });

    test("updates meta on existing tools when it changed", async ({
      makeInternalMcpCatalog,
    }) => {
      const catalog = await makeInternalMcpCatalog();

      const originalMeta = {
        _meta: { ui: { resourceUri: "mcp://app/original" } },
      };

      // Create the tool first
      const [created] = await ToolModel.bulkCreateToolsIfNotExists([
        {
          name: "bulk-tool-existing",
          description: "Tool",
          parameters: {},
          catalogId: catalog.id,
          meta: originalMeta,
        },
      ]);

      const updatedMeta = {
        _meta: { ui: { resourceUri: "mcp://app/different" } },
      };

      // Call again with different meta — should update meta on existing tool
      const result = await ToolModel.bulkCreateToolsIfNotExists([
        {
          name: "bulk-tool-existing",
          description: "Tool",
          parameters: {},
          catalogId: catalog.id,
          meta: updatedMeta,
        },
      ]);

      expect(result).toHaveLength(1);
      expect(result[0].id).toBe(created.id);
      expect(result[0].meta).toEqual(updatedMeta);
    });

    test("preserves meta on existing tools when it has not changed", async ({
      makeInternalMcpCatalog,
    }) => {
      const catalog = await makeInternalMcpCatalog();

      const meta = {
        _meta: { ui: { resourceUri: "mcp://app/original" } },
      };

      // Create the tool first
      const [created] = await ToolModel.bulkCreateToolsIfNotExists([
        {
          name: "bulk-tool-same-meta",
          description: "Tool",
          parameters: {},
          catalogId: catalog.id,
          meta,
        },
      ]);

      // Call again with the same meta — should return existing tool unchanged
      const result = await ToolModel.bulkCreateToolsIfNotExists([
        {
          name: "bulk-tool-same-meta",
          description: "Tool",
          parameters: {},
          catalogId: catalog.id,
          meta,
        },
      ]);

      expect(result).toHaveLength(1);
      expect(result[0].id).toBe(created.id);
      expect(result[0].meta).toEqual(meta);
    });
  });

  describe("parseArchestraBuiltInName", () => {
    test("parses default built-in tool names", () => {
      expect(parseArchestraBuiltInName("archestra__create_agent")).toEqual({
        serverName: "archestra",
        shortName: "create_agent",
      });
    });

    test("parses white-labeled built-in tool names", () => {
      expect(parseArchestraBuiltInName("acme_copilot__create_agent")).toEqual({
        serverName: "acme_copilot",
        shortName: "create_agent",
      });
    });

    test("returns null shortName for non-built-in tool names", () => {
      expect(parseArchestraBuiltInName("github__list_issues")).toEqual({
        serverName: "github",
        shortName: null,
      });
    });
  });

  describe("seedArchestraTools", () => {
    test("creates the built-in catalog entry with current metadata", async () => {
      const catalogId = randomUUID();
      archestraMcpBranding.syncFromOrganization(null);
      const metadata = getArchestraMcpCatalogMetadata();

      await ToolModel.seedArchestraTools(catalogId);

      const [catalog] = await db
        .select()
        .from(schema.internalMcpCatalogTable)
        .where(eq(schema.internalMcpCatalogTable.id, catalogId));

      expect(catalog).toBeDefined();
      expect(catalog?.name).toBe(metadata.name);
      expect(catalog?.description).toBe(metadata.description);
      expect(catalog?.docsUrl).toBe(metadata.docsUrl);
      expect(catalog?.serverType).toBe(metadata.serverType);
      expect(catalog?.requiresAuth).toBe(metadata.requiresAuth);
    });

    test("updates stale built-in catalog metadata on reseed", async () => {
      const catalogId = randomUUID();
      archestraMcpBranding.syncFromOrganization(null);
      const metadata = getArchestraMcpCatalogMetadata();

      await db.insert(schema.internalMcpCatalogTable).values({
        id: catalogId,
        name: "Old Archestra",
        description: "Outdated description",
        docsUrl: "https://example.com/old-docs",
        serverType: "builtin",
        requiresAuth: true,
      });

      await ToolModel.seedArchestraTools(catalogId);

      const [catalog] = await db
        .select()
        .from(schema.internalMcpCatalogTable)
        .where(eq(schema.internalMcpCatalogTable.id, catalogId));

      expect(catalog).toBeDefined();
      expect(catalog?.name).toBe(metadata.name);
      expect(catalog?.description).toBe(metadata.description);
      expect(catalog?.docsUrl).toBe(metadata.docsUrl);
      expect(catalog?.serverType).toBe(metadata.serverType);
      expect(catalog?.requiresAuth).toBe(metadata.requiresAuth);
    });

    test("seeds default invocation + trusted data policies for query_knowledge_sources", async () => {
      const catalogId = randomUUID();
      archestraMcpBranding.syncFromOrganization(null);

      await ToolModel.seedArchestraTools(catalogId);

      const [kbTool] = await db
        .select({ id: schema.toolsTable.id })
        .from(schema.toolsTable)
        .where(
          eq(schema.toolsTable.name, TOOL_QUERY_KNOWLEDGE_SOURCES_FULL_NAME),
        );
      expect(kbTool).toBeDefined();

      const invocationPolicies = await db
        .select()
        .from(schema.toolInvocationPoliciesTable)
        .where(eq(schema.toolInvocationPoliciesTable.toolId, kbTool.id));
      const trustedDataPolicies = await db
        .select()
        .from(schema.trustedDataPoliciesTable)
        .where(eq(schema.trustedDataPoliciesTable.toolId, kbTool.id));

      expect(invocationPolicies).toHaveLength(1);
      expect(invocationPolicies[0].conditions).toEqual([]);
      expect(invocationPolicies[0].action).toBe(
        "allow_when_context_is_untrusted",
      );

      expect(trustedDataPolicies).toHaveLength(1);
      expect(trustedDataPolicies[0].conditions).toEqual([]);
      expect(trustedDataPolicies[0].action).toBe("mark_as_untrusted");
    });

    test("does not overwrite admin-customized policies for query_knowledge_sources on reseed", async () => {
      const catalogId = randomUUID();
      archestraMcpBranding.syncFromOrganization(null);

      await ToolModel.seedArchestraTools(catalogId);

      const [kbTool] = await db
        .select({ id: schema.toolsTable.id })
        .from(schema.toolsTable)
        .where(
          eq(schema.toolsTable.name, TOOL_QUERY_KNOWLEDGE_SOURCES_FULL_NAME),
        );

      // Admin changes both policies via the UI.
      await db
        .update(schema.toolInvocationPoliciesTable)
        .set({ action: "block_when_context_is_untrusted" })
        .where(eq(schema.toolInvocationPoliciesTable.toolId, kbTool.id));
      await db
        .update(schema.trustedDataPoliciesTable)
        .set({ action: "mark_as_trusted" })
        .where(eq(schema.trustedDataPoliciesTable.toolId, kbTool.id));

      // Re-seed (simulates a server restart).
      await ToolModel.seedArchestraTools(catalogId);

      const invocationPolicies = await db
        .select()
        .from(schema.toolInvocationPoliciesTable)
        .where(eq(schema.toolInvocationPoliciesTable.toolId, kbTool.id));
      const trustedDataPolicies = await db
        .select()
        .from(schema.trustedDataPoliciesTable)
        .where(eq(schema.trustedDataPoliciesTable.toolId, kbTool.id));

      expect(invocationPolicies).toHaveLength(1);
      expect(invocationPolicies[0].action).toBe(
        "block_when_context_is_untrusted",
      );

      expect(trustedDataPolicies).toHaveLength(1);
      expect(trustedDataPolicies[0].action).toBe("mark_as_trusted");
    });

    test("rebrands built-in catalog metadata and tool names on sync for white-labeled orgs", async ({
      makeOrganization,
    }) => {
      const org = await makeOrganization();
      await OrganizationModel.patch(org.id, {
        appName: "Acme Copilot",
        iconLogo: "https://cdn.example.com/logo.png",
      });

      await ToolModel.syncArchestraBuiltInCatalog({
        organization: {
          appName: "Acme Copilot",
          iconLogo: "https://cdn.example.com/logo.png",
        },
      });

      const [catalog] = await db
        .select()
        .from(schema.internalMcpCatalogTable)
        .where(
          eq(
            schema.internalMcpCatalogTable.id,
            "00000000-0000-4000-8000-000000000001",
          ),
        );
      const [todoTool] = await db
        .select()
        .from(schema.toolsTable)
        .where(
          eq(
            schema.toolsTable.name,
            getArchestraToolFullName(TOOL_TODO_WRITE_SHORT_NAME, {
              appName: "Acme Copilot",
              fullWhiteLabeling: true,
            }),
          ),
        );

      expect(catalog?.name).toBe(
        getArchestraMcpCatalogName({
          appName: "Acme Copilot",
          fullWhiteLabeling: true,
        }),
      );
      expect(catalog?.docsUrl).toBeNull();
      expect(catalog?.icon).toBe("https://cdn.example.com/logo.png");
      expect(catalog?.description).not.toContain("Archestra");
      expect(todoTool).toBeDefined();
    });

    test("does not crash startup when a legacy/branded prefix duplicate exists", async ({
      makeOrganization,
    }) => {
      const org = await makeOrganization();
      await OrganizationModel.patch(org.id, { appName: "Acme Copilot" });
      const brandedOrg = { appName: "Acme Copilot", iconLogo: null };

      archestraMcpBranding.syncFromOrganization(brandedOrg);
      const brandedName = archestraMcpBranding.getToolName(
        TOOL_TODO_WRITE_SHORT_NAME,
      );
      const legacyName = getArchestraToolFullName(TOOL_TODO_WRITE_SHORT_NAME, {
        appName: null,
        fullWhiteLabeling: false,
      });

      await db.insert(schema.internalMcpCatalogTable).values({
        id: ARCHESTRA_MCP_CATALOG_ID,
        ...getArchestraMcpCatalogMetadata(),
      });
      // Stage a legacy + branded sibling for one built-in (same short name,
      // different prefix). Branded row first so reconciliation keeps the legacy
      // row and attempts the colliding rename onto the existing branded name.
      await db.insert(schema.toolsTable).values({
        name: brandedName,
        parameters: {},
        catalogId: ARCHESTRA_MCP_CATALOG_ID,
        agentId: null,
      });
      await db.insert(schema.toolsTable).values({
        name: legacyName,
        parameters: {},
        catalogId: ARCHESTRA_MCP_CATALOG_ID,
        agentId: null,
      });

      // Reseeding under branding renames the legacy row toward the branded name,
      // which collides with the staged sibling. One built-in conflict must not
      // crash startup — seeding resolves.
      await expect(
        ToolModel.seedArchestraTools(ARCHESTRA_MCP_CATALOG_ID, brandedOrg),
      ).resolves.not.toThrow();

      // The branded built-in converges to exactly one row.
      const brandedRows = await db
        .select()
        .from(schema.toolsTable)
        .where(
          and(
            eq(schema.toolsTable.catalogId, ARCHESTRA_MCP_CATALOG_ID),
            eq(schema.toolsTable.name, brandedName),
          ),
        );
      expect(brandedRows).toHaveLength(1);
    });

    test("does not duplicate built-in tool rows across repeated seeds", async () => {
      archestraMcpBranding.syncFromOrganization(null);

      await ToolModel.seedArchestraTools(ARCHESTRA_MCP_CATALOG_ID);
      await ToolModel.seedArchestraTools(ARCHESTRA_MCP_CATALOG_ID);

      const rows = await db
        .select()
        .from(schema.toolsTable)
        .where(
          and(
            eq(schema.toolsTable.catalogId, ARCHESTRA_MCP_CATALOG_ID),
            eq(schema.toolsTable.name, "archestra__whoami"),
          ),
        );

      expect(rows).toHaveLength(1);
    });

    test("reports only freshly inserted tools as newly created", async () => {
      archestraMcpBranding.syncFromOrganization(null);

      const firstRun = await ToolModel.seedArchestraTools(
        ARCHESTRA_MCP_CATALOG_ID,
      );
      expect(firstRun).toContain("archestra__whoami");

      const secondRun = await ToolModel.seedArchestraTools(
        ARCHESTRA_MCP_CATALOG_ID,
      );
      expect(secondRun).toEqual([]);
    });

    test("keeps a registered built-in but prunes a truly-removed one", async () => {
      // A pre-existing row whose short name is still in the registry must
      // survive reseed; a row whose short name is gone from the registry is the
      // only kind that is genuinely stale.
      archestraMcpBranding.syncFromOrganization(null);
      const catalogId = randomUUID();
      await ToolModel.seedArchestraTools(catalogId);

      const flaggedOffName = "archestra__scaffold_app";
      const removedName = "archestra__obsolete_tool";
      await db.insert(schema.toolsTable).values([
        { name: flaggedOffName, parameters: {}, catalogId, agentId: null },
        { name: removedName, parameters: {}, catalogId, agentId: null },
      ]);

      await ToolModel.seedArchestraTools(catalogId);

      const survivors = await db
        .select({ name: schema.toolsTable.name })
        .from(schema.toolsTable)
        .where(eq(schema.toolsTable.catalogId, catalogId));
      const names = new Set(survivors.map((t) => t.name));
      expect(names.has(flaggedOffName)).toBe(true);
      expect(names.has(removedName)).toBe(false);
    });

    test("rejects a duplicate built-in tool row at the database level", async () => {
      archestraMcpBranding.syncFromOrganization(null);
      await ToolModel.seedArchestraTools(ARCHESTRA_MCP_CATALOG_ID);

      await expect(
        db.insert(schema.toolsTable).values({
          name: "archestra__whoami",
          parameters: {},
          catalogId: ARCHESTRA_MCP_CATALOG_ID,
          agentId: null,
        }),
      ).rejects.toThrow();
    });

    test("upsert reports a conflicting row as not freshly inserted", async () => {
      archestraMcpBranding.syncFromOrganization(null);
      await ToolModel.seedArchestraTools(ARCHESTRA_MCP_CATALOG_ID);

      // Mimics a concurrent seed's bulk insert landing on a row another process
      // already inserted: the conflict path must report inserted=false (xmax != 0) so
      // seedArchestraTools does not re-announce it as newly created.
      const conflictResult = await db
        .insert(schema.toolsTable)
        .values({
          name: "archestra__whoami",
          parameters: {},
          catalogId: ARCHESTRA_MCP_CATALOG_ID,
          agentId: null,
        })
        .onConflictDoUpdate({
          target: [schema.toolsTable.catalogId, schema.toolsTable.name],
          targetWhere: sql`${schema.toolsTable.catalogId} = ${sql.raw(`'${ARCHESTRA_MCP_CATALOG_ID}'`)} and ${schema.toolsTable.agentId} is null and ${schema.toolsTable.delegateToAgentId} is null`,
          set: { description: sql`excluded.description` },
        })
        .returning({ inserted: sql<boolean>`(xmax = 0)` });

      expect(conflictResult).toHaveLength(1);
      expect(conflictResult[0].inserted).toBe(false);

      const rows = await db
        .select()
        .from(schema.toolsTable)
        .where(
          and(
            eq(schema.toolsTable.catalogId, ARCHESTRA_MCP_CATALOG_ID),
            eq(schema.toolsTable.name, "archestra__whoami"),
          ),
        );
      expect(rows).toHaveLength(1);
    });

    test("promotes only one discovered row per name without violating the unique index", async () => {
      archestraMcpBranding.syncFromOrganization(null);

      // Two legacy "discovered" rows (catalog_id NULL) for the same built-in name.
      await db.insert(schema.toolsTable).values([
        {
          name: "archestra__whoami",
          parameters: {},
          catalogId: null,
          agentId: null,
        },
        {
          name: "archestra__whoami",
          parameters: {},
          catalogId: null,
          agentId: null,
        },
      ]);

      await ToolModel.seedArchestraTools(ARCHESTRA_MCP_CATALOG_ID);

      const catalogRows = await db
        .select()
        .from(schema.toolsTable)
        .where(
          and(
            eq(schema.toolsTable.catalogId, ARCHESTRA_MCP_CATALOG_ID),
            eq(schema.toolsTable.name, "archestra__whoami"),
          ),
        );
      expect(catalogRows).toHaveLength(1);
    });

    test("does not promote a default-prefixed discovery when the branded short-name twin is already cataloged", async () => {
      const brandedOrg = { appName: "Acme Copilot", iconLogo: null };
      archestraMcpBranding.syncFromOrganization(brandedOrg);

      const brandedName = archestraMcpBranding.getToolName(
        TOOL_TODO_WRITE_SHORT_NAME,
      );
      const defaultName = getArchestraToolFullName(TOOL_TODO_WRITE_SHORT_NAME, {
        appName: null,
        fullWhiteLabeling: false,
      });
      expect(brandedName).not.toBe(defaultName); // branded env: prefixes differ

      await db.insert(schema.internalMcpCatalogTable).values({
        id: ARCHESTRA_MCP_CATALOG_ID,
        ...getArchestraMcpCatalogMetadata(),
      });
      // The canonical, branded built-in already lives in the catalog.
      await db.insert(schema.toolsTable).values({
        name: brandedName,
        parameters: {},
        catalogId: ARCHESTRA_MCP_CATALOG_ID,
        agentId: null,
      });
      // Off-brand discovery: the same built-in arrived under the DEFAULT prefix as
      // a shared proxy tool (catalog_id NULL) — what LLM-proxy auto-discovery used
      // to persist before the persistTools guard recognized both prefixes.
      const [discovered] = await db
        .insert(schema.toolsTable)
        .values({
          name: defaultName,
          parameters: {},
          catalogId: null,
          agentId: null,
        })
        .returning();

      await ToolModel.seedArchestraTools(ARCHESTRA_MCP_CATALOG_ID, brandedOrg);

      // The discovered twin must NOT be promoted into the catalog…
      const [after] = await db
        .select()
        .from(schema.toolsTable)
        .where(eq(schema.toolsTable.id, discovered.id));
      expect(after?.catalogId).toBeNull();

      // …and the catalog holds exactly one row for the todo_write short name.
      const catalogRows = await db
        .select({ name: schema.toolsTable.name })
        .from(schema.toolsTable)
        .where(eq(schema.toolsTable.catalogId, ARCHESTRA_MCP_CATALOG_ID));
      const twins = catalogRows
        .map((r) => r.name)
        .filter((name) => name === brandedName || name === defaultName);
      expect(twins).toEqual([brandedName]);
    });
  });

  describe("findAllWithAssignments", () => {
    test("returns deterministic order for tools with identical createdAt timestamps", async ({
      makeAdmin,
      makeAgent,
      makeAgentTool,
    }) => {
      const admin = await makeAdmin();
      const agent = await makeAgent({ name: "TestAgent" });

      // Create multiple tools with the exact same createdAt timestamp
      const sharedTimestamp = new Date("2024-01-01T00:00:00Z");
      const toolNames = ["tool-c", "tool-a", "tool-b"];
      const tools = [];
      for (const name of toolNames) {
        const tool = await ToolModel.create({
          name,
          description: `Description for ${name}`,
          parameters: {},
          createdAt: sharedTimestamp,
          updatedAt: sharedTimestamp,
        });
        await makeAgentTool(agent.id, tool.id);
        tools.push(tool);
      }

      // Fetch multiple times and verify ordering is identical
      const result1 = await ToolModel.findAllWithAssignments({
        userId: admin.id,
        isAgentAdmin: true,
      });
      const result2 = await ToolModel.findAllWithAssignments({
        userId: admin.id,
        isAgentAdmin: true,
      });

      const ids1 = result1.data.map((t) => t.id);
      const ids2 = result2.data.map((t) => t.id);
      expect(ids1).toEqual(ids2);
    });

    test("exposes MCP annotations stored in the tool's meta", async ({
      makeAdmin,
      makeAgent,
      makeAgentTool,
      makeInternalMcpCatalog,
    }) => {
      const admin = await makeAdmin();
      const agent = await makeAgent({ name: "AnnotationsAgent" });
      const catalog = await makeInternalMcpCatalog();

      const [withMeta] = await ToolModel.bulkCreateToolsIfNotExists([
        {
          name: "annotated-tool",
          description: "Tool with annotations",
          parameters: { type: "object" },
          catalogId: catalog.id,
          meta: {
            _meta: {},
            annotations: { readOnlyHint: true, destructiveHint: false },
          },
        },
      ]);
      const plain = await ToolModel.create({
        name: "plain-tool",
        description: "Tool without meta",
        parameters: {},
      });
      await makeAgentTool(agent.id, withMeta.id);
      await makeAgentTool(agent.id, plain.id);

      const result = await ToolModel.findAllWithAssignments({
        userId: admin.id,
        isAgentAdmin: true,
      });

      const annotated = result.data.find((t) => t.id === withMeta.id);
      expect(annotated?.annotations).toEqual({
        readOnlyHint: true,
        destructiveHint: false,
      });
      const unannotated = result.data.find((t) => t.id === plain.id);
      expect(unannotated?.annotations).toBeNull();
    });

    test("excludes the white-labeled knowledge tool from assignment listings", async ({
      makeOrganization,
      makeAgent,
      makeAgentTool,
    }) => {
      const org = await makeOrganization();
      await OrganizationModel.patch(org.id, { appName: "Acme Copilot" });
      // Agent created BEFORE the built-ins are synced so the create path has
      // nothing to auto-assign — this test assigns the knowledge tool itself.
      const agent = await makeAgent({ organizationId: org.id });
      await ToolModel.syncArchestraBuiltInCatalog({
        organization: { appName: "Acme Copilot", iconLogo: null },
      });

      const [kbTool] = await db
        .select()
        .from(schema.toolsTable)
        .where(
          eq(
            schema.toolsTable.name,
            getArchestraToolFullName(TOOL_QUERY_KNOWLEDGE_SOURCES_SHORT_NAME, {
              appName: "Acme Copilot",
              fullWhiteLabeling: true,
            }),
          ),
        );

      expect(kbTool).toBeDefined();
      await makeAgentTool(agent.id, kbTool?.id);

      const result = await ToolModel.findAllWithAssignments({
        filters: {},
      });

      expect(
        result.data.some(
          (tool) =>
            tool.name ===
            getArchestraToolFullName(TOOL_QUERY_KNOWLEDGE_SOURCES_SHORT_NAME, {
              appName: "Acme Copilot",
              fullWhiteLabeling: true,
            }),
        ),
      ).toBe(false);
    });

    test("includes the white-labeled knowledge tool when explicitly requested", async ({
      makeOrganization,
      makeAgent,
      makeAgentTool,
    }) => {
      const org = await makeOrganization();
      await OrganizationModel.patch(org.id, { appName: "Acme Copilot" });
      // Agent created BEFORE the built-ins are synced so the create path has
      // nothing to auto-assign — this test assigns the knowledge tool itself.
      const agent = await makeAgent({ organizationId: org.id });
      await ToolModel.syncArchestraBuiltInCatalog({
        organization: { appName: "Acme Copilot", iconLogo: null },
      });

      const brandedKbToolName = getArchestraToolFullName(
        TOOL_QUERY_KNOWLEDGE_SOURCES_SHORT_NAME,
        {
          appName: "Acme Copilot",
          fullWhiteLabeling: true,
        },
      );

      const [kbTool] = await db
        .select()
        .from(schema.toolsTable)
        .where(eq(schema.toolsTable.name, brandedKbToolName));

      expect(kbTool).toBeDefined();
      await makeAgentTool(agent.id, kbTool?.id);

      const result = await ToolModel.findAllWithAssignments({
        filters: {
          excludeArchestraTools: true,
          includeKnowledgeSourcesTool: true,
        },
      });

      expect(result.data.some((tool) => tool.name === brandedKbToolName)).toBe(
        true,
      );
    });

    test("includeKnowledgeSourcesTool does not leak other built-in tools", async ({
      makeOrganization,
      makeAgent,
      makeAgentTool,
    }) => {
      const org = await makeOrganization();
      await OrganizationModel.patch(org.id, { appName: "Acme Copilot" });
      // Agent created BEFORE the built-ins are synced so the create path has
      // nothing to auto-assign — this test assigns its built-ins manually.
      const agent = await makeAgent({ organizationId: org.id });
      await ToolModel.syncArchestraBuiltInCatalog({
        organization: { appName: "Acme Copilot", iconLogo: null },
      });

      // Assign a few built-in tools to the agent
      const brandedKbToolName = getArchestraToolFullName(
        TOOL_QUERY_KNOWLEDGE_SOURCES_SHORT_NAME,
        { appName: "Acme Copilot", fullWhiteLabeling: true },
      );
      const brandedTodoToolName = getArchestraToolFullName(
        TOOL_TODO_WRITE_SHORT_NAME,
        { appName: "Acme Copilot", fullWhiteLabeling: true },
      );

      const [kbTool] = await db
        .select()
        .from(schema.toolsTable)
        .where(eq(schema.toolsTable.name, brandedKbToolName));
      const [todoTool] = await db
        .select()
        .from(schema.toolsTable)
        .where(eq(schema.toolsTable.name, brandedTodoToolName));

      expect(kbTool).toBeDefined();
      expect(todoTool).toBeDefined();
      await makeAgentTool(agent.id, kbTool?.id);
      await makeAgentTool(agent.id, todoTool?.id);

      const result = await ToolModel.findAllWithAssignments({
        filters: {
          excludeArchestraTools: true,
          includeKnowledgeSourcesTool: true,
        },
      });

      // KB tool should be present
      expect(result.data.some((tool) => tool.name === brandedKbToolName)).toBe(
        true,
      );

      // Other built-in tools must NOT be present
      expect(
        result.data.some((tool) => tool.name === brandedTodoToolName),
      ).toBe(false);
    });
  });
});

describe("ToolModel.cloneToolsAndPoliciesFromCatalog", () => {
  test("copies tools and both policy types as provisional", async ({
    makeOrganization,
    makeInternalMcpCatalog,
  }) => {
    const org = await makeOrganization();
    const source = await makeInternalMcpCatalog({ organizationId: org.id });
    const clone = await makeInternalMcpCatalog({
      organizationId: org.id,
      clonedFrom: source.id,
    });

    const sourceTool = await ToolModel.create({
      catalogId: source.id,
      name: ToolModel.slugifyName(source.name, "search"),
      parameters: { type: "object" },
      description: "search desc",
    });
    await ToolInvocationPolicyModel.create({
      toolId: sourceTool.id,
      conditions: [],
      action: "block_always",
      reason: "custom",
    });
    await TrustedDataPolicyModel.create({
      toolId: sourceTool.id,
      conditions: [],
      action: "mark_as_trusted",
      description: "custom",
    });

    await ToolModel.cloneToolsAndPoliciesFromCatalog({
      sourceCatalogId: source.id,
      targetCatalogId: clone.id,
      targetCatalogName: clone.name,
    });

    const cloned = await db
      .select()
      .from(schema.toolsTable)
      .where(eq(schema.toolsTable.catalogId, clone.id));
    expect(cloned).toHaveLength(1);
    expect(cloned[0].clonedPendingDiscovery).toBe(true);
    expect(cloned[0].name).toBe(ToolModel.slugifyName(clone.name, "search"));
    expect(cloned[0].description).toBe("search desc");

    const inv = await db
      .select()
      .from(schema.toolInvocationPoliciesTable)
      .where(eq(schema.toolInvocationPoliciesTable.toolId, cloned[0].id));
    expect(inv).toHaveLength(1);
    expect(inv[0].action).toBe("block_always");

    const trusted = await db
      .select()
      .from(schema.trustedDataPoliciesTable)
      .where(eq(schema.trustedDataPoliciesTable.toolId, cloned[0].id));
    expect(trusted).toHaveLength(1);
    expect(trusted[0].action).toBe("mark_as_trusted");

    const assignments = await db
      .select()
      .from(schema.agentToolsTable)
      .where(eq(schema.agentToolsTable.toolId, cloned[0].id));
    expect(assignments).toHaveLength(0);
  });
});

describe("ToolModel.reconcileClonedCatalogTools", () => {
  test("confirms matches, deletes unmatched provisional, keeps policies", async ({
    makeOrganization,
    makeInternalMcpCatalog,
  }) => {
    const org = await makeOrganization();
    const cat = await makeInternalMcpCatalog({ organizationId: org.id });

    const kept = await ToolModel.create({
      catalogId: cat.id,
      name: ToolModel.slugifyName(cat.name, "kept"),
      parameters: {},
      description: "old",
      clonedPendingDiscovery: true,
    });
    await ToolInvocationPolicyModel.create({
      toolId: kept.id,
      conditions: [],
      action: "block_always",
      reason: "keep-me",
    });
    const dropped = await ToolModel.create({
      catalogId: cat.id,
      name: ToolModel.slugifyName(cat.name, "dropped"),
      parameters: {},
      description: null,
      clonedPendingDiscovery: true,
    });

    expect(await ToolModel.countProvisionalForCatalog(cat.id)).toBe(2);

    await ToolModel.reconcileClonedCatalogTools({
      catalogId: cat.id,
      discoveredToolNames: new Set([ToolModel.slugifyName(cat.name, "kept")]),
    });

    const keptRow = await ToolModel.findById(kept.id);
    expect(keptRow?.clonedPendingDiscovery).toBe(false);
    const inv = await db
      .select()
      .from(schema.toolInvocationPoliciesTable)
      .where(eq(schema.toolInvocationPoliciesTable.toolId, kept.id));
    expect(inv[0]?.action).toBe("block_always");

    const droppedRow = await ToolModel.findById(dropped.id);
    expect(droppedRow).toBeNull();

    expect(await ToolModel.countProvisionalForCatalog(cat.id)).toBe(0);
  });

  test("matches discovered tools by slug, not lossy raw name (spaces/case)", async ({
    makeOrganization,
    makeInternalMcpCatalog,
  }) => {
    const org = await makeOrganization();
    const cat = await makeInternalMcpCatalog({ organizationId: org.id });

    // Provisional tool whose raw name has a space + uppercase, e.g. "Create Issue".
    const provisionalName = ToolModel.slugifyName(cat.name, "Create Issue");
    const kept = await ToolModel.create({
      catalogId: cat.id,
      name: provisionalName,
      parameters: {},
      description: null,
      clonedPendingDiscovery: true,
    });

    // Discovery slugifies the same raw name with the same catalog name.
    const discoveredToolNames = new Set([
      ToolModel.slugifyName(cat.name, "Create Issue"),
    ]);

    const { confirmedToolIds } = await ToolModel.reconcileClonedCatalogTools({
      catalogId: cat.id,
      discoveredToolNames,
    });

    expect(confirmedToolIds).toContain(kept.id);
    const keptRow = await ToolModel.findById(kept.id);
    expect(keptRow?.clonedPendingDiscovery).toBe(false);
  });
});

describe("provisional tools are gated from assignment", () => {
  test("findByCatalogId excludes provisional tools", async ({
    makeOrganization,
    makeInternalMcpCatalog,
  }) => {
    const org = await makeOrganization();
    const cat = await makeInternalMcpCatalog({ organizationId: org.id });
    await ToolModel.create({
      catalogId: cat.id,
      name: ToolModel.slugifyName(cat.name, "real"),
      parameters: {},
      description: null,
    });
    await ToolModel.create({
      catalogId: cat.id,
      name: ToolModel.slugifyName(cat.name, "provisional"),
      parameters: {},
      description: null,
      clonedPendingDiscovery: true,
    });

    const tools = await ToolModel.findByCatalogId(cat.id);
    const names = tools.map((t) => t.name);
    expect(names).toContain(ToolModel.slugifyName(cat.name, "real"));
    expect(names).not.toContain(ToolModel.slugifyName(cat.name, "provisional"));
  });

  test("findAllWithAssignments INCLUDES provisional tools (guardrails management view)", async ({
    makeOrganization,
    makeInternalMcpCatalog,
  }) => {
    const org = await makeOrganization();
    const cat = await makeInternalMcpCatalog({ organizationId: org.id });
    const provisional = await ToolModel.create({
      catalogId: cat.id,
      name: ToolModel.slugifyName(cat.name, "provisional"),
      parameters: {},
      description: null,
      clonedPendingDiscovery: true,
    });

    const result = await ToolModel.findAllWithAssignments({
      pagination: { limit: 50, offset: 0 },
      filters: { origin: cat.id },
      isAgentAdmin: true,
    });

    const ids = result.data.map((t) => t.id);
    expect(ids).toContain(provisional.id);
  });
});

describe("policy configurator and cloned tools", () => {
  test("clone copy and reconcile do not trigger the configurator", async ({
    makeOrganization,
    makeInternalMcpCatalog,
  }) => {
    // triggerAutoConfigureIfEnabled is private; cast to access it for spying.
    const spy = vi
      // biome-ignore lint/suspicious/noExplicitAny: spy on private static method
      .spyOn(ToolModel as any, "triggerAutoConfigureIfEnabled")
      .mockResolvedValue(undefined);
    try {
      const org = await makeOrganization();
      const source = await makeInternalMcpCatalog({ organizationId: org.id });
      await ToolModel.create({
        catalogId: source.id,
        name: ToolModel.slugifyName(source.name, "search"),
        parameters: {},
        description: null,
      });
      const clone = await makeInternalMcpCatalog({
        organizationId: org.id,
        clonedFrom: source.id,
      });

      await ToolModel.cloneToolsAndPoliciesFromCatalog({
        sourceCatalogId: source.id,
        targetCatalogId: clone.id,
        targetCatalogName: clone.name,
      });
      await ToolModel.reconcileClonedCatalogTools({
        catalogId: clone.id,
        discoveredToolNames: new Set([
          ToolModel.slugifyName(clone.name, "search"),
        ]),
      });

      expect(spy).not.toHaveBeenCalled();
    } finally {
      spy.mockRestore();
    }
  });
});

describe("getMcpToolsAccessibleToUser requireUiResource", () => {
  // The route-level tools/list tests cannot isolate this filter: the in-memory
  // providesUiResource gate in filterExposedTools hides non-UI tools anyway, so
  // a dropped requireUiResource is invisible there (it only changes how many
  // rows are loaded and re-filtered). This pins the DB-side predicate directly.
  test("restricts to tools carrying a ui:// resource (canonical or legacy key)", async ({
    makeOrganization,
    makeUser,
    makeInternalMcpCatalog,
    makeMcpServer,
    makeTool,
  }) => {
    const org = await makeOrganization();
    const user = await makeUser();
    const catalog = await makeInternalMcpCatalog({ organizationId: org.id });
    const canonicalTool = await makeTool({
      catalogId: catalog.id,
      name: "widget__open_canonical",
      meta: { _meta: { ui: { resourceUri: "ui://widget/canonical.html" } } },
    });
    const legacyTool = await makeTool({
      catalogId: catalog.id,
      name: "widget__open_legacy",
      meta: { _meta: { "ui/resourceUri": "ui://widget/legacy.html" } },
    });
    const nonUiTool = await makeTool({
      catalogId: catalog.id,
      name: "widget__list",
    });
    const nonUiSchemeTool = await makeTool({
      catalogId: catalog.id,
      name: "widget__https_scheme",
      meta: { _meta: { ui: { resourceUri: "https://example.com/page" } } },
    });
    await makeMcpServer({ catalogId: catalog.id, scope: "org" });

    const uiOnlyNames = (
      await ToolModel.getMcpToolsAccessibleToUser({
        userId: user.id,
        organizationId: org.id,
        isAdmin: true,
        environmentId: null,
        requireUiResource: true,
      })
    ).map((tool) => tool.name);
    expect(uiOnlyNames).toContain(canonicalTool.name);
    expect(uiOnlyNames).toContain(legacyTool.name);
    expect(uiOnlyNames).not.toContain(nonUiTool.name);
    expect(uiOnlyNames).not.toContain(nonUiSchemeTool.name);

    // Without the flag the filter must not apply — the same non-UI tools stay
    // in the unrestricted discovery set search_tools/run_tool rely on.
    const allNames = (
      await ToolModel.getMcpToolsAccessibleToUser({
        userId: user.id,
        organizationId: org.id,
        isAdmin: true,
        environmentId: null,
      })
    ).map((tool) => tool.name);
    expect(allNames).toContain(nonUiTool.name);
    expect(allNames).toContain(nonUiSchemeTool.name);
  });
});
