test(server): verify cross-provider mode discovery through the daemon

Exercise the MCP contract, refusal without a child, provider inspection, and explicit-mode creation through the public daemon interfaces. Qualify unattended advice because targets without an unattended mode still require an explicit choice.

Co-authored-by: Noah <noahguerin20@gmail.com>
This commit is contained in:
paseo-bot[bot]
2026-11-01 16:54:15 +00:00
co-authored by Noah
parent 711bf91904
commit e6987ef9bc
3 changed files with 69 additions and 167 deletions
@@ -39,7 +39,7 @@ import type {
AgentStreamEvent,
ProviderSnapshotEntry,
} from "./agent-sdk-types.js";
import { ProviderSnapshotManager } from "./provider-snapshot-manager.js";
import type { ProviderSnapshotManager } from "./provider-snapshot-manager.js";
import { createProviderSnapshotManagerStub } from "../test-utils/session-stubs.js";
import {
AgentListItemPayloadSchema,
@@ -1526,64 +1526,6 @@ describe("terminal MCP tools", () => {
});
});
const CLAUDE_CATALOG_MODES: AgentMode[] = [
{ id: "default", label: "Default" },
{ id: "bypassPermissions", label: "Bypass", isUnattended: true },
];
const CODEX_CATALOG_MODES: AgentMode[] = [
{ id: "auto", label: "Auto" },
{ id: "full-access", label: "Full Access", isUnattended: true },
];
class ModeCatalogAgentClient implements AgentClient {
readonly capabilities = {
supportsStreaming: false,
supportsSessionPersistence: false,
supportsSessionListing: false,
supportsDynamicModes: false,
supportsMcpServers: false,
supportsReasoningStream: false,
supportsToolInvocations: false,
} as const;
constructor(
readonly provider: AgentProvider,
private readonly modes: AgentMode[],
) {}
async isAvailable(): Promise<boolean> {
return true;
}
async fetchCatalog() {
return { models: [], modes: this.modes };
}
async createSession(): Promise<AgentSession> {
throw new Error("ModeCatalogAgentClient only serves catalogs");
}
async resumeSession(): Promise<AgentSession> {
throw new Error("ModeCatalogAgentClient only serves catalogs");
}
}
// A real snapshot manager, so create_agent runs the provider's shared mode resolver.
function createModeCatalogManager(): ProviderSnapshotManager {
return new ProviderSnapshotManager({
logger: createTestLogger(),
providerOverrides: {
copilot: { enabled: false },
opencode: { enabled: false },
pi: { enabled: false },
},
extraClients: {
claude: new ModeCatalogAgentClient("claude", CLAUDE_CATALOG_MODES),
codex: new ModeCatalogAgentClient("codex", CODEX_CATALOG_MODES),
},
});
}
describe("create_agent MCP tool", () => {
const logger = createTestLogger();
const existingCwd = process.cwd();
@@ -3490,113 +3432,6 @@ describe("create_agent MCP tool", () => {
expect.any(Object),
);
});
it("advertises the cross-provider mode rule in the create_agent description", async () => {
const { agentManager, agentStorage } = createTestDeps();
const server = await createAgentMcpServer({
agentManager,
agentStorage,
providerSnapshotManager: createOpenCodeManager().manager,
callerAgentId: "parent-agent",
logger,
});
const client = await connectInMemoryMcpClient(server);
try {
const { tools } = await client.listTools();
const createAgent = tools.find((tool) => tool.name === "create_agent")!;
expect(createAgent.description).toContain(
"A different provider does not inherit your mode: set settings.modeId from list_providers, or omit it if you run unattended so the child does too.",
);
expect(tools.map((tool) => tool.name)).toContain("list_providers");
} finally {
await client.close();
await server.close();
}
});
function claudeCaller(currentModeId: string): ManagedAgent {
return {
id: "parent-agent",
config: {},
cwd: existingCwd,
workspaceId: "wks_parent",
provider: "claude",
currentModeId,
availableModes: CLAUDE_CATALOG_MODES,
} as ManagedAgent;
}
it("refuses to carry an attended caller's mode to a different provider", async () => {
const { agentManager, agentStorage, spies } = createTestDeps();
spies.agentManager.getAgent.mockReturnValue(claudeCaller("default"));
const providerSnapshotManager = createModeCatalogManager();
try {
const server = await createAgentMcpServer({
agentManager,
agentStorage,
providerSnapshotManager,
callerAgentId: "parent-agent",
logger,
});
const tool = registeredTool(server, "create_agent");
await expect(
tool.handler({
...subagentCurrentWorkspace(),
title: "Child",
provider: "codex/gpt-5.4",
initialPrompt: "Do work",
}),
).rejects.toThrow(
"cannot inherit mode 'default' from caller (provider 'claude') for new agent (provider 'codex'). Pass an explicit mode in settings.modeId. Available modes for 'codex': auto, full-access",
);
expect(spies.agentManager.createAgent).not.toHaveBeenCalled();
} finally {
providerSnapshotManager.destroy();
}
});
it("gives an unattended caller's child the target provider's unattended mode", async () => {
const { agentManager, agentStorage, spies } = createTestDeps();
spies.agentManager.getAgent.mockReturnValue(claudeCaller("bypassPermissions"));
spies.agentManager.createAgent.mockResolvedValue({
id: "child-agent",
cwd: existingCwd,
lifecycle: "idle",
currentModeId: "full-access",
availableModes: CODEX_CATALOG_MODES,
config: { title: "Child" },
} as ManagedAgent);
const providerSnapshotManager = createModeCatalogManager();
try {
const server = await createAgentMcpServer({
agentManager,
agentStorage,
providerSnapshotManager,
callerAgentId: "parent-agent",
logger,
});
const tool = registeredTool(server, "create_agent");
await tool.handler({
...subagentCurrentWorkspace(),
title: "Child",
provider: "codex/gpt-5.4",
initialPrompt: "Do work",
});
expect(spies.agentManager.createAgent).toHaveBeenCalledWith(
expect.objectContaining({ modeId: "full-access" }),
undefined,
expect.any(Object),
);
} finally {
providerSnapshotManager.destroy();
}
});
});
const HELD_TURN_CAPABILITIES = {
@@ -1590,7 +1590,7 @@ function createToolCatalog(
{
title: "Create agent",
description:
"Create an agent. Agent-scoped creation defaults to your workspace and creates your subagent. Top-level creation without workspaceId creates a new local workspace. Requires provider/model (for example codex/gpt-5.4) and an initial prompt. A different provider does not inherit your mode: set settings.modeId from list_providers, or omit it if you run unattended so the child does too. Do not guess; call list_providers and list_models first if uncertain.",
"Create an agent. Agent-scoped creation defaults to your workspace and creates your subagent. Top-level creation without workspaceId creates a new local workspace. Requires provider/model (for example codex/gpt-5.4) and an initial prompt. For a different provider, set settings.modeId from inspect_provider; omission is accepted for targets with no modes and for unattended execution supported by the target. Do not guess; call list_providers and list_models first if uncertain.",
inputSchema: createAgentInputSchema,
outputSchema: {
agentId: z.string(),
@@ -0,0 +1,67 @@
import { mkdir, mkdtemp, rm } from "node:fs/promises";
import path from "node:path";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { expect, test } from "vitest";
import { createDaemonTestContext } from "../test-utils/index.js";
test("an attended MCP caller can discover and supply a cross-provider child mode", async () => {
const artifacts = path.join(process.cwd(), ".artifacts");
await mkdir(artifacts, { recursive: true });
const root = await mkdtemp(path.join(artifacts, "create-agent-mode-"));
const ctx = await createDaemonTestContext({ paseoHomeRoot: root, mcpEnabled: true });
const mcp = new Client({ name: "mode-contract-test", version: "1" });
try {
const parent = await ctx.client.createAgent({
provider: "claude",
cwd: root,
modeId: "default",
title: "Attended parent",
});
await mcp.connect(
new StreamableHTTPClientTransport(
new URL(`http://127.0.0.1:${ctx.daemon.port}/mcp/agents?callerAgentId=${parent.id}`),
),
);
const { tools } = await mcp.listTools();
const create = tools.find((tool) => tool.name === "create_agent");
expect.soft(create?.description).toContain("settings.modeId");
const args = {
provider: "codex/gpt-5.4-mini",
title: "Cross-provider child",
initialPrompt: "Reply with OK",
};
const refused = await mcp.callTool({ name: "create_agent", arguments: args });
expect(refused.isError).toBe(true);
expect.soft(refused.content).toEqual([
expect.objectContaining({
type: "text",
text: expect.stringContaining("Pass an explicit mode in settings.modeId"),
}),
]);
expect((await ctx.client.fetchAgents({ subscribe: {} })).entries).toHaveLength(1);
const inspected = await mcp.callTool({
name: "inspect_provider",
arguments: { provider: "codex" },
});
expect(inspected.isError).not.toBe(true);
expect(inspected.structuredContent).toMatchObject({
modes: expect.arrayContaining([expect.objectContaining({ id: "auto" })]),
});
const created = await mcp.callTool({
name: "create_agent",
arguments: { ...args, settings: { modeId: "auto" } },
});
expect(created.isError).not.toBe(true);
expect(created.structuredContent).toMatchObject({ type: "codex", currentModeId: "auto" });
const childId = (created.structuredContent as { agentId: string }).agentId;
expect((await ctx.client.fetchAgent(childId))?.agent.currentModeId).toBe("auto");
expect((await ctx.client.waitForFinish(childId, 5000)).status).toBe("idle");
} finally {
await mcp.close();
await ctx.cleanup();
await rm(root, { recursive: true, force: true });
}
}, 30_000);