diff --git a/src/cli/config-cli.integration.test.ts b/src/cli/config-cli.integration.test.ts index b0516ccd7393..8d2026ad7eef 100644 --- a/src/cli/config-cli.integration.test.ts +++ b/src/cli/config-cli.integration.test.ts @@ -3,11 +3,31 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import JSON5 from "json5"; -import { beforeAll, describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { clearConfigCache, clearRuntimeConfigSnapshot } from "../config/config.js"; import { captureEnv, deleteTestEnvValue, setTestEnvValue } from "../test-utils/env.js"; import { runConfigSet } from "./config-cli.js"; +// Config mutation owns these assertions; plugin discovery suites own registry breadth. +// Keep the two real schemas this suite exercises, but build their metadata only once. +vi.mock("../plugins/plugin-metadata-snapshot.js", async (importOriginal) => { + const actual = await importOriginal(); + let snapshot: ReturnType | undefined; + return { + ...actual, + resolvePluginMetadataSnapshot: ( + params: Parameters[0], + ) => { + snapshot ??= actual.loadPluginMetadataSnapshot({ + ...params, + pluginIds: ["discord", "openclaw-mem0"], + pluginIdScope: undefined, + }); + return snapshot; + }, + }; +}); + function createTestRuntime() { const logs: string[] = []; const errors: string[] = []; @@ -113,30 +133,6 @@ async function withExecDryRunConfigHarness( } describe("config cli integration", () => { - beforeAll(async () => { - const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-config-cli-warmup-")); - const configPath = path.join(tempDir, "openclaw.json"); - const envSnapshot = captureEnv(["OPENCLAW_CONFIG_PATH", "OPENCLAW_TEST_FAST"]); - try { - fs.writeFileSync(configPath, `${JSON.stringify({ gateway: { port: 18789 } }, null, 2)}\n`); - setTestEnvValue("OPENCLAW_TEST_FAST", "1"); - setTestEnvValue("OPENCLAW_CONFIG_PATH", configPath); - clearConfigCache(); - clearRuntimeConfigSnapshot(); - await runConfigSet({ - path: "gateway.port", - value: "18790", - cliOptions: {}, - runtime: createTestRuntime().runtime, - }); - } finally { - envSnapshot.restore(); - clearConfigCache(); - clearRuntimeConfigSnapshot(); - fs.rmSync(tempDir, { recursive: true, force: true }); - } - }); - it("accepts plugin hook conversation-access policy via config set", async () => { const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-config-cli-plugin-hooks-")); const configPath = path.join(tempDir, "openclaw.json"); diff --git a/src/cli/mcp-cli.test.ts b/src/cli/mcp-cli.test.ts index bf3281893d8b..aaf85c6b8e69 100644 --- a/src/cli/mcp-cli.test.ts +++ b/src/cli/mcp-cli.test.ts @@ -9,6 +9,9 @@ import { withTempHome } from "../config/home-env.test-harness.js"; import { createDeferred } from "../shared/deferred.js"; import { registerMcpCli } from "./mcp-cli.js"; +type CreateSessionMcpRuntime = + typeof import("../agents/agent-bundle-mcp-runtime.js").createSessionMcpRuntime; + const mocks = vi.hoisted(() => { const runtime = { log: vi.fn(), @@ -26,6 +29,7 @@ const mocks = vi.hoisted(() => { clearMcpOAuthCredentials: vi.fn(), readMcpOAuthCredentialsStatus: vi.fn(), runMcpOAuthLogin: vi.fn(), + createSessionMcpRuntimeOverride: undefined as CreateSessionMcpRuntime | undefined, }; }); @@ -51,6 +55,15 @@ vi.mock("../agents/mcp-oauth.js", () => ({ runMcpOAuthLogin: mocks.runMcpOAuthLogin, })); +vi.mock("../agents/agent-bundle-mcp-runtime.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + createSessionMcpRuntime: (params: Parameters[0]) => + mocks.createSessionMcpRuntimeOverride?.(params) ?? actual.createSessionMcpRuntime(params), + }; +}); + const tempDirs: string[] = []; async function createWorkspace(): Promise { @@ -145,6 +158,7 @@ describe("mcp cli", () => { beforeEach(() => { vi.clearAllMocks(); + mocks.createSessionMcpRuntimeOverride = undefined; readMcpOAuthCredentialsStatus.mockResolvedValue({ hasTokens: false, requiresAuthorization: false, @@ -336,42 +350,53 @@ describe("mcp cli", () => { }, ); - it( - "bounds initialize with a five-second probe timeout when no flag is supplied", - { timeout: 8_000 }, - async () => { - await withTempHome("openclaw-cli-mcp-home-", async (home) => { - const workspaceDir = await createWorkspace(); - const serverPath = path.join(workspaceDir, "probe-server.mjs"); - const configPath = path.join(home, ".openclaw", "openclaw.json"); - await writeProbeMcpServer(serverPath); - vi.spyOn(process, "cwd").mockReturnValue(workspaceDir); + it("passes a five-second default initialize timeout to the probe runtime", async () => { + await withTempHome("openclaw-cli-mcp-home-", async (home) => { + const workspaceDir = await createWorkspace(); + const configPath = path.join(home, ".openclaw", "openclaw.json"); + vi.spyOn(process, "cwd").mockReturnValue(workspaceDir); + let probeTimeoutMs: unknown; + mocks.createSessionMcpRuntimeOverride = (params) => { + probeTimeoutMs = params.cfg?.mcp?.servers?.["hung-default"]?.connectionTimeoutMs; + return { + sessionId: params.sessionId, + workspaceDir: params.workspaceDir, + configFingerprint: "cli-probe-test", + createdAt: 0, + lastUsedAt: 0, + getCatalog: async () => ({ + version: 1, + generatedAt: Date.now(), + servers: {}, + tools: [], + diagnostics: [ + { + serverName: "hung-default", + safeServerName: "hung-default", + launchSummary: process.execPath, + message: + 'MCP server "hung-default" timed out: did not complete initialize within 5s', + }, + ], + }), + peekCatalog: () => null, + markUsed: () => {}, + callTool: async () => ({ content: [] }), + dispose: async () => {}, + }; + }; - const startedAt = performance.now(); - await expect( - runMcpCommand([ - "mcp", - "add", - "hung-default", - "--command", - process.execPath, - "--arg", - serverPath, - "--env", - "MCP_MODE=hang-start", - ]), - ).rejects.toThrow("__exit__:1"); - const elapsedMs = performance.now() - startedAt; + await expect( + runMcpCommand(["mcp", "add", "hung-default", "--command", process.execPath]), + ).rejects.toThrow("__exit__:1"); - expect(elapsedMs).toBeGreaterThanOrEqual(4_500); - expect(elapsedMs).toBeLessThan(6_500); - expect(lastErrorLine()).toContain( - 'MCP server "hung-default" timed out: did not complete initialize within 5s', - ); - await expect(fs.readFile(configPath, "utf8")).rejects.toMatchObject({ code: "ENOENT" }); - }); - }, - ); + expect(probeTimeoutMs).toBe(5_000); + expect(lastErrorLine()).toContain( + 'MCP server "hung-default" timed out: did not complete initialize within 5s', + ); + await expect(fs.readFile(configPath, "utf8")).rejects.toMatchObject({ code: "ENOENT" }); + }); + }); it("labels listed MCP servers as OpenClaw-managed", async () => { await withTempHome("openclaw-cli-mcp-home-", async () => {