diff --git a/scripts/openclaw-npm-postpublish-verify.ts b/scripts/openclaw-npm-postpublish-verify.ts index 7fd04e41297..fce132b7167 100644 --- a/scripts/openclaw-npm-postpublish-verify.ts +++ b/scripts/openclaw-npm-postpublish-verify.ts @@ -41,6 +41,8 @@ type InstalledBundledExtensionManifestRecord = { }; const MAX_BUNDLED_EXTENSION_MANIFEST_BYTES = 1024 * 1024; +const LEGACY_CONTEXT_ENGINE_UNRESOLVED_RUNTIME_MARKER = + "Failed to load legacy context engine runtime."; const NPM_UPDATE_COMPAT_EXTENSION_DIRS = new Set( [...NPM_UPDATE_COMPAT_SIDECAR_PATHS].map((relativePath) => { const pathParts = relativePath.split("/"); @@ -101,6 +103,7 @@ export function collectInstalledPackageErrors(params: { } } + errors.push(...collectInstalledContextEngineRuntimeErrors(params.packageRoot)); errors.push(...collectInstalledMirroredRootDependencyManifestErrors(params.packageRoot)); return errors; @@ -112,6 +115,48 @@ export function normalizeInstalledBinaryVersion(output: string): string { return versionMatch?.[0] ?? trimmed; } +function listDistJavaScriptFiles(packageRoot: string): string[] { + const distDir = join(packageRoot, "dist"); + if (!existsSync(distDir)) { + return []; + } + + const pending = [distDir]; + const files: string[] = []; + while (pending.length > 0) { + const currentDir = pending.pop(); + if (!currentDir) { + continue; + } + for (const entry of readdirSync(currentDir, { withFileTypes: true })) { + const entryPath = join(currentDir, entry.name); + if (entry.isDirectory()) { + pending.push(entryPath); + continue; + } + if (entry.isFile() && entry.name.endsWith(".js")) { + files.push(entryPath); + } + } + } + + return files; +} + +export function collectInstalledContextEngineRuntimeErrors(packageRoot: string): string[] { + const errors: string[] = []; + for (const filePath of listDistJavaScriptFiles(packageRoot)) { + const contents = readFileSync(filePath, "utf8"); + if (contents.includes(LEGACY_CONTEXT_ENGINE_UNRESOLVED_RUNTIME_MARKER)) { + errors.push( + "installed package includes unresolved legacy context engine runtime loader; rebuild with a bundler-traceable LegacyContextEngine import.", + ); + break; + } + } + return errors; +} + export function resolveInstalledBinaryPath(prefixDir: string, platform = process.platform): string { return platform === "win32" ? join(prefixDir, "openclaw.cmd") diff --git a/src/context-engine/legacy.registration.ts b/src/context-engine/legacy.registration.ts index 3bd131114be..769681d33e5 100644 --- a/src/context-engine/legacy.registration.ts +++ b/src/context-engine/legacy.registration.ts @@ -1,32 +1,8 @@ +import { LegacyContextEngine } from "./legacy.js"; import { registerContextEngineForOwner } from "./registry.js"; -import type { ContextEngine } from "./types.js"; - -type LegacyContextEngineModule = { - LegacyContextEngine: new () => ContextEngine; -}; - -async function loadLegacyContextEngineModule(): Promise { - try { - return (await import("./legacy.js")) as LegacyContextEngineModule; - } catch { - try { - return (await import("./legacy.ts")) as LegacyContextEngineModule; - } catch { - throw new Error("Failed to load legacy context engine runtime."); - } - } -} export function registerLegacyContextEngine(): void { - registerContextEngineForOwner( - "legacy", - async () => { - const { LegacyContextEngine } = await loadLegacyContextEngineModule(); - return new LegacyContextEngine(); - }, - "core", - { - allowSameOwnerRefresh: true, - }, - ); + registerContextEngineForOwner("legacy", async () => new LegacyContextEngine(), "core", { + allowSameOwnerRefresh: true, + }); } diff --git a/test/openclaw-npm-postpublish-verify.test.ts b/test/openclaw-npm-postpublish-verify.test.ts index 61faa26eeb4..176448210bb 100644 --- a/test/openclaw-npm-postpublish-verify.test.ts +++ b/test/openclaw-npm-postpublish-verify.test.ts @@ -5,6 +5,7 @@ import { describe, expect, it } from "vitest"; import { buildPublishedInstallCommandArgs, buildPublishedInstallScenarios, + collectInstalledContextEngineRuntimeErrors, collectInstalledMirroredRootDependencyManifestErrors, collectInstalledPackageErrors, normalizeInstalledBinaryVersion, @@ -79,6 +80,48 @@ describe("collectInstalledPackageErrors", () => { }); }); +describe("collectInstalledContextEngineRuntimeErrors", () => { + function makeInstalledPackageRoot(): string { + return mkdtempSync(join(tmpdir(), "openclaw-postpublish-context-engine-")); + } + + it("rejects packaged bundles with unresolved legacy context engine runtime loaders", () => { + const packageRoot = makeInstalledPackageRoot(); + + try { + mkdirSync(join(packageRoot, "dist"), { recursive: true }); + writeFileSync( + join(packageRoot, "dist", "runtime-plugins-BUG.js"), + 'throw new Error("Failed to load legacy context engine runtime.");\n', + "utf8", + ); + + expect(collectInstalledContextEngineRuntimeErrors(packageRoot)).toEqual([ + "installed package includes unresolved legacy context engine runtime loader; rebuild with a bundler-traceable LegacyContextEngine import.", + ]); + } finally { + rmSync(packageRoot, { recursive: true, force: true }); + } + }); + + it("accepts packaged bundles that inline the legacy context engine registration", () => { + const packageRoot = makeInstalledPackageRoot(); + + try { + mkdirSync(join(packageRoot, "dist"), { recursive: true }); + writeFileSync( + join(packageRoot, "dist", "runtime-plugins-OK.js"), + "registerContextEngineForOwner('legacy', async () => new LegacyContextEngine());\n", + "utf8", + ); + + expect(collectInstalledContextEngineRuntimeErrors(packageRoot)).toEqual([]); + } finally { + rmSync(packageRoot, { recursive: true, force: true }); + } + }); +}); + describe("normalizeInstalledBinaryVersion", () => { it("accepts decorated CLI version output", () => { expect(normalizeInstalledBinaryVersion("OpenClaw 2026.4.8 (9ece252)")).toBe("2026.4.8");