From d605efc17f72a3bfa1a50dfac2e8416aeee65f1e Mon Sep 17 00:00:00 2001 From: Ruben Cuevas Date: Tue, 5 May 2026 19:57:32 -0400 Subject: [PATCH] fix(gateway): include legacy runtime output checks --- scripts/runtime-postbuild.mjs | 171 +++++++++++++++++++++------------- src/infra/run-node.test.ts | 41 ++++++++ 2 files changed, 145 insertions(+), 67 deletions(-) diff --git a/scripts/runtime-postbuild.mjs b/scripts/runtime-postbuild.mjs index c605708b692..035c77c95c6 100644 --- a/scripts/runtime-postbuild.mjs +++ b/scripts/runtime-postbuild.mjs @@ -138,49 +138,14 @@ export function listOfficialChannelCatalogOutputs() { return [OFFICIAL_CHANNEL_CATALOG_OUTPUT]; } -export function listStableRootRuntimeAliasOutputs(params = {}) { - const rootDir = params.rootDir ?? ROOT; - const distDir = path.join(rootDir, "dist"); - const fsImpl = params.fs ?? fs; +function collectStableRootRuntimeAliasCandidates(params) { + const distDir = params.distDir; + const fsImpl = params.fs; let entries = []; try { entries = fsImpl.readdirSync(distDir, { withFileTypes: true }); } catch { - return []; - } - - return entries - .filter((entry) => entry.isFile()) - .map((entry) => entry.name.match(ROOT_RUNTIME_ALIAS_PATTERN)?.groups?.base) - .filter((base) => typeof base === "string" && base.length > 0) - .map((base) => `dist/${base}.js`) - .toSorted((left, right) => left.localeCompare(right)); -} - -export function listLegacyCliExitCompatOutputs(params = {}) { - const chunks = params.chunks ?? LEGACY_CLI_EXIT_COMPAT_CHUNKS; - return chunks - .map(({ dest }) => dest.replace(/\\/g, "/")) - .toSorted((left, right) => left.localeCompare(right)); -} - -export function listCoreRuntimePostBuildOutputs(params = {}) { - return [ - ...listPluginSdkRootAliasOutputs(), - ...listOfficialChannelCatalogOutputs(), - ...listStableRootRuntimeAliasOutputs(params), - ...listLegacyCliExitCompatOutputs(params), - ].toSorted((left, right) => left.localeCompare(right)); -} -export function writeStableRootRuntimeAliases(params = {}) { - const rootDir = params.rootDir ?? ROOT; - const distDir = path.join(rootDir, "dist"); - const fsImpl = params.fs ?? fs; - let entries = []; - try { - entries = fsImpl.readdirSync(distDir, { withFileTypes: true }); - } catch { - return; + return new Map(); } const candidatesByAlias = new Map(); @@ -197,42 +162,114 @@ export function writeStableRootRuntimeAliases(params = {}) { candidates.push(entry.name); candidatesByAlias.set(aliasFileName, candidates); } + return candidatesByAlias; +} - const resolveAliasCandidate = (aliasFileName, candidates) => { - if (candidates.length === 1) { - return candidates[0]; +function resolveStableRootRuntimeAliasCandidate(params) { + const { aliasFileName, candidates, distDir, fsImpl } = params; + if (candidates.length === 1) { + return candidates[0]; + } + if (aliasFileName === PLUGIN_INSTALL_RUNTIME_ALIAS.aliasFileName) { + return resolveRootRuntimeCandidateByMarkers({ + distDir, + fsImpl, + aliasFileName, + sourceIncludes: PLUGIN_INSTALL_RUNTIME_ALIAS.sourceIncludes, + }); + } + const candidateSet = new Set(candidates); + const wrappers = candidates.filter((candidate) => { + const filePath = path.join(distDir, candidate); + let source; + try { + source = fsImpl.readFileSync(filePath, "utf8"); + } catch { + return false; } - if (aliasFileName === PLUGIN_INSTALL_RUNTIME_ALIAS.aliasFileName) { - return resolveRootRuntimeCandidateByMarkers({ + return candidates.some( + (target) => + target !== candidate && + candidateSet.has(target) && + source.includes(`"./${target}"`) && + !source.includes("\n//#region "), + ); + }); + return wrappers.length === 1 ? wrappers[0] : null; +} + +export function listStableRootRuntimeAliasOutputs(params = {}) { + const rootDir = params.rootDir ?? ROOT; + const distDir = path.join(rootDir, "dist"); + const fsImpl = params.fs ?? fs; + return [...collectStableRootRuntimeAliasCandidates({ distDir, fs: fsImpl })] + .filter(([aliasFileName, candidates]) => + resolveStableRootRuntimeAliasCandidate({ distDir, fsImpl, aliasFileName, - sourceIncludes: PLUGIN_INSTALL_RUNTIME_ALIAS.sourceIncludes, - }); - } - const candidateSet = new Set(candidates); - const wrappers = candidates.filter((candidate) => { - const filePath = path.join(distDir, candidate); - let source; - try { - source = fsImpl.readFileSync(filePath, "utf8"); - } catch { - return false; - } - return candidates.some( - (target) => - target !== candidate && - candidateSet.has(target) && - source.includes(`"./${target}"`) && - !source.includes("\n//#region "), - ); - }); - return wrappers.length === 1 ? wrappers[0] : null; - }; + candidates, + }), + ) + .map(([aliasFileName]) => `dist/${aliasFileName}`) + .toSorted((left, right) => left.localeCompare(right)); +} + +export function listLegacyCliExitCompatOutputs(params = {}) { + const chunks = params.chunks ?? LEGACY_CLI_EXIT_COMPAT_CHUNKS; + return chunks + .map(({ dest }) => dest.replace(/\\/g, "/")) + .toSorted((left, right) => left.localeCompare(right)); +} + +export function listLegacyRootRuntimeCompatOutputs(params = {}) { + const rootDir = params.rootDir ?? ROOT; + const distDir = path.join(rootDir, "dist"); + const fsImpl = params.fs ?? fs; + return [ + ...LEGACY_ROOT_RUNTIME_COMPAT_ALIASES.map(([legacyFileName, aliasFileName]) => ({ + legacyFileName, + aliasFileName, + })), + ...LEGACY_PLUGIN_INSTALL_RUNTIME_COMPAT_ALIASES, + ] + .filter((entry) => + resolveLegacyRootRuntimeCompatTarget({ + distDir, + fsImpl, + legacyFileName: entry.legacyFileName, + aliasFileName: entry.aliasFileName, + sourceIncludes: entry.sourceIncludes, + }), + ) + .map(({ legacyFileName }) => `dist/${legacyFileName}`) + .toSorted((left, right) => left.localeCompare(right)); +} + +export function listCoreRuntimePostBuildOutputs(params = {}) { + return [ + ...listPluginSdkRootAliasOutputs(), + ...listOfficialChannelCatalogOutputs(), + ...listStableRootRuntimeAliasOutputs(params), + ...listLegacyRootRuntimeCompatOutputs(params), + ...listLegacyCliExitCompatOutputs(params), + ].toSorted((left, right) => left.localeCompare(right)); +} + +export function writeStableRootRuntimeAliases(params = {}) { + const rootDir = params.rootDir ?? ROOT; + const distDir = path.join(rootDir, "dist"); + const fsImpl = params.fs ?? fs; + const candidatesByAlias = collectStableRootRuntimeAliasCandidates({ distDir, fs: fsImpl }); for (const [aliasFileName, candidates] of candidatesByAlias) { const aliasPath = path.join(distDir, aliasFileName); - const candidate = resolveAliasCandidate(aliasFileName, candidates); + const candidate = resolveStableRootRuntimeAliasCandidate({ + distDir, + fsImpl, + aliasFileName, + candidates, + }); if (!candidate) { fsImpl.rmSync?.(aliasPath, { force: true }); continue; diff --git a/src/infra/run-node.test.ts b/src/infra/run-node.test.ts index 1c46a38b8de..00f55ecda43 100644 --- a/src/infra/run-node.test.ts +++ b/src/infra/run-node.test.ts @@ -35,7 +35,10 @@ const DIST_CHANNEL_CATALOG = "dist/channel-catalog.json"; const DIST_LEGACY_CLI_EXIT_COMPAT = "dist/memory-state-CcqRgDZU.js"; const DIST_LEGACY_CLI_EXIT_COMPAT_ALT = "dist/memory-state-DwGdReW4.js"; const DIST_STABLE_ROOT_RUNTIME_SOURCE = "dist/model-catalog.runtime-AbCd1234.js"; +const DIST_STABLE_ROOT_RUNTIME_SOURCE_ALT = "dist/model-catalog.runtime-EfGh5678.js"; const DIST_STABLE_ROOT_RUNTIME_ALIAS = "dist/model-catalog.runtime.js"; +const DIST_LEGACY_ROOT_RUNTIME_TARGET = "dist/abort.runtime.js"; +const DIST_LEGACY_ROOT_RUNTIME_COMPAT = "dist/abort.runtime-DX6vo4yJ.js"; const QA_LAB_PLUGIN_SDK_ENTRY = "dist/plugin-sdk/qa-lab.js"; const QA_RUNTIME_PLUGIN_SDK_ENTRY = "dist/plugin-sdk/qa-runtime.js"; const EXTENSION_INDEX = bundledPluginFile("demo", "index.ts"); @@ -1911,6 +1914,7 @@ describe("run-node script", () => { DIST_CHANNEL_CATALOG, DIST_LEGACY_CLI_EXIT_COMPAT, DIST_STABLE_ROOT_RUNTIME_ALIAS, + DIST_LEGACY_ROOT_RUNTIME_COMPAT, ]) { await withTempDir({ prefix: "openclaw-run-node-" }, async (tmp) => { await setupTrackedProject(tmp, { @@ -1919,6 +1923,8 @@ describe("run-node script", () => { [DIST_STABLE_ROOT_RUNTIME_SOURCE]: "export const value = 1;\n", [DIST_STABLE_ROOT_RUNTIME_ALIAS]: "export * from './model-catalog.runtime-AbCd1234.js';\n", + [DIST_LEGACY_ROOT_RUNTIME_TARGET]: "export const aborted = true;\n", + [DIST_LEGACY_ROOT_RUNTIME_COMPAT]: "export * from './abort.runtime.js';\n", [RUNTIME_POSTBUILD_STAMP]: '{"head":"abc123"}\n', }, buildPaths: [ @@ -1926,6 +1932,8 @@ describe("run-node script", () => { DIST_ENTRY, DIST_STABLE_ROOT_RUNTIME_SOURCE, DIST_STABLE_ROOT_RUNTIME_ALIAS, + DIST_LEGACY_ROOT_RUNTIME_TARGET, + DIST_LEGACY_ROOT_RUNTIME_COMPAT, BUILD_STAMP, RUNTIME_POSTBUILD_STAMP, ], @@ -1947,6 +1955,39 @@ describe("run-node script", () => { } }); + it("does not require ambiguous stable runtime aliases that postbuild cannot create", async () => { + await withTempDir({ prefix: "openclaw-run-node-" }, async (tmp) => { + await setupTrackedProject(tmp, { + files: { + [ROOT_SRC]: "export const value = 1;\n", + [DIST_STABLE_ROOT_RUNTIME_SOURCE]: "export const value = 1;\n", + [DIST_STABLE_ROOT_RUNTIME_SOURCE_ALT]: "export const value = 2;\n", + [RUNTIME_POSTBUILD_STAMP]: '{"head":"abc123"}\n', + }, + buildPaths: [ + ROOT_SRC, + DIST_ENTRY, + DIST_STABLE_ROOT_RUNTIME_SOURCE, + DIST_STABLE_ROOT_RUNTIME_SOURCE_ALT, + BUILD_STAMP, + RUNTIME_POSTBUILD_STAMP, + ], + }); + + const requirement = resolveRuntimePostBuildRequirement( + createBuildRequirementDeps(tmp, { + gitHead: "abc123\n", + gitStatus: "", + }), + ); + + expect(requirement).toEqual({ + shouldSync: false, + reason: "clean", + }); + }); + }); + it("reports missing runtime skill outputs even when stamps match HEAD", async () => { await withTempDir({ prefix: "openclaw-run-node-" }, async (tmp) => { await setupTrackedProject(tmp, {