refactor(server/tests): replace unsafe type assertions with Reflect + Zod (T1.b partial) (#752)

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.
This commit is contained in:
Mohamed Boudra
2026-05-05 15:44:16 +08:00
parent b1d3867da9
commit eb16829f51
6 changed files with 233 additions and 185 deletions

View File

@@ -22,7 +22,7 @@ interface McpToolResult {
}
interface McpClient {
callTool: (input: { name: string; args?: StructuredContent }) => Promise<unknown>;
callTool: (input: { name: string; args?: StructuredContent }) => Promise<McpToolResult>;
close: () => Promise<void>;
}
@@ -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<McpClient> {
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<void> {
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);

View File

@@ -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<unknown>;
callTool: (input: { name: string; args?: StructuredContent }) => Promise<McpToolResult>;
close: () => Promise<void>;
}
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<McpClient> {
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<StructuredContent> {
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<void> {
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<void> {
@@ -118,7 +136,7 @@ async function makeCwd(prefix: string): Promise<string> {
}
async function createTopLevelAgent(args?: Partial<StructuredContent>): Promise<string> {
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<StructuredContent>): Promise<s
background: true,
...args,
});
return payload.agentId as string;
return str(payload.agentId);
}
async function createChildAgent(args?: Partial<StructuredContent>): Promise<string> {
@@ -139,7 +157,7 @@ async function createChildAgent(args?: Partial<StructuredContent>): Promise<stri
background: true,
...args,
});
return payload.agentId as string;
return str(payload.agentId);
}
async function archiveAgentIfPresent(agentId: string | null | undefined): Promise<void> {
@@ -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<StructuredContent>;
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<StructuredContent>;
const terminals = recordArr(payload.terminals);
return terminals.some((terminal) => terminal.id === created.id) ? null : payload;
},
});
const terminals = listed.terminals as Array<StructuredContent>;
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<StructuredContent>;
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<StructuredContent>;
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<StructuredContent>;
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<StructuredContent>;
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<StructuredContent>;
const worktrees = recordArr(listed.worktrees);
expect(worktrees.some((worktree) => worktree.path === created.worktreePath)).toBe(false);
} finally {
await archiveWorktreeIfPresent({ cwd: worktreeRepoCwd, worktreePath });

View File

@@ -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<string, RegisteredMcpTool>;
}
function lookupTool(
server: Awaited<ReturnType<typeof createAgentMcpServer>>,
name: string,
): RegisteredMcpTool | undefined {
return (server as unknown as McpServerInternals)._registeredTools[name];
const tools: Record<string, RegisteredMcpTool> = Reflect.get(server, "_registeredTools");
return tools[name];
}
function registeredTool(
@@ -73,7 +69,7 @@ function registeredTool(
function agentsOf(response: {
structuredContent: LooseStructuredContent;
}): Array<Record<string, unknown>> {
return response.structuredContent.agents as Array<Record<string, unknown>>;
return z.array(z.record(z.unknown())).parse(response.structuredContent.agents);
}
type AgentManagerSpies = ReturnType<typeof buildAgentManagerSpies>;
@@ -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<Record<string, unknown>> };
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<AgentProvider, ProviderDefinition>,
},
});
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<string, unknown>;
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<AgentProvider, ProviderDefinition>,
},
});
const tool = registeredTool(server, "get_agent_status");
@@ -1834,7 +1832,7 @@ describe("agent snapshot MCP serialization", () => {
logger,
providerRegistry: {
claude: createProviderDefinition({}),
} as unknown as Record<AgentProvider, ProviderDefinition>,
},
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<AgentProvider, ProviderDefinition>,
},
});
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<AgentProvider, ProviderDefinition>,
},
});
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<AgentProvider, ProviderDefinition>,
},
});
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<AgentProvider, ProviderDefinition>,
},
});
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<AgentProvider, ProviderDefinition>,
},
});
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<AgentProvider, ProviderDefinition>,
},
});
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<AgentProvider, ProviderDefinition>,
},
});
const tool = registeredTool(server, "get_agent_activity");
const response = await tool.callback({ agentId: "live-activity-agent-2", limit: 2 });

View File

@@ -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<typeof vi.fn> }).streamAgent,
).not.toHaveBeenCalled();
expect(
(agentManager as unknown as { replaceAgentRun: ReturnType<typeof vi.fn> }).replaceAgentRun,
).not.toHaveBeenCalled();
expect(streamAgentSpy).not.toHaveBeenCalled();
expect(replaceAgentRunSpy).not.toHaveBeenCalled();
});

View File

@@ -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<typeof buildProviderRegistry>;
function makeMockRegistry(
entries: Record<string, { enabled: boolean; fetchModels: ReturnType<typeof vi.fn> }>,
): 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",

View File

@@ -23,11 +23,12 @@ const mockState = vi.hoisted(() => {
isCommandAvailable: vi.fn(async (_command: string) => false),
runtimeModels: new Map<string, AgentModelDefinition[]>(),
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<boolean> {
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<boolean> {
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<boolean> {
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);
});