mirror of
https://github.com/openclaw/openclaw.git
synced 2026-09-28 05:54:09 +08:00
* fix(update): prevent stale runtime modules after Git updates Promote root and nested dist-runtime outputs through the existing candidate runtime transaction so activation and rollback keep every runtime generation aligned. Extend real-Git execution probes for runtime overlays and consolidate repeated fixture writes. * fix(update): complete source runtime artifacts on first hop Complete canonical plugin overlays and SDK aliases before target-owned plugin convergence when an older published updater omitted those outputs. Preserve exact online retries and legacy target contracts, and require current offline authority for changed physical runtime publication and rollback. Share artifact ownership with build/postbuild writers, join smoke cancellation, and keep each directory swap synchronous after its authority check. * fix(daemon): preserve proven systemd service absence Recognize the native systemd missing-unit-file response during strict non-loading inspection. Carry affirmative user-unit absence to the Linux absence owner, which separately excludes system-scope ownership before reporting an offline service. Keep failed manager inspection and other platforms conservative. This allows first-hop Git runtime artifact completion on CLI-only Linux installations without weakening publication authority. * fix(update): preserve canonical artifacts in smoke consumers Keep the built singleton smoke on canonical runtime preparation and verify its cleanup leaves a no-op. Release the QA fixture port reservation before packaged maintenance so offline publication can inspect the configured port. Align update routing and lifecycle fixtures with their real target identity and lease contracts while retaining consent, cleanup, and restart assertions. * test: rebalance plugin state type checks
368 lines
14 KiB
TypeScript
368 lines
14 KiB
TypeScript
// Package script tests validate root package script invariants.
|
|
import fs from "node:fs";
|
|
import { expectDefined } from "@openclaw/normalization-core";
|
|
import { describe, expect, it } from "vitest";
|
|
import { parseBuildAllArgs, resolveBuildAllSteps } from "../scripts/build-all.mts";
|
|
import { detectChangedScope } from "../scripts/ci-changed-scope.mjs";
|
|
|
|
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 readWindowsCiPartScripts(): [string, string] {
|
|
const scripts = readPackageJson().scripts;
|
|
return [
|
|
expectDefined(scripts["test:windows:ci:1"], "Windows CI part 1 script"),
|
|
expectDefined(scripts["test:windows:ci:2"], "Windows CI part 2 script"),
|
|
];
|
|
}
|
|
|
|
function readWindowsCiCoverageScript(): string {
|
|
return readWindowsCiPartScripts().join(" ");
|
|
}
|
|
|
|
function readProjectTestTargets(script: string): string[] {
|
|
const tokens = tokenizeCommand(script);
|
|
const runnerIndex = tokens.indexOf("scripts/test-projects.mts");
|
|
return runnerIndex < 0 ? [] : tokens.slice(runnerIndex + 1);
|
|
}
|
|
|
|
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 ./scripts/tsx.mjs scripts/release-check.ts && node --max-old-space-size=8192 --import ./scripts/tsx.mjs scripts/plugin-sdk-surface-report.mts && env BAR=1 node -r ./preload.cjs scripts/check.ts",
|
|
),
|
|
).toEqual([
|
|
"scripts/release-check.ts",
|
|
"scripts/plugin-sdk-surface-report.mts",
|
|
"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("keeps direct Node package scripts off POSIX-only env assignment prefixes", () => {
|
|
const packageJson = readPackageJson();
|
|
const directNodeEnvScripts = Object.entries(packageJson.scripts).flatMap(([name, script]) =>
|
|
script
|
|
.split(/\s*(?:&&|\|\||;)\s*/u)
|
|
.filter((command) => {
|
|
const tokens = tokenizeCommand(command);
|
|
let index = tokens[0] === "env" ? 1 : 0;
|
|
const hasEnvPrefix = ENV_ASSIGNMENT_RE.test(tokens[index] ?? "");
|
|
while (ENV_ASSIGNMENT_RE.test(tokens[index] ?? "")) {
|
|
index += 1;
|
|
}
|
|
return hasEnvPrefix && tokens[index] === "node";
|
|
})
|
|
.map((command) => `${name}: ${command}`),
|
|
);
|
|
|
|
expect(directNodeEnvScripts).toEqual([]);
|
|
});
|
|
|
|
it.each([{ scriptName: "build:docker", expectedCount: 2 }])(
|
|
"runs TypeScript steps in $scriptName through the tooling bootstrap",
|
|
({ scriptName, expectedCount }) => {
|
|
const script = expectDefined(
|
|
readPackageJson().scripts[scriptName],
|
|
`package script ${scriptName}`,
|
|
);
|
|
|
|
expect(script).not.toContain("--experimental-strip-types");
|
|
expect(
|
|
script.match(/node --import \.\/scripts\/tsx\.mjs scripts\/[^\s]+\.ts(?=\s|$)/gu),
|
|
).toHaveLength(expectedCount);
|
|
},
|
|
);
|
|
|
|
it("enables live cache validation in the package script", () => {
|
|
expect(readPackageJson().scripts["test:live:cache"]).toBe(
|
|
"node --import ./scripts/tsx.mjs scripts/run-with-env.mts OPENCLAW_LIVE_TEST=1 OPENCLAW_LIVE_CACHE_TEST=1 -- node --import ./scripts/tsx.mjs scripts/check-live-cache.ts",
|
|
);
|
|
});
|
|
|
|
it("builds runtime artifacts before browser bootstrap E2E against real Chromium", () => {
|
|
expect(readPackageJson().scripts["test:e2e:browser-extension"]).toBe(
|
|
"node --import ./scripts/tsx.mjs scripts/build-all.mts qaRuntime && node --import ./scripts/tsx.mjs scripts/run-with-env.mts PLAYWRIGHT_BROWSERS_PATH=.artifacts/playwright-browsers -- node --import ./scripts/tsx.mjs scripts/ensure-playwright-chromium.mts --require-playwright-chromium && node --import ./scripts/tsx.mjs scripts/run-with-env.mts PLAYWRIGHT_BROWSERS_PATH=.artifacts/playwright-browsers OPENCLAW_BROWSER_EXTENSION_E2E=1 OPENCLAW_E2E_WORKERS=1 -- node scripts/run-vitest.mjs extensions/browser/chrome-extension/bootstrap.chromium.test.ts",
|
|
);
|
|
});
|
|
|
|
it("gives the plugin SDK usage scan enough heap for repository-wide analysis", () => {
|
|
expect(readPackageJson().scripts["plugin-sdk:usage"]).toBe(
|
|
"node --max-old-space-size=8192 --import ./scripts/tsx.mjs scripts/analyze-plugin-sdk-usage.ts",
|
|
);
|
|
});
|
|
|
|
it("runs dead-code reports fail-fast", () => {
|
|
expect(readPackageJson().scripts["deadcode:report"]).toBe(
|
|
"pnpm deadcode:full && pnpm deadcode:exports",
|
|
);
|
|
});
|
|
|
|
it.each(["build:strict-smoke", "build:plugin-sdk:strict-smoke"])(
|
|
"%s publishes canonical declarations before strict export checks",
|
|
(scriptName) => {
|
|
const script = expectDefined(readPackageJson().scripts[scriptName], scriptName);
|
|
const tokens = tokenizeCommand(script);
|
|
const buildAllIndex = tokens.indexOf("scripts/build-all.mts");
|
|
const targets =
|
|
buildAllIndex < 0
|
|
? extractNodeScriptTargets(script)
|
|
: resolveBuildAllSteps(parseBuildAllArgs(tokens.slice(buildAllIndex + 1)).profile)
|
|
.filter((step) => step.kind !== "pnpm")
|
|
.flatMap((step) => extractNodeScriptTargets(["node", ...step.args].join(" ")));
|
|
const check = targets.indexOf("scripts/check-plugin-sdk-exports.mts");
|
|
|
|
expect(check).toBeGreaterThanOrEqual(0);
|
|
for (const prerequisite of [
|
|
"scripts/runtime-postbuild.mts",
|
|
"scripts/write-plugin-sdk-entry-dts.ts",
|
|
]) {
|
|
const publication = targets.indexOf(prerequisite);
|
|
expect(publication, prerequisite).toBeGreaterThanOrEqual(0);
|
|
expect(publication, prerequisite).toBeLessThan(check);
|
|
}
|
|
},
|
|
);
|
|
|
|
it("builds generated plugin assets before Docker runtime postbuild", () => {
|
|
const commands = expectDefined(
|
|
readPackageJson().scripts["build:docker"],
|
|
"package script build:docker",
|
|
).split(" && ");
|
|
|
|
const assets = commands.indexOf("pnpm plugins:assets:build");
|
|
const postbuild = commands.indexOf("node scripts/runtime-postbuild.mjs");
|
|
expect(assets).toBeGreaterThanOrEqual(0);
|
|
expect(postbuild).toBeGreaterThanOrEqual(0);
|
|
expect(assets).toBeLessThan(postbuild);
|
|
});
|
|
|
|
it("cleans package builds before validating release contents", () => {
|
|
const scripts = readPackageJson().scripts;
|
|
|
|
expect(scripts["build:package"]).toBe(
|
|
"node --import ./scripts/tsx.mjs scripts/build-all.mts package",
|
|
);
|
|
expect(scripts["release:check"]).toBe(
|
|
"pnpm build:package && pnpm release:generated:check && node --import ./scripts/tsx.mjs scripts/release-check.ts",
|
|
);
|
|
});
|
|
|
|
it("uses the shipped package launcher for npm start", () => {
|
|
expect(readPackageJson().scripts.start).toBe("node openclaw.mjs");
|
|
});
|
|
|
|
it("builds iOS against a generic simulator by default", () => {
|
|
const script = readPackageJson().scripts["ios:build"];
|
|
|
|
expect(script).toContain("${IOS_DEST:-generic/platform=iOS Simulator}");
|
|
expect(script).not.toContain("name=iPhone");
|
|
});
|
|
|
|
it("keeps the Wear app in the root Android contributor gates", () => {
|
|
const scripts = readPackageJson().scripts;
|
|
|
|
expect(scripts["android:assemble"]).toContain(":wear:assembleDebug");
|
|
expect(scripts["android:format"]).toContain(":wear:ktlintFormat");
|
|
expect(scripts["android:lint"]).toContain(":wear:ktlintCheck");
|
|
expect(scripts["android:lint:android"]).toContain(":wear:lintDebug");
|
|
expect(scripts["android:test"]).toContain(":wear:testDebugUnitTest");
|
|
});
|
|
|
|
it("routes every declared Windows CI test to its native lane", () => {
|
|
const missedTargets = readWindowsCiPartScripts()
|
|
.flatMap(readProjectTestTargets)
|
|
.filter((target) => !detectChangedScope([target]).runWindows);
|
|
expect(missedTargets).toEqual([]);
|
|
});
|
|
|
|
it.for([
|
|
{ platform: "windows", parts: [1, 2] },
|
|
{ platform: "macos", parts: [1, 2, 3] },
|
|
])(
|
|
"partitions $platform CI coverage into disjoint explicit test lists",
|
|
({ platform, parts }) => {
|
|
const scripts = readPackageJson().scripts;
|
|
const partScripts = parts.map((part) =>
|
|
expectDefined(scripts[`test:${platform}:ci:${part}`], `${platform} CI part ${part}`),
|
|
);
|
|
const partTargets = partScripts.map(readProjectTestTargets);
|
|
|
|
expect(scripts[`test:${platform}:ci`]).toBe(
|
|
parts.map((part) => `pnpm test:${platform}:ci:${part}`).join(" && "),
|
|
);
|
|
expect(scripts[`test:${platform}:ci:${parts.length + 1}`]).toBeUndefined();
|
|
for (const [partIndex, targets] of partTargets.entries()) {
|
|
expect(targets.length).toBeGreaterThan(0);
|
|
expect(
|
|
targets.every((target) => target.endsWith(".test.ts") && fs.existsSync(target)),
|
|
).toBe(true);
|
|
const laterTargets = new Set(partTargets.slice(partIndex + 1).flat());
|
|
expect(
|
|
targets.filter((target) => laterTargets.has(target)),
|
|
`${platform} CI part ${partIndex + 1} overlaps a later part`,
|
|
).toEqual([]);
|
|
}
|
|
},
|
|
);
|
|
|
|
it("keeps required native coverage in Windows CI", () => {
|
|
const requiredTargets = [
|
|
"src/node-host/node-worker-transfer-client.test.ts",
|
|
"test/scripts/format-generated-module.test.ts",
|
|
"test/scripts/direct-run-entrypoints.test.ts",
|
|
"test/scripts/vitest-worker-artifacts.test.ts",
|
|
"test/scripts/vitest-worker-artifacts.transforms.test.ts",
|
|
"test/e2e/qa-lab/runtime/package-openclaw-for-docker.e2e.test.ts",
|
|
"src/commands/doctor-gateway-auth-token.windows.test.ts",
|
|
"src/infra/state-migrations.legacy-session-store.test.ts",
|
|
"src/infra/sqlite-snapshot.test.ts",
|
|
"src/state/openclaw-state-ownership.test.ts",
|
|
"src/media/local-media-path.windows.test.ts",
|
|
"src/auto-reply/reply.triggers.trigger-handling.stages-inbound-media-into-sandbox-workspace.test.ts",
|
|
"src/infra/ssh-client.windows.test.ts",
|
|
"src/infra/ports.test.ts",
|
|
"src/infra/advertised-lan-host.windows.test.ts",
|
|
"src/test-utils/openclaw-test-state.test.ts",
|
|
"src/snapshot/local-repository.windows.test.ts",
|
|
"src/commands/backup-verify.test.ts",
|
|
"src/config/sessions/session-accessor.sqlite-archive.worker.test.ts",
|
|
"test/scripts/openclaw-cross-os-installer.windows.test.ts",
|
|
"test/scripts/run-with-env.test.ts",
|
|
"test/scripts/ts-topology.test.ts",
|
|
"extensions/mxc/test/mxc-backend.test.ts",
|
|
"extensions/mxc/test/sandbox-policy-loader.test.ts",
|
|
"src/agents/bash-tools.exec.script-preflight.test.ts",
|
|
"src/infra/exec-allowlist-pattern.test.ts",
|
|
"src/infra/executable-path.test.ts",
|
|
"src/plugin-sdk/node-host.test.ts",
|
|
"src/process/terminal-pty.test.ts",
|
|
"src/tui/tui.resolve-codex-bin.test.ts",
|
|
"src/infra/fs-safe-remove.test.ts",
|
|
"src/agents/tools/media-tool-file-url.windows.test.ts",
|
|
"src/media/web-media.file-url.windows.test.ts",
|
|
"extensions/msteams/src/media-helpers.test.ts",
|
|
"extensions/msteams/src/messenger.test.ts",
|
|
"src/auto-reply/usage-bar/template.windows.test.ts",
|
|
"src/media-understanding/attachments.file-url.windows.test.ts",
|
|
"src/utils.test.ts",
|
|
"src/commands/agents.commands.list.test.ts",
|
|
"src/cli/daemon-cli/status.print.test.ts",
|
|
"packages/terminal-core/src/display-string.test.ts",
|
|
"src/agents/sandbox/fs-paths.test.ts",
|
|
"src/agents/sessions/tools/render-utils.test.ts",
|
|
"src/agents/agent-tools.read.windows.test.ts",
|
|
"src/agents/agent-tools.read.host-operations.test.ts",
|
|
"src/agents/sessions/tools/path-utils.test.ts",
|
|
"src/agents/provider-local-service.env-case.test.ts",
|
|
"src/infra/process-env.test.ts",
|
|
"src/cli/mcp-cli.path-case.windows.test.ts",
|
|
"extensions/memory-core/src/memory-extra-file-path.windows.test.ts",
|
|
];
|
|
const actualTargets = new Set(readWindowsCiPartScripts().flatMap(readProjectTestTargets));
|
|
|
|
expect(requiredTargets.filter((target) => !actualTargets.has(target))).toEqual([]);
|
|
});
|
|
|
|
it("keeps the native Scheduled Task lifecycle proof opt-in", () => {
|
|
const scripts = readPackageJson().scripts;
|
|
|
|
expect(readWindowsCiCoverageScript()).not.toContain("schtasks.integration.e2e.test.ts");
|
|
expect(scripts["test:windows:schtasks:integration"]).toContain(
|
|
"CI_WINDOWS_SCHTASKS_INTEGRATION=1",
|
|
);
|
|
expect(scripts["test:windows:schtasks:integration"]).toContain(
|
|
"src/daemon/schtasks.integration.e2e.test.ts",
|
|
);
|
|
});
|
|
|
|
it("runs cross-OS installer behavior coverage in Windows CI", () => {
|
|
expect(
|
|
readWindowsCiPartScripts()
|
|
.flatMap(readProjectTestTargets)
|
|
.filter((target) => target === "test/scripts/install-ps1.test.ts"),
|
|
).toHaveLength(1);
|
|
});
|
|
});
|