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 {
|
interface McpClient {
|
||||||
callTool: (input: { name: string; args?: StructuredContent }) => Promise<unknown>;
|
callTool: (input: { name: string; args?: StructuredContent }) => Promise<McpToolResult>;
|
||||||
close: () => Promise<void>;
|
close: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,23 +61,29 @@ function getStructuredContent(result: McpToolResult): StructuredContent | null {
|
|||||||
}
|
}
|
||||||
const content = result.content?.[0];
|
const content = result.content?.[0];
|
||||||
if (content && typeof content === "object" && "structuredContent" in content) {
|
if (content && typeof content === "object" && "structuredContent" in content) {
|
||||||
const structured = (content as { structuredContent?: StructuredContent }).structuredContent;
|
if (content.structuredContent) return content.structuredContent;
|
||||||
if (structured) return structured;
|
|
||||||
}
|
}
|
||||||
if (content && typeof content === "object") {
|
if (content && typeof content === "object") {
|
||||||
return content as StructuredContent;
|
return content;
|
||||||
}
|
}
|
||||||
return null;
|
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: {
|
async function waitForAgentCompletion(options: {
|
||||||
client: McpClient;
|
client: McpClient;
|
||||||
agentId: string;
|
agentId: string;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const waitResult = (await options.client.callTool({
|
const waitResult = await options.client.callTool({
|
||||||
name: "wait_for_agent",
|
name: "wait_for_agent",
|
||||||
args: { agentId: options.agentId },
|
args: { agentId: options.agentId },
|
||||||
})) as McpToolResult;
|
});
|
||||||
const payload = getStructuredContent(waitResult);
|
const payload = getStructuredContent(waitResult);
|
||||||
if (!payload) {
|
if (!payload) {
|
||||||
throw new Error("wait_for_agent returned no structured payload");
|
throw new Error("wait_for_agent returned no structured payload");
|
||||||
@@ -87,7 +93,7 @@ async function waitForAgentCompletion(options: {
|
|||||||
}
|
}
|
||||||
const status = payload.status;
|
const status = payload.status;
|
||||||
if (status === "running" || status === "initializing") {
|
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" }));
|
const daemon = await createPaseoDaemon(daemonConfig, pino({ level: "silent" }));
|
||||||
await daemon.start();
|
await daemon.start();
|
||||||
|
|
||||||
const transport = new StreamableHTTPClientTransport(
|
const client = await createMcpClient(`http://127.0.0.1:${port}/mcp/agents`);
|
||||||
new URL(`http://127.0.0.1:${port}/mcp/agents`),
|
|
||||||
);
|
|
||||||
const client = (await experimental_createMCPClient({ transport })) as McpClient;
|
|
||||||
|
|
||||||
let agentId: string | null = null;
|
let agentId: string | null = null;
|
||||||
try {
|
try {
|
||||||
@@ -129,7 +132,7 @@ describe("agent MCP end-to-end (offline)", () => {
|
|||||||
"Do not respond before the command finishes.",
|
"Do not respond before the command finishes.",
|
||||||
].join("\n");
|
].join("\n");
|
||||||
|
|
||||||
const result = (await client.callTool({
|
const result = await client.callTool({
|
||||||
name: "create_agent",
|
name: "create_agent",
|
||||||
args: {
|
args: {
|
||||||
cwd: agentCwd,
|
cwd: agentCwd,
|
||||||
@@ -139,10 +142,10 @@ describe("agent MCP end-to-end (offline)", () => {
|
|||||||
initialPrompt,
|
initialPrompt,
|
||||||
background: false,
|
background: false,
|
||||||
},
|
},
|
||||||
})) as McpToolResult;
|
});
|
||||||
|
|
||||||
const payload = getStructuredContent(result);
|
const payload = getStructuredContent(result);
|
||||||
agentId = (payload?.agentId as string | undefined) ?? null;
|
agentId = typeof payload?.agentId === "string" ? payload.agentId : null;
|
||||||
expect(agentId).toBeTruthy();
|
expect(agentId).toBeTruthy();
|
||||||
|
|
||||||
await waitForAgentCompletion({ client, agentId: agentId! });
|
await waitForAgentCompletion({ client, agentId: agentId! });
|
||||||
@@ -186,10 +189,7 @@ describe("agent MCP end-to-end (offline)", () => {
|
|||||||
const daemon = await createPaseoDaemon(daemonConfig, pino({ level: "silent" }));
|
const daemon = await createPaseoDaemon(daemonConfig, pino({ level: "silent" }));
|
||||||
await daemon.start();
|
await daemon.start();
|
||||||
|
|
||||||
const transport = new StreamableHTTPClientTransport(
|
const client = await createMcpClient(`http://127.0.0.1:${port}/mcp/agents`);
|
||||||
new URL(`http://127.0.0.1:${port}/mcp/agents`),
|
|
||||||
);
|
|
||||||
const client = (await experimental_createMCPClient({ transport })) as McpClient;
|
|
||||||
|
|
||||||
const disabledPaseoHome = await mkdtemp(path.join(os.tmpdir(), "paseo-home-disabled-"));
|
const disabledPaseoHome = await mkdtemp(path.join(os.tmpdir(), "paseo-home-disabled-"));
|
||||||
const disabledStaticDir = await mkdtemp(path.join(os.tmpdir(), "paseo-static-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" }));
|
const disabledDaemon = await createPaseoDaemon(disabledDaemonConfig, pino({ level: "silent" }));
|
||||||
await disabledDaemon.start();
|
await disabledDaemon.start();
|
||||||
|
|
||||||
const disabledTransport = new StreamableHTTPClientTransport(
|
const disabledClient = await createMcpClient(`http://127.0.0.1:${disabledPort}/mcp/agents`);
|
||||||
new URL(`http://127.0.0.1:${disabledPort}/mcp/agents`),
|
|
||||||
);
|
|
||||||
const disabledClient = (await experimental_createMCPClient({
|
|
||||||
transport: disabledTransport,
|
|
||||||
})) as McpClient;
|
|
||||||
|
|
||||||
let agentId: string | null = null;
|
let agentId: string | null = null;
|
||||||
let disabledAgentId: string | null = null;
|
let disabledAgentId: string | null = null;
|
||||||
try {
|
try {
|
||||||
const result = (await client.callTool({
|
const result = await client.callTool({
|
||||||
name: "create_agent",
|
name: "create_agent",
|
||||||
args: {
|
args: {
|
||||||
cwd: agentCwd,
|
cwd: agentCwd,
|
||||||
@@ -230,9 +225,9 @@ describe("agent MCP end-to-end (offline)", () => {
|
|||||||
initialPrompt: "reply with done and stop",
|
initialPrompt: "reply with done and stop",
|
||||||
background: true,
|
background: true,
|
||||||
},
|
},
|
||||||
})) as McpToolResult;
|
});
|
||||||
const payload = getStructuredContent(result);
|
const payload = getStructuredContent(result);
|
||||||
agentId = (payload?.agentId as string | undefined) ?? null;
|
agentId = typeof payload?.agentId === "string" ? payload.agentId : null;
|
||||||
expect(agentId).toBeTruthy();
|
expect(agentId).toBeTruthy();
|
||||||
|
|
||||||
const injectedAgent = daemon.agentManager.getAgent(agentId!);
|
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",
|
name: "create_agent",
|
||||||
args: {
|
args: {
|
||||||
cwd: disabledAgentCwd,
|
cwd: disabledAgentCwd,
|
||||||
@@ -253,9 +248,10 @@ describe("agent MCP end-to-end (offline)", () => {
|
|||||||
initialPrompt: "reply with done and stop",
|
initialPrompt: "reply with done and stop",
|
||||||
background: true,
|
background: true,
|
||||||
},
|
},
|
||||||
})) as McpToolResult;
|
});
|
||||||
const disabledPayload = getStructuredContent(disabledResult);
|
const disabledPayload = getStructuredContent(disabledResult);
|
||||||
disabledAgentId = (disabledPayload?.agentId as string | undefined) ?? null;
|
disabledAgentId =
|
||||||
|
typeof disabledPayload?.agentId === "string" ? disabledPayload.agentId : null;
|
||||||
expect(disabledAgentId).toBeTruthy();
|
expect(disabledAgentId).toBeTruthy();
|
||||||
|
|
||||||
const disabledAgent = disabledDaemon.agentManager.getAgent(disabledAgentId!);
|
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" }));
|
const daemon = await createPaseoDaemon(daemonConfig, pino({ level: "silent" }));
|
||||||
await daemon.start();
|
await daemon.start();
|
||||||
|
|
||||||
const transport = new StreamableHTTPClientTransport(
|
const client = await createMcpClient(`http://127.0.0.1:${port}/mcp/agents`);
|
||||||
new URL(`http://127.0.0.1:${port}/mcp/agents`),
|
|
||||||
);
|
|
||||||
const client = (await experimental_createMCPClient({ transport })) as McpClient;
|
|
||||||
|
|
||||||
let agentId: string | null = null;
|
let agentId: string | null = null;
|
||||||
try {
|
try {
|
||||||
@@ -339,7 +332,7 @@ describe("agent MCP end-to-end (offline)", () => {
|
|||||||
stdio: "pipe",
|
stdio: "pipe",
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = (await withTimeout({
|
const result = await withTimeout({
|
||||||
promise: client.callTool({
|
promise: client.callTool({
|
||||||
name: "create_agent",
|
name: "create_agent",
|
||||||
args: {
|
args: {
|
||||||
@@ -355,12 +348,12 @@ describe("agent MCP end-to-end (offline)", () => {
|
|||||||
}),
|
}),
|
||||||
timeoutMs: 2500,
|
timeoutMs: 2500,
|
||||||
label: "create_agent should not block on setup",
|
label: "create_agent should not block on setup",
|
||||||
})) as McpToolResult;
|
});
|
||||||
|
|
||||||
const payload = getStructuredContent(result);
|
const payload = getStructuredContent(result);
|
||||||
agentId = (payload?.agentId as string | undefined) ?? null;
|
agentId = typeof payload?.agentId === "string" ? payload.agentId : null;
|
||||||
expect(agentId).toBeTruthy();
|
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(worktreePath).toContain(`${path.sep}worktrees${path.sep}`);
|
||||||
expect(existsSync(path.join(worktreePath, "setup-done.txt"))).toBe(false);
|
expect(existsSync(path.join(worktreePath, "setup-done.txt"))).toBe(false);
|
||||||
expect(existsSync(path.join(worktreePath, "dev-terminal.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 { afterAll, beforeAll, describe, expect, test } from "vitest";
|
||||||
import { experimental_createMCPClient } from "ai";
|
import { experimental_createMCPClient } from "ai";
|
||||||
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { AGENT_WAIT_TIMEOUT_MS } from "./mcp-shared.js";
|
import { AGENT_WAIT_TIMEOUT_MS } from "./mcp-shared.js";
|
||||||
import { createTestPaseoDaemon, type TestPaseoDaemon } from "../test-utils/paseo-daemon.js";
|
import { createTestPaseoDaemon, type TestPaseoDaemon } from "../test-utils/paseo-daemon.js";
|
||||||
@@ -20,10 +21,22 @@ interface McpToolResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface McpClient {
|
interface McpClient {
|
||||||
callTool: (input: { name: string; args?: StructuredContent }) => Promise<unknown>;
|
callTool: (input: { name: string; args?: StructuredContent }) => Promise<McpToolResult>;
|
||||||
close: () => Promise<void>;
|
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 {
|
function formatHostForHttpUrl(host: string): string {
|
||||||
return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
|
return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
|
||||||
}
|
}
|
||||||
@@ -43,20 +56,21 @@ function getStructuredContent(result: McpToolResult): StructuredContent | null {
|
|||||||
}
|
}
|
||||||
const content = result.content?.[0];
|
const content = result.content?.[0];
|
||||||
if (content && typeof content === "object" && "structuredContent" in content) {
|
if (content && typeof content === "object" && "structuredContent" in content) {
|
||||||
const structured = (content as { structuredContent?: StructuredContent }).structuredContent;
|
if (content.structuredContent) {
|
||||||
if (structured) {
|
return content.structuredContent;
|
||||||
return structured;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (content && typeof content === "object") {
|
if (content && typeof content === "object") {
|
||||||
return content as StructuredContent;
|
return content;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createMcpClient(url: string): Promise<McpClient> {
|
async function createMcpClient(url: string): Promise<McpClient> {
|
||||||
const transport = new StreamableHTTPClientTransport(new URL(url));
|
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(
|
async function callToolStructured(
|
||||||
@@ -64,7 +78,7 @@ async function callToolStructured(
|
|||||||
name: string,
|
name: string,
|
||||||
args?: StructuredContent,
|
args?: StructuredContent,
|
||||||
): Promise<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);
|
const payload = getStructuredContent(result);
|
||||||
if (!payload) {
|
if (!payload) {
|
||||||
throw new Error(`${name} returned no structured payload`);
|
throw new Error(`${name} returned no structured payload`);
|
||||||
@@ -78,10 +92,14 @@ async function expectToolError(
|
|||||||
args: StructuredContent,
|
args: StructuredContent,
|
||||||
pattern: RegExp,
|
pattern: RegExp,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const result = (await client.callTool({ name, args })) as McpToolResult;
|
const result = await client.callTool({ name, args });
|
||||||
expect(result.isError).toBe(true);
|
expect(result.isError).toBe(true);
|
||||||
const content = result.content?.[0] as { text?: string } | undefined;
|
const contentItem = result.content?.[0];
|
||||||
expect(content?.text ?? "").toMatch(pattern);
|
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> {
|
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> {
|
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", {
|
const payload = await callToolStructured(topLevelClient, "create_agent", {
|
||||||
cwd,
|
cwd,
|
||||||
title: "Parity agent",
|
title: "Parity agent",
|
||||||
@@ -128,7 +146,7 @@ async function createTopLevelAgent(args?: Partial<StructuredContent>): Promise<s
|
|||||||
background: true,
|
background: true,
|
||||||
...args,
|
...args,
|
||||||
});
|
});
|
||||||
return payload.agentId as string;
|
return str(payload.agentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createChildAgent(args?: Partial<StructuredContent>): Promise<string> {
|
async function createChildAgent(args?: Partial<StructuredContent>): Promise<string> {
|
||||||
@@ -139,7 +157,7 @@ async function createChildAgent(args?: Partial<StructuredContent>): Promise<stri
|
|||||||
background: true,
|
background: true,
|
||||||
...args,
|
...args,
|
||||||
});
|
});
|
||||||
return payload.agentId as string;
|
return str(payload.agentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function archiveAgentIfPresent(agentId: string | null | undefined): Promise<void> {
|
async function archiveAgentIfPresent(agentId: string | null | undefined): Promise<void> {
|
||||||
@@ -213,7 +231,7 @@ beforeAll(async () => {
|
|||||||
mode: "bypassPermissions",
|
mode: "bypassPermissions",
|
||||||
background: true,
|
background: true,
|
||||||
});
|
});
|
||||||
parentAgentId = parentPayload.agentId as string;
|
parentAgentId = str(parentPayload.agentId);
|
||||||
|
|
||||||
agentScopedClient = await createMcpClient(
|
agentScopedClient = await createMcpClient(
|
||||||
`http://127.0.0.1:${daemonHandle.port}/mcp/agents?callerAgentId=${parentAgentId}`,
|
`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", {
|
const created = await callToolStructured(agentScopedClient, "create_terminal", {
|
||||||
name: "Parity terminal",
|
name: "Parity terminal",
|
||||||
});
|
});
|
||||||
terminalId = created.id as string;
|
terminalId = str(created.id);
|
||||||
|
|
||||||
const listed = await callToolStructured(agentScopedClient, "list_terminals");
|
const listed = await callToolStructured(agentScopedClient, "list_terminals");
|
||||||
const terminals = listed.terminals as Array<StructuredContent>;
|
const terminals = recordArr(listed.terminals);
|
||||||
expect(terminals).toEqual(
|
expect(terminals).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@@ -386,7 +404,7 @@ describe("Suite B: Terminal Tools", () => {
|
|||||||
const created = await callToolStructured(agentScopedClient, "create_terminal", {
|
const created = await callToolStructured(agentScopedClient, "create_terminal", {
|
||||||
name: "Parity capture terminal",
|
name: "Parity capture terminal",
|
||||||
});
|
});
|
||||||
terminalId = created.id as string;
|
terminalId = str(created.id);
|
||||||
|
|
||||||
await callToolStructured(agentScopedClient, "send_terminal_keys", {
|
await callToolStructured(agentScopedClient, "send_terminal_keys", {
|
||||||
terminalId,
|
terminalId,
|
||||||
@@ -404,7 +422,7 @@ describe("Suite B: Terminal Tools", () => {
|
|||||||
terminalId,
|
terminalId,
|
||||||
scrollback: true,
|
scrollback: true,
|
||||||
});
|
});
|
||||||
const lines = (payload.lines as string[] | undefined) ?? [];
|
const lines = strArrOptional(payload.lines) ?? [];
|
||||||
return lines.some((line) => line.includes("hello")) ? payload : null;
|
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", {
|
const created = await callToolStructured(agentScopedClient, "create_terminal", {
|
||||||
name: "Parity kill terminal",
|
name: "Parity kill terminal",
|
||||||
});
|
});
|
||||||
terminalId = created.id as string;
|
terminalId = str(created.id);
|
||||||
|
|
||||||
await callToolStructured(agentScopedClient, "kill_terminal", { terminalId });
|
await callToolStructured(agentScopedClient, "kill_terminal", { terminalId });
|
||||||
terminalId = null;
|
terminalId = null;
|
||||||
@@ -432,11 +450,11 @@ describe("Suite B: Terminal Tools", () => {
|
|||||||
label: "terminal removal",
|
label: "terminal removal",
|
||||||
check: async () => {
|
check: async () => {
|
||||||
const payload = await callToolStructured(agentScopedClient, "list_terminals");
|
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;
|
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);
|
expect(terminals.some((terminal) => terminal.id === created.id)).toBe(false);
|
||||||
} finally {
|
} finally {
|
||||||
await killTerminalIfPresent(terminalId);
|
await killTerminalIfPresent(terminalId);
|
||||||
@@ -462,10 +480,10 @@ describe("Suite C: Schedule Tools", () => {
|
|||||||
every: "5m",
|
every: "5m",
|
||||||
name: "Parity schedule list",
|
name: "Parity schedule list",
|
||||||
});
|
});
|
||||||
scheduleId = created.id as string;
|
scheduleId = str(created.id);
|
||||||
|
|
||||||
const listed = await callToolStructured(topLevelClient, "list_schedules");
|
const listed = await callToolStructured(topLevelClient, "list_schedules");
|
||||||
const schedules = listed.schedules as Array<StructuredContent>;
|
const schedules = recordArr(listed.schedules);
|
||||||
expect(schedules).toEqual(
|
expect(schedules).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@@ -488,7 +506,7 @@ describe("Suite C: Schedule Tools", () => {
|
|||||||
name: "Parity provider schedule",
|
name: "Parity provider schedule",
|
||||||
provider: "codex/gpt-5.4",
|
provider: "codex/gpt-5.4",
|
||||||
});
|
});
|
||||||
scheduleId = created.id as string;
|
scheduleId = str(created.id);
|
||||||
expect(created.target).toMatchObject({
|
expect(created.target).toMatchObject({
|
||||||
type: "new-agent",
|
type: "new-agent",
|
||||||
config: {
|
config: {
|
||||||
@@ -509,7 +527,7 @@ describe("Suite C: Schedule Tools", () => {
|
|||||||
every: "5m",
|
every: "5m",
|
||||||
name: "Parity inspect schedule",
|
name: "Parity inspect schedule",
|
||||||
});
|
});
|
||||||
scheduleId = created.id as string;
|
scheduleId = str(created.id);
|
||||||
|
|
||||||
const inspected = await callToolStructured(topLevelClient, "inspect_schedule", {
|
const inspected = await callToolStructured(topLevelClient, "inspect_schedule", {
|
||||||
id: scheduleId,
|
id: scheduleId,
|
||||||
@@ -533,7 +551,7 @@ describe("Suite C: Schedule Tools", () => {
|
|||||||
every: "5m",
|
every: "5m",
|
||||||
name: "Parity pause schedule",
|
name: "Parity pause schedule",
|
||||||
});
|
});
|
||||||
scheduleId = created.id as string;
|
scheduleId = str(created.id);
|
||||||
|
|
||||||
await callToolStructured(topLevelClient, "pause_schedule", { id: scheduleId });
|
await callToolStructured(topLevelClient, "pause_schedule", { id: scheduleId });
|
||||||
const paused = await callToolStructured(topLevelClient, "inspect_schedule", {
|
const paused = await callToolStructured(topLevelClient, "inspect_schedule", {
|
||||||
@@ -559,13 +577,13 @@ describe("Suite C: Schedule Tools", () => {
|
|||||||
every: "5m",
|
every: "5m",
|
||||||
name: "Parity delete schedule",
|
name: "Parity delete schedule",
|
||||||
});
|
});
|
||||||
scheduleId = created.id as string;
|
scheduleId = str(created.id);
|
||||||
|
|
||||||
await callToolStructured(topLevelClient, "delete_schedule", { id: scheduleId });
|
await callToolStructured(topLevelClient, "delete_schedule", { id: scheduleId });
|
||||||
scheduleId = null;
|
scheduleId = null;
|
||||||
|
|
||||||
const listed = await callToolStructured(topLevelClient, "list_schedules");
|
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);
|
expect(schedules.some((schedule) => schedule.id === created.id)).toBe(false);
|
||||||
} finally {
|
} finally {
|
||||||
await deleteScheduleIfPresent(scheduleId);
|
await deleteScheduleIfPresent(scheduleId);
|
||||||
@@ -581,7 +599,7 @@ describe("Suite C: Schedule Tools", () => {
|
|||||||
name: "Parity self schedule",
|
name: "Parity self schedule",
|
||||||
target: "self",
|
target: "self",
|
||||||
});
|
});
|
||||||
scheduleId = created.id as string;
|
scheduleId = str(created.id);
|
||||||
expect(created.target).toMatchObject({
|
expect(created.target).toMatchObject({
|
||||||
type: "agent",
|
type: "agent",
|
||||||
agentId: parentAgentId,
|
agentId: parentAgentId,
|
||||||
@@ -599,7 +617,7 @@ describe("Suite C: Schedule Tools", () => {
|
|||||||
every: "5m",
|
every: "5m",
|
||||||
provider: "codex/gpt-5.4",
|
provider: "codex/gpt-5.4",
|
||||||
});
|
});
|
||||||
scheduleId = created.id as string;
|
scheduleId = str(created.id);
|
||||||
expect(created.target).toMatchObject({
|
expect(created.target).toMatchObject({
|
||||||
type: "new-agent",
|
type: "new-agent",
|
||||||
config: {
|
config: {
|
||||||
@@ -629,7 +647,7 @@ describe("Suite C: Schedule Tools", () => {
|
|||||||
describe("Suite D: Provider Tools", () => {
|
describe("Suite D: Provider Tools", () => {
|
||||||
test("list_providers returns providers", async () => {
|
test("list_providers returns providers", async () => {
|
||||||
const payload = await callToolStructured(topLevelClient, "list_providers");
|
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(Array.isArray(providers)).toBe(true);
|
||||||
expect(providers.length).toBeGreaterThan(0);
|
expect(providers.length).toBeGreaterThan(0);
|
||||||
expect(providers[0]).toEqual(
|
expect(providers[0]).toEqual(
|
||||||
@@ -667,12 +685,12 @@ describe("Suite E: Worktree Tools", () => {
|
|||||||
branchName,
|
branchName,
|
||||||
baseBranch: "main",
|
baseBranch: "main",
|
||||||
});
|
});
|
||||||
worktreePath = created.worktreePath as string;
|
worktreePath = str(created.worktreePath);
|
||||||
|
|
||||||
const listed = await callToolStructured(topLevelClient, "list_worktrees", {
|
const listed = await callToolStructured(topLevelClient, "list_worktrees", {
|
||||||
cwd: worktreeRepoCwd,
|
cwd: worktreeRepoCwd,
|
||||||
});
|
});
|
||||||
const worktrees = listed.worktrees as Array<StructuredContent>;
|
const worktrees = recordArr(listed.worktrees);
|
||||||
expect(worktrees).toEqual(
|
expect(worktrees).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@@ -695,7 +713,7 @@ describe("Suite E: Worktree Tools", () => {
|
|||||||
branchName,
|
branchName,
|
||||||
baseBranch: "main",
|
baseBranch: "main",
|
||||||
});
|
});
|
||||||
worktreePath = created.worktreePath as string;
|
worktreePath = str(created.worktreePath);
|
||||||
|
|
||||||
await callToolStructured(topLevelClient, "archive_worktree", {
|
await callToolStructured(topLevelClient, "archive_worktree", {
|
||||||
cwd: worktreeRepoCwd,
|
cwd: worktreeRepoCwd,
|
||||||
@@ -706,7 +724,7 @@ describe("Suite E: Worktree Tools", () => {
|
|||||||
const listed = await callToolStructured(topLevelClient, "list_worktrees", {
|
const listed = await callToolStructured(topLevelClient, "list_worktrees", {
|
||||||
cwd: worktreeRepoCwd,
|
cwd: worktreeRepoCwd,
|
||||||
});
|
});
|
||||||
const worktrees = listed.worktrees as Array<StructuredContent>;
|
const worktrees = recordArr(listed.worktrees);
|
||||||
expect(worktrees.some((worktree) => worktree.path === created.worktreePath)).toBe(false);
|
expect(worktrees.some((worktree) => worktree.path === created.worktreePath)).toBe(false);
|
||||||
} finally {
|
} finally {
|
||||||
await archiveWorktreeIfPresent({ cwd: worktreeRepoCwd, worktreePath });
|
await archiveWorktreeIfPresent({ cwd: worktreeRepoCwd, worktreePath });
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import { AgentListItemPayloadSchema, AgentSnapshotPayloadSchema } from "../../sh
|
|||||||
import type { PersistedProjectRecord, PersistedWorkspaceRecord } from "../workspace-registry.js";
|
import type { PersistedProjectRecord, PersistedWorkspaceRecord } from "../workspace-registry.js";
|
||||||
import type { CreateScheduleInput, StoredSchedule } from "../schedule/types.js";
|
import type { CreateScheduleInput, StoredSchedule } from "../schedule/types.js";
|
||||||
import type { ScheduleService } from "../schedule/service.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 type { WorkspaceGitService } from "../workspace-git-service.js";
|
||||||
import {
|
import {
|
||||||
createPaseoWorktree as createPaseoWorktreeService,
|
createPaseoWorktree as createPaseoWorktreeService,
|
||||||
@@ -48,15 +47,12 @@ interface RegisteredMcpTool {
|
|||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface McpServerInternals {
|
|
||||||
_registeredTools: Record<string, RegisteredMcpTool>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function lookupTool(
|
function lookupTool(
|
||||||
server: Awaited<ReturnType<typeof createAgentMcpServer>>,
|
server: Awaited<ReturnType<typeof createAgentMcpServer>>,
|
||||||
name: string,
|
name: string,
|
||||||
): RegisteredMcpTool | undefined {
|
): RegisteredMcpTool | undefined {
|
||||||
return (server as unknown as McpServerInternals)._registeredTools[name];
|
const tools: Record<string, RegisteredMcpTool> = Reflect.get(server, "_registeredTools");
|
||||||
|
return tools[name];
|
||||||
}
|
}
|
||||||
|
|
||||||
function registeredTool(
|
function registeredTool(
|
||||||
@@ -73,7 +69,7 @@ function registeredTool(
|
|||||||
function agentsOf(response: {
|
function agentsOf(response: {
|
||||||
structuredContent: LooseStructuredContent;
|
structuredContent: LooseStructuredContent;
|
||||||
}): Array<Record<string, unknown>> {
|
}): 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>;
|
type AgentManagerSpies = ReturnType<typeof buildAgentManagerSpies>;
|
||||||
@@ -736,7 +732,7 @@ describe("create_agent MCP tool", () => {
|
|||||||
background: true,
|
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" })
|
const initialBranch = execSync("git branch --show-current", { cwd: agentCwd, stdio: "pipe" })
|
||||||
.toString()
|
.toString()
|
||||||
.trim();
|
.trim();
|
||||||
@@ -808,7 +804,7 @@ describe("create_agent MCP tool", () => {
|
|||||||
background: true,
|
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(
|
expect(
|
||||||
execSync("git branch --show-current", { cwd: agentCwd, stdio: "pipe" }).toString().trim(),
|
execSync("git branch --show-current", { cwd: agentCwd, stdio: "pipe" }).toString().trim(),
|
||||||
).toBe("existing-feature");
|
).toBe("existing-feature");
|
||||||
@@ -1611,7 +1607,9 @@ describe("agent snapshot MCP serialization", () => {
|
|||||||
const server = await createAgentMcpServer({ agentManager, agentStorage, logger });
|
const server = await createAgentMcpServer({ agentManager, agentStorage, logger });
|
||||||
const tool = registeredTool(server, "list_agents");
|
const tool = registeredTool(server, "list_agents");
|
||||||
const response = await tool.callback({});
|
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({
|
expect(structured).toEqual({
|
||||||
agents: [
|
agents: [
|
||||||
@@ -1659,7 +1657,7 @@ describe("agent snapshot MCP serialization", () => {
|
|||||||
logger,
|
logger,
|
||||||
providerRegistry: {
|
providerRegistry: {
|
||||||
claude: createProviderDefinition({}),
|
claude: createProviderDefinition({}),
|
||||||
} as unknown as Record<AgentProvider, ProviderDefinition>,
|
},
|
||||||
});
|
});
|
||||||
const tool = registeredTool(server, "get_agent_status");
|
const tool = registeredTool(server, "get_agent_status");
|
||||||
const response = await tool.callback({ agentId: "archived-agent" });
|
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 server = await createAgentMcpServer({ agentManager, agentStorage, logger });
|
||||||
const tool = registeredTool(server, "get_agent_status");
|
const tool = registeredTool(server, "get_agent_status");
|
||||||
const response = await tool.callback({ agentId: "full-detail-agent" });
|
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);
|
const parsed = AgentSnapshotPayloadSchema.safeParse(snapshot);
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
@@ -1790,7 +1788,7 @@ describe("agent snapshot MCP serialization", () => {
|
|||||||
logger,
|
logger,
|
||||||
providerRegistry: {
|
providerRegistry: {
|
||||||
claude: createProviderDefinition({}),
|
claude: createProviderDefinition({}),
|
||||||
} as unknown as Record<AgentProvider, ProviderDefinition>,
|
},
|
||||||
});
|
});
|
||||||
const tool = registeredTool(server, "get_agent_status");
|
const tool = registeredTool(server, "get_agent_status");
|
||||||
|
|
||||||
@@ -1834,7 +1832,7 @@ describe("agent snapshot MCP serialization", () => {
|
|||||||
logger,
|
logger,
|
||||||
providerRegistry: {
|
providerRegistry: {
|
||||||
claude: createProviderDefinition({}),
|
claude: createProviderDefinition({}),
|
||||||
} as unknown as Record<AgentProvider, ProviderDefinition>,
|
},
|
||||||
callerAgentId: "caller-agent",
|
callerAgentId: "caller-agent",
|
||||||
});
|
});
|
||||||
const tool = registeredTool(server, "list_agents");
|
const tool = registeredTool(server, "list_agents");
|
||||||
@@ -1883,7 +1881,7 @@ describe("agent snapshot MCP serialization", () => {
|
|||||||
logger,
|
logger,
|
||||||
providerRegistry: {
|
providerRegistry: {
|
||||||
claude: createProviderDefinition({}),
|
claude: createProviderDefinition({}),
|
||||||
} as unknown as Record<AgentProvider, ProviderDefinition>,
|
},
|
||||||
});
|
});
|
||||||
const tool = registeredTool(server, "list_agents");
|
const tool = registeredTool(server, "list_agents");
|
||||||
const response = await tool.callback({
|
const response = await tool.callback({
|
||||||
@@ -1924,7 +1922,7 @@ describe("agent snapshot MCP serialization", () => {
|
|||||||
logger,
|
logger,
|
||||||
providerRegistry: {
|
providerRegistry: {
|
||||||
claude: createProviderDefinition({}),
|
claude: createProviderDefinition({}),
|
||||||
} as unknown as Record<AgentProvider, ProviderDefinition>,
|
},
|
||||||
});
|
});
|
||||||
const tool = registeredTool(server, "list_agents");
|
const tool = registeredTool(server, "list_agents");
|
||||||
const response = await tool.callback({ includeArchived: true });
|
const response = await tool.callback({ includeArchived: true });
|
||||||
@@ -1967,7 +1965,7 @@ describe("agent snapshot MCP serialization", () => {
|
|||||||
logger,
|
logger,
|
||||||
providerRegistry: {
|
providerRegistry: {
|
||||||
claude: createProviderDefinition({}),
|
claude: createProviderDefinition({}),
|
||||||
} as unknown as Record<AgentProvider, ProviderDefinition>,
|
},
|
||||||
});
|
});
|
||||||
const tool = registeredTool(server, "list_agents");
|
const tool = registeredTool(server, "list_agents");
|
||||||
const response = await tool.callback({ cwd: "/tmp/repo", includeArchived: true });
|
const response = await tool.callback({ cwd: "/tmp/repo", includeArchived: true });
|
||||||
@@ -2075,7 +2073,7 @@ describe("agent snapshot MCP serialization", () => {
|
|||||||
logger,
|
logger,
|
||||||
providerRegistry: {
|
providerRegistry: {
|
||||||
claude: createProviderDefinition({}),
|
claude: createProviderDefinition({}),
|
||||||
} as unknown as Record<AgentProvider, ProviderDefinition>,
|
},
|
||||||
});
|
});
|
||||||
const tool = registeredTool(server, "list_agents");
|
const tool = registeredTool(server, "list_agents");
|
||||||
const response = await tool.callback({ includeArchived: true });
|
const response = await tool.callback({ includeArchived: true });
|
||||||
@@ -2115,7 +2113,7 @@ describe("agent snapshot MCP serialization", () => {
|
|||||||
logger,
|
logger,
|
||||||
providerRegistry: {
|
providerRegistry: {
|
||||||
claude: createProviderDefinition({}),
|
claude: createProviderDefinition({}),
|
||||||
} as unknown as Record<AgentProvider, ProviderDefinition>,
|
},
|
||||||
});
|
});
|
||||||
const tool = registeredTool(server, "get_agent_activity");
|
const tool = registeredTool(server, "get_agent_activity");
|
||||||
const response = await tool.callback({ agentId: "archived-activity-agent" });
|
const response = await tool.callback({ agentId: "archived-activity-agent" });
|
||||||
@@ -2153,7 +2151,7 @@ describe("agent snapshot MCP serialization", () => {
|
|||||||
logger: createTestLogger(),
|
logger: createTestLogger(),
|
||||||
providerRegistry: {
|
providerRegistry: {
|
||||||
claude: createProviderDefinition({}),
|
claude: createProviderDefinition({}),
|
||||||
} as unknown as Record<AgentProvider, ProviderDefinition>,
|
},
|
||||||
});
|
});
|
||||||
const tool = registeredTool(server, "get_agent_activity");
|
const tool = registeredTool(server, "get_agent_activity");
|
||||||
const response = await tool.callback({ agentId: "live-activity-agent", limit: 1 });
|
const response = await tool.callback({ agentId: "live-activity-agent", limit: 1 });
|
||||||
@@ -2184,7 +2182,7 @@ describe("agent snapshot MCP serialization", () => {
|
|||||||
logger: createTestLogger(),
|
logger: createTestLogger(),
|
||||||
providerRegistry: {
|
providerRegistry: {
|
||||||
claude: createProviderDefinition({}),
|
claude: createProviderDefinition({}),
|
||||||
} as unknown as Record<AgentProvider, ProviderDefinition>,
|
},
|
||||||
});
|
});
|
||||||
const tool = registeredTool(server, "get_agent_activity");
|
const tool = registeredTool(server, "get_agent_activity");
|
||||||
const response = await tool.callback({ agentId: "live-activity-agent-2", limit: 2 });
|
const response = await tool.callback({ agentId: "live-activity-agent-2", limit: 2 });
|
||||||
|
|||||||
@@ -1,49 +1,60 @@
|
|||||||
import { expect, it, vi } from "vitest";
|
import { expect, it, vi } from "vitest";
|
||||||
|
|
||||||
import { createTestLogger } from "../../test-utils/test-logger.js";
|
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 { setupFinishNotification } from "./mcp-shared.js";
|
||||||
import type { AgentManager, AgentManagerEvent, ManagedAgent } from "./agent-manager.js";
|
import type { AgentManagerEvent, ManagedAgent } from "./agent-manager.js";
|
||||||
import type { AgentStorage } from "./agent-storage.js";
|
|
||||||
|
|
||||||
it("does not notify archived callers", async () => {
|
it("does not notify archived callers", async () => {
|
||||||
let subscriber: ((event: AgentManagerEvent) => void) | null = null;
|
let subscriber: ((event: AgentManagerEvent) => void) | null = null;
|
||||||
|
|
||||||
const childAgent = {
|
const childAgent: ManagedAgent = Object.create(null);
|
||||||
id: "child-agent",
|
Reflect.set(childAgent, "id", "child-agent");
|
||||||
lifecycle: "idle",
|
Reflect.set(childAgent, "lifecycle", "idle");
|
||||||
config: { title: "Child Agent" },
|
Reflect.set(childAgent, "config", { title: "Child Agent" });
|
||||||
} as ManagedAgent;
|
|
||||||
|
|
||||||
const agentManager = {
|
const callerAgent: ManagedAgent = Object.create(null);
|
||||||
getAgent: vi.fn((agentId: string) => {
|
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") {
|
if (agentId === "child-agent") {
|
||||||
return childAgent;
|
return childAgent;
|
||||||
}
|
}
|
||||||
if (agentId === "caller-agent") {
|
if (agentId === "caller-agent") {
|
||||||
return {
|
return callerAgent;
|
||||||
id: "caller-agent",
|
|
||||||
lifecycle: "idle",
|
|
||||||
config: { title: "Caller Agent" },
|
|
||||||
} as ManagedAgent;
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}),
|
}),
|
||||||
subscribe: vi.fn((callback: (event: AgentManagerEvent) => void) => {
|
);
|
||||||
|
Reflect.set(
|
||||||
|
agentManager,
|
||||||
|
"subscribe",
|
||||||
|
vi.fn((callback: (event: AgentManagerEvent) => void) => {
|
||||||
subscriber = callback;
|
subscriber = callback;
|
||||||
return () => {
|
return () => {
|
||||||
subscriber = null;
|
subscriber = null;
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
hasInFlightRun: vi.fn().mockReturnValue(false),
|
);
|
||||||
streamAgent: vi.fn(() => (async function* noop() {})()),
|
Reflect.set(agentManager, "hasInFlightRun", vi.fn().mockReturnValue(false));
|
||||||
replaceAgentRun: vi.fn(() => (async function* noop() {})()),
|
Reflect.set(agentManager, "streamAgent", streamAgentSpy);
|
||||||
} as unknown as AgentManager;
|
Reflect.set(agentManager, "replaceAgentRun", replaceAgentRunSpy);
|
||||||
|
|
||||||
const agentStorage = {
|
const agentStorageGetSpy = vi.fn(async (agentId: string) =>
|
||||||
get: vi.fn(async (agentId: string) =>
|
agentId === "caller-agent" ? { archivedAt: "2024-01-01" } : null,
|
||||||
agentId === "caller-agent" ? { archivedAt: "2024-01-01" } : null,
|
);
|
||||||
),
|
const agentStorage: AgentStorage = Object.create(AgentStorage.prototype);
|
||||||
} as unknown as AgentStorage;
|
Reflect.set(agentStorage, "get", agentStorageGetSpy);
|
||||||
|
|
||||||
setupFinishNotification({
|
setupFinishNotification({
|
||||||
agentManager,
|
agentManager,
|
||||||
@@ -68,13 +79,9 @@ it("does not notify archived callers", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await vi.waitFor(() => {
|
await vi.waitFor(() => {
|
||||||
expect(agentStorage.get).toHaveBeenCalledWith("caller-agent");
|
expect(agentStorageGetSpy).toHaveBeenCalledWith("caller-agent");
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(
|
expect(streamAgentSpy).not.toHaveBeenCalled();
|
||||||
(agentManager as unknown as { streamAgent: ReturnType<typeof vi.fn> }).streamAgent,
|
expect(replaceAgentRunSpy).not.toHaveBeenCalled();
|
||||||
).not.toHaveBeenCalled();
|
|
||||||
expect(
|
|
||||||
(agentManager as unknown as { replaceAgentRun: ReturnType<typeof vi.fn> }).replaceAgentRun,
|
|
||||||
).not.toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { Logger } from "pino";
|
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import { createTestLogger } from "../../test-utils/test-logger.js";
|
||||||
import { resolveAgentModel } from "./model-resolver.js";
|
import { resolveAgentModel } from "./model-resolver.js";
|
||||||
|
|
||||||
vi.mock("./provider-registry.js", () => ({
|
vi.mock("./provider-registry.js", () => ({
|
||||||
@@ -11,10 +11,20 @@ vi.mock("./provider-registry.js", () => ({
|
|||||||
import { buildProviderRegistry } from "./provider-registry.js";
|
import { buildProviderRegistry } from "./provider-registry.js";
|
||||||
|
|
||||||
const mockedBuildProviderRegistry = vi.mocked(buildProviderRegistry);
|
const mockedBuildProviderRegistry = vi.mocked(buildProviderRegistry);
|
||||||
const testLoggerWarn = vi.fn();
|
const testLogger = createTestLogger();
|
||||||
const testLogger = { warn: testLoggerWarn } as unknown as Logger;
|
const testLoggerWarn = vi.spyOn(testLogger, "warn");
|
||||||
type ProviderRegistryMock = ReturnType<typeof buildProviderRegistry>;
|
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", () => {
|
describe("resolveAgentModel", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockedBuildProviderRegistry.mockReset();
|
mockedBuildProviderRegistry.mockReset();
|
||||||
@@ -22,11 +32,13 @@ describe("resolveAgentModel", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("returns the trimmed requested model when provided", async () => {
|
it("returns the trimmed requested model when provided", async () => {
|
||||||
mockedBuildProviderRegistry.mockReturnValue({
|
mockedBuildProviderRegistry.mockReturnValue(
|
||||||
claude: { enabled: true, fetchModels: vi.fn() },
|
makeMockRegistry({
|
||||||
codex: { enabled: true, fetchModels: vi.fn() },
|
claude: { enabled: true, fetchModels: vi.fn() },
|
||||||
opencode: { enabled: true, fetchModels: vi.fn() },
|
codex: { enabled: true, fetchModels: vi.fn() },
|
||||||
} as unknown as ProviderRegistryMock);
|
opencode: { enabled: true, fetchModels: vi.fn() },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
const result = await resolveAgentModel({
|
const result = await resolveAgentModel({
|
||||||
provider: "codex",
|
provider: "codex",
|
||||||
@@ -44,11 +56,13 @@ describe("resolveAgentModel", () => {
|
|||||||
{ id: "claude-3.5-haiku", isDefault: false },
|
{ id: "claude-3.5-haiku", isDefault: false },
|
||||||
{ id: "claude-3.5-sonnet", isDefault: true },
|
{ id: "claude-3.5-sonnet", isDefault: true },
|
||||||
]);
|
]);
|
||||||
mockedBuildProviderRegistry.mockReturnValue({
|
mockedBuildProviderRegistry.mockReturnValue(
|
||||||
claude: { enabled: true, fetchModels },
|
makeMockRegistry({
|
||||||
codex: { enabled: true, fetchModels: vi.fn() },
|
claude: { enabled: true, fetchModels },
|
||||||
opencode: { enabled: true, fetchModels: vi.fn() },
|
codex: { enabled: true, fetchModels: vi.fn() },
|
||||||
} as unknown as ProviderRegistryMock);
|
opencode: { enabled: true, fetchModels: vi.fn() },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
const result = await resolveAgentModel({
|
const result = await resolveAgentModel({
|
||||||
provider: "claude",
|
provider: "claude",
|
||||||
@@ -68,11 +82,13 @@ describe("resolveAgentModel", () => {
|
|||||||
{ id: "model-a", isDefault: false },
|
{ id: "model-a", isDefault: false },
|
||||||
{ id: "model-b", isDefault: false },
|
{ id: "model-b", isDefault: false },
|
||||||
]);
|
]);
|
||||||
mockedBuildProviderRegistry.mockReturnValue({
|
mockedBuildProviderRegistry.mockReturnValue(
|
||||||
claude: { enabled: true, fetchModels: vi.fn() },
|
makeMockRegistry({
|
||||||
codex: { enabled: true, fetchModels },
|
claude: { enabled: true, fetchModels: vi.fn() },
|
||||||
opencode: { enabled: true, fetchModels: vi.fn() },
|
codex: { enabled: true, fetchModels },
|
||||||
} as unknown as ProviderRegistryMock);
|
opencode: { enabled: true, fetchModels: vi.fn() },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
const result = await resolveAgentModel({ provider: "codex", logger: testLogger });
|
const result = await resolveAgentModel({ provider: "codex", logger: testLogger });
|
||||||
|
|
||||||
@@ -81,11 +97,13 @@ describe("resolveAgentModel", () => {
|
|||||||
|
|
||||||
it("returns undefined when the catalog lookup fails", async () => {
|
it("returns undefined when the catalog lookup fails", async () => {
|
||||||
const fetchModels = vi.fn().mockRejectedValue(new Error("boom"));
|
const fetchModels = vi.fn().mockRejectedValue(new Error("boom"));
|
||||||
mockedBuildProviderRegistry.mockReturnValue({
|
mockedBuildProviderRegistry.mockReturnValue(
|
||||||
claude: { enabled: true, fetchModels: vi.fn() },
|
makeMockRegistry({
|
||||||
codex: { enabled: true, fetchModels },
|
claude: { enabled: true, fetchModels: vi.fn() },
|
||||||
opencode: { enabled: true, fetchModels: vi.fn() },
|
codex: { enabled: true, fetchModels },
|
||||||
} as unknown as ProviderRegistryMock);
|
opencode: { enabled: true, fetchModels: vi.fn() },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
const result = await resolveAgentModel({ provider: "codex", logger: testLogger });
|
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 () => {
|
it("returns undefined for a disabled provider without fetching default models", async () => {
|
||||||
const fetchModels = vi.fn().mockResolvedValue([{ id: "model-a", isDefault: true }]);
|
const fetchModels = vi.fn().mockResolvedValue([{ id: "model-a", isDefault: true }]);
|
||||||
mockedBuildProviderRegistry.mockReturnValue({
|
mockedBuildProviderRegistry.mockReturnValue(
|
||||||
claude: { enabled: true, fetchModels: vi.fn() },
|
makeMockRegistry({
|
||||||
codex: { enabled: false, fetchModels },
|
claude: { enabled: true, fetchModels: vi.fn() },
|
||||||
opencode: { enabled: true, fetchModels: vi.fn() },
|
codex: { enabled: false, fetchModels },
|
||||||
} as unknown as ProviderRegistryMock);
|
opencode: { enabled: true, fetchModels: vi.fn() },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
const result = await resolveAgentModel({ provider: "codex", logger: testLogger });
|
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 () => {
|
it("returns undefined for a requested model from a disabled provider", async () => {
|
||||||
const fetchModels = vi.fn();
|
const fetchModels = vi.fn();
|
||||||
mockedBuildProviderRegistry.mockReturnValue({
|
mockedBuildProviderRegistry.mockReturnValue(
|
||||||
claude: { enabled: true, fetchModels: vi.fn() },
|
makeMockRegistry({
|
||||||
codex: { enabled: false, fetchModels },
|
claude: { enabled: true, fetchModels: vi.fn() },
|
||||||
opencode: { enabled: true, fetchModels: vi.fn() },
|
codex: { enabled: false, fetchModels },
|
||||||
} as unknown as ProviderRegistryMock);
|
opencode: { enabled: true, fetchModels: vi.fn() },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
const result = await resolveAgentModel({
|
const result = await resolveAgentModel({
|
||||||
provider: "codex",
|
provider: "codex",
|
||||||
|
|||||||
@@ -23,11 +23,12 @@ const mockState = vi.hoisted(() => {
|
|||||||
isCommandAvailable: vi.fn(async (_command: string) => false),
|
isCommandAvailable: vi.fn(async (_command: string) => false),
|
||||||
runtimeModels: new Map<string, AgentModelDefinition[]>(),
|
runtimeModels: new Map<string, AgentModelDefinition[]>(),
|
||||||
reset() {
|
reset() {
|
||||||
for (const key of Object.keys(this.constructorArgs) as Array<
|
this.constructorArgs.claude = [];
|
||||||
keyof typeof this.constructorArgs
|
this.constructorArgs.codex = [];
|
||||||
>) {
|
this.constructorArgs.copilot = [];
|
||||||
this.constructorArgs[key] = [];
|
this.constructorArgs.opencode = [];
|
||||||
}
|
this.constructorArgs.pi = [];
|
||||||
|
this.constructorArgs.genericAcp = [];
|
||||||
this.isCommandAvailable.mockReset();
|
this.isCommandAvailable.mockReset();
|
||||||
this.isCommandAvailable.mockImplementation(async (_command: string) => false);
|
this.isCommandAvailable.mockImplementation(async (_command: string) => false);
|
||||||
this.runtimeModels.clear();
|
this.runtimeModels.clear();
|
||||||
@@ -76,8 +77,10 @@ vi.mock("./providers/claude-agent.js", () => ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
async isAvailable(): Promise<boolean> {
|
async isAvailable(): Promise<boolean> {
|
||||||
const command = (this.runtimeSettings as { command?: { mode?: string; argv?: string[] } })
|
const command: { mode?: string; argv?: string[] } | undefined =
|
||||||
?.command;
|
typeof this.runtimeSettings === "object" && this.runtimeSettings !== null
|
||||||
|
? Reflect.get(this.runtimeSettings, "command")
|
||||||
|
: undefined;
|
||||||
if (command?.mode === "replace") {
|
if (command?.mode === "replace") {
|
||||||
const { isCommandAvailable } = await import("../../utils/executable.js");
|
const { isCommandAvailable } = await import("../../utils/executable.js");
|
||||||
return await isCommandAvailable(command.argv?.[0] ?? "");
|
return await isCommandAvailable(command.argv?.[0] ?? "");
|
||||||
@@ -122,8 +125,10 @@ vi.mock("./providers/codex-app-server-agent.js", () => ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
async isAvailable(): Promise<boolean> {
|
async isAvailable(): Promise<boolean> {
|
||||||
const command = (this.runtimeSettings as { command?: { mode?: string; argv?: string[] } })
|
const command: { mode?: string; argv?: string[] } | undefined =
|
||||||
?.command;
|
typeof this.runtimeSettings === "object" && this.runtimeSettings !== null
|
||||||
|
? Reflect.get(this.runtimeSettings, "command")
|
||||||
|
: undefined;
|
||||||
if (command?.mode === "replace") {
|
if (command?.mode === "replace") {
|
||||||
const { isCommandAvailable } = await import("../../utils/executable.js");
|
const { isCommandAvailable } = await import("../../utils/executable.js");
|
||||||
return await isCommandAvailable(command.argv?.[0] ?? "");
|
return await isCommandAvailable(command.argv?.[0] ?? "");
|
||||||
@@ -170,8 +175,10 @@ vi.mock("./providers/copilot-acp-agent.js", () => ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
async isAvailable(): Promise<boolean> {
|
async isAvailable(): Promise<boolean> {
|
||||||
const command = (this.runtimeSettings as { command?: { mode?: string; argv?: string[] } })
|
const command: { mode?: string; argv?: string[] } | undefined =
|
||||||
?.command;
|
typeof this.runtimeSettings === "object" && this.runtimeSettings !== null
|
||||||
|
? Reflect.get(this.runtimeSettings, "command")
|
||||||
|
: undefined;
|
||||||
if (command?.mode === "replace") {
|
if (command?.mode === "replace") {
|
||||||
const { isCommandAvailable } = await import("../../utils/executable.js");
|
const { isCommandAvailable } = await import("../../utils/executable.js");
|
||||||
return await isCommandAvailable(command.argv?.[0] ?? "");
|
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(
|
const zaiArgs = mockState.constructorArgs.claude.find((entry) => {
|
||||||
(entry) =>
|
const disallowedTools: string[] | undefined =
|
||||||
Array.isArray((entry.runtimeSettings as { disallowedTools?: string[] })?.disallowedTools) &&
|
typeof entry.runtimeSettings === "object" && entry.runtimeSettings !== null
|
||||||
(entry.runtimeSettings as { disallowedTools: string[] }).disallowedTools.includes(
|
? Reflect.get(entry.runtimeSettings, "disallowedTools")
|
||||||
"ComputerUse",
|
: undefined;
|
||||||
),
|
return Array.isArray(disallowedTools) && disallowedTools.includes("ComputerUse");
|
||||||
);
|
});
|
||||||
expect(zaiArgs).toBeDefined();
|
expect(zaiArgs).toBeDefined();
|
||||||
expect((zaiArgs!.runtimeSettings as { disallowedTools: string[] }).disallowedTools).toEqual([
|
const zaiDisallowedTools: string[] =
|
||||||
"WebSearch",
|
typeof zaiArgs!.runtimeSettings === "object" && zaiArgs!.runtimeSettings !== null
|
||||||
"ComputerUse",
|
? Reflect.get(zaiArgs!.runtimeSettings, "disallowedTools")
|
||||||
]);
|
: [];
|
||||||
|
expect(zaiDisallowedTools).toEqual(["WebSearch", "ComputerUse"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("extension inherits base override — override claude command, zai extends claude gets overridden command", () => {
|
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).toHaveLength(2);
|
||||||
expect(
|
expect(
|
||||||
mockState.constructorArgs.claude.every(
|
mockState.constructorArgs.claude.every((entry) => {
|
||||||
(entry) =>
|
const command: { argv?: string[] } | undefined =
|
||||||
(entry.runtimeSettings as { command?: { argv?: string[] } }).command?.argv?.[0] ===
|
typeof entry.runtimeSettings === "object" && entry.runtimeSettings !== null
|
||||||
"/opt/custom-claude",
|
? Reflect.get(entry.runtimeSettings, "command")
|
||||||
),
|
: undefined;
|
||||||
|
return command?.argv?.[0] === "/opt/custom-claude";
|
||||||
|
}),
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user