fix: tighten release tooling checks

This commit is contained in:
Peter Steinberger
2026-05-15 21:20:58 +01:00
parent 0b7ff665f6
commit 333f65fc8a
11 changed files with 232 additions and 27 deletions

View File

@@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Release tooling: align the published launcher Node floor, `npm start`, package script checks, sharded lint locking, Vitest root project coverage, and plugin-SDK declaration build cache metadata so release/package validation does not silently skip or ship stale surfaces.
- Codex app-server/MCP: scope user MCP servers to specific OpenClaw agent ids through an optional `mcp.servers.<name>.codex.agents` list and accept `codex.defaultToolsApprovalMode` (`auto`/`prompt`/`approve`) for native Codex approval defaults; OpenClaw strips the `codex` block before handing `mcp_servers` config to Codex. (#82180) Thanks @sercada.
- Agents/OpenAI Responses: clamp `input_tokens - cached_tokens` at zero and reconstruct `totalTokens` from input + output + cached components so Responses-API streams report consistent usage when providers under-report `input_tokens` relative to `cached_tokens`.
- Plugins: reject malformed `package.json` `openclaw.extensions` metadata during install, discovery, and post-update payload smoke instead of silently dropping invalid entries.

View File

@@ -9,7 +9,7 @@ import path from "node:path";
import { fileURLToPath } from "node:url";
const MIN_NODE_MAJOR = 22;
const MIN_NODE_MINOR = 12;
const MIN_NODE_MINOR = 16;
const MIN_NODE_VERSION = `${MIN_NODE_MAJOR}.${MIN_NODE_MINOR}`;
const parseNodeVersion = (rawVersion) => {

View File

@@ -1360,9 +1360,6 @@
"build:plugin-sdk:dts": "node scripts/run-tsgo.mjs -p tsconfig.plugin-sdk.dts.json --declaration true",
"build:plugin-sdk:strict-smoke": "pnpm build:plugin-sdk:dts && node --experimental-strip-types scripts/write-plugin-sdk-entry-dts.ts",
"build:strict-smoke": "pnpm plugins:assets:build && node scripts/tsdown-build.mjs && node scripts/check-cli-bootstrap-imports.mjs && node scripts/runtime-postbuild.mjs && node scripts/build-stamp.mjs && node scripts/runtime-postbuild-stamp.mjs && pnpm build:plugin-sdk:dts && node --experimental-strip-types scripts/write-plugin-sdk-entry-dts.ts && node scripts/check-plugin-sdk-exports.mjs",
"canon:check": "node scripts/canon.mjs check",
"canon:check:json": "node scripts/canon.mjs check --json",
"canon:enforce": "node scripts/canon.mjs enforce --json",
"canvas:a2ui:bundle": "node scripts/bundle-a2ui.mjs",
"changed:lanes": "node scripts/changed-lanes.mjs",
"check": "node scripts/check.mjs",
@@ -1572,7 +1569,7 @@
"rtt": "node --import tsx scripts/rtt.ts",
"runtime-sidecars:check": "node --import tsx scripts/generate-runtime-sidecar-paths-baseline.ts --check",
"runtime-sidecars:gen": "node --import tsx scripts/generate-runtime-sidecar-paths-baseline.ts --write",
"start": "node scripts/run-node.mjs",
"start": "node openclaw.mjs",
"test": "node scripts/test-projects.mjs",
"test:all": "pnpm lint && pnpm build && pnpm test && pnpm test:e2e && pnpm test:live && pnpm test:docker:all",
"test:auth:compat": "node scripts/run-vitest.mjs run --config test/vitest/vitest.gateway.config.ts src/gateway/server.auth.compat-baseline.test.ts src/gateway/client.test.ts src/gateway/reconnect-gating.test.ts src/gateway/protocol/connect-error-details.test.ts",

View File

@@ -35,11 +35,12 @@ export const BUILD_ALL_STEPS = [
"tsconfig.json",
"tsconfig.plugin-sdk.dts.json",
"src/plugin-sdk",
"packages/memory-host-sdk/src",
"src/types",
"src/video-generation/dashscope-compatible.ts",
"src/video-generation/types.ts",
],
outputs: ["dist/plugin-sdk/.tsbuildinfo", "dist/plugin-sdk/src"],
outputs: ["dist/plugin-sdk/.tsbuildinfo", "dist/plugin-sdk/packages", "dist/plugin-sdk/src"],
},
},
{

View File

@@ -1,24 +1,33 @@
import { spawn, spawnSync } from "node:child_process";
import path from "node:path";
import {
acquireLocalHeavyCheckLockSync,
resolveLocalHeavyCheckEnv,
shouldAcquireLocalHeavyCheckLockForOxlint,
} from "./lib/local-heavy-check-runtime.mjs";
const extraArgs = process.argv.slice(2);
const runner = path.resolve("scripts", "run-oxlint.mjs");
const prepareResult = spawnSync(
process.execPath,
[path.resolve("scripts", "prepare-extension-package-boundary-artifacts.mjs")],
{
stdio: "inherit",
env: process.env,
},
const env = resolveLocalHeavyCheckEnv(process.env);
const hasMetadataOnlyFlag = extraArgs.some((arg) =>
["--help", "-h", "--version", "-V", "--rules", "--print-config", "--init"].includes(arg),
);
if (prepareResult.error) {
throw prepareResult.error;
}
if ((prepareResult.status ?? 1) !== 0) {
process.exit(prepareResult.status ?? 1);
}
const shouldAcquireParentLock =
!hasMetadataOnlyFlag ||
shouldAcquireLocalHeavyCheckLockForOxlint(extraArgs, {
cwd: process.cwd(),
env,
});
const releaseLock =
env.OPENCLAW_OXLINT_SKIP_LOCK === "1"
? () => {}
: shouldAcquireParentLock
? acquireLocalHeavyCheckLockSync({
cwd: process.cwd(),
env,
toolName: "oxlint shards",
})
: () => {};
const shards = [
{
@@ -35,11 +44,31 @@ const shards = [
},
];
const runSerial = process.env.OPENCLAW_OXLINT_SHARDS_SERIAL === "1";
const results = runSerial
? await runShardsSerial(shards)
: await Promise.all(shards.map((shard) => runShard(shard)));
process.exitCode = results.find((status) => status !== 0) ?? 0;
try {
const prepareResult = spawnSync(
process.execPath,
[path.resolve("scripts", "prepare-extension-package-boundary-artifacts.mjs")],
{
stdio: "inherit",
env,
},
);
if (prepareResult.error) {
throw prepareResult.error;
}
if ((prepareResult.status ?? 1) !== 0) {
process.exitCode = prepareResult.status ?? 1;
} else {
const runSerial = env.OPENCLAW_OXLINT_SHARDS_SERIAL === "1";
const results = runSerial
? await runShardsSerial(shards)
: await Promise.all(shards.map((shard) => runShard(shard)));
process.exitCode = results.find((status) => status !== 0) ?? 0;
}
} finally {
releaseLock();
}
async function runShardsSerial(entries) {
const results = [];
@@ -54,7 +83,7 @@ async function runShard(shard) {
const child = spawn(process.execPath, [runner, ...shard.args, ...extraArgs], {
stdio: "inherit",
env: {
...process.env,
...env,
OPENCLAW_OXLINT_SKIP_LOCK: "1",
OPENCLAW_OXLINT_SKIP_PREPARE: "1",
},

View File

@@ -139,6 +139,30 @@ describe("openclaw launcher", () => {
cleanupTempDirs(fixtureRoots);
});
it("keeps the bootstrap Node floor aligned with package and runtime guards", async () => {
const [launcher, runtimeGuard, packageJsonRaw] = await Promise.all([
fs.readFile(path.resolve(process.cwd(), "openclaw.mjs"), "utf8"),
fs.readFile(path.resolve(process.cwd(), "src/infra/runtime-guard.ts"), "utf8"),
fs.readFile(path.resolve(process.cwd(), "package.json"), "utf8"),
]);
const packageJson = JSON.parse(packageJsonRaw) as { engines?: { node?: string } };
const launcherMatch = launcher.match(
/const MIN_NODE_MAJOR = (\d+);\s+const MIN_NODE_MINOR = (\d+);/u,
);
const runtimeMatch = runtimeGuard.match(
/const MIN_NODE: Semver = \{ major: (\d+), minor: (\d+), patch: (\d+) \};/u,
);
const engineMatch = packageJson.engines?.node?.match(/^>=(\d+)\.(\d+)\.(\d+)$/u);
expect(launcherMatch).not.toBeNull();
expect(runtimeMatch).not.toBeNull();
expect(engineMatch).not.toBeNull();
expect(`${launcherMatch?.[1]}.${launcherMatch?.[2]}.0`).toBe(
`${engineMatch?.[1]}.${engineMatch?.[2]}.${engineMatch?.[3]}`,
);
expect(runtimeMatch?.slice(1, 4)).toEqual(engineMatch?.slice(1, 4));
});
it("surfaces transitive entry import failures instead of masking them as missing dist", async () => {
const fixtureRoot = await makeLauncherFixture(fixtureRoots);
await fs.writeFile(

View File

@@ -0,0 +1,104 @@
import fs from "node:fs";
import { describe, expect, it } from "vitest";
type RootPackageJson = {
scripts: Record<string, string>;
};
const ENV_ASSIGNMENT_RE = /^[A-Za-z_][A-Za-z0-9_]*=/u;
const NODE_OPTIONS_WITH_VALUE = new Set([
"--conditions",
"--env-file",
"--env-file-if-exists",
"--import",
"--loader",
"--max-old-space-size",
"--require",
"--test-name-pattern",
"--test-reporter",
"-C",
"-r",
]);
function readPackageJson(): RootPackageJson {
return JSON.parse(fs.readFileSync("package.json", "utf8")) as RootPackageJson;
}
function tokenizeCommand(command: string): string[] {
return (
command
.match(/"[^"]*"|'[^']*'|[^\s]+/gu)
?.map((token) => token.replace(/^(['"])(.*)\1$/u, "$2")) ?? []
);
}
function extractNodeScriptTargets(script: string): string[] {
return script.split(/\s*(?:&&|\|\||;)\s*/u).flatMap((command) => {
const tokens = tokenizeCommand(command);
let index = tokens[0] === "env" ? 1 : 0;
while (ENV_ASSIGNMENT_RE.test(tokens[index] ?? "")) {
index += 1;
}
if (tokens[index] !== "node") {
return [];
}
for (let tokenIndex = index + 1; tokenIndex < tokens.length; tokenIndex += 1) {
const token = tokens[tokenIndex];
if (!token) {
continue;
}
if (token.startsWith("scripts/")) {
return [token];
}
if (token === "--") {
continue;
}
if (token.startsWith("--") && token.includes("=")) {
continue;
}
if (NODE_OPTIONS_WITH_VALUE.has(token)) {
tokenIndex += 1;
continue;
}
if (token.startsWith("-")) {
continue;
}
return [];
}
return [];
});
}
describe("package scripts", () => {
it("finds node script targets after env assignments and valued node options", () => {
expect(
extractNodeScriptTargets(
"FOO=1 node --import tsx scripts/release-check.ts && node --max-old-space-size=8192 scripts/plugin-sdk-surface-report.mjs && env BAR=1 node -r tsx scripts/check.ts",
),
).toEqual([
"scripts/release-check.ts",
"scripts/plugin-sdk-surface-report.mjs",
"scripts/check.ts",
]);
});
it("keeps direct node script targets present in the source checkout", () => {
const packageJson = readPackageJson();
const missingTargets = Object.entries(packageJson.scripts).flatMap(([name, script]) =>
extractNodeScriptTargets(script)
.filter((target) => !fs.existsSync(target))
.map((target) => `${name}: ${target}`),
);
expect(missingTargets).toEqual([]);
});
it("uses the shipped package launcher for npm start", () => {
expect(readPackageJson().scripts.start).toBe("node openclaw.mjs");
});
});

View File

@@ -124,6 +124,13 @@ describe("resolveBuildAllStep", () => {
},
});
});
it("keeps plugin-sdk dts cache metadata aligned with declaration inputs", () => {
const step = getBuildAllStep("build:plugin-sdk:dts");
expect(step.cache?.inputs).toEqual(expect.arrayContaining(["packages/memory-host-sdk/src"]));
expect(step.cache?.outputs).toEqual(expect.arrayContaining(["dist/plugin-sdk/packages"]));
});
});
describe("resolveBuildAllSteps", () => {

View File

@@ -34,6 +34,20 @@ describe("run-oxlint", () => {
expect(shardedLintRunner).toContain('OPENCLAW_OXLINT_SKIP_PREPARE: "1"');
});
it("holds one parent heavy-check lock for sharded lint runs", () => {
const shardedLintRunner = readFileSync("scripts/run-oxlint-shards.mjs", "utf8");
const skipLockIndex = shardedLintRunner.indexOf('env.OPENCLAW_OXLINT_SKIP_LOCK === "1"');
const lockIndex = shardedLintRunner.indexOf("acquireLocalHeavyCheckLockSync({");
const childSkipIndex = shardedLintRunner.indexOf('OPENCLAW_OXLINT_SKIP_LOCK: "1"');
expect(shardedLintRunner).toContain("resolveLocalHeavyCheckEnv");
expect(shardedLintRunner).toContain("shouldAcquireLocalHeavyCheckLockForOxlint");
expect(skipLockIndex).toBeGreaterThan(-1);
expect(lockIndex).toBeGreaterThan(-1);
expect(lockIndex).toBeGreaterThan(skipLockIndex);
expect(childSkipIndex).toBeGreaterThan(lockIndex);
});
it("lets dev update preflight run oxlint shards serially", () => {
const shardedLintRunner = readFileSync("scripts/run-oxlint-shards.mjs", "utf8");

View File

@@ -25,6 +25,7 @@ import {
resolveSharedVitestWorkerConfig,
sharedVitestConfig,
} from "./vitest/vitest.shared.config.ts";
import { fullSuiteVitestShards } from "./vitest/vitest.test-shards.mjs";
import { createUiVitestConfig, unitUiIncludePatterns } from "./vitest/vitest.ui.config.ts";
import { createUnitFastVitestConfig } from "./vitest/vitest.unit-fast.config.ts";
import unitUiConfig from "./vitest/vitest.unit-ui.config.ts";
@@ -58,6 +59,29 @@ describe("projects vitest config", () => {
expect(requireTestConfig(baseConfig).projects).toEqual([...rootVitestProjects]);
});
it("keeps root watch projects aligned with dedicated extension shard lanes", () => {
const extensionShard = fullSuiteVitestShards.find(
(shard) => shard.config === "test/vitest/vitest.full-extensions.config.ts",
);
expect(extensionShard?.projects).toEqual(
expect.arrayContaining([
"test/vitest/vitest.extension-browser.config.ts",
"test/vitest/vitest.extension-qa.config.ts",
"test/vitest/vitest.extension-media.config.ts",
"test/vitest/vitest.extension-misc.config.ts",
]),
);
expect(rootVitestProjects).toEqual(
expect.arrayContaining([
"test/vitest/vitest.extension-browser.config.ts",
"test/vitest/vitest.extension-qa.config.ts",
"test/vitest/vitest.extension-media.config.ts",
"test/vitest/vitest.extension-misc.config.ts",
]),
);
});
it("disables vite env-file loading for vitest lanes", () => {
expect(baseConfig.envFile).toBe(false);
expect(sharedVitestConfig.envFile).toBe(false);

View File

@@ -74,6 +74,10 @@ export const rootVitestProjects = [
"test/vitest/vitest.extension-voice-call.config.ts",
"test/vitest/vitest.extension-whatsapp.config.ts",
"test/vitest/vitest.extension-zalo.config.ts",
"test/vitest/vitest.extension-browser.config.ts",
"test/vitest/vitest.extension-qa.config.ts",
"test/vitest/vitest.extension-media.config.ts",
"test/vitest/vitest.extension-misc.config.ts",
"test/vitest/vitest.extensions.config.ts",
] as const;