fix(gateway): include legacy runtime output checks

This commit is contained in:
Ruben Cuevas
2026-05-05 19:57:32 -04:00
committed by Peter Steinberger
parent 31f74259cb
commit d605efc17f
2 changed files with 145 additions and 67 deletions

View File

@@ -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;

View File

@@ -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, {