diff --git a/src/gateway/server-startup-plugins.test.ts b/src/gateway/server-startup-plugins.test.ts index b81c6a90fa9..687253e59b2 100644 --- a/src/gateway/server-startup-plugins.test.ts +++ b/src/gateway/server-startup-plugins.test.ts @@ -19,6 +19,7 @@ const loadGatewayStartupPlugins = vi.hoisted(() => ); const prepareBundledPluginRuntimeLoadRoot = vi.hoisted(() => vi.fn((params: unknown) => params)); const registerBundledRuntimeDependencyJitiAliases = vi.hoisted(() => vi.fn()); +const isSourceCheckoutRoot = vi.hoisted(() => vi.fn((_packageRoot: string) => false)); const pruneUnknownBundledRuntimeDepsRoots = vi.hoisted(() => vi.fn((_params: unknown) => ({ scanned: 0, removed: 0, skippedLocked: 0 })), ); @@ -138,6 +139,7 @@ vi.mock("../plugins/bundled-runtime-deps.js", () => ({ })); vi.mock("../plugins/bundled-runtime-deps-roots.js", () => ({ + isSourceCheckoutRoot: (packageRoot: string) => isSourceCheckoutRoot(packageRoot), pruneUnknownBundledRuntimeDepsRoots: (params: unknown) => pruneUnknownBundledRuntimeDepsRoots(params), })); @@ -197,6 +199,7 @@ describe("prepareGatewayPluginBootstrap runtime-deps staging", () => { loadGatewayStartupPlugins.mockClear(); prepareBundledPluginRuntimeLoadRoot.mockReset().mockImplementation((params: unknown) => params); registerBundledRuntimeDependencyJitiAliases.mockClear(); + isSourceCheckoutRoot.mockClear().mockReturnValue(false); pruneUnknownBundledRuntimeDepsRoots.mockClear().mockReturnValue({ scanned: 0, removed: 0, @@ -325,6 +328,35 @@ describe("prepareGatewayPluginBootstrap runtime-deps staging", () => { ); }); + it("repairs source-checkout startup plugin deps before verify-only load", async () => { + repairBundledRuntimeDepsPackagePlanAsync.mockResolvedValueOnce({ + repairedSpecs: [], + }); + isSourceCheckoutRoot.mockReturnValueOnce(true); + const log = createLog(); + const { prepareGatewayPluginBootstrap } = await import("./server-startup-plugins.js"); + + await prepareGatewayPluginBootstrap({ + cfgAtStart: {}, + startupRuntimeConfig: {}, + minimalTestGateway: false, + log, + }); + + expect(isSourceCheckoutRoot).toHaveBeenCalledWith("/package"); + expect(prepareBundledPluginRuntimeLoadRoot).toHaveBeenCalledWith( + expect.objectContaining({ + pluginId: "telegram", + installMissingDeps: true, + memoizePreparedRoot: true, + logInstalled: expect.any(Function), + }), + ); + expect(loadGatewayStartupPlugins).toHaveBeenCalledWith( + expect.objectContaining({ installBundledRuntimeDeps: false }), + ); + }); + it("can defer runtime-deps staging and startup plugin loading until after HTTP bind", async () => { const log = createLog(); const { prepareGatewayPluginBootstrap } = await import("./server-startup-plugins.js"); diff --git a/src/gateway/server-startup-plugins.ts b/src/gateway/server-startup-plugins.ts index 93ab7da7aca..1250298d501 100644 --- a/src/gateway/server-startup-plugins.ts +++ b/src/gateway/server-startup-plugins.ts @@ -6,7 +6,10 @@ import type { OpenClawConfig } from "../config/types.openclaw.js"; import { measureDiagnosticsTimelineSpan } from "../infra/diagnostics-timeline.js"; import { resolveOpenClawPackageRootSync } from "../infra/openclaw-root.js"; import { registerBundledRuntimeDependencyJitiAliases } from "../plugins/bundled-runtime-deps-jiti-aliases.js"; -import { pruneUnknownBundledRuntimeDepsRoots } from "../plugins/bundled-runtime-deps-roots.js"; +import { + isSourceCheckoutRoot, + pruneUnknownBundledRuntimeDepsRoots, +} from "../plugins/bundled-runtime-deps-roots.js"; import { repairBundledRuntimeDepsPackagePlanAsync } from "../plugins/bundled-runtime-deps.js"; import { prepareBundledPluginRuntimeLoadRoot } from "../plugins/bundled-runtime-root.js"; import type { PluginManifestRegistry } from "../plugins/manifest-registry.js"; @@ -106,6 +109,7 @@ async function prestageGatewayBundledRuntimeDepsImpl(params: { } prestageGatewayBundledRuntimeMirrors({ ...params, + ...(packageRoot ? { packageRoot } : {}), previousRepairError: repairError, }); return repairError === undefined ? {} : { repairError }; @@ -116,9 +120,14 @@ function prestageGatewayBundledRuntimeMirrors(params: { manifestRegistry: PluginManifestRegistry; pluginIds: readonly string[]; log: GatewayPluginBootstrapLog; + packageRoot?: string; previousRepairError?: unknown; }): void { const pluginIdSet = new Set(params.pluginIds); + const allowSourceCheckoutRepair = + params.previousRepairError === undefined && + typeof params.packageRoot === "string" && + isSourceCheckoutRoot(params.packageRoot); const startedAt = Date.now(); const preparedPluginIds: string[] = []; for (const record of params.manifestRegistry.plugins) { @@ -133,10 +142,15 @@ function prestageGatewayBundledRuntimeMirrors(params: { ...(record.setupSource ? { setupModulePath: record.setupSource } : {}), env: process.env, config: params.cfg, - installMissingDeps: false, + installMissingDeps: allowSourceCheckoutRepair, previousRepairError: params.previousRepairError, memoizePreparedRoot: true, registerRuntimeAliasRoot: registerBundledRuntimeDependencyJitiAliases, + logInstalled: (installedSpecs) => { + params.log.info( + `[plugins] ${record.id} installed bundled runtime deps in source checkout pre-stage: ${installedSpecs.join(", ")}`, + ); + }, }); preparedPluginIds.push(record.id); } catch (error) {