diff --git a/apps/cli/src/__tests__/startup-graph.test.ts b/apps/cli/src/__tests__/startup-graph.test.ts index c978ba0800..e7f5cdf629 100644 --- a/apps/cli/src/__tests__/startup-graph.test.ts +++ b/apps/cli/src/__tests__/startup-graph.test.ts @@ -1,5 +1,6 @@ import { execFile } from "node:child_process"; import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { createServer } from "node:http"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import { promisify } from "node:util"; @@ -93,7 +94,11 @@ describe("bb startup module graph", () => { await rm(tempDir, { recursive: true, force: true }); }); - async function runCli(entry: CliEntry, args: string[]): Promise { + async function runCli( + entry: CliEntry, + args: string[], + serverUrl?: string, + ): Promise { const logPath = join( tempDir, `${entry}_${args.join("_").replace(/\W/g, "_")}.log`, @@ -106,6 +111,7 @@ describe("bb startup module graph", () => { ); env.BB_CLI_REEXEC = "1"; env.BB_STARTUP_GRAPH_LOG = logPath; + if (serverUrl !== undefined) env.BB_SERVER_URL = serverUrl; const entryArgs = entry === "source" ? [ @@ -251,5 +257,62 @@ describe("bb startup module graph", () => { expect(loaded(run, `${chunkDirUrl}${name}-`), name).toEqual([]); } }, 30_000); + + it("executes plugin commands from the split artifact", async () => { + const server = createServer(async (request, response) => { + response.setHeader("content-type", "application/json"); + if (request.url === "/api/v1/plugins/contributions") { + response.end( + JSON.stringify({ + cliCommands: [ + { pluginId: "fixture-plugin", name: "fixture" }, + ], + }), + ); + return; + } + if (request.url !== "/api/v1/plugins/fixture-plugin/cli") { + response.statusCode = 404; + response.end(); + return; + } + let body = ""; + for await (const chunk of request) body += chunk; + const { argv } = z + .object({ argv: z.array(z.string()) }) + .parse(JSON.parse(body)); + response.end( + JSON.stringify({ + exitCode: 0, + stdout: `fixture ran: ${argv.join(" ")}`, + stderr: "", + }), + ); + }); + await new Promise((resolvePromise) => + server.listen(0, "127.0.0.1", resolvePromise), + ); + const address = server.address(); + if (address === null || typeof address === "string") { + throw new Error("Fixture server did not bind to a TCP port"); + } + const serverUrl = `http://127.0.0.1:${address.port}`; + + try { + for (const args of [ + ["fixture", "--help"], + ["plugin", "run", "fixture-plugin", "--help"], + ]) { + const run = await runCli("dist", args, serverUrl); + expect(run.stdout).toBe("fixture ran: --help\n"); + } + } finally { + await new Promise((resolvePromise, rejectPromise) => + server.close((error) => + error ? rejectPromise(error) : resolvePromise(), + ), + ); + } + }, 30_000); }); }); diff --git a/apps/cli/src/plugin-cli-proxy.ts b/apps/cli/src/plugin-cli-proxy.ts index 9f5852833b..c4b1bb1d2b 100644 --- a/apps/cli/src/plugin-cli-proxy.ts +++ b/apps/cli/src/plugin-cli-proxy.ts @@ -2,7 +2,7 @@ import { resolveContextProjectId, resolveContextThreadId, } from "./context-env.js"; -import type { Dispatcher } from "undici"; +import { Agent, type Dispatcher } from "undici"; import { cliFetch } from "./client.js"; /** @@ -373,15 +373,15 @@ async function writePluginCliOutput( * interaction (`ui.requestInput` allows at most 60 minutes), so the server's * own deadline decides first, but keep it finite: the server has no general * plugin-command deadline, and a plugin that never resolves must not hold the - * CLI process and its socket forever. undici is imported lazily so built-in - * `bb` commands do not pay its startup cost. + * CLI process and its socket forever. This module is imported lazily so + * built-in `bb` commands do not pay undici's startup cost. */ export const PLUGIN_CLI_HEADERS_TIMEOUT_MS = 65 * 60 * 1000; -let pluginCliDispatcher: Promise | undefined; -function getPluginCliDispatcher(): Promise { - pluginCliDispatcher ??= import("undici").then( - ({ Agent }) => new Agent({ headersTimeout: PLUGIN_CLI_HEADERS_TIMEOUT_MS }), - ); +let pluginCliDispatcher: Dispatcher | undefined; +function getPluginCliDispatcher(): Dispatcher { + pluginCliDispatcher ??= new Agent({ + headersTimeout: PLUGIN_CLI_HEADERS_TIMEOUT_MS, + }); return pluginCliDispatcher; } @@ -414,7 +414,7 @@ export async function runPluginCliCommand( ...(threadId ? { threadId } : {}), ...(projectId ? { projectId } : {}), }), - dispatcher: await getPluginCliDispatcher(), + dispatcher: getPluginCliDispatcher(), }, ); const result = (await response.json().catch(() => null)) as {