mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-18 15:34:46 +00:00
fix: tighten release tooling checks
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"],
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
@@ -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(
|
||||
|
||||
104
test/package-scripts.test.ts
Normal file
104
test/package-scripts.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user