mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
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:
@@ -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);
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user