mirror of
https://github.com/openclaw/openclaw.git
synced 2026-09-28 14:12:28 +08:00
Refresh application, plugin, native, build and container dependencies through the fixed 2026-09-19T16:27:11Z cutoff. Migrate native TypeScript snapshot/printer APIs while preserving compilation and filesystem contracts; retain existing patches and compatibility holds. Document offline container-image preparation. Include the verified compiler process-census and loading-clock fixture repairs and deterministic warm-history regression. Adopt the canonical production history fixes from #159924 and #159955. Land under the maintainer's explicit approval to treat proven pre-existing CI failures as non-blocking and repair main afterward. CI36365552098 failed an unchanged Android Compose fixture's asynchronous catalog projection assertion (3518 passed,1 failed); the Android/Gradle tree matches its main parent byte-for-byte. Security and dependency reviews passed. The final rebase preserves reviewed source changes and regenerates only the intentional Node-image documentation fingerprint. See PR159401 for complete validation and the follow-up repair obligation.
718 lines
31 KiB
TypeScript
718 lines
31 KiB
TypeScript
// Check Workflows tests cover check workflows script behavior.
|
|
import { spawnSync } from "node:child_process";
|
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
import path from "node:path";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
import { parse } from "yaml";
|
|
import { createGatewayTaskSupervisorProbe } from "../../src/daemon/schtasks.task-supervisor.native-test-support.js";
|
|
import { resolveTestNodeExecPath } from "../../src/test-utils/node-process.js";
|
|
import { cleanupTempDirs, makeTempDir } from "../helpers/temp-dir.js";
|
|
|
|
const scriptPath = path.resolve("scripts/check-workflows.mts");
|
|
const tempDirs: string[] = [];
|
|
const testNodeExecPath = resolveTestNodeExecPath();
|
|
|
|
type WorkflowStep = {
|
|
name: string;
|
|
id?: string;
|
|
if?: string;
|
|
uses?: string;
|
|
run?: string;
|
|
env?: Record<string, string | number | boolean>;
|
|
with?: Record<string, string | number | boolean>;
|
|
"timeout-minutes"?: number;
|
|
"continue-on-error"?: boolean | string;
|
|
};
|
|
|
|
type WorkflowJob = {
|
|
if?: string;
|
|
needs?: string | string[];
|
|
permissions?: Record<string, string>;
|
|
env?: Record<string, string>;
|
|
"runs-on": string;
|
|
"continue-on-error"?: boolean | string;
|
|
steps: WorkflowStep[];
|
|
};
|
|
|
|
function readWindowsProbe() {
|
|
const workflow = parse(readFileSync(".github/workflows/windows-testbox-probe.yml", "utf8")) as {
|
|
on: {
|
|
workflow_dispatch: {
|
|
inputs: Record<string, { default: unknown; type: string; description: string }>;
|
|
};
|
|
};
|
|
jobs: Record<string, WorkflowJob>;
|
|
};
|
|
const native = Object.values(workflow.jobs).find((job) =>
|
|
job.steps.some((step) => step.id === "native_schtasks"),
|
|
);
|
|
const probe = workflow.jobs.probe;
|
|
if (!probe || !native) {
|
|
throw new Error("Windows probe must declare both headless CI and native proof jobs");
|
|
}
|
|
return { workflow, probe, native };
|
|
}
|
|
|
|
afterEach(() => {
|
|
cleanupTempDirs(tempDirs);
|
|
});
|
|
|
|
describe("check-workflows", () => {
|
|
it("prints an actionable diagnostic when actionlint and go are unavailable", () => {
|
|
const result = spawnSync(testNodeExecPath, ["--import", "tsx", scriptPath], {
|
|
encoding: "utf8",
|
|
env: {
|
|
...process.env,
|
|
PATH: "",
|
|
},
|
|
});
|
|
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain("missing workflow linter");
|
|
expect(result.stderr).toContain("install actionlint built from");
|
|
expect(result.stderr).toContain("011a6d15e749bb3f2d771eed9c7aa0e7e3e10ee7");
|
|
});
|
|
|
|
it.each([
|
|
{ version: undefined, tool: "go" },
|
|
{ version: "1.7.12", tool: "go" },
|
|
{ version: "unknown", tool: "pre-commit" },
|
|
{ version: "v1.7.13-0.20260419144658-011a6d15e749", tool: "installed" },
|
|
{ version: "1.7.12", tool: "pre-commit", acquireStatus: 1 },
|
|
{ version: "1.7.12", tool: "go", lintStatus: 7 },
|
|
{ version: "1.7.12", tool: "unavailable", lintStatus: 1 },
|
|
])(
|
|
"selects $tool actionlint for installed version $version ($acquireStatus/$lintStatus)",
|
|
({ version, tool, acquireStatus = 0, lintStatus = 0 }) => {
|
|
const tempDir = makeTempDir(tempDirs, "check-workflows-");
|
|
const binDir = path.join(tempDir, "bin");
|
|
const markerPath = path.join(tempDir, "go-install.txt");
|
|
const binMarkerPath = path.join(tempDir, "go-bin.txt");
|
|
const pinnedMarkerPath = path.join(tempDir, "pinned-actionlint.txt");
|
|
const preCommitMarkerPath = path.join(tempDir, "pre-commit.txt");
|
|
const actionlintMarkerPath = path.join(tempDir, "actionlint.txt");
|
|
mkdirSync(binDir);
|
|
if (version) {
|
|
writeFileSync(
|
|
path.join(binDir, "actionlint"),
|
|
[
|
|
"#!/bin/sh",
|
|
`if [ "$1" = "--version" ]; then printf '%s\\n' '${version}'; exit 0; fi`,
|
|
'printf "%s\\n" "$*" > "$ACTIONLINT_MARKER"',
|
|
"",
|
|
].join("\n"),
|
|
{ mode: 0o755 },
|
|
);
|
|
}
|
|
if (tool === "go" || acquireStatus !== 0) {
|
|
writeFileSync(
|
|
path.join(binDir, "pinned-actionlint"),
|
|
[
|
|
"#!/bin/sh",
|
|
'printf "%s\\n" "$*" > "$PINNED_ACTIONLINT_MARKER"',
|
|
`exit ${lintStatus}`,
|
|
"",
|
|
].join("\n"),
|
|
{ mode: 0o755 },
|
|
);
|
|
writeFileSync(
|
|
path.join(binDir, "go"),
|
|
[
|
|
"#!/bin/sh",
|
|
'if [ "$1" = "version" ]; then exit 0; fi',
|
|
'if [ "$1" = "install" ]; then',
|
|
' printf "%s\\n" "$*" > "$GO_FALLBACK_MARKER"',
|
|
' printf "%s\\n" "$GOBIN" > "$GO_BIN_MARKER"',
|
|
` if [ ${acquireStatus} != 0 ]; then exit ${acquireStatus}; fi`,
|
|
' /bin/cp "$PINNED_ACTIONLINT" "$GOBIN/actionlint"',
|
|
" exit 0",
|
|
"fi",
|
|
"exit 1",
|
|
"",
|
|
].join("\n"),
|
|
{ mode: 0o755 },
|
|
);
|
|
}
|
|
if (tool !== "unavailable") {
|
|
writeFileSync(
|
|
path.join(binDir, "pre-commit"),
|
|
[
|
|
"#!/bin/sh",
|
|
'if [ "$1" = "--version" ]; then exit 0; fi',
|
|
'printf "%s\\n" "$*" >> "$PRE_COMMIT_MARKER"',
|
|
"exit 0",
|
|
"",
|
|
].join("\n"),
|
|
{ mode: 0o755 },
|
|
);
|
|
}
|
|
for (const command of tool === "unavailable" ? ["node"] : ["python3", "node"]) {
|
|
writeFileSync(path.join(binDir, command), "#!/bin/sh\nexit 0\n", { mode: 0o755 });
|
|
}
|
|
|
|
const result = spawnSync(testNodeExecPath, ["--import", "tsx", scriptPath], {
|
|
encoding: "utf8",
|
|
env: {
|
|
...process.env,
|
|
GO_FALLBACK_MARKER: markerPath,
|
|
GO_BIN_MARKER: binMarkerPath,
|
|
PINNED_ACTIONLINT: path.join(binDir, "pinned-actionlint"),
|
|
PINNED_ACTIONLINT_MARKER: pinnedMarkerPath,
|
|
PRE_COMMIT_MARKER: preCommitMarkerPath,
|
|
ACTIONLINT_MARKER: actionlintMarkerPath,
|
|
PATH: binDir,
|
|
},
|
|
});
|
|
|
|
expect(result.status).toBe(lintStatus);
|
|
expect(existsSync(actionlintMarkerPath)).toBe(tool === "installed");
|
|
const acquired = tool === "go" || acquireStatus !== 0;
|
|
expect(existsSync(markerPath)).toBe(acquired);
|
|
if (acquired) {
|
|
expect(readFileSync(markerPath, "utf8")).toContain(
|
|
"install github.com/rhysd/actionlint/cmd/actionlint@011a6d15e749bb3f2d771eed9c7aa0e7e3e10ee7",
|
|
);
|
|
expect(existsSync(readFileSync(binMarkerPath, "utf8").trim())).toBe(false);
|
|
} else if (tool === "installed") {
|
|
expect(readFileSync(actionlintMarkerPath, "utf8")).toContain(".github/workflows/ci.yml");
|
|
}
|
|
expect(existsSync(pinnedMarkerPath)).toBe(tool === "go");
|
|
if (tool === "go") {
|
|
expect(readFileSync(pinnedMarkerPath, "utf8")).toContain(".github/workflows/ci.yml");
|
|
}
|
|
if (lintStatus !== 0) {
|
|
expect(existsSync(preCommitMarkerPath)).toBe(false);
|
|
if (tool === "unavailable") {
|
|
expect(result.stderr).toContain(
|
|
"missing workflow linter: install actionlint built from 011a6d15e749bb3f2d771eed9c7aa0e7e3e10ee7",
|
|
);
|
|
expect(result.stderr).toContain("Go to acquire that revision, or a pre-commit runtime");
|
|
}
|
|
return;
|
|
}
|
|
const preCommitArgs = readFileSync(preCommitMarkerPath, "utf8");
|
|
expect(preCommitArgs.includes(" actionlint --files")).toBe(tool === "pre-commit");
|
|
expect(preCommitArgs).toContain("run --config .pre-commit-config.yaml zizmor --files");
|
|
expect(preCommitArgs).toContain(".github/workflows/ci.yml");
|
|
expect(preCommitArgs).toContain(".github/workflows/windows-testbox-probe.yml");
|
|
},
|
|
);
|
|
|
|
it("bootstraps pinned pre-commit in a temporary Python venv when needed", () => {
|
|
const tempDir = makeTempDir(tempDirs, "check-workflows-");
|
|
const binDir = path.join(tempDir, "bin");
|
|
const markerPath = path.join(tempDir, "python.txt");
|
|
mkdirSync(binDir);
|
|
writeFileSync(path.join(binDir, "node"), "#!/bin/sh\nexit 0\n", { mode: 0o755 });
|
|
writeFileSync(
|
|
path.join(binDir, "python3"),
|
|
[
|
|
"#!/bin/sh",
|
|
'if [ "$1" = "--version" ]; then exit 0; fi',
|
|
'if [ "$1" = "-m" ] && [ "$2" = "pre_commit" ] && [ "$3" = "--version" ]; then exit 1; fi',
|
|
'if [ "$1" = "-m" ] && [ "$2" = "pip" ]; then',
|
|
' printf "%s\\n" "$*" >> "$PRE_COMMIT_BOOTSTRAP_MARKER"',
|
|
" exit 0",
|
|
"fi",
|
|
'if [ "$1" = "-m" ] && [ "$2" = "pre_commit" ]; then',
|
|
' printf "%s\\n" "$*" >> "$PRE_COMMIT_BOOTSTRAP_MARKER"',
|
|
" exit 0",
|
|
"fi",
|
|
'if [ "$1" = "-m" ] && [ "$2" = "venv" ]; then',
|
|
' /bin/mkdir -p "$3/bin"',
|
|
' /bin/cp "$0" "$3/bin/python"',
|
|
' /bin/chmod +x "$3/bin/python"',
|
|
" exit 0",
|
|
"fi",
|
|
"exit 0",
|
|
"",
|
|
].join("\n"),
|
|
{ mode: 0o755 },
|
|
);
|
|
|
|
const result = spawnSync(testNodeExecPath, ["--import", "tsx", scriptPath], {
|
|
encoding: "utf8",
|
|
env: {
|
|
...process.env,
|
|
PATH: binDir,
|
|
PRE_COMMIT_BOOTSTRAP_MARKER: markerPath,
|
|
},
|
|
});
|
|
|
|
expect(result.status).toBe(0);
|
|
const pythonArgs = readFileSync(markerPath, "utf8");
|
|
expect(pythonArgs).toContain("-m pip install --disable-pip-version-check pre-commit==4.6.2");
|
|
expect(pythonArgs).toContain(
|
|
"-m pre_commit run --config .pre-commit-config.yaml actionlint --files",
|
|
);
|
|
expect(pythonArgs).toContain(
|
|
"-m pre_commit run --config .pre-commit-config.yaml zizmor --files",
|
|
);
|
|
});
|
|
|
|
it("rejects a python3 below the pinned pre-commit runtime floor before building a venv", () => {
|
|
const tempDir = makeTempDir(tempDirs, "check-workflows-");
|
|
const binDir = path.join(tempDir, "bin");
|
|
const markerPath = path.join(tempDir, "venv-attempt.txt");
|
|
mkdirSync(binDir);
|
|
writeFileSync(
|
|
path.join(binDir, "python3"),
|
|
[
|
|
"#!/bin/sh",
|
|
'if [ "$1" = "--version" ]; then printf "Python 3.9.6\\n"; exit 0; fi',
|
|
'if [ "$1" = "-m" ] && [ "$2" = "pre_commit" ] && [ "$3" = "--version" ]; then exit 1; fi',
|
|
'printf "%s\\n" "$*" >> "$VENV_ATTEMPT_MARKER"',
|
|
"exit 1",
|
|
"",
|
|
].join("\n"),
|
|
{ mode: 0o755 },
|
|
);
|
|
|
|
const result = spawnSync(testNodeExecPath, ["--import", "tsx", scriptPath], {
|
|
encoding: "utf8",
|
|
env: {
|
|
...process.env,
|
|
PATH: binDir,
|
|
VENV_ATTEMPT_MARKER: markerPath,
|
|
},
|
|
});
|
|
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain("python3 is 3.9.6");
|
|
expect(result.stderr).toContain("pre-commit 4.6.2 requires Python >=3.10");
|
|
expect(existsSync(markerPath)).toBe(false);
|
|
});
|
|
|
|
it("prints the missing runtime diagnostic when Python venv support is unavailable", () => {
|
|
const tempDir = makeTempDir(tempDirs, "check-workflows-");
|
|
const binDir = path.join(tempDir, "bin");
|
|
mkdirSync(binDir);
|
|
writeFileSync(path.join(binDir, "node"), "#!/bin/sh\nexit 0\n", { mode: 0o755 });
|
|
writeFileSync(
|
|
path.join(binDir, "python3"),
|
|
[
|
|
"#!/bin/sh",
|
|
'if [ "$1" = "--version" ]; then exit 0; fi',
|
|
'if [ "$1" = "-m" ] && [ "$2" = "pre_commit" ] && [ "$3" = "--version" ]; then exit 1; fi',
|
|
'if [ "$1" = "-m" ] && [ "$2" = "venv" ]; then',
|
|
' printf "%s\\n" "python venv unavailable" >&2',
|
|
" exit 1",
|
|
"fi",
|
|
"exit 1",
|
|
"",
|
|
].join("\n"),
|
|
{ mode: 0o755 },
|
|
);
|
|
|
|
const result = spawnSync(testNodeExecPath, ["--import", "tsx", scriptPath], {
|
|
encoding: "utf8",
|
|
env: {
|
|
...process.env,
|
|
PATH: binDir,
|
|
},
|
|
});
|
|
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain("python venv unavailable");
|
|
expect(result.stderr).toContain("missing pre-commit runtime for actionlint");
|
|
expect(result.stderr).toContain("Python venv support for pre-commit 4.6.2");
|
|
});
|
|
|
|
it("cleans the temporary Python venv before exiting on hook failure", () => {
|
|
const tempDir = makeTempDir(tempDirs, "check-workflows-");
|
|
const binDir = path.join(tempDir, "bin");
|
|
const markerPath = path.join(tempDir, "venv-path.txt");
|
|
mkdirSync(binDir);
|
|
writeFileSync(path.join(binDir, "node"), "#!/bin/sh\nexit 0\n", { mode: 0o755 });
|
|
writeFileSync(
|
|
path.join(binDir, "python3"),
|
|
[
|
|
"#!/bin/sh",
|
|
'if [ "$1" = "--version" ]; then exit 0; fi',
|
|
'if [ "$1" = "-m" ] && [ "$2" = "pre_commit" ] && [ "$3" = "--version" ]; then exit 1; fi',
|
|
'if [ "$1" = "-m" ] && [ "$2" = "venv" ]; then',
|
|
' /bin/mkdir -p "$3/bin"',
|
|
' /bin/cp "$0" "$3/bin/python"',
|
|
' /bin/chmod +x "$3/bin/python"',
|
|
' printf "%s\\n" "$3" > "$PRE_COMMIT_VENV_MARKER"',
|
|
" exit 0",
|
|
"fi",
|
|
'if [ "$1" = "-m" ] && [ "$2" = "pip" ]; then exit 0; fi',
|
|
'if [ "$1" = "-m" ] && [ "$2" = "pre_commit" ]; then',
|
|
' printf "%s\\n" "hook failed" >&2',
|
|
" exit 13",
|
|
"fi",
|
|
"exit 1",
|
|
"",
|
|
].join("\n"),
|
|
{ mode: 0o755 },
|
|
);
|
|
|
|
const result = spawnSync(testNodeExecPath, ["--import", "tsx", scriptPath], {
|
|
encoding: "utf8",
|
|
env: {
|
|
...process.env,
|
|
PATH: binDir,
|
|
PRE_COMMIT_VENV_MARKER: markerPath,
|
|
},
|
|
});
|
|
|
|
expect(result.status).toBe(13);
|
|
expect(result.stderr).toContain("hook failed");
|
|
expect(existsSync(readFileSync(markerPath, "utf8").trim())).toBe(false);
|
|
});
|
|
|
|
it("keeps Windows WSL2 probe output normalized through the shared wrapper", () => {
|
|
const workflow = readFileSync(".github/workflows/windows-testbox-probe.yml", "utf8");
|
|
|
|
expect(workflow).toContain(
|
|
'$import = Invoke-WslText -Arguments @("--import", "UbuntuProbe", $wslRoot, $rootfs, "--version", "2")',
|
|
);
|
|
expect(workflow).toContain("function Resolve-UbuntuWslRootfsUrl");
|
|
expect(workflow).toContain('"x64" { $wslArch = "amd64" }');
|
|
expect(workflow).toContain('"arm64" { $wslArch = "arm64" }');
|
|
expect(workflow).toContain("ubuntu-noble-wsl-$wslArch-wsl.rootfs.tar.gz");
|
|
expect(workflow).toContain("ubuntu_wsl_rootfs_arch=$wslArch");
|
|
expect(workflow).toContain("-ConnectionTimeoutSeconds 15");
|
|
expect(workflow).toContain("-OperationTimeoutSeconds 120");
|
|
expect(workflow).toContain('Write-Host "wsl_import_exit=$($import.Code)"');
|
|
expect(workflow).toContain("wsl2_restart_required=true");
|
|
expect(workflow).toContain("import_ubuntu_wsl2=skipped_restart_required");
|
|
expect(workflow).toContain("wsl_exec_skipped=restart_required");
|
|
expect(workflow).toContain(
|
|
'"wsl2_restart_required=$($restartRequired.ToString().ToLowerInvariant())"',
|
|
);
|
|
expect(workflow).toContain(
|
|
'$exec = Invoke-WslText -Arguments @("-d", $distro, "--exec", "bash", "-lc"',
|
|
);
|
|
expect(workflow).toContain('Write-Host "wsl_exec_exit=$($exec.Code)"');
|
|
expect(workflow).not.toContain("wsl.exe --import UbuntuProbe");
|
|
expect(workflow).not.toContain("Microsoft-Hyper-V-All");
|
|
});
|
|
|
|
it("requests independent headless CI and native qualification without allowing either to fail", () => {
|
|
const { workflow, probe, native } = readWindowsProbe();
|
|
expect(workflow.on.workflow_dispatch.inputs.run_windows_ci).toMatchObject({
|
|
description:
|
|
"Run Windows CI and native Tasks, or installed native proof with a package binding",
|
|
default: false,
|
|
type: "boolean",
|
|
});
|
|
expect(workflow.on.workflow_dispatch.inputs.runner_label?.default).toBe(
|
|
"blacksmith-16vcpu-windows-2025",
|
|
);
|
|
expect(native).not.toBe(probe);
|
|
expect(native.if).toBe(
|
|
"${{ inputs.run_windows_ci && inputs.installed_startup_package == '' && !inputs.run_private_node_provisioning && !inputs.installed_repair_worker && inputs.windows_ci_replay == '' }}",
|
|
);
|
|
expect(native["runs-on"]).toBe("windows-2025");
|
|
expect(probe.if).toBeUndefined();
|
|
expect(probe["runs-on"]).toBe("${{ inputs.runner_label }}");
|
|
for (const job of [probe, native]) {
|
|
expect(job.needs).toBeUndefined();
|
|
for (const entry of [job, ...job.steps]) {
|
|
expect(entry["continue-on-error"]).toBeUndefined();
|
|
}
|
|
}
|
|
const ci = probe.steps.find((step) => step.name === "Run Windows CI tests")!;
|
|
expect(ci.if).toBe("${{ inputs.run_windows_ci && inputs.installed_startup_package == '' }}");
|
|
expect(ci.run).toContain("pnpm test:windows:ci");
|
|
expect(ci.env).toMatchObject({ OPENCLAW_VITEST_MAX_WORKERS: 1 });
|
|
expect(native.steps).not.toContainEqual(ci);
|
|
expect(probe.steps.some((step) => step.id?.startsWith("native_schtasks"))).toBe(false);
|
|
expect(
|
|
probe.steps.find((step) => step.name === "Keep runner alive for SSH inspection")?.if,
|
|
).toBe(
|
|
"${{ always() && !cancelled() && !inputs.run_private_node_provisioning && inputs.windows_ci_replay == '' }}",
|
|
);
|
|
expect(probe.steps.find((step) => step.name === "Enforce WSL2 requirement")?.if).toBe(
|
|
"${{ always() && !cancelled() && inputs.require_wsl2 }}",
|
|
);
|
|
|
|
const isolation = native.steps[0]!;
|
|
expect(isolation.id).toBe("native_isolation");
|
|
expect(isolation.if).toBeUndefined();
|
|
expect(isolation.env).toEqual({
|
|
NATIVE_RUNNER_ENVIRONMENT: "${{ runner.environment }}",
|
|
EXPECTED_HEAD: "${{ inputs.target_ref }}",
|
|
NATIVE_ISOLATION_PROOF_NAME: "windows-schtasks-isolation.json",
|
|
});
|
|
const preflight = native.steps[1]!;
|
|
expect(preflight.name).toBe("Preflight native Scheduled Task session");
|
|
// The job excludes private proof and replay before allocation; native steps retain the CI opt-in.
|
|
expect(preflight.if).toBe("${{ inputs.run_windows_ci }}");
|
|
expect(preflight.run).toContain(
|
|
'if (-not [Environment]::UserInteractive) {\n throw "Native Scheduled Task proof requires an interactive Windows runner session."\n}',
|
|
);
|
|
for (const diagnostic of [
|
|
"identity=",
|
|
"session_id=",
|
|
"user_interactive=",
|
|
"administrator=",
|
|
"query user",
|
|
]) {
|
|
expect(preflight.run).toContain(diagnostic);
|
|
}
|
|
for (const name of [
|
|
"Checkout",
|
|
"Setup Node.js",
|
|
"Setup pnpm",
|
|
"Runtime versions",
|
|
"Capture node path",
|
|
"Install dependencies",
|
|
]) {
|
|
const setup = probe.steps.find((step) => step.name === name)!;
|
|
expect(setup).toBeDefined();
|
|
expect(native.steps.find((step) => step.name === name)).toEqual(setup);
|
|
}
|
|
expect(native.steps.find((step) => step.name === "Checkout")?.with).toMatchObject({
|
|
ref: "${{ inputs.target_ref || github.ref }}",
|
|
"persist-credentials": false,
|
|
});
|
|
expect(native.steps.find((step) => step.name === "Setup Node.js")?.env).toMatchObject({
|
|
REQUESTED_NODE_VERSION:
|
|
"${{ inputs.windows_ci_replay != '' && env.OPENCLAW_WINDOWS_REPLAY_NODE_VERSION || inputs.installed_startup_package != '' && inputs.startup_node_version || '24.x' }}",
|
|
});
|
|
expect(native.steps.find((step) => step.name === "Setup pnpm")?.uses).toBe(
|
|
"./.github/actions/setup-pnpm-store-cache",
|
|
);
|
|
expect(native.steps.find((step) => step.name === "Install dependencies")?.run).toContain(
|
|
"pnpm install --frozen-lockfile --prefer-offline",
|
|
);
|
|
});
|
|
|
|
it("keeps installed startup measurement opt-in and binds the package independently from tooling", () => {
|
|
const { workflow, probe, native } = readWindowsProbe();
|
|
expect(workflow.on.workflow_dispatch.inputs.installed_startup_package).toMatchObject({
|
|
default: "",
|
|
type: "string",
|
|
});
|
|
expect(workflow.on.workflow_dispatch.inputs.startup_node_version?.default).toBe("26.9.0");
|
|
expect(workflow.on.workflow_dispatch.inputs.installed_startup_cpu_diagnostic).toMatchObject({
|
|
default: false,
|
|
type: "boolean",
|
|
});
|
|
const validation = probe.steps.find((step) => step.id === "startup_input")!;
|
|
expect(validation.env).toMatchObject({
|
|
STARTUP_PACKAGE: "${{ inputs.installed_startup_package }}",
|
|
CPU_DIAGNOSTIC: "${{ inputs.installed_startup_cpu_diagnostic }}",
|
|
});
|
|
expect(validation.run).toContain("$producer.run_attempt");
|
|
expect(validation.run).toContain("$artifact.digest");
|
|
const install = probe.steps.find((step) => step.id === "installed_package")!;
|
|
expect(install.run).toContain(
|
|
"scripts/resolve-openclaw-package-candidate.mts --source artifact",
|
|
);
|
|
expect(install.run).toContain(
|
|
"npm install --prefix $installRoot --no-audit --no-fund --ignore-scripts=false",
|
|
);
|
|
expect(install.run).toContain("npm rebuild --prefix $installRoot --ignore-scripts=false");
|
|
expect(install.run).toContain(".openclaw-lifecycle-pending");
|
|
expect(install.run).toContain("dist/openclaw-install-guard");
|
|
const measure = probe.steps.find((step) => step.name === "Measure installed startup cohort")!;
|
|
expect(measure.if).toBe(
|
|
"${{ inputs.installed_startup_package != '' && !inputs.installed_repair_worker && !inputs.run_windows_ci }}",
|
|
);
|
|
expect(measure.run).toContain("scripts/bench-gateway-startup.ts --installed-cohort");
|
|
expect(measure.env).toMatchObject({
|
|
CPU_DIAGNOSTIC: "${{ inputs.installed_startup_cpu_diagnostic }}",
|
|
});
|
|
expect(measure.run).toContain('if ($env:CPU_DIAGNOSTIC -eq "true")');
|
|
expect(measure.run).toContain('@("--installed-cpu-diagnostic")');
|
|
expect(native.steps).not.toContainEqual(measure);
|
|
const upload = probe.steps.find((step) => step.name === "Upload installed startup evidence")!;
|
|
expect(upload.if).toBe("${{ always() && inputs.installed_startup_package != '' }}");
|
|
expect(upload.with?.path).toBe(
|
|
[
|
|
".artifacts/windows-installed-startup/*.json",
|
|
"${{ runner.temp }}/windows-repair-isolation.json",
|
|
".artifacts/windows-installed-startup/*.log",
|
|
".artifacts/windows-installed-startup/results.json.profiles/*.cpuprofile",
|
|
".artifacts/windows-installed-startup/results.json.profiles/*.json",
|
|
"",
|
|
].join("\n"),
|
|
);
|
|
expect(upload.with?.["if-no-files-found"]).toBe("error");
|
|
});
|
|
|
|
it("keeps installed native proof on the same owner with a current-run read-only handoff", () => {
|
|
const { workflow, native } = readWindowsProbe();
|
|
const installed = workflow.jobs["native-schtasks-package"]!;
|
|
expect(installed.needs).toBe("probe");
|
|
expect(installed.permissions).toEqual({ contents: "read" });
|
|
expect(installed["runs-on"]).toBe("windows-2025");
|
|
expect(installed.steps).toBe(native.steps);
|
|
expect(installed.if).toContain("inputs.installed_startup_package != ''");
|
|
expect(native.if).toContain("inputs.installed_startup_package == ''");
|
|
expect(installed.env).toMatchObject({
|
|
NATIVE_PACKAGE_ID: "${{ needs.probe.outputs.native_package_id }}",
|
|
NATIVE_PACKAGE_DIGEST: "${{ needs.probe.outputs.native_package_digest }}",
|
|
NATIVE_MANIFEST_SHA256: "${{ needs.probe.outputs.native_manifest_sha256 }}",
|
|
});
|
|
const download = installed.steps.find(
|
|
(step) => step.name === "Download this run's verified native package",
|
|
)!;
|
|
expect(download.with).toMatchObject({
|
|
"artifact-ids": "${{ env.NATIVE_PACKAGE_ID }}",
|
|
"skip-decompress": true,
|
|
"digest-mismatch": "error",
|
|
});
|
|
for (const field of ["github-token", "repository", "run-id"]) {
|
|
expect(download.with?.[field]).toBeUndefined();
|
|
}
|
|
for (const entry of [installed, ...installed.steps]) {
|
|
expect(entry["continue-on-error"]).toBeUndefined();
|
|
}
|
|
let previousRetirement: string | undefined;
|
|
for (const suffix of ["fresh", "9_3", "9_4"]) {
|
|
const ids = ["prepare", "schtasks", "cleanup", "cell_proof", "retire"].map(
|
|
(phase) => `native_${phase}_${suffix}`,
|
|
);
|
|
const positions = ids.map((id) => installed.steps.findIndex((step) => step.id === id));
|
|
expect(positions.every((index) => index >= 0)).toBe(true);
|
|
expect(positions).toEqual(positions.toSorted((left, right) => left - right));
|
|
for (const index of positions) {
|
|
expect(installed.steps[index]?.if).toContain("inputs.installed_startup_package != ''");
|
|
}
|
|
const prepare = installed.steps[positions[0]!]!;
|
|
if (previousRetirement) {
|
|
expect(prepare.if).toContain(`steps.${previousRetirement}.outcome == 'success'`);
|
|
}
|
|
const retire = installed.steps[positions[4]!]!;
|
|
expect(retire.if).toContain(`steps.native_schtasks_${suffix}.outcome == 'success'`);
|
|
expect(retire.if).toContain(
|
|
`steps.native_cleanup_${suffix}.outputs.owned_package_cleanup == 'true'`,
|
|
);
|
|
expect(retire.if).toContain(`steps.native_cell_proof_${suffix}.outcome == 'success'`);
|
|
previousRetirement = retire.id;
|
|
}
|
|
});
|
|
|
|
it("admits repair workers through the existing native and installed-package owners", () => {
|
|
const { workflow, probe, native } = readWindowsProbe();
|
|
expect(workflow.on.workflow_dispatch.inputs.installed_repair_worker).toMatchObject({
|
|
default: false,
|
|
type: "boolean",
|
|
});
|
|
const isolation = probe.steps.find((step) => step.id === "repair_isolation")!;
|
|
const validation = probe.steps.find((step) => step.id === "startup_input")!;
|
|
const install = probe.steps.find((step) => step.id === "installed_package")!;
|
|
const published = probe.steps.find(
|
|
(step) => step.name === "Install authenticated published repair controllers",
|
|
)!;
|
|
const proof = probe.steps.find(
|
|
(step) => step.name === "Prove installed repair worker compatibility and cleanup",
|
|
)!;
|
|
for (const step of [isolation, validation, install, published, proof]) {
|
|
expect(step).toBeDefined();
|
|
}
|
|
expect(isolation.if).toBe("${{ inputs.installed_repair_worker }}");
|
|
expect(isolation.run).toBe(native.steps[0]?.run);
|
|
expect(probe.steps.indexOf(isolation)).toBeLessThan(probe.steps.indexOf(validation));
|
|
expect(probe.steps.indexOf(validation)).toBeLessThan(probe.steps.indexOf(install));
|
|
expect(probe.steps.indexOf(install)).toBeLessThan(probe.steps.indexOf(proof));
|
|
expect(validation.env).toMatchObject({
|
|
REPAIR_WORKER: "${{ inputs.installed_repair_worker }}",
|
|
WORKFLOW_SHA: "${{ github.workflow_sha }}",
|
|
});
|
|
expect(validation.run).toContain("$env:WORKFLOW_SHA -cne $toolingSha");
|
|
expect(validation.run).toContain('$env:RUNNER_LABEL -ne "windows-2025"');
|
|
expect(validation.run).toContain('$env:KEEPALIVE_MINUTES -ne "0"');
|
|
expect(published.if).toBe("${{ inputs.installed_repair_worker }}");
|
|
expect(published.run).toMatch(
|
|
/\$integrity -cne \$parent\.integrity[\s\S]*npm install --prefix \$prefix/u,
|
|
);
|
|
expect(proof.if).toBe("${{ inputs.installed_repair_worker }}");
|
|
expect(proof.run).toContain("scripts/windows-repair-worker-probe.mjs");
|
|
expect(proof.run).toContain("repair-results.json");
|
|
expect(proof["continue-on-error"]).toBeUndefined();
|
|
});
|
|
|
|
it("retains exact-source native proof and cleanup evidence even on failure", () => {
|
|
const { native } = readWindowsProbe();
|
|
const proof = native.steps.find((step) => step.id === "native_schtasks")!;
|
|
const cleanup = native.steps.find((step) => step.id === "native_cleanup")!;
|
|
const upload = native.steps.find((step) => step.id === "native_proof_upload")!;
|
|
const remove = native.steps.find(
|
|
(step) => step.name === "Remove retained native Scheduled Task evidence",
|
|
)!;
|
|
expect(proof["timeout-minutes"]).toBe(5);
|
|
expect(proof.if).toBe("${{ inputs.run_windows_ci && inputs.installed_startup_package == '' }}");
|
|
expect(proof.env).toMatchObject({
|
|
EXPECTED_HEAD: "${{ inputs.target_ref }}",
|
|
CI_WINDOWS_SCHTASKS_ROOT:
|
|
"${{ runner.temp }}\\openclaw-schtasks-${{ github.run_id }}-${{ github.run_attempt }}",
|
|
CI_WINDOWS_SCHTASKS_TEST_ID: "${{ github.run_id }}-${{ github.run_attempt }}",
|
|
CI_WINDOWS_SCHTASKS_PROOF_PATH:
|
|
"${{ github.workspace }}\\.artifacts\\windows-schtasks\\proof.json",
|
|
});
|
|
expect(proof.run).toContain('if [[ ! "$EXPECTED_HEAD" =~ ^[0-9a-f]{40}$ ]]; then');
|
|
expect(proof.run).toContain('CI_WINDOWS_SCHTASKS_HEAD="$(git rev-parse HEAD)"');
|
|
expect(proof.run).toContain('if [[ "$CI_WINDOWS_SCHTASKS_HEAD" != "$EXPECTED_HEAD" ]]; then');
|
|
expect(proof.run).toContain("export CI_WINDOWS_SCHTASKS_HEAD");
|
|
expect(proof.run).toContain("pnpm test:windows:schtasks:integration");
|
|
const enteredNativeStep = [
|
|
"native_schtasks",
|
|
"native_schtasks_fresh",
|
|
"native_schtasks_9_3",
|
|
"native_schtasks_9_4",
|
|
]
|
|
.map(
|
|
(name) => `contains(fromJSON('["success","failure","cancelled"]'), steps.${name}.outcome)`,
|
|
)
|
|
.join(" || ");
|
|
expect(cleanup.if).toBe(
|
|
"${{ always() && inputs.run_windows_ci && steps.native_isolation.outcome == 'success' && (" +
|
|
enteredNativeStep +
|
|
") }}",
|
|
);
|
|
expect(upload.if).toBe("${{ always() && inputs.run_windows_ci }}");
|
|
expect(cleanup.env).toEqual({
|
|
TEST_ID: proof.env?.CI_WINDOWS_SCHTASKS_TEST_ID,
|
|
TEST_ROOT: proof.env?.CI_WINDOWS_SCHTASKS_ROOT,
|
|
INSTALLED_PACKAGE_MODE: "${{ inputs.installed_startup_package != '' }}",
|
|
});
|
|
expect(remove.env).toEqual({
|
|
TEST_ID: proof.env?.CI_WINDOWS_SCHTASKS_TEST_ID,
|
|
TEST_ROOT: proof.env?.CI_WINDOWS_SCHTASKS_ROOT,
|
|
});
|
|
expect(cleanup.run).toContain('"proof_outcome=${{ steps.native_schtasks.outcome }}"');
|
|
expect(cleanup.run).toContain("schtasks.exe /Delete /F /TN $taskName");
|
|
expect(cleanup.run).toContain('$service = New-Object -ComObject "Schedule.Service"');
|
|
expect(cleanup.run).toContain('throw ($cleanupErrors -join " ")');
|
|
expect(upload.uses).toMatch(/^actions\/upload-artifact@[0-9a-f]{40}$/u);
|
|
expect(upload.with?.path).toContain(".artifacts/windows-schtasks/proof.json");
|
|
expect(upload.with?.path).toContain("windows-schtasks-isolation.json");
|
|
expect(upload.with?.path).toContain("failure-diagnostics.json");
|
|
expect(upload.with?.path).toContain("cleanup-summary.txt");
|
|
expect(upload.with?.path).not.toContain("task-before-cleanup.xml");
|
|
expect(cleanup.run).not.toContain("Copy-Item -LiteralPath $stateDir");
|
|
expect(remove.if).toBe(
|
|
"${{ always() && inputs.run_windows_ci && inputs.installed_startup_package == '' && steps.native_cleanup.outcome == 'success' && steps.native_proof_upload.outcome == 'success' }}",
|
|
);
|
|
const sourceStepOrder = [proof, cleanup, upload, remove].map((step) =>
|
|
native.steps.indexOf(step),
|
|
);
|
|
expect(sourceStepOrder).toEqual(sourceStepOrder.toSorted((left, right) => left - right));
|
|
});
|
|
|
|
it("identifies the producer's exact native probe before emergency process-tree cleanup", () => {
|
|
const { native } = readWindowsProbe();
|
|
const cleanup = native.steps.find((step) => step.id === "native_cleanup")!.run;
|
|
const probe = createGatewayTaskSupervisorProbe("probe-root");
|
|
expect(cleanup).toContain(
|
|
`$probePath = Join-Path $env:TEST_ROOT "${path.basename(probe.probePath)}"`,
|
|
);
|
|
expect(cleanup).toContain('$activePidPath = Join-Path $env:TEST_ROOT "active-pid.txt"');
|
|
expect(cleanup).toContain('$eventsPath = Join-Path $env:TEST_ROOT "runs.txt"');
|
|
expect(cleanup).toContain(
|
|
'$process.CommandLine -like "*$probePath*" -and\n $process.CommandLine -like "*$eventsPath*"',
|
|
);
|
|
expect(cleanup).toContain(
|
|
'throw "Refusing to kill reused or unverifiable process id $probePid."',
|
|
);
|
|
expect(cleanup).toContain("taskkill.exe /F /T /PID $probePid");
|
|
expect(cleanup).toContain("[DateTime]::UtcNow.AddSeconds(30)");
|
|
});
|
|
});
|