From eb16829f51502e82c10be1af79a919c066ea7795 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Tue, 5 May 2026 15:44:16 +0800 Subject: [PATCH] refactor(server/tests): replace unsafe type assertions with Reflect + Zod (T1.b partial) (#752) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cluster T1.b — first wave of no-unsafe-type-assertion fixes across agent provider and MCP test files. Clears all type-aware lint errors in 6 files; remaining 4 files (class-mock patterns) deferred to T1.b2. --- .../src/server/agent/agent-mcp.e2e.test.ts | 69 +++++++-------- .../src/server/agent/mcp-parity.e2e.test.ts | 88 +++++++++++-------- .../src/server/agent/mcp-server.test.ts | 40 ++++----- .../src/server/agent/mcp-shared.test.ts | 69 ++++++++------- .../src/server/agent/model-resolver.test.ts | 88 ++++++++++++------- .../server/agent/provider-registry.test.ts | 64 ++++++++------ 6 files changed, 233 insertions(+), 185 deletions(-) diff --git a/packages/server/src/server/agent/agent-mcp.e2e.test.ts b/packages/server/src/server/agent/agent-mcp.e2e.test.ts index 800b7941c..9d231bd06 100644 --- a/packages/server/src/server/agent/agent-mcp.e2e.test.ts +++ b/packages/server/src/server/agent/agent-mcp.e2e.test.ts @@ -22,7 +22,7 @@ interface McpToolResult { } interface McpClient { - callTool: (input: { name: string; args?: StructuredContent }) => Promise; + callTool: (input: { name: string; args?: StructuredContent }) => Promise; close: () => Promise; } @@ -61,23 +61,29 @@ function getStructuredContent(result: McpToolResult): StructuredContent | null { } const content = result.content?.[0]; if (content && typeof content === "object" && "structuredContent" in content) { - const structured = (content as { structuredContent?: StructuredContent }).structuredContent; - if (structured) return structured; + if (content.structuredContent) return content.structuredContent; } if (content && typeof content === "object") { - return content as StructuredContent; + return content; } return null; } +async function createMcpClient(url: string): Promise { + const transport = new StreamableHTTPClientTransport(new URL(url)); + const rawClient = await experimental_createMCPClient({ transport }); + const boundCallTool: McpClient["callTool"] = Reflect.get(rawClient, "callTool").bind(rawClient); + return { callTool: boundCallTool, close: () => rawClient.close() }; +} + async function waitForAgentCompletion(options: { client: McpClient; agentId: string; }): Promise { - const waitResult = (await options.client.callTool({ + const waitResult = await options.client.callTool({ name: "wait_for_agent", args: { agentId: options.agentId }, - })) as McpToolResult; + }); const payload = getStructuredContent(waitResult); if (!payload) { throw new Error("wait_for_agent returned no structured payload"); @@ -87,7 +93,7 @@ async function waitForAgentCompletion(options: { } const status = payload.status; if (status === "running" || status === "initializing") { - throw new Error(`Agent still running after wait_for_agent (status=${String(status)})`); + throw new Error(`Agent still running after wait_for_agent (status=${status})`); } } @@ -113,10 +119,7 @@ describe("agent MCP end-to-end (offline)", () => { const daemon = await createPaseoDaemon(daemonConfig, pino({ level: "silent" })); await daemon.start(); - const transport = new StreamableHTTPClientTransport( - new URL(`http://127.0.0.1:${port}/mcp/agents`), - ); - const client = (await experimental_createMCPClient({ transport })) as McpClient; + const client = await createMcpClient(`http://127.0.0.1:${port}/mcp/agents`); let agentId: string | null = null; try { @@ -129,7 +132,7 @@ describe("agent MCP end-to-end (offline)", () => { "Do not respond before the command finishes.", ].join("\n"); - const result = (await client.callTool({ + const result = await client.callTool({ name: "create_agent", args: { cwd: agentCwd, @@ -139,10 +142,10 @@ describe("agent MCP end-to-end (offline)", () => { initialPrompt, background: false, }, - })) as McpToolResult; + }); const payload = getStructuredContent(result); - agentId = (payload?.agentId as string | undefined) ?? null; + agentId = typeof payload?.agentId === "string" ? payload.agentId : null; expect(agentId).toBeTruthy(); await waitForAgentCompletion({ client, agentId: agentId! }); @@ -186,10 +189,7 @@ describe("agent MCP end-to-end (offline)", () => { const daemon = await createPaseoDaemon(daemonConfig, pino({ level: "silent" })); await daemon.start(); - const transport = new StreamableHTTPClientTransport( - new URL(`http://127.0.0.1:${port}/mcp/agents`), - ); - const client = (await experimental_createMCPClient({ transport })) as McpClient; + const client = await createMcpClient(`http://127.0.0.1:${port}/mcp/agents`); const disabledPaseoHome = await mkdtemp(path.join(os.tmpdir(), "paseo-home-disabled-")); const disabledStaticDir = await mkdtemp(path.join(os.tmpdir(), "paseo-static-disabled-")); @@ -210,17 +210,12 @@ describe("agent MCP end-to-end (offline)", () => { const disabledDaemon = await createPaseoDaemon(disabledDaemonConfig, pino({ level: "silent" })); await disabledDaemon.start(); - const disabledTransport = new StreamableHTTPClientTransport( - new URL(`http://127.0.0.1:${disabledPort}/mcp/agents`), - ); - const disabledClient = (await experimental_createMCPClient({ - transport: disabledTransport, - })) as McpClient; + const disabledClient = await createMcpClient(`http://127.0.0.1:${disabledPort}/mcp/agents`); let agentId: string | null = null; let disabledAgentId: string | null = null; try { - const result = (await client.callTool({ + const result = await client.callTool({ name: "create_agent", args: { cwd: agentCwd, @@ -230,9 +225,9 @@ describe("agent MCP end-to-end (offline)", () => { initialPrompt: "reply with done and stop", background: true, }, - })) as McpToolResult; + }); const payload = getStructuredContent(result); - agentId = (payload?.agentId as string | undefined) ?? null; + agentId = typeof payload?.agentId === "string" ? payload.agentId : null; expect(agentId).toBeTruthy(); const injectedAgent = daemon.agentManager.getAgent(agentId!); @@ -243,7 +238,7 @@ describe("agent MCP end-to-end (offline)", () => { }, }); - const disabledResult = (await disabledClient.callTool({ + const disabledResult = await disabledClient.callTool({ name: "create_agent", args: { cwd: disabledAgentCwd, @@ -253,9 +248,10 @@ describe("agent MCP end-to-end (offline)", () => { initialPrompt: "reply with done and stop", background: true, }, - })) as McpToolResult; + }); const disabledPayload = getStructuredContent(disabledResult); - disabledAgentId = (disabledPayload?.agentId as string | undefined) ?? null; + disabledAgentId = + typeof disabledPayload?.agentId === "string" ? disabledPayload.agentId : null; expect(disabledAgentId).toBeTruthy(); const disabledAgent = disabledDaemon.agentManager.getAgent(disabledAgentId!); @@ -301,10 +297,7 @@ describe("agent MCP end-to-end (offline)", () => { const daemon = await createPaseoDaemon(daemonConfig, pino({ level: "silent" })); await daemon.start(); - const transport = new StreamableHTTPClientTransport( - new URL(`http://127.0.0.1:${port}/mcp/agents`), - ); - const client = (await experimental_createMCPClient({ transport })) as McpClient; + const client = await createMcpClient(`http://127.0.0.1:${port}/mcp/agents`); let agentId: string | null = null; try { @@ -339,7 +332,7 @@ describe("agent MCP end-to-end (offline)", () => { stdio: "pipe", }); - const result = (await withTimeout({ + const result = await withTimeout({ promise: client.callTool({ name: "create_agent", args: { @@ -355,12 +348,12 @@ describe("agent MCP end-to-end (offline)", () => { }), timeoutMs: 2500, label: "create_agent should not block on setup", - })) as McpToolResult; + }); const payload = getStructuredContent(result); - agentId = (payload?.agentId as string | undefined) ?? null; + agentId = typeof payload?.agentId === "string" ? payload.agentId : null; expect(agentId).toBeTruthy(); - const worktreePath = (payload?.cwd as string | undefined) ?? ""; + const worktreePath = typeof payload?.cwd === "string" ? payload.cwd : ""; expect(worktreePath).toContain(`${path.sep}worktrees${path.sep}`); expect(existsSync(path.join(worktreePath, "setup-done.txt"))).toBe(false); expect(existsSync(path.join(worktreePath, "dev-terminal.txt"))).toBe(false); diff --git a/packages/server/src/server/agent/mcp-parity.e2e.test.ts b/packages/server/src/server/agent/mcp-parity.e2e.test.ts index 0a7945a0d..0a0ab4ba7 100644 --- a/packages/server/src/server/agent/mcp-parity.e2e.test.ts +++ b/packages/server/src/server/agent/mcp-parity.e2e.test.ts @@ -5,6 +5,7 @@ import { mkdtemp, rm, writeFile } from "node:fs/promises"; import { afterAll, beforeAll, describe, expect, test } from "vitest"; import { experimental_createMCPClient } from "ai"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; +import { z } from "zod"; import { AGENT_WAIT_TIMEOUT_MS } from "./mcp-shared.js"; import { createTestPaseoDaemon, type TestPaseoDaemon } from "../test-utils/paseo-daemon.js"; @@ -20,10 +21,22 @@ interface McpToolResult { } interface McpClient { - callTool: (input: { name: string; args?: StructuredContent }) => Promise; + callTool: (input: { name: string; args?: StructuredContent }) => Promise; close: () => Promise; } +function str(val: unknown): string { + return z.string().parse(val); +} + +function recordArr(val: unknown): StructuredContent[] { + return z.array(z.record(z.unknown())).parse(val); +} + +function strArrOptional(val: unknown): string[] | undefined { + return z.array(z.string()).optional().parse(val); +} + function formatHostForHttpUrl(host: string): string { return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host; } @@ -43,20 +56,21 @@ function getStructuredContent(result: McpToolResult): StructuredContent | null { } const content = result.content?.[0]; if (content && typeof content === "object" && "structuredContent" in content) { - const structured = (content as { structuredContent?: StructuredContent }).structuredContent; - if (structured) { - return structured; + if (content.structuredContent) { + return content.structuredContent; } } if (content && typeof content === "object") { - return content as StructuredContent; + return content; } return null; } async function createMcpClient(url: string): Promise { const transport = new StreamableHTTPClientTransport(new URL(url)); - return (await experimental_createMCPClient({ transport })) as McpClient; + const rawClient = await experimental_createMCPClient({ transport }); + const boundCallTool: McpClient["callTool"] = Reflect.get(rawClient, "callTool").bind(rawClient); + return { callTool: boundCallTool, close: () => rawClient.close() }; } async function callToolStructured( @@ -64,7 +78,7 @@ async function callToolStructured( name: string, args?: StructuredContent, ): Promise { - const result = (await client.callTool({ name, args: args ?? {} })) as McpToolResult; + const result = await client.callTool({ name, args: args ?? {} }); const payload = getStructuredContent(result); if (!payload) { throw new Error(`${name} returned no structured payload`); @@ -78,10 +92,14 @@ async function expectToolError( args: StructuredContent, pattern: RegExp, ): Promise { - const result = (await client.callTool({ name, args })) as McpToolResult; + const result = await client.callTool({ name, args }); expect(result.isError).toBe(true); - const content = result.content?.[0] as { text?: string } | undefined; - expect(content?.text ?? "").toMatch(pattern); + const contentItem = result.content?.[0]; + const contentText: string | undefined = + contentItem != null && typeof contentItem === "object" + ? Reflect.get(contentItem, "text") + : undefined; + expect(contentText ?? "").toMatch(pattern); } async function sleep(ms: number): Promise { @@ -118,7 +136,7 @@ async function makeCwd(prefix: string): Promise { } async function createTopLevelAgent(args?: Partial): Promise { - const cwd = (args?.cwd as string | undefined) ?? (await makeCwd("agent-cwd")); + const cwd = typeof args?.cwd === "string" ? args.cwd : await makeCwd("agent-cwd"); const payload = await callToolStructured(topLevelClient, "create_agent", { cwd, title: "Parity agent", @@ -128,7 +146,7 @@ async function createTopLevelAgent(args?: Partial): Promise): Promise { @@ -139,7 +157,7 @@ async function createChildAgent(args?: Partial): Promise { @@ -213,7 +231,7 @@ beforeAll(async () => { mode: "bypassPermissions", background: true, }); - parentAgentId = parentPayload.agentId as string; + parentAgentId = str(parentPayload.agentId); agentScopedClient = await createMcpClient( `http://127.0.0.1:${daemonHandle.port}/mcp/agents?callerAgentId=${parentAgentId}`, @@ -362,10 +380,10 @@ describe("Suite B: Terminal Tools", () => { const created = await callToolStructured(agentScopedClient, "create_terminal", { name: "Parity terminal", }); - terminalId = created.id as string; + terminalId = str(created.id); const listed = await callToolStructured(agentScopedClient, "list_terminals"); - const terminals = listed.terminals as Array; + const terminals = recordArr(listed.terminals); expect(terminals).toEqual( expect.arrayContaining([ expect.objectContaining({ @@ -386,7 +404,7 @@ describe("Suite B: Terminal Tools", () => { const created = await callToolStructured(agentScopedClient, "create_terminal", { name: "Parity capture terminal", }); - terminalId = created.id as string; + terminalId = str(created.id); await callToolStructured(agentScopedClient, "send_terminal_keys", { terminalId, @@ -404,7 +422,7 @@ describe("Suite B: Terminal Tools", () => { terminalId, scrollback: true, }); - const lines = (payload.lines as string[] | undefined) ?? []; + const lines = strArrOptional(payload.lines) ?? []; return lines.some((line) => line.includes("hello")) ? payload : null; }, }); @@ -421,7 +439,7 @@ describe("Suite B: Terminal Tools", () => { const created = await callToolStructured(agentScopedClient, "create_terminal", { name: "Parity kill terminal", }); - terminalId = created.id as string; + terminalId = str(created.id); await callToolStructured(agentScopedClient, "kill_terminal", { terminalId }); terminalId = null; @@ -432,11 +450,11 @@ describe("Suite B: Terminal Tools", () => { label: "terminal removal", check: async () => { const payload = await callToolStructured(agentScopedClient, "list_terminals"); - const terminals = payload.terminals as Array; + const terminals = recordArr(payload.terminals); return terminals.some((terminal) => terminal.id === created.id) ? null : payload; }, }); - const terminals = listed.terminals as Array; + const terminals = recordArr(listed.terminals); expect(terminals.some((terminal) => terminal.id === created.id)).toBe(false); } finally { await killTerminalIfPresent(terminalId); @@ -462,10 +480,10 @@ describe("Suite C: Schedule Tools", () => { every: "5m", name: "Parity schedule list", }); - scheduleId = created.id as string; + scheduleId = str(created.id); const listed = await callToolStructured(topLevelClient, "list_schedules"); - const schedules = listed.schedules as Array; + const schedules = recordArr(listed.schedules); expect(schedules).toEqual( expect.arrayContaining([ expect.objectContaining({ @@ -488,7 +506,7 @@ describe("Suite C: Schedule Tools", () => { name: "Parity provider schedule", provider: "codex/gpt-5.4", }); - scheduleId = created.id as string; + scheduleId = str(created.id); expect(created.target).toMatchObject({ type: "new-agent", config: { @@ -509,7 +527,7 @@ describe("Suite C: Schedule Tools", () => { every: "5m", name: "Parity inspect schedule", }); - scheduleId = created.id as string; + scheduleId = str(created.id); const inspected = await callToolStructured(topLevelClient, "inspect_schedule", { id: scheduleId, @@ -533,7 +551,7 @@ describe("Suite C: Schedule Tools", () => { every: "5m", name: "Parity pause schedule", }); - scheduleId = created.id as string; + scheduleId = str(created.id); await callToolStructured(topLevelClient, "pause_schedule", { id: scheduleId }); const paused = await callToolStructured(topLevelClient, "inspect_schedule", { @@ -559,13 +577,13 @@ describe("Suite C: Schedule Tools", () => { every: "5m", name: "Parity delete schedule", }); - scheduleId = created.id as string; + scheduleId = str(created.id); await callToolStructured(topLevelClient, "delete_schedule", { id: scheduleId }); scheduleId = null; const listed = await callToolStructured(topLevelClient, "list_schedules"); - const schedules = listed.schedules as Array; + const schedules = recordArr(listed.schedules); expect(schedules.some((schedule) => schedule.id === created.id)).toBe(false); } finally { await deleteScheduleIfPresent(scheduleId); @@ -581,7 +599,7 @@ describe("Suite C: Schedule Tools", () => { name: "Parity self schedule", target: "self", }); - scheduleId = created.id as string; + scheduleId = str(created.id); expect(created.target).toMatchObject({ type: "agent", agentId: parentAgentId, @@ -599,7 +617,7 @@ describe("Suite C: Schedule Tools", () => { every: "5m", provider: "codex/gpt-5.4", }); - scheduleId = created.id as string; + scheduleId = str(created.id); expect(created.target).toMatchObject({ type: "new-agent", config: { @@ -629,7 +647,7 @@ describe("Suite C: Schedule Tools", () => { describe("Suite D: Provider Tools", () => { test("list_providers returns providers", async () => { const payload = await callToolStructured(topLevelClient, "list_providers"); - const providers = payload.providers as Array; + const providers = recordArr(payload.providers); expect(Array.isArray(providers)).toBe(true); expect(providers.length).toBeGreaterThan(0); expect(providers[0]).toEqual( @@ -667,12 +685,12 @@ describe("Suite E: Worktree Tools", () => { branchName, baseBranch: "main", }); - worktreePath = created.worktreePath as string; + worktreePath = str(created.worktreePath); const listed = await callToolStructured(topLevelClient, "list_worktrees", { cwd: worktreeRepoCwd, }); - const worktrees = listed.worktrees as Array; + const worktrees = recordArr(listed.worktrees); expect(worktrees).toEqual( expect.arrayContaining([ expect.objectContaining({ @@ -695,7 +713,7 @@ describe("Suite E: Worktree Tools", () => { branchName, baseBranch: "main", }); - worktreePath = created.worktreePath as string; + worktreePath = str(created.worktreePath); await callToolStructured(topLevelClient, "archive_worktree", { cwd: worktreeRepoCwd, @@ -706,7 +724,7 @@ describe("Suite E: Worktree Tools", () => { const listed = await callToolStructured(topLevelClient, "list_worktrees", { cwd: worktreeRepoCwd, }); - const worktrees = listed.worktrees as Array; + const worktrees = recordArr(listed.worktrees); expect(worktrees.some((worktree) => worktree.path === created.worktreePath)).toBe(false); } finally { await archiveWorktreeIfPresent({ cwd: worktreeRepoCwd, worktreePath }); diff --git a/packages/server/src/server/agent/mcp-server.test.ts b/packages/server/src/server/agent/mcp-server.test.ts index b972937a9..f8f763684 100644 --- a/packages/server/src/server/agent/mcp-server.test.ts +++ b/packages/server/src/server/agent/mcp-server.test.ts @@ -14,7 +14,6 @@ import { AgentListItemPayloadSchema, AgentSnapshotPayloadSchema } from "../../sh import type { PersistedProjectRecord, PersistedWorkspaceRecord } from "../workspace-registry.js"; import type { CreateScheduleInput, StoredSchedule } from "../schedule/types.js"; import type { ScheduleService } from "../schedule/service.js"; -import type { AgentProvider } from "./agent-sdk-types.js"; import type { WorkspaceGitService } from "../workspace-git-service.js"; import { createPaseoWorktree as createPaseoWorktreeService, @@ -48,15 +47,12 @@ interface RegisteredMcpTool { }>; } -interface McpServerInternals { - _registeredTools: Record; -} - function lookupTool( server: Awaited>, name: string, ): RegisteredMcpTool | undefined { - return (server as unknown as McpServerInternals)._registeredTools[name]; + const tools: Record = Reflect.get(server, "_registeredTools"); + return tools[name]; } function registeredTool( @@ -73,7 +69,7 @@ function registeredTool( function agentsOf(response: { structuredContent: LooseStructuredContent; }): Array> { - return response.structuredContent.agents as Array>; + return z.array(z.record(z.unknown())).parse(response.structuredContent.agents); } type AgentManagerSpies = ReturnType; @@ -736,7 +732,7 @@ describe("create_agent MCP tool", () => { background: true, }); - const agentCwd = spies.agentManager.createAgent.mock.calls[0]?.[0].cwd as string; + const agentCwd = z.string().parse(spies.agentManager.createAgent.mock.calls[0]?.[0].cwd); const initialBranch = execSync("git branch --show-current", { cwd: agentCwd, stdio: "pipe" }) .toString() .trim(); @@ -808,7 +804,7 @@ describe("create_agent MCP tool", () => { background: true, }); - const agentCwd = spies.agentManager.createAgent.mock.calls[0]?.[0].cwd as string; + const agentCwd = z.string().parse(spies.agentManager.createAgent.mock.calls[0]?.[0].cwd); expect( execSync("git branch --show-current", { cwd: agentCwd, stdio: "pipe" }).toString().trim(), ).toBe("existing-feature"); @@ -1611,7 +1607,9 @@ describe("agent snapshot MCP serialization", () => { const server = await createAgentMcpServer({ agentManager, agentStorage, logger }); const tool = registeredTool(server, "list_agents"); const response = await tool.callback({}); - const structured = response.structuredContent as { agents: Array> }; + const structured = z + .object({ agents: z.array(z.record(z.unknown())) }) + .parse(response.structuredContent); expect(structured).toEqual({ agents: [ @@ -1659,7 +1657,7 @@ describe("agent snapshot MCP serialization", () => { logger, providerRegistry: { claude: createProviderDefinition({}), - } as unknown as Record, + }, }); const tool = registeredTool(server, "get_agent_status"); const response = await tool.callback({ agentId: "archived-agent" }); @@ -1719,7 +1717,7 @@ describe("agent snapshot MCP serialization", () => { const server = await createAgentMcpServer({ agentManager, agentStorage, logger }); const tool = registeredTool(server, "get_agent_status"); const response = await tool.callback({ agentId: "full-detail-agent" }); - const snapshot = response.structuredContent.snapshot as Record; + const snapshot = z.record(z.unknown()).parse(response.structuredContent.snapshot); const parsed = AgentSnapshotPayloadSchema.safeParse(snapshot); if (!parsed.success) { @@ -1790,7 +1788,7 @@ describe("agent snapshot MCP serialization", () => { logger, providerRegistry: { claude: createProviderDefinition({}), - } as unknown as Record, + }, }); const tool = registeredTool(server, "get_agent_status"); @@ -1834,7 +1832,7 @@ describe("agent snapshot MCP serialization", () => { logger, providerRegistry: { claude: createProviderDefinition({}), - } as unknown as Record, + }, callerAgentId: "caller-agent", }); const tool = registeredTool(server, "list_agents"); @@ -1883,7 +1881,7 @@ describe("agent snapshot MCP serialization", () => { logger, providerRegistry: { claude: createProviderDefinition({}), - } as unknown as Record, + }, }); const tool = registeredTool(server, "list_agents"); const response = await tool.callback({ @@ -1924,7 +1922,7 @@ describe("agent snapshot MCP serialization", () => { logger, providerRegistry: { claude: createProviderDefinition({}), - } as unknown as Record, + }, }); const tool = registeredTool(server, "list_agents"); const response = await tool.callback({ includeArchived: true }); @@ -1967,7 +1965,7 @@ describe("agent snapshot MCP serialization", () => { logger, providerRegistry: { claude: createProviderDefinition({}), - } as unknown as Record, + }, }); const tool = registeredTool(server, "list_agents"); const response = await tool.callback({ cwd: "/tmp/repo", includeArchived: true }); @@ -2075,7 +2073,7 @@ describe("agent snapshot MCP serialization", () => { logger, providerRegistry: { claude: createProviderDefinition({}), - } as unknown as Record, + }, }); const tool = registeredTool(server, "list_agents"); const response = await tool.callback({ includeArchived: true }); @@ -2115,7 +2113,7 @@ describe("agent snapshot MCP serialization", () => { logger, providerRegistry: { claude: createProviderDefinition({}), - } as unknown as Record, + }, }); const tool = registeredTool(server, "get_agent_activity"); const response = await tool.callback({ agentId: "archived-activity-agent" }); @@ -2153,7 +2151,7 @@ describe("agent snapshot MCP serialization", () => { logger: createTestLogger(), providerRegistry: { claude: createProviderDefinition({}), - } as unknown as Record, + }, }); const tool = registeredTool(server, "get_agent_activity"); const response = await tool.callback({ agentId: "live-activity-agent", limit: 1 }); @@ -2184,7 +2182,7 @@ describe("agent snapshot MCP serialization", () => { logger: createTestLogger(), providerRegistry: { claude: createProviderDefinition({}), - } as unknown as Record, + }, }); const tool = registeredTool(server, "get_agent_activity"); const response = await tool.callback({ agentId: "live-activity-agent-2", limit: 2 }); diff --git a/packages/server/src/server/agent/mcp-shared.test.ts b/packages/server/src/server/agent/mcp-shared.test.ts index 0316c7df6..ff0d12cc7 100644 --- a/packages/server/src/server/agent/mcp-shared.test.ts +++ b/packages/server/src/server/agent/mcp-shared.test.ts @@ -1,49 +1,60 @@ import { expect, it, vi } from "vitest"; import { createTestLogger } from "../../test-utils/test-logger.js"; +import { AgentManager } from "./agent-manager.js"; +import { AgentStorage } from "./agent-storage.js"; import { setupFinishNotification } from "./mcp-shared.js"; -import type { AgentManager, AgentManagerEvent, ManagedAgent } from "./agent-manager.js"; -import type { AgentStorage } from "./agent-storage.js"; +import type { AgentManagerEvent, ManagedAgent } from "./agent-manager.js"; it("does not notify archived callers", async () => { let subscriber: ((event: AgentManagerEvent) => void) | null = null; - const childAgent = { - id: "child-agent", - lifecycle: "idle", - config: { title: "Child Agent" }, - } as ManagedAgent; + const childAgent: ManagedAgent = Object.create(null); + Reflect.set(childAgent, "id", "child-agent"); + Reflect.set(childAgent, "lifecycle", "idle"); + Reflect.set(childAgent, "config", { title: "Child Agent" }); - const agentManager = { - getAgent: vi.fn((agentId: string) => { + const callerAgent: ManagedAgent = Object.create(null); + Reflect.set(callerAgent, "id", "caller-agent"); + Reflect.set(callerAgent, "lifecycle", "idle"); + Reflect.set(callerAgent, "config", { title: "Caller Agent" }); + + const streamAgentSpy = vi.fn(() => (async function* noop() {})()); + const replaceAgentRunSpy = vi.fn(() => (async function* noop() {})()); + + const agentManager: AgentManager = Object.create(AgentManager.prototype); + Reflect.set( + agentManager, + "getAgent", + vi.fn((agentId: string) => { if (agentId === "child-agent") { return childAgent; } if (agentId === "caller-agent") { - return { - id: "caller-agent", - lifecycle: "idle", - config: { title: "Caller Agent" }, - } as ManagedAgent; + return callerAgent; } return null; }), - subscribe: vi.fn((callback: (event: AgentManagerEvent) => void) => { + ); + Reflect.set( + agentManager, + "subscribe", + vi.fn((callback: (event: AgentManagerEvent) => void) => { subscriber = callback; return () => { subscriber = null; }; }), - hasInFlightRun: vi.fn().mockReturnValue(false), - streamAgent: vi.fn(() => (async function* noop() {})()), - replaceAgentRun: vi.fn(() => (async function* noop() {})()), - } as unknown as AgentManager; + ); + Reflect.set(agentManager, "hasInFlightRun", vi.fn().mockReturnValue(false)); + Reflect.set(agentManager, "streamAgent", streamAgentSpy); + Reflect.set(agentManager, "replaceAgentRun", replaceAgentRunSpy); - const agentStorage = { - get: vi.fn(async (agentId: string) => - agentId === "caller-agent" ? { archivedAt: "2024-01-01" } : null, - ), - } as unknown as AgentStorage; + const agentStorageGetSpy = vi.fn(async (agentId: string) => + agentId === "caller-agent" ? { archivedAt: "2024-01-01" } : null, + ); + const agentStorage: AgentStorage = Object.create(AgentStorage.prototype); + Reflect.set(agentStorage, "get", agentStorageGetSpy); setupFinishNotification({ agentManager, @@ -68,13 +79,9 @@ it("does not notify archived callers", async () => { }); await vi.waitFor(() => { - expect(agentStorage.get).toHaveBeenCalledWith("caller-agent"); + expect(agentStorageGetSpy).toHaveBeenCalledWith("caller-agent"); }); - expect( - (agentManager as unknown as { streamAgent: ReturnType }).streamAgent, - ).not.toHaveBeenCalled(); - expect( - (agentManager as unknown as { replaceAgentRun: ReturnType }).replaceAgentRun, - ).not.toHaveBeenCalled(); + expect(streamAgentSpy).not.toHaveBeenCalled(); + expect(replaceAgentRunSpy).not.toHaveBeenCalled(); }); diff --git a/packages/server/src/server/agent/model-resolver.test.ts b/packages/server/src/server/agent/model-resolver.test.ts index 8985bec31..2c25517a7 100644 --- a/packages/server/src/server/agent/model-resolver.test.ts +++ b/packages/server/src/server/agent/model-resolver.test.ts @@ -1,6 +1,6 @@ -import type { Logger } from "pino"; import { beforeEach, describe, expect, it, vi } from "vitest"; +import { createTestLogger } from "../../test-utils/test-logger.js"; import { resolveAgentModel } from "./model-resolver.js"; vi.mock("./provider-registry.js", () => ({ @@ -11,10 +11,20 @@ vi.mock("./provider-registry.js", () => ({ import { buildProviderRegistry } from "./provider-registry.js"; const mockedBuildProviderRegistry = vi.mocked(buildProviderRegistry); -const testLoggerWarn = vi.fn(); -const testLogger = { warn: testLoggerWarn } as unknown as Logger; +const testLogger = createTestLogger(); +const testLoggerWarn = vi.spyOn(testLogger, "warn"); type ProviderRegistryMock = ReturnType; +function makeMockRegistry( + entries: Record }>, +): ProviderRegistryMock { + const registry: ProviderRegistryMock = Object.create(null); + for (const [key, val] of Object.entries(entries)) { + Reflect.set(registry, key, val); + } + return registry; +} + describe("resolveAgentModel", () => { beforeEach(() => { mockedBuildProviderRegistry.mockReset(); @@ -22,11 +32,13 @@ describe("resolveAgentModel", () => { }); it("returns the trimmed requested model when provided", async () => { - mockedBuildProviderRegistry.mockReturnValue({ - claude: { enabled: true, fetchModels: vi.fn() }, - codex: { enabled: true, fetchModels: vi.fn() }, - opencode: { enabled: true, fetchModels: vi.fn() }, - } as unknown as ProviderRegistryMock); + mockedBuildProviderRegistry.mockReturnValue( + makeMockRegistry({ + claude: { enabled: true, fetchModels: vi.fn() }, + codex: { enabled: true, fetchModels: vi.fn() }, + opencode: { enabled: true, fetchModels: vi.fn() }, + }), + ); const result = await resolveAgentModel({ provider: "codex", @@ -44,11 +56,13 @@ describe("resolveAgentModel", () => { { id: "claude-3.5-haiku", isDefault: false }, { id: "claude-3.5-sonnet", isDefault: true }, ]); - mockedBuildProviderRegistry.mockReturnValue({ - claude: { enabled: true, fetchModels }, - codex: { enabled: true, fetchModels: vi.fn() }, - opencode: { enabled: true, fetchModels: vi.fn() }, - } as unknown as ProviderRegistryMock); + mockedBuildProviderRegistry.mockReturnValue( + makeMockRegistry({ + claude: { enabled: true, fetchModels }, + codex: { enabled: true, fetchModels: vi.fn() }, + opencode: { enabled: true, fetchModels: vi.fn() }, + }), + ); const result = await resolveAgentModel({ provider: "claude", @@ -68,11 +82,13 @@ describe("resolveAgentModel", () => { { id: "model-a", isDefault: false }, { id: "model-b", isDefault: false }, ]); - mockedBuildProviderRegistry.mockReturnValue({ - claude: { enabled: true, fetchModels: vi.fn() }, - codex: { enabled: true, fetchModels }, - opencode: { enabled: true, fetchModels: vi.fn() }, - } as unknown as ProviderRegistryMock); + mockedBuildProviderRegistry.mockReturnValue( + makeMockRegistry({ + claude: { enabled: true, fetchModels: vi.fn() }, + codex: { enabled: true, fetchModels }, + opencode: { enabled: true, fetchModels: vi.fn() }, + }), + ); const result = await resolveAgentModel({ provider: "codex", logger: testLogger }); @@ -81,11 +97,13 @@ describe("resolveAgentModel", () => { it("returns undefined when the catalog lookup fails", async () => { const fetchModels = vi.fn().mockRejectedValue(new Error("boom")); - mockedBuildProviderRegistry.mockReturnValue({ - claude: { enabled: true, fetchModels: vi.fn() }, - codex: { enabled: true, fetchModels }, - opencode: { enabled: true, fetchModels: vi.fn() }, - } as unknown as ProviderRegistryMock); + mockedBuildProviderRegistry.mockReturnValue( + makeMockRegistry({ + claude: { enabled: true, fetchModels: vi.fn() }, + codex: { enabled: true, fetchModels }, + opencode: { enabled: true, fetchModels: vi.fn() }, + }), + ); const result = await resolveAgentModel({ provider: "codex", logger: testLogger }); @@ -95,11 +113,13 @@ describe("resolveAgentModel", () => { it("returns undefined for a disabled provider without fetching default models", async () => { const fetchModels = vi.fn().mockResolvedValue([{ id: "model-a", isDefault: true }]); - mockedBuildProviderRegistry.mockReturnValue({ - claude: { enabled: true, fetchModels: vi.fn() }, - codex: { enabled: false, fetchModels }, - opencode: { enabled: true, fetchModels: vi.fn() }, - } as unknown as ProviderRegistryMock); + mockedBuildProviderRegistry.mockReturnValue( + makeMockRegistry({ + claude: { enabled: true, fetchModels: vi.fn() }, + codex: { enabled: false, fetchModels }, + opencode: { enabled: true, fetchModels: vi.fn() }, + }), + ); const result = await resolveAgentModel({ provider: "codex", logger: testLogger }); @@ -110,11 +130,13 @@ describe("resolveAgentModel", () => { it("returns undefined for a requested model from a disabled provider", async () => { const fetchModels = vi.fn(); - mockedBuildProviderRegistry.mockReturnValue({ - claude: { enabled: true, fetchModels: vi.fn() }, - codex: { enabled: false, fetchModels }, - opencode: { enabled: true, fetchModels: vi.fn() }, - } as unknown as ProviderRegistryMock); + mockedBuildProviderRegistry.mockReturnValue( + makeMockRegistry({ + claude: { enabled: true, fetchModels: vi.fn() }, + codex: { enabled: false, fetchModels }, + opencode: { enabled: true, fetchModels: vi.fn() }, + }), + ); const result = await resolveAgentModel({ provider: "codex", diff --git a/packages/server/src/server/agent/provider-registry.test.ts b/packages/server/src/server/agent/provider-registry.test.ts index f2946b0dc..ff689fdbc 100644 --- a/packages/server/src/server/agent/provider-registry.test.ts +++ b/packages/server/src/server/agent/provider-registry.test.ts @@ -23,11 +23,12 @@ const mockState = vi.hoisted(() => { isCommandAvailable: vi.fn(async (_command: string) => false), runtimeModels: new Map(), reset() { - for (const key of Object.keys(this.constructorArgs) as Array< - keyof typeof this.constructorArgs - >) { - this.constructorArgs[key] = []; - } + this.constructorArgs.claude = []; + this.constructorArgs.codex = []; + this.constructorArgs.copilot = []; + this.constructorArgs.opencode = []; + this.constructorArgs.pi = []; + this.constructorArgs.genericAcp = []; this.isCommandAvailable.mockReset(); this.isCommandAvailable.mockImplementation(async (_command: string) => false); this.runtimeModels.clear(); @@ -76,8 +77,10 @@ vi.mock("./providers/claude-agent.js", () => ({ } async isAvailable(): Promise { - const command = (this.runtimeSettings as { command?: { mode?: string; argv?: string[] } }) - ?.command; + const command: { mode?: string; argv?: string[] } | undefined = + typeof this.runtimeSettings === "object" && this.runtimeSettings !== null + ? Reflect.get(this.runtimeSettings, "command") + : undefined; if (command?.mode === "replace") { const { isCommandAvailable } = await import("../../utils/executable.js"); return await isCommandAvailable(command.argv?.[0] ?? ""); @@ -122,8 +125,10 @@ vi.mock("./providers/codex-app-server-agent.js", () => ({ } async isAvailable(): Promise { - const command = (this.runtimeSettings as { command?: { mode?: string; argv?: string[] } }) - ?.command; + const command: { mode?: string; argv?: string[] } | undefined = + typeof this.runtimeSettings === "object" && this.runtimeSettings !== null + ? Reflect.get(this.runtimeSettings, "command") + : undefined; if (command?.mode === "replace") { const { isCommandAvailable } = await import("../../utils/executable.js"); return await isCommandAvailable(command.argv?.[0] ?? ""); @@ -170,8 +175,10 @@ vi.mock("./providers/copilot-acp-agent.js", () => ({ } async isAvailable(): Promise { - const command = (this.runtimeSettings as { command?: { mode?: string; argv?: string[] } }) - ?.command; + const command: { mode?: string; argv?: string[] } | undefined = + typeof this.runtimeSettings === "object" && this.runtimeSettings !== null + ? Reflect.get(this.runtimeSettings, "command") + : undefined; if (command?.mode === "replace") { const { isCommandAvailable } = await import("../../utils/executable.js"); return await isCommandAvailable(command.argv?.[0] ?? ""); @@ -545,18 +552,19 @@ test("derived provider inherits and merges disallowedTools from base", () => { }, }); - const zaiArgs = mockState.constructorArgs.claude.find( - (entry) => - Array.isArray((entry.runtimeSettings as { disallowedTools?: string[] })?.disallowedTools) && - (entry.runtimeSettings as { disallowedTools: string[] }).disallowedTools.includes( - "ComputerUse", - ), - ); + const zaiArgs = mockState.constructorArgs.claude.find((entry) => { + const disallowedTools: string[] | undefined = + typeof entry.runtimeSettings === "object" && entry.runtimeSettings !== null + ? Reflect.get(entry.runtimeSettings, "disallowedTools") + : undefined; + return Array.isArray(disallowedTools) && disallowedTools.includes("ComputerUse"); + }); expect(zaiArgs).toBeDefined(); - expect((zaiArgs!.runtimeSettings as { disallowedTools: string[] }).disallowedTools).toEqual([ - "WebSearch", - "ComputerUse", - ]); + const zaiDisallowedTools: string[] = + typeof zaiArgs!.runtimeSettings === "object" && zaiArgs!.runtimeSettings !== null + ? Reflect.get(zaiArgs!.runtimeSettings, "disallowedTools") + : []; + expect(zaiDisallowedTools).toEqual(["WebSearch", "ComputerUse"]); }); test("extension inherits base override — override claude command, zai extends claude gets overridden command", () => { @@ -574,11 +582,13 @@ test("extension inherits base override — override claude command, zai extends expect(mockState.constructorArgs.claude).toHaveLength(2); expect( - mockState.constructorArgs.claude.every( - (entry) => - (entry.runtimeSettings as { command?: { argv?: string[] } }).command?.argv?.[0] === - "/opt/custom-claude", - ), + mockState.constructorArgs.claude.every((entry) => { + const command: { argv?: string[] } | undefined = + typeof entry.runtimeSettings === "object" && entry.runtimeSettings !== null + ? Reflect.get(entry.runtimeSettings, "command") + : undefined; + return command?.argv?.[0] === "/opt/custom-claude"; + }), ).toBe(true); });