openclaw/test/scripts/ci-workflow-guards.test.ts

21413 lines
885 KiB
TypeScript

// Ci Workflow Guards tests cover ci workflow guards script behavior.
import { execFileSync, spawn, spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import {
appendFileSync,
chmodSync,
copyFileSync,
existsSync,
globSync,
mkdirSync,
mkdtempSync,
readdirSync,
readFileSync,
rmSync,
statSync,
symlinkSync,
writeFileSync,
} from "node:fs";
import { connect } from "node:net";
import { devNull, tmpdir } from "node:os";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { runInNewContext } from "node:vm";
import { expectDefined } from "@openclaw/normalization-core";
import { minimatch } from "minimatch";
import { afterEach, describe, expect, it } from "vitest";
import { parse } from "yaml";
import * as qaEvidence from "../../extensions/qa-lab/api.js";
import {
detectChangedScope,
detectNodeFastScope,
shouldRunNativeI18n,
writeGitHubOutput,
} from "../../scripts/ci-changed-scope.mjs";
import { resolveShardPlans, runShardPlans } from "../../scripts/ci-run-node-test-shard.mts";
import { resolveChangedDockerSeedLanes } from "../../scripts/lib/ci-changed-node-test-plan.mts";
import { createNodeTestShardBundles } from "../../scripts/lib/ci-node-test-plan.mts";
import { pnpmLockfileDocuments } from "../../scripts/lib/pnpm-lockfile-documents.mjs";
import { resolveRunVitestSpawnEnv } from "../../scripts/lib/vitest-process-env.mts";
import { NATIVE_I18N_LOCALES } from "../../scripts/native-i18n-locales.ts";
import { resolvePnpmRunner } from "../../scripts/pnpm-runner.mts";
import {
BOUNDARY_CHECKS,
selectChecksForShard,
} from "../../scripts/run-additional-boundary-checks.mts";
import { buildVitestRunPlans } from "../../scripts/test-projects.test-support.mts";
import { resolveTestNodeExecPath } from "../../src/test-utils/node-process.js";
import { createTempDirTracker, useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
import { resolveWorkflowBash } from "../helpers/workflow-bash.js";
import { sharedVitestConfig } from "../vitest/vitest.shared.config.ts";
import {
createUiE2eVitestConfig,
uiE2eRealGatewayTestFiles,
} from "../vitest/vitest.ui-e2e.config.ts";
import { runCiGitStep } from "./ci-git-owner.test-support.js";
import { runDependencyFreePreflight } from "./ci-preflight-dependencies.test-support.js";
import { assertControlUiE2eOwnership } from "./ci-ui-e2e-ownership.test-support.js";
import { runGeneratedPublisherScenario } from "./generated-publisher.test-support.js";
const CHECKOUT_V6 = "actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1";
const CACHE_V5 = "actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9";
const CACHE_SAVE_V5 = "actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9";
const SETUP_GRADLE_V6 = "gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb";
const SETUP_GO_V6 = "actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e";
const UPLOAD_ARTIFACT_V7 = "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a";
const DOWNLOAD_ARTIFACT_V8 = "actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c";
const CREATE_GITHUB_APP_TOKEN_V3 =
"actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1";
const MANTIS_MANUAL_ONLY_WORKFLOWS = [
".github/workflows/mantis-web-ui-chat-proof.yml",
".github/workflows/mantis-discord-status-reactions.yml",
".github/workflows/mantis-discord-thread-attachment.yml",
] as const;
const MANTIS_GITHUB_APP_CLIENT_ID = "Iv23liPJCozR0uHm6P7G";
const OPENGREP_PR_DIFF_WORKFLOW = ".github/workflows/opengrep-precise.yml";
const OPENGREP_FULL_WORKFLOW = ".github/workflows/opengrep-precise-full.yml";
const CONTROL_UI_LOCALE_REFRESH_WORKFLOW = ".github/workflows/control-ui-locale-refresh.yml";
const NATIVE_APP_LOCALE_REFRESH_WORKFLOW = ".github/workflows/native-app-locale-refresh.yml";
const CREATE_GENERATED_PR_TOKENS_ACTION = ".github/actions/create-generated-pr-tokens/action.yml";
const PUBLISH_GENERATED_PR_ACTION = ".github/actions/publish-generated-pr/action.yml";
const SETUP_ANDROID_TOOLCHAIN_ACTION = ".github/actions/setup-android-toolchain/action.yml";
const MATURITY_SCORECARD_WORKFLOW = ".github/workflows/maturity-scorecard.yml";
const MATURITY_SCORECARD_WORKFLOW_REF =
"openclaw/openclaw/.github/workflows/maturity-scorecard.yml@refs/heads/main";
const OIDC_BOUND_MAIN_REUSABLE_WORKFLOWS = new Set<string>();
const AMBIGUOUS_MAIN_PUSH_DIAGNOSTIC =
"::error title=ambiguous main push::github.event.before is zero; refusing to infer a diff base for a created or recreated main branch.";
const AMBIGUOUS_MAIN_PUSH_GUARD = `if [ "$GITHUB_EVENT_NAME" = "push" ] && [[ "$base_sha" =~ ^0+$ ]]; then
echo "${AMBIGUOUS_MAIN_PUSH_DIAGNOSTIC}" >&2
exit 1
fi`;
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
const testNodeExecPath = resolveTestNodeExecPath();
const rootPackageManager = (
JSON.parse(readFileSync("package.json", "utf8")) as {
packageManager: string;
}
).packageManager;
const TSX_IMPORT = import.meta.resolve("tsx");
const TYPESCRIPT_NODE_MODULES = path.dirname(
path.dirname(fileURLToPath(import.meta.resolve("typescript/package.json"))),
);
const MATURITY_GENERATED_PR_PATHS = [
"qa/maturity-scores.yaml",
"docs/maturity/scorecard.md",
"docs/maturity/taxonomy.md",
];
type WorkflowStep = {
"continue-on-error"?: boolean;
env?: Record<string, unknown>;
id?: string;
if?: string;
name?: string;
run?: string;
uses?: string;
with?: Record<string, unknown>;
"working-directory"?: string;
};
const readCiWorkflow = (() => {
// The checked-in workflow is fixed for this suite; clones keep fixture mutations local.
const workflow = parse(readFileSync(".github/workflows/ci.yml", "utf8"));
return () => structuredClone(workflow);
})();
function evaluateWorkflowExpression(
expression: unknown,
context: {
action?: string;
actor?: string;
githubEvent?: Record<string, unknown>;
maintainerCommands?: string;
// Runner routing keys off contributor trust, so pull-request cases default
// to CONTRIBUTOR: same-repo PRs always come from someone with write access.
authorAssociation?: string;
cancelled?: boolean;
dispatchId?: string;
draft?: boolean;
eventName:
| "pull_request"
| "pull_request_target"
| "issue_comment"
| "issues"
| "push"
| "workflow_dispatch"
| "repository_dispatch"
| "schedule";
failed?: boolean;
env?: Record<string, string>;
frozenTarget?: boolean;
fileHashes?: Record<string, string>;
headRepository?: string;
headSha?: string;
hostedRunnerProfileContract?: boolean;
matrix?: Record<string, unknown>;
preflightOutputs?: Record<string, string>;
pullRequestNumber?: number;
ref?: string;
resolveTargetOutputs?: Record<string, string>;
releaseGate?: boolean;
releaseScope?: string;
repository: string;
runCheck?: boolean;
runnerBackend?: "" | "blacksmith" | "github" | "hybrid";
runnerEnvironment?: "" | "github-hosted" | "self-hosted";
runnerProfile?: "blacksmith" | "github" | "hybrid";
runAttempt: number;
runId?: number;
runNumber?: number;
sha?: string;
steps?: Record<
string,
{ outputs: Record<string, string>; outcome?: "success" | "failure" | "cancelled" | "skipped" }
>;
targetContextRef?: string;
targetRef?: string;
useGithubHostedRunners?: boolean;
workflow?: string;
workflowSha?: string;
workflowToken?: string;
workspace?: string;
},
) {
if (typeof expression !== "string") {
throw new TypeError("workflow expression must be a string");
}
const match = expression.match(/^\$\{\{\s*([\s\S]*?)\s*\}\}$/u);
if (!match) {
throw new Error(`invalid workflow expression: ${expression}`);
}
const source = match[1];
if (source === undefined) {
throw new Error(`workflow expression has no body: ${expression}`);
}
// Actions permits dashes in property names; preserve quoted literals while
// translating those accesses for the JavaScript fixture evaluator.
const evaluableSource = source.replace(
/'(?:[^']|'')*'|\.([A-Za-z_][\w-]*)/gu,
(token: string, property: string | undefined) =>
property?.includes("-") ? `[${JSON.stringify(property)}]` : token,
);
return runInNewContext(evaluableSource, {
always: () => true,
failure: () => context.failed ?? false,
cancelled: () => context.cancelled ?? false,
// GitHub expression builtins the runner-routing clauses use.
contains: (haystack: unknown, needle: unknown) =>
Array.isArray(haystack)
? haystack.includes(needle)
: String(haystack).includes(String(needle)),
endsWith: (value: unknown, suffix: unknown) =>
String(value).toLowerCase().endsWith(String(suffix).toLowerCase()),
fromJSON: (value: string) => JSON.parse(value) as unknown,
format: (value: string, ...args: unknown[]) =>
value.replace(/\{(\d+)\}/gu, (_match, index: string) => String(args[Number(index)])),
hashFiles: (file: string) => context.fileHashes?.[file] ?? "",
startsWith: (value: unknown, prefix: unknown) => String(value).startsWith(String(prefix)),
toJson: (value: unknown) => JSON.stringify(value),
github: {
actor: context.actor ?? "",
event_name: context.eventName,
repository: context.repository,
ref: context.ref ?? "refs/heads/main",
run_attempt: context.runAttempt,
run_id: context.runId,
run_number: context.runNumber,
sha: context.sha,
workflow: context.workflow,
workflow_sha: context.workflowSha,
workspace: context.workspace,
token: context.workflowToken,
event:
context.githubEvent ??
(context.headRepository || context.eventName === "pull_request"
? {
action: context.action,
pull_request: {
author_association: context.authorAssociation ?? "CONTRIBUTOR",
draft: context.draft ?? false,
number: context.pullRequestNumber,
head: {
sha: context.headSha,
repo: { full_name: context.headRepository ?? context.repository },
},
},
}
: {}),
},
inputs: {
dispatch_id: context.dispatchId ?? "",
release_gate: context.releaseGate ?? false,
release_scope: context.releaseScope ?? "full",
target_context_ref: context.targetContextRef ?? "",
target_ref: context.targetRef ?? "",
use_github_hosted_runners: context.useGithubHostedRunners ?? false,
},
env: context.env ?? {},
matrix: context.matrix ?? {},
runner: { environment: context.runnerEnvironment ?? "" },
steps: context.steps ?? {},
needs: {
resolve_target: { outputs: context.resolveTargetOutputs ?? {} },
preflight: {
outputs: {
frozen_target: String(context.frozenTarget ?? false),
hosted_runner_profile_contract: String(context.hostedRunnerProfileContract ?? true),
run_check: String(context.runCheck ?? true),
runner_profile: context.runnerProfile ?? context.runnerBackend ?? "blacksmith",
...context.preflightOutputs,
},
},
},
vars: {
MAINTAINER_COMMAND_REACTIONS: context.maintainerCommands ?? "",
OPENCLAW_CI_RUNNER_BACKEND: context.runnerBackend ?? "",
},
});
}
function runCiGateFixture(jobResults: string) {
const gateStep = readCiWorkflow().jobs["ci-gate"].steps.find(
(step: WorkflowStep) => step.name === "Verify selected CI lanes",
);
// Homebrew Bash 5.3 can block writing this here-string before its reader starts.
const bash = process.platform === "darwin" ? "/bin/bash" : "bash";
return spawnSync(bash, ["-c", gateStep.run], {
encoding: "utf8",
env: {
...process.env,
JOB_RESULTS: jobResults,
},
});
}
function renderCiGateEnvironment(
context: Partial<Parameters<typeof evaluateWorkflowExpression>[1]> = {},
results: Record<string, string> = {},
) {
const workflow = readCiWorkflow();
const step = workflow.jobs["ci-gate"].steps.find(
(candidate: WorkflowStep) => candidate.name === "Verify selected CI lanes",
);
const preflightOutputs = {
...Object.fromEntries(
Object.keys(workflow.jobs.preflight.outputs)
.filter((key) => key.startsWith("run_"))
.map((key) => [key, "true"]),
),
compatibility_target: "false",
release_scope: "full",
...context.preflightOutputs,
};
const jobResults: string = step.env.JOB_RESULTS;
return jobResults.replace(/\$\{\{[\s\S]*?\}\}/gu, (expression) => {
const result = expression.match(/^\$\{\{\s*needs\.([\w-]+)\.result\s*\}\}$/u);
if (result) {
return results[expectDefined(result[1], expression)] ?? "success";
}
return String(
evaluateWorkflowExpression(expression, {
eventName: "workflow_dispatch",
repository: "openclaw/openclaw",
runAttempt: 1,
...context,
preflightOutputs,
}) ?? "",
);
});
}
function quoteShell(value: string): string {
return `'${value.replaceAll("'", `'"'"'`)}'`;
}
function runPreflightNodeInvocation(
script: string,
options: {
checkoutRevision: string;
eventName: "pull_request" | "push" | "workflow_dispatch";
workflowRevision: string;
},
) {
const root = tempDirs.make("openclaw-preflight-runtime-");
const binDir = path.join(root, "bin");
const argsPath = path.join(root, "node-args");
mkdirSync(binDir, { recursive: true });
const nodePath = path.join(binDir, "node");
writeFileSync(
nodePath,
'#!/bin/sh\nprintf \'%s\\n\' "$@" > "$OPENCLAW_NODE_ARGS"\ncat >/dev/null\n',
);
chmodSync(nodePath, 0o755);
// Avoid Darwin Bash 5.3 heredoc deadlocks while preserving the PATH node spy.
const bash = process.platform === "darwin" ? "/bin/bash" : "bash";
const result = spawnSync(bash, ["-c", script], {
encoding: "utf8",
env: {
...process.env,
GITHUB_EVENT_NAME: options.eventName,
OPENCLAW_CI_CHECKOUT_REVISION: options.checkoutRevision,
OPENCLAW_CI_WORKFLOW_REVISION: options.workflowRevision,
OPENCLAW_NODE_ARGS: argsPath,
PATH: `${binDir}${path.delimiter}${process.env.PATH ?? ""}`,
},
});
expect(result.status, `${result.stdout}${result.stderr}`).toBe(0);
return readFileSync(argsPath, "utf8").trim().split("\n");
}
let linuxWorkflowBash: string | undefined;
function runWorkflowShellScript(
script: string,
options: { cwd?: string; env?: NodeJS.ProcessEnv; linuxWorkflow?: boolean; tempDir?: string },
) {
const { linuxWorkflow, tempDir, ...spawnOptions } = options;
const root = mkdtempSync(path.join(tmpdir(), "openclaw-workflow-shell-"));
const childTempDir = tempDir ?? root;
const modulePaths: string[] = [];
try {
let moduleIndex = 0;
const moduleRoot = options.cwd ?? process.cwd();
const rewritten = script
.replace(
/node (?:(--import tsx |"\$\{manifest_node_args\[@\]\}" ))?--input-type=module <<'([A-Z][A-Z0-9_]*)'\n([\s\S]*?)\n\2(?=\n|$)/gu,
(_match, nodeOptions: string | undefined, _marker: string, body: string) => {
const modulePath = path.join(
moduleRoot,
`.openclaw-${path.basename(root)}-${moduleIndex}.mjs`,
);
moduleIndex += 1;
modulePaths.push(modulePath);
writeFileSync(modulePath, `${body}\n`, "utf8");
const loader =
nodeOptions === "--import tsx "
? `--import ${quoteShell(TSX_IMPORT)} `
: (nodeOptions ?? "");
return `${quoteShell(testNodeExecPath)} ${loader}${quoteShell(modulePath)}`;
},
)
.replaceAll(
"manifest_node_args+=(--import tsx)",
`manifest_node_args+=(--import ${quoteShell(TSX_IMPORT)})`,
);
const scriptPath = path.join(root, "run.sh");
writeFileSync(scriptPath, rewritten.endsWith("\n") ? rewritten : `${rewritten}\n`, "utf8");
const bash =
linuxWorkflow && process.platform === "darwin"
? (linuxWorkflowBash ??= resolveWorkflowBash())
: "bash";
return spawnSync(bash, [scriptPath], {
...spawnOptions,
encoding: "utf8",
// Child caches and temporary artifacts share the fixture's cleanup owner.
// Inheriting a huge host tsx cache makes startup depend on unrelated runs.
env: {
...(options.env ?? process.env),
TMPDIR: childTempDir,
TMP: childTempDir,
TEMP: childTempDir,
},
});
} finally {
for (const modulePath of modulePaths) {
rmSync(modulePath, { force: true });
}
rmSync(root, { force: true, recursive: true });
}
}
function workflowOccurrenceEvidence(
instances: {
scenarioId: string;
attempts: qaEvidence.QaEvidenceStatus[];
selected?: number;
retry?: boolean;
}[],
evidenceMode: "full" | "slim" = "full",
) {
const identity = {
source: { ref: null, integrity: null },
runtime: { id: null, version: null },
package: null,
protocol: null,
accountRef: null,
proofClass: null,
};
const occurrences: qaEvidence.QaEvidenceOccurrence[] = [];
const entries: qaEvidence.QaEvidenceSummaryV3Entry[] = [];
for (const [index, instance] of instances.entries()) {
const anchorId = `instance-${index}`;
const parentCell = {
scenarioId: instance.scenarioId,
executionKind: "flow" as const,
channel: null,
};
occurrences.push({
id: anchorId,
parentCell,
scenario: {
kind: "instance",
resultOccurrenceId:
instance.selected === undefined ? null : `${anchorId}-attempt-${instance.selected}`,
},
retryOf: null,
terminalStatus: null,
assertions: null,
launch: identity,
receipts: [],
});
for (const [attempt, status] of instance.attempts.entries()) {
const id = `${anchorId}-attempt-${attempt}`;
occurrences.push({
id,
parentCell,
scenario: { kind: "observation", instanceOccurrenceId: anchorId },
retryOf:
attempt === 0 || instance.retry === false ? null : `${anchorId}-attempt-${attempt - 1}`,
terminalStatus: status,
assertions: null,
launch: identity,
receipts: [],
});
entries.push({
test: { kind: "scenario", id: instance.scenarioId, title: instance.scenarioId },
coverage: [],
result: { status },
binding: { occurrenceId: id, assertionId: null, receiptId: null },
effective: instance.retry === false || attempt === instance.selected,
});
}
}
return qaEvidence.buildQaOccurrenceEvidenceSummary({
generatedAt: "2026-08-05T00:00:00.000Z",
evidenceMode,
occurrences,
entries,
});
}
function writeWorkflowEvidenceApi(root: string, accessors = true) {
const apiPath = path.join(root, "extensions/qa-lab/api.ts");
mkdirSync(path.dirname(apiPath), { recursive: true });
writeFileSync(
path.join(root, "tsconfig.json"),
JSON.stringify({ extends: path.resolve("tsconfig.json") }),
);
const source = pathToFileURL(path.resolve("extensions/qa-lab/src/evidence-summary.ts")).href;
// Historical checkouts can validate v2 without exporting the v3 readers.
writeFileSync(
apiPath,
`export { validateQaEvidenceSummaryJson${accessors ? ", getEffectiveQaEvidenceEntries, projectQaEvidenceScenarioOutcomes" : ""} } from ${JSON.stringify(source)};\n`,
);
}
function runMantisEvidenceReader(
kind: "status-reactions" | "thread-attachment",
evidence: unknown,
accessors = true,
) {
const root = tempDirs.make("openclaw-mantis-reader-");
const laneRoot = path.join(root, "lanes/baseline");
const outputDir = path.join(root, "evidence/baseline");
mkdirSync(outputDir, { recursive: true });
const moduleRoot = kind === "status-reactions" ? root : laneRoot;
writeWorkflowEvidenceApi(moduleRoot, accessors);
// A reader must not silently use a newer neighboring checkout's API.
const otherRoot = kind === "status-reactions" ? laneRoot : root;
writeWorkflowEvidenceApi(otherRoot);
writeFileSync(
path.join(otherRoot, "extensions/qa-lab/api.ts"),
'throw new Error("wrong evidence API owner");\n',
);
if (evidence !== undefined) {
writeFileSync(
path.join(outputDir, "qa-evidence.json"),
typeof evidence === "string" ? evidence : JSON.stringify(evidence),
);
}
const scenarioId =
kind === "status-reactions"
? "discord-status-reactions-tool-only"
: "discord-thread-reply-filepath-attachment";
writeFileSync(
path.join(outputDir, "discord-qa-summary.json"),
JSON.stringify({ scenarios: [{ id: scenarioId, status: "pass" }] }),
);
const workflow = readWorkflow(`.github/workflows/mantis-discord-${kind}.yml`);
const job = kind === "status-reactions" ? "run_status_reactions" : "run_thread_attachment";
const script = expectDefined(
workflow.jobs[job].steps.find((step: WorkflowStep) => step.id === "run_mantis")?.run,
"Mantis run script",
);
const functionName =
kind === "status-reactions"
? "read_discord_status_reaction_status"
: "read_discord_thread_attachment_status";
const start = script.indexOf(`${functionName}()`);
const end = script.indexOf("\nbaseline_status=", start);
expect(start).toBeGreaterThanOrEqual(0);
expect(end).toBeGreaterThan(start);
return runWorkflowShellScript(
`set -euo pipefail\nroot=${quoteShell(path.join(root, "evidence"))}\nworktree_root=lanes\n${script.slice(start, end)}\n${functionName} baseline\n`,
{ cwd: root, env: { ...process.env, GITHUB_WORKSPACE: root } },
);
}
function runCiChangedScopeFixture(changedPaths: string[]): Record<string, string> {
const outputPath = path.join(tempDirs.make("openclaw-ci-scope-"), "scope.out");
writeGitHubOutput(
detectChangedScope(changedPaths),
outputPath,
undefined,
detectNodeFastScope(changedPaths),
shouldRunNativeI18n(changedPaths),
changedPaths,
);
return Object.fromEntries(
readFileSync(outputPath, "utf8")
.trim()
.split("\n")
.map((line) => {
const separator = line.indexOf("=");
return [line.slice(0, separator), line.slice(separator + 1)];
}),
);
}
function runCiManifestFixture(options: {
bundledPlanner: boolean;
nodeTestShards?: Record<string, unknown>[];
nodeTestGroupsCodec?: boolean;
startupCorpusCoverage?: boolean;
changedPlannerSource?: string | null;
changedPlannerDependencies?: string[];
changedPaths?: string[] | null;
changedCoreTestSupport?: boolean;
repository?: string;
eventName?: "pull_request" | "push" | "workflow_dispatch";
historicalCompatibility?: boolean;
iosCapabilities?: boolean;
iosBuildCapability?: boolean;
androidCiCapabilities?: boolean;
nativeI18nCapabilities?: boolean;
macosNodeParts?: boolean;
openClawKitTests?: boolean;
protocolCoverage?: boolean;
packageVersion?: string;
qaSmokePlan?: boolean;
formatCheck?: boolean;
releaseCandidateCompatibility?: boolean;
releaseGate?: boolean;
targetContextCompatibility?: boolean;
nodeFastOnly?: boolean;
nodeFastPluginContracts?: boolean;
nodeFastCiRouting?: boolean;
runNode?: boolean;
historicalReader?: boolean;
runnerBackend?: "blacksmith" | "github" | "hybrid";
runnerProfile?: "blacksmith" | "github" | "hybrid";
targetHostedRunnerProfileContract?: boolean;
uiE2eProjectsCapability?: boolean;
remoteTagRefs?: Record<string, string>;
scopeEnv?: Record<string, string>;
}) {
const root = mkdtempSync(path.join(tmpdir(), "openclaw-ci-manifest-"));
try {
const scriptsDir = path.join(root, "scripts", "lib");
mkdirSync(scriptsDir, { recursive: true });
for (const dependency of options.changedPlannerDependencies ?? []) {
const destination = path.join(root, dependency);
mkdirSync(path.dirname(destination), { recursive: true });
writeFileSync(destination, readFileSync(dependency));
}
// The manifest packs grouped Node rows through the target's codec and the
// shard runner unpacks them; targets that predate the codec omit it.
if (options.nodeTestGroupsCodec ?? true) {
writeFileSync(
path.join(scriptsDir, "ci-node-test-groups-codec.mts"),
readFileSync("scripts/lib/ci-node-test-groups-codec.mts"),
);
}
writeFileSync(
path.join(scriptsDir, "ci-node-test-plan.mts"),
options.nodeTestShards
? `export const createNodeTestShards = () => ${JSON.stringify(options.nodeTestShards)};
export const createNodeTestShardBundles = createNodeTestShards;`
: options.bundledPlanner
? `
export const createNodeTestShards = () => [{
checkName: "legacy-node-plan",
configs: ["test/vitest/legacy.config.ts"],
requiresDist: false,
runner: "ubuntu-24.04",
shardName: "legacy-node-plan",
}];
export const createNodeTestShardBundles = (options = {}) => [{
checkName: "bundled-node-plan",
configs: ["test/vitest/bundled.config.ts"],
includePatterns: options.changedPaths,
env: {
OPENCLAW_CI_TEST_COMPACT_MODE: options.compactMode ?? "full",
OPENCLAW_CI_TEST_RUNNER_BACKEND: options.runnerBackend ?? "",
},
requiresDist: false,
runner: "ubuntu-24.04",
shardName: "bundled-node-plan",
}];
`
: `
export const createNodeTestShards = () => [{
checkName: "legacy-node-plan",
configs: ["test/vitest/legacy.config.ts"],
requiresDist: false,
runner: "ubuntu-24.04",
shardName: "legacy-node-plan",
}];
`,
"utf8",
);
if (options.startupCorpusCoverage) {
appendFileSync(
path.join(scriptsDir, "ci-node-test-plan.mts"),
`\nexport { hasCompleteStartupCorpusCoverage } from ${JSON.stringify(pathToFileURL(path.resolve("scripts/lib/ci-node-test-plan.mts")).href)};\n`,
);
}
if (options.changedCoreTestSupport) {
for (const file of [
"scripts/changed-lanes.mts",
"scripts/lib/changed-path-facts.mjs",
"scripts/lib/release-changelog.mjs",
"scripts/lib/arg-utils.mts",
"scripts/lib/arg-utils.runtime.mjs",
"scripts/lib/direct-run.mjs",
"scripts/lib/merge-head-diff-base.mjs",
"scripts/lib/record-shared.mjs",
"packages/normalization-core/src/stable-stringify.ts",
"scripts/run-tsgo-core-test-shards.mts",
"scripts/run-additional-boundary-checks.mts",
]) {
const target = path.join(root, file);
mkdirSync(path.dirname(target), { recursive: true });
writeFileSync(target, readFileSync(file));
}
}
const iosCapabilities = options.iosCapabilities ?? options.bundledPlanner;
const iosBuildCapability = options.iosBuildCapability ?? iosCapabilities;
const nativeI18nCapabilities = options.nativeI18nCapabilities ?? options.bundledPlanner;
const macosNodeParts = options.macosNodeParts ?? options.bundledPlanner;
const packageScripts = options.bundledPlanner
? {
...(nativeI18nCapabilities
? {
"android:i18n:check": "true",
"apple:i18n:check": "true",
"native:i18n:check": "true",
}
: {}),
...(iosBuildCapability ? { "ios:build": "true" } : {}),
...(macosNodeParts
? Object.fromEntries([1, 2, 3].map((part) => [`test:macos:ci:${part}`, "true"]))
: {}),
"check:assertion-safety": "true",
"check:max-lines-ratchet": "true",
}
: {};
writeFileSync(
path.join(root, "package.json"),
`${JSON.stringify({ version: options.packageVersion, scripts: packageScripts })}\n`,
);
if (options.bundledPlanner && options.changedPlannerSource !== null) {
writeFileSync(
path.join(scriptsDir, "ci-changed-node-test-plan.mts"),
options.changedPlannerSource ??
`
export const createChangedNodeTestShards = (changedPaths) =>
changedPaths.includes("src/focused.ts") ||
changedPaths.includes("test/scripts/sqlite-sessions-transcripts-flip-proof.built-cli.e2e.test.ts")
? [{
checkName: "changed-node-plan",
configs: [],
requiresDist: false,
runner: "ubuntu-24.04",
shardName: "changed-node-plan",
targets: changedPaths.includes("src/focused.ts")
? ["src/focused.test.ts"]
: ["test/scripts/sqlite-sessions-transcripts-flip-proof.built-cli.e2e.test.ts"],
}]
: null;
export const createChangedExtensionFallbackShards = (changedPaths) =>
changedPaths.some((changedPath) => changedPath.startsWith("extensions/"))
? changedPaths.some((changedPath) => changedPath.startsWith("extensions/matrix/"))
? [{
checkName: "changed-extension-fallback-plan",
configs: ["test/vitest/vitest.extension-matrix.config.ts"],
includePatterns: [
"extensions/matrix/src/client.test.ts",
"extensions/matrix/src/monitor.test.ts",
],
requiresDist: false,
runner: "ubuntu-24.04",
shardName: "changed-extension-fallback-plan",
predictedSeconds: 120,
}]
: [{
checkName: "changed-extension-fallback-plan",
configs: [],
requiresDist: false,
runner: "ubuntu-24.04",
shardName: "changed-extension-fallback-plan",
predictedSeconds: 120,
targets: ["extensions/codex/src/focused.test.ts"],
}]
: [];
export const hasBuildArtifactAffectingChange = (changedPaths) =>
!changedPaths.includes("test/scripts/sqlite-sessions-transcripts-flip-proof.built-cli.e2e.test.ts");
export const hasSqliteSessionLifecycleAffectingChange = (changedPaths) =>
changedPaths.includes("src/sqlite-session-owner.ts") ||
changedPaths.includes("test/scripts/sqlite-sessions-transcripts-flip-proof.built-cli.e2e.test.ts");
export const resolveChangedDockerSeedLanes = (changedPaths) => changedPaths.includes("scripts/e2e/docker-openai-seed.ts") ? ["mcp-channels", "cron-mcp-cleanup"] : [];
`,
"utf8",
);
}
if (options.bundledPlanner) {
const sqliteLifecycleProof = path.join(
root,
"test/scripts/sqlite-sessions-transcripts-flip-proof.built-cli.e2e.test.ts",
);
mkdirSync(path.dirname(sqliteLifecycleProof), { recursive: true });
writeFileSync(sqliteLifecycleProof, "export {};\n");
writeFileSync(
path.join(scriptsDir, "channel-contract-test-plan.mts"),
`export const createChannelContractTestShards = () => ["a", "b"].map((suffix) => ({
checkName: "channel-contracts-" + suffix,
includePatterns: ["src/channels/plugins/contracts/fixture-" + suffix + ".test.ts"],
runtime: "node",
task: "contracts-channels",
}));\n`,
);
writeFileSync(
path.join(scriptsDir, "plugin-contract-test-plan.mts"),
`export const createPluginContractTestShards = () => ["a", "b"].map((suffix) => ({
checkName: "plugin-contracts-" + suffix,
includePatterns: ["src/plugins/contracts/fixture-" + suffix + ".test.ts"],
runtime: "node",
task: "contracts-plugins",
}));\n`,
);
}
if (options.qaSmokePlan ?? options.bundledPlanner) {
const smokePlan = path.join(root, "extensions", "qa-lab", "src", "ci-smoke-plan.ts");
mkdirSync(path.dirname(smokePlan), { recursive: true });
writeFileSync(smokePlan, "export {};\n");
}
if (iosCapabilities) {
for (const name of [
"install-swift-tools.sh",
"install-xcodegen.sh",
"lint-swift.sh",
"format-swift.sh",
]) {
writeFileSync(path.join(root, "scripts", name), "#!/bin/sh\n");
}
}
if (options.protocolCoverage ?? options.bundledPlanner) {
writeFileSync(path.join(root, "scripts", "check-protocol-event-coverage.mjs"), "");
}
const targetWorkflow = path.join(root, ".github", "workflows", "ci.yml");
mkdirSync(path.dirname(targetWorkflow), { recursive: true });
writeFileSync(
targetWorkflow,
[
...((options.formatCheck ?? options.bundledPlanner)
? ["pnpm format:check", "pnpm format:check"]
: []),
...((options.androidCiCapabilities ?? options.bundledPlanner)
? ["android-ci-contract-v2"]
: []),
...((options.openClawKitTests ?? options.bundledPlanner)
? ["openclawkit-tests-contract-v1"]
: []),
...(options.bundledPlanner ? ["docker-seed-e2e-contract-v1"] : []),
...((options.targetHostedRunnerProfileContract ?? options.bundledPlanner)
? ["hosted-runner-profile-contract-v1"]
: []),
].join("\n"),
);
const uiE2eConfig = path.join(root, "test", "vitest", "vitest.ui-e2e.config.ts");
mkdirSync(path.dirname(uiE2eConfig), { recursive: true });
writeFileSync(
uiE2eConfig,
(options.uiE2eProjectsCapability ?? options.bundledPlanner)
? "// ui-e2e-projects-contract-v1\n"
: 'export default { test: { name: "ui-e2e" } };\n',
);
const outputPath = path.join(root, "manifest.out");
const summaryPath = path.join(root, "summary.md");
const gitOwner = ".github/actions/git-owner";
const trustedGitOwner = path.join(root, ".ci-harness", gitOwner);
mkdirSync(trustedGitOwner, { recursive: true });
for (const name of ["test-prerequisites.mjs", "test-prerequisites.json"]) {
writeFileSync(path.join(trustedGitOwner, name), readFileSync(path.join(gitOwner, name)));
}
const trustedReleasePolicy = path.join(root, ".ci-harness/scripts/lib");
mkdirSync(trustedReleasePolicy, { recursive: true });
for (const name of ["release-context.mjs", "release-version.mjs"]) {
writeFileSync(path.join(trustedReleasePolicy, name), readFileSync(`scripts/lib/${name}`));
}
const fixtureBin = path.join(root, "bin");
let correctionBaseSha = "";
if (options.remoteTagRefs) {
mkdirSync(fixtureBin);
const ghFixture = path.join(root, "gh.mjs");
writeFileSync(
ghFixture,
`
const [command, endpoint, queryFlag, query] = process.argv.slice(2);
const baseRef = ${JSON.stringify(`refs/tags/v${options.packageVersion}`)};
if (process.env.GH_TOKEN !== "test-token" || command !== "api" ||
endpoint !== "repos/openclaw/openclaw/commits/" + encodeURIComponent(baseRef) ||
queryFlag !== "--jq" || query !== ".sha") {
throw new Error("Expected authenticated, fully qualified correction base lookup");
}
const refs = ${JSON.stringify(options.remoteTagRefs)};
if (!refs[baseRef]) throw new Error("gh: Not Found (HTTP 404)");
process.stdout.write((refs[baseRef + "^{}"] ?? refs[baseRef]) + "\\n");
`,
);
writeExecutable(path.join(fixtureBin, "gh"), [
"#!/bin/sh",
`exec ${quoteShell(process.execPath)} ${quoteShell(ghFixture)} "$@"`,
]);
writeExecutable(path.join(fixtureBin, "git"), [
"#!/bin/sh",
"echo 'Anonymous Git transport is unavailable' >&2",
"exit 128",
]);
const correctionStep = expectDefined(
readCiWorkflow().jobs.preflight.steps.find(
(step: WorkflowStep) => step.name === "Resolve release correction base",
),
"trusted correction base producer",
);
const correctionOutput = path.join(root, "correction.out");
writeFileSync(correctionOutput, "");
const correction = runWorkflowShellScript(correctionStep.run, {
cwd: root,
env: {
PATH: `${fixtureBin}${path.delimiter}${process.env.PATH ?? ""}`,
GH_TOKEN: correctionStep.env?.GH_TOKEN === "${{ github.token }}" ? "test-token" : "",
GITHUB_REPOSITORY: "openclaw/openclaw",
GITHUB_OUTPUT: correctionOutput,
TARGET_CONTEXT_REF:
options.scopeEnv?.OPENCLAW_CI_TARGET_CONTEXT_TARGET === "true"
? options.scopeEnv.OPENCLAW_CI_TARGET_CONTEXT_REF
: options.scopeEnv?.OPENCLAW_CI_HISTORICAL_TARGET_TAG,
},
});
if (correction.status !== 0) {
return {
output: `${correction.stdout}${correction.stderr}`,
outputs: {} as Record<string, string>,
status: correction.status,
summary: "",
};
}
correctionBaseSha = readWorkflowOutputs(correctionOutput).sha ?? "";
}
if (options.historicalReader) {
const reader = path.join(root, "src/audit/message-delivery-progress-store.test.ts");
mkdirSync(path.dirname(reader), { recursive: true });
writeFileSync(reader, "export {};\n");
}
writeFileSync(outputPath, "", "utf8");
writeFileSync(summaryPath, "", "utf8");
const manifestStep = readCiWorkflow().jobs.preflight.steps.find(
(step: { name?: string }) => step.name === "Build CI manifest",
);
const run = runWorkflowShellScript(manifestStep.run, {
cwd: root,
env: {
...process.env,
GH_TOKEN: "",
GITHUB_TOKEN: "",
GITHUB_OUTPUT: outputPath,
GITHUB_RUN_ATTEMPT: "1",
GITHUB_EVENT_NAME: options.eventName ?? "workflow_dispatch",
GITHUB_STEP_SUMMARY: summaryPath,
RUNNER_TEMP: root,
PATH: options.remoteTagRefs
? `${fixtureBin}${path.delimiter}${process.env.PATH ?? ""}`
: process.env.PATH,
OPENCLAW_CI_CHANGED_PATHS_JSON:
options.changedPaths === undefined ? undefined : JSON.stringify(options.changedPaths),
OPENCLAW_CI_CHECKOUT_REVISION: "a".repeat(40),
OPENCLAW_CI_CORRECTION_BASE_SHA: correctionBaseSha,
OPENCLAW_CI_DOCS_CHANGED: "true",
OPENCLAW_CI_DOCS_ONLY: "false",
OPENCLAW_CI_EVENT_NAME: options.eventName ?? "workflow_dispatch",
OPENCLAW_CI_HISTORICAL_TARGET:
(options.historicalCompatibility ?? true) &&
(options.eventName ?? "workflow_dispatch") === "workflow_dispatch"
? "true"
: "false",
OPENCLAW_CI_RELEASE_GATE: String(options.releaseGate ?? false),
OPENCLAW_CI_RELEASE_CANDIDATE_TARGET:
options.releaseCandidateCompatibility === true ? "true" : "false",
OPENCLAW_CI_TARGET_CONTEXT_TARGET:
options.targetContextCompatibility === true ? "true" : "false",
OPENCLAW_CI_REPOSITORY: options.repository ?? "openclaw/openclaw",
OPENCLAW_CI_RUN_ANDROID: "true",
OPENCLAW_CI_RUN_CONTROL_UI_I18N: "true",
OPENCLAW_CI_RUN_IOS_BUILD: "true",
OPENCLAW_CI_RUN_MACOS: "true",
OPENCLAW_CI_RUN_NATIVE_I18N: "true",
OPENCLAW_CI_RUN_NODE: String(options.runNode ?? true),
OPENCLAW_CI_RUN_NODE_FAST_CI_ROUTING: String(options.nodeFastCiRouting ?? false),
OPENCLAW_CI_RUN_NODE_FAST_ONLY: String(options.nodeFastOnly ?? false),
OPENCLAW_CI_RUN_NODE_FAST_PLUGIN_CONTRACTS: String(
options.nodeFastPluginContracts ?? false,
),
OPENCLAW_CI_AUTHOR_ASSOCIATION: "CONTRIBUTOR",
OPENCLAW_CI_HEAD_REPOSITORY: options.repository ?? "openclaw/openclaw",
OPENCLAW_CI_RUNNER_BACKEND: options.runnerBackend ?? options.runnerProfile ?? "",
OPENCLAW_CI_RUNNER_PROFILE: options.runnerProfile ?? options.runnerBackend ?? "blacksmith",
OPENCLAW_CI_RUN_SKILLS_PYTHON: "true",
OPENCLAW_CI_RUN_WINDOWS: "true",
OPENCLAW_CI_WORKFLOW_REVISION: "b".repeat(40),
...options.scopeEnv,
},
});
const outputs = Object.fromEntries(
readFileSync(outputPath, "utf8")
.trim()
.split("\n")
.map((line) => {
const separator = line.indexOf("=");
return [line.slice(0, separator), line.slice(separator + 1)];
}),
);
return {
output: `${run.stdout}${run.stderr}`,
outputChars: readFileSync(outputPath, "utf8").length,
outputs,
status: run.status,
summary: readFileSync(summaryPath, "utf8"),
};
} finally {
rmSync(root, { force: true, recursive: true });
}
}
const readFrozenAdditionalCheckRows = (() => {
let rows: Array<{ check_name: string; group: string; runner: string }> | undefined;
return () => {
if (!rows) {
const manifest = runCiManifestFixture({
bundledPlanner: true,
eventName: "workflow_dispatch",
historicalCompatibility: true,
changedPaths: [],
scopeEnv: {
OPENCLAW_CI_CHECKOUT_REVISION: "a".repeat(40),
OPENCLAW_CI_WORKFLOW_REVISION: "b".repeat(40),
},
});
expect(manifest.status, manifest.output).toBe(0);
rows = JSON.parse(
expectDefined(manifest.outputs.check_additional_matrix, "additional check matrix"),
).include;
}
return structuredClone(expectDefined(rows, "frozen additional check rows"));
};
})();
function runRunnerProfileFixture(options: {
authorAssociation?: string;
configuredProfile?: string;
eventName: "pull_request" | "push" | "workflow_dispatch";
headRepository?: string;
repository?: string;
runAttempt?: number;
targetSupportsContract: boolean;
}) {
const root = mkdtempSync(path.join(tmpdir(), "openclaw-ci-runner-profile-"));
try {
const workflowPath = path.join(root, ".github", "workflows", "ci.yml");
mkdirSync(path.dirname(workflowPath), { recursive: true });
writeFileSync(
workflowPath,
options.targetSupportsContract ? "hosted-runner-profile-contract-v1\n" : "name: legacy\n",
"utf8",
);
const outputPath = path.join(root, "profile.out");
writeFileSync(outputPath, "", "utf8");
const step = expectDefined(
readCiWorkflow().jobs.preflight.steps.find(
(candidate: WorkflowStep) => candidate.name === "Resolve logical runner profile",
),
"logical runner profile preflight step",
);
const result = runWorkflowShellScript(expectDefined(step.run, "runner profile script"), {
cwd: root,
env: {
...process.env,
AUTHOR_ASSOCIATION: options.authorAssociation ?? "",
CONFIGURED_RUNNER_PROFILE: options.configuredProfile ?? "",
GITHUB_EVENT_NAME: options.eventName,
GITHUB_OUTPUT: outputPath,
GITHUB_REPOSITORY: options.repository ?? "openclaw/openclaw",
HEAD_REPOSITORY: options.headRepository ?? options.repository ?? "openclaw/openclaw",
GITHUB_RUN_ATTEMPT: String(options.runAttempt ?? 1),
},
});
const outputs = Object.fromEntries(
readFileSync(outputPath, "utf8")
.trim()
.split("\n")
.filter(Boolean)
.map((line) => {
const separator = line.indexOf("=");
return [line.slice(0, separator), line.slice(separator + 1)];
}),
);
return { output: `${result.stdout}${result.stderr}`, outputs, status: result.status };
} finally {
rmSync(root, { force: true, recursive: true });
}
}
function runCiReleaseRefValidation(options: {
kind?: "context" | "historical" | "candidate";
ref: string;
targetSha: string;
resolvedSha?: string;
comparisonStatus?: string;
apiError?: "ref" | "comparison";
}) {
const root = tempDirs.make("openclaw-ci-target-context-");
const outputPath = path.join(root, "github-output");
const binPath = path.join(root, "bin");
const resolvedSha = options.resolvedSha ?? "b".repeat(40);
const kind = options.kind ?? "context";
const ref = `refs/${kind === "historical" ? "tags" : "heads"}/${options.ref}`;
mkdirSync(binPath);
writeFileSync(
path.join(root, "ci-git-owner.py"),
readFileSync(".github/actions/git-owner/owner.py"),
);
writeFileSync(outputPath, "", "utf8");
writeFileSync(
path.join(binPath, "git"),
`#!/usr/bin/env bash
set -euo pipefail
if [[ "$1" == "-C" ]]; then shift 2; fi
if [[ "$*" == "remote get-url origin" ]]; then
printf '%s\\n' 'https://github.com/openclaw/openclaw.git'
else
echo 'fatal: could not read Username for https://github.com: terminal prompts disabled' >&2
exit 128
fi
`,
"utf8",
);
writeFileSync(
path.join(binPath, "gh"),
`#!/usr/bin/env bash
set -euo pipefail
[[ "\${GH_TOKEN:-}" == "test-token" ]] || exit 4
[[ "$1" == "api" ]] || exit 64
shift
if [[ "$1" == "--method" && "$2" == "GET" ]]; then shift 2; fi
[[ "$#" == 3 && "$2" == "--jq" ]] || exit 64
case "$1" in
"$MOCK_REF_ENDPOINT") kind=ref; value="$MOCK_REF_SHA"; query=.sha ;;
"$MOCK_COMPARE_ENDPOINT") kind=comparison; value="$MOCK_COMPARE_STATUS"; query=.status ;;
*) echo "Unexpected GitHub API endpoint: $1" >&2; exit 64 ;;
esac
[[ "$3" == "$query" ]] || exit 64
# Valid-looking partial output must not authorize a failed request.
printf '%s\\n' "$value"
if [[ "$MOCK_API_ERROR" == "$kind" ]]; then
echo 'gh: Service Unavailable (HTTP 503)' >&2
exit 1
fi
`,
"utf8",
);
chmodSync(path.join(binPath, "git"), 0o755);
chmodSync(path.join(binPath, "gh"), 0o755);
const stepName = {
context: "Validate target context",
historical: "Validate historical release target",
candidate: "Validate release candidate target",
}[kind];
const step = expectDefined(
readCiWorkflow().jobs.preflight.steps.find(
(candidate: WorkflowStep) => candidate.name === stepName,
),
stepName,
);
const run = spawnSync(
"bash",
["-c", expectDefined(step.run, "target context validation script")],
{
encoding: "utf8",
env: {
...process.env,
GH_TOKEN: step.env?.GH_TOKEN === "${{ github.token }}" ? "test-token" : "",
GITHUB_REPOSITORY: "openclaw/openclaw",
GITHUB_OUTPUT: outputPath,
MOCK_REF_ENDPOINT: `repos/openclaw/openclaw/commits/${encodeURIComponent(ref)}`,
MOCK_REF_SHA: resolvedSha,
MOCK_COMPARE_ENDPOINT: `repos/openclaw/openclaw/compare/${options.targetSha}...${resolvedSha}`,
MOCK_COMPARE_STATUS: options.comparisonStatus ?? "ahead",
MOCK_API_ERROR: options.apiError ?? "",
RUNNER_TEMP: root,
PATH: `${binPath}:${process.env.PATH ?? ""}`,
TARGET_CONTEXT_REF: options.ref,
TARGET_REF: options.targetSha,
EXPECTED_SHA: options.targetSha,
HISTORICAL_TARGET_TAG: options.ref,
RELEASE_CANDIDATE_REF: options.ref,
},
},
);
return {
output: `${run.stdout}${run.stderr}`,
outputs: readWorkflowOutputs(outputPath),
status: run.status,
};
}
function runCandidateTrustClassification(options: {
checkoutRevision: string;
defaultRevision?: string;
eventName: "pull_request" | "push" | "workflow_dispatch";
historicalTarget?: boolean;
ref?: string;
releaseCandidateTarget?: boolean;
releaseGate?: boolean;
targetContextTarget?: boolean;
targetRef?: string;
workflowRevision?: string;
}) {
const root = tempDirs.make("openclaw-ci-candidate-trust-");
const outputPath = path.join(root, "github-output");
const binPath = path.join(root, "bin");
const defaultRevision = options.defaultRevision ?? "b".repeat(40);
mkdirSync(binPath);
writeFileSync(outputPath, "", "utf8");
for (const command of ["git", "gh"]) {
writeExecutable(path.join(binPath, command), [
"#!/bin/sh",
"echo 'Cache trust must consume the resolved default SHA without another lookup' >&2",
"exit 128",
]);
}
const step = expectDefined(
readCiWorkflow().jobs.preflight.steps.find(
(candidate: WorkflowStep) => candidate.name === "Classify candidate cache trust",
),
"candidate cache trust step",
);
const script = expectDefined(step.run, "candidate cache trust script");
const run = spawnSync("bash", ["-c", script], {
encoding: "utf8",
env: {
...process.env,
CHECKOUT_REVISION: options.checkoutRevision,
DEFAULT_SHA: defaultRevision,
GITHUB_EVENT_NAME: options.eventName,
GITHUB_OUTPUT: outputPath,
GITHUB_REF: options.ref ?? "",
HISTORICAL_TARGET: String(options.historicalTarget ?? false),
RUNNER_TEMP: root,
PATH: `${binPath}:${process.env.PATH ?? ""}`,
RELEASE_CANDIDATE_TARGET: String(options.releaseCandidateTarget ?? false),
RELEASE_GATE: String(options.releaseGate ?? false),
TARGET_CONTEXT_TARGET: String(options.targetContextTarget ?? false),
TARGET_REF: options.targetRef ?? "",
WORKFLOW_REVISION: options.workflowRevision ?? "a".repeat(40),
},
});
return {
output: `${run.stdout}${run.stderr}`,
outputs: readWorkflowOutputs(outputPath),
status: run.status,
};
}
function readAndroidReleaseWorkflow() {
return parse(readFileSync(".github/workflows/android-release.yml", "utf8"));
}
function readAndroidToolchainAction() {
return parse(readFileSync(SETUP_ANDROID_TOOLCHAIN_ACTION, "utf8"));
}
function readBuildArtifactsTestboxWorkflow() {
return parse(readFileSync(".github/workflows/ci-build-artifacts-testbox.yml", "utf8"));
}
function readTestboxWorkflow() {
return parse(readFileSync(".github/workflows/ci-check-testbox.yml", "utf8"));
}
function readWorkflowSanityWorkflow() {
return parse(readFileSync(".github/workflows/workflow-sanity.yml", "utf8"));
}
function readRealBehaviorProofWorkflow() {
return parse(readFileSync(".github/workflows/real-behavior-proof.yml", "utf8"));
}
function readMaturityScorecardWorkflow() {
return parse(readFileSync(MATURITY_SCORECARD_WORKFLOW, "utf8"));
}
function runMaturityInvocationScenario(options: {
callerEventName: string;
callerWorkflowRef: string;
jobWorkflowRef?: string;
publishPullRequest: boolean;
}) {
const workflow = readMaturityScorecardWorkflow();
const authorizeStep = workflow.jobs.validate_selected_ref.steps.find(
(step: { name?: string }) => step.name === "Authorize workflow invocation",
);
const authorizeRun = spawnSync("bash", ["-c", authorizeStep.run], {
encoding: "utf8",
env: {
CALLER_EVENT_NAME: options.callerEventName,
CALLER_WORKFLOW_REF: options.callerWorkflowRef,
JOB_WORKFLOW_FILE_PATH: MATURITY_SCORECARD_WORKFLOW,
JOB_WORKFLOW_REF: options.jobWorkflowRef ?? MATURITY_SCORECARD_WORKFLOW_REF,
JOB_WORKFLOW_REPOSITORY: "openclaw/openclaw",
PATH: process.env.PATH ?? "",
PUBLISH_PULL_REQUEST: String(options.publishPullRequest),
},
});
return {
output: `${authorizeRun.stdout}${authorizeRun.stderr}`,
status: authorizeRun.status,
};
}
function runMaturityArtifactCopyScenario(
options: { destinationSymlink?: boolean; extraFile?: boolean; sourceSymlink?: boolean } = {},
) {
const workflow = readMaturityScorecardWorkflow();
const copyStep = workflow.jobs.publish_generated_pr.steps.find(
(step: { name?: string }) => step.name === "Validate and copy generated PR files",
);
const root = mkdtempSync(path.join(tmpdir(), "openclaw-maturity-copy-"));
const staging = path.join(root, "staging");
try {
for (const generatedPath of MATURITY_GENERATED_PR_PATHS) {
const staged = path.join(staging, generatedPath);
const selected = path.join(root, "selected", generatedPath);
mkdirSync(path.dirname(staged), { recursive: true });
mkdirSync(path.dirname(selected), { recursive: true });
writeFileSync(staged, `new ${generatedPath}\n`, "utf8");
writeFileSync(selected, `old ${generatedPath}\n`, "utf8");
}
if (options.extraFile) {
writeFileSync(path.join(staging, "unexpected.txt"), "unexpected\n", "utf8");
}
const firstGeneratedPath = expectDefined(
MATURITY_GENERATED_PR_PATHS[0],
"first maturity generated PR path",
);
if (options.sourceSymlink) {
const staged = path.join(staging, firstGeneratedPath);
rmSync(staged);
symlinkSync("missing-score-source", staged);
}
const escaped = path.join(root, "escaped.txt");
if (options.destinationSymlink) {
const selected = path.join(root, "selected", firstGeneratedPath);
writeFileSync(escaped, "outside\n", "utf8");
rmSync(selected);
symlinkSync(escaped, selected);
}
const run = spawnSync("bash", ["-c", copyStep.run], {
cwd: root,
encoding: "utf8",
env: { PATH: process.env.PATH ?? "", STAGING_DIR: staging },
});
return {
copied: MATURITY_GENERATED_PR_PATHS.map((generatedPath) =>
readFileSync(path.join(root, "selected", generatedPath), "utf8"),
),
escaped: existsSync(escaped) ? readFileSync(escaped, "utf8") : "",
output: `${run.stdout}${run.stderr}`,
status: run.status,
};
} finally {
rmSync(root, { force: true, recursive: true });
}
}
function readQaProfileEvidenceWorkflow() {
return parse(readFileSync(".github/workflows/qa-profile-evidence.yml", "utf8"));
}
type QaProfileTimeoutFixtureMode = "natural-124" | "self-kill" | "term" | "kill";
function runQaProfileTimeoutFixture(mode: QaProfileTimeoutFixtureMode) {
const root = mkdtempSync(path.join(tmpdir(), "openclaw-qa-profile-timeout-"));
try {
const selectedRoot = path.join(root, "selected");
mkdirSync(selectedRoot);
const binDir = path.join(root, "bin");
mkdirSync(binDir);
const fakePnpm = path.join(binDir, "pnpm");
writeFileSync(
fakePnpm,
`#!/usr/bin/env bash
set -u
echo "child-stderr-sentinel:\${FAKE_PNPM_MODE}" >&2
echo "child-locale:\${LC_ALL-unset}" >&2
case "\${FAKE_PNPM_MODE}" in
natural-124)
echo "timeout: sending signal KILL to command 'spoofed-child'" >&2
exit 124
;;
self-kill)
kill -KILL "$$"
;;
term)
trap 'exit 0' TERM
while :; do sleep 0.01; done
;;
kill)
trap '' TERM
while :; do sleep 0.01 || true; done
;;
esac
`,
"utf8",
);
chmodSync(fakePnpm, 0o755);
const fixturePath = `${binDir}:${process.env.PATH ?? ""}`;
const timeoutVersion = spawnSync("timeout", ["--version"], {
encoding: "utf8",
env: { ...process.env, PATH: fixturePath },
});
if (timeoutVersion.status !== 0) {
throw new Error(
`QA timeout fixture requires timeout --version: ${timeoutVersion.stdout}${timeoutVersion.stderr}`,
);
}
const workflow = readQaProfileEvidenceWorkflow();
const runProfileStep = expectDefined(
workflow.jobs.run_qa_profile_shard.steps.find(
(step: WorkflowStep) => step.name === "Run QA profile shard",
),
"Run QA profile shard step",
);
let script = runProfileStep.run
.replace("--kill-after=30s 110m", "--kill-after=0.05s 0.4s")
.replaceAll("110 minutes", "0.4 seconds")
.replaceAll("30-second", "0.05-second");
const timeoutSupervisorCapture = path.join(root, "timeout-supervisor.log");
const timeoutClassificationStart = `supervisor_tee_pid=""
timeout_outcome="none"`;
// Bash writes killed-job diagnostics outside timeout's redirected stream. Capture the
// authoritative supervisor log before the workflow's EXIT trap removes it.
const capturedScript = script.replace(
timeoutClassificationStart,
`supervisor_tee_pid=""
cp "$timeout_supervisor_log" "$TIMEOUT_SUPERVISOR_CAPTURE"
timeout_outcome="none"`,
);
if (capturedScript === script) {
throw new Error("QA timeout fixture could not capture the timeout supervisor log");
}
script = capturedScript;
const githubOutput = path.join(root, "github-output");
const run = runWorkflowShellScript(script, {
cwd: selectedRoot,
env: {
...process.env,
FAKE_PNPM_MODE: mode,
GITHUB_OUTPUT: githubOutput,
GITHUB_RUN_ATTEMPT: "1",
GITHUB_RUN_ID: "42",
GITHUB_WORKSPACE: root,
LC_ALL: "POSIX",
PATH: fixturePath,
CATEGORY_IDS_JSON: '["fixture.category"]',
PROTOCOL_SINCE_BASE_SHA: "b".repeat(40),
QA_PROFILE: "all",
QA_SHARD_ID: "shard-01",
REQUESTED_REF: "fixture",
SCENARIO_IDS_JSON: '["fixture-scenario"]',
TARGET_SHA: "a".repeat(40),
TIMEOUT_SUPERVISOR_CAPTURE: timeoutSupervisorCapture,
},
});
const outputDir = path.join(
selectedRoot,
".artifacts",
"qa-e2e",
"profile-all-42-1",
"shard-01",
);
const status = JSON.parse(
readFileSync(path.join(outputDir, "qa-profile-run-status.json"), "utf8"),
) as {
exitCode: number;
target: { protocolBaseSha: string };
timedOut: boolean;
timeoutOutcome: "none" | "term" | "kill";
};
return {
commandStatus: run.status,
githubOutput: readFileSync(githubOutput, "utf8"),
status,
stderr: run.stderr,
stdout: run.stdout,
timeoutSupervisorLog: readFileSync(timeoutSupervisorCapture, "utf8"),
timeoutVersion: `${timeoutVersion.stdout}${timeoutVersion.stderr}`.trim(),
};
} finally {
rmSync(root, { force: true, recursive: true });
}
}
function runQaProfileFailureGate(options: { allowFailures: boolean; qaExitCode?: string }) {
const workflow = readQaProfileEvidenceWorkflow();
const failStep = workflow.jobs.aggregate_qa_profile.steps.find(
(step: WorkflowStep) => step.name === "Fail if QA profile failed",
);
return spawnSync("bash", ["-c", failStep.run], {
encoding: "utf8",
env: {
ALLOW_FAILURES: String(options.allowFailures),
PATH: process.env.PATH ?? "",
QA_EXIT_CODE: options.qaExitCode ?? "",
QA_PROFILE: "all",
},
});
}
function readReleaseChecksWorkflow() {
return parse(readFileSync(".github/workflows/openclaw-release-checks.yml", "utf8"));
}
function readCriticalQualityWorkflow() {
return readFileSync(".github/workflows/codeql-critical-quality.yml", "utf8");
}
function readWorkflow(filePath: string) {
return parse(readFileSync(filePath, "utf8"));
}
const PULL_REQUEST_EDIT_FIELDS = ["title", "body", "base"] as const;
function readPullRequestEditFields(condition: unknown) {
const expression = typeof condition === "string" ? condition : "";
return PULL_REQUEST_EDIT_FIELDS.filter((field) =>
expression.includes(`github.event.changes.${field}`),
);
}
function readTrackedText(relativePath: string): string {
if (existsSync(relativePath)) {
return readFileSync(relativePath, "utf8");
}
return execFileSync("git", ["show", `:${relativePath}`], { encoding: "utf8" });
}
function readAndroidCompileSdk(relativePath: string): number {
const match = readTrackedText(relativePath).match(/^\s*compileSdk\s*=\s*(\d+)\s*$/mu);
if (!match) {
throw new Error(`Missing compileSdk in ${relativePath}`);
}
return Number(match[1]);
}
function findYamlFiles(directory: string): string[] {
return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
const entryPath = `${directory}/${entry.name}`;
if (entry.isDirectory()) {
return findYamlFiles(entryPath);
}
return entry.isFile() && /\.ya?ml$/u.test(entry.name) ? [entryPath] : [];
});
}
function findUnpinnedExternalActions(): string[] {
const violations: string[] = [];
for (const workflowPath of [
...findYamlFiles(".github/workflows"),
...findYamlFiles(".github/actions"),
]) {
for (const [index, line] of readFileSync(workflowPath, "utf8").split("\n").entries()) {
const uses = line.match(/^\s*(?:-\s*)?uses:\s*([^#\s]+)/u)?.[1];
if (
!uses ||
uses.startsWith("./") ||
uses.startsWith("docker://") ||
OIDC_BOUND_MAIN_REUSABLE_WORKFLOWS.has(uses)
) {
continue;
}
const at = uses.lastIndexOf("@");
if (at < 1 || !/^[a-f0-9]{40}$/u.test(uses.slice(at + 1))) {
violations.push(`${workflowPath}:${index + 1}: ${uses}`);
}
}
}
return violations;
}
function runGit(cwd: string, args: string[]): string {
return execFileSync("git", args, { cwd, encoding: "utf8" }).trim();
}
function runReleaseFallbackHistoryFixture(options: {
route: "branch" | "tag" | "orphan" | "non-release-tag";
many?: boolean;
failure?: "fetch-branches" | "fetch-tags" | "branch-producer" | "tag-producer";
}) {
const ownedDirs = createTempDirTracker();
const root = ownedDirs.make("openclaw-release-fallback-");
const origin = path.join(root, "origin.git");
const checkout = path.join(root, "checkout");
const bin = path.join(root, "bin");
const home = path.join(root, "home");
const hooks = path.join(root, "hooks");
const records = path.join(root, "git-results.jsonl");
const fixtureEnv: NodeJS.ProcessEnv = {
PATH: [path.dirname(testNodeExecPath), "/usr/local/bin", "/usr/bin", "/bin"].join(
path.delimiter,
),
HOME: home,
XDG_CONFIG_HOME: home,
LC_ALL: "C",
GIT_CONFIG_NOSYSTEM: "1",
GIT_CONFIG_GLOBAL: devNull,
GIT_ALLOW_PROTOCOL: "file",
GIT_TERMINAL_PROMPT: "0",
GIT_CONFIG_COUNT: "6",
GIT_CONFIG_KEY_0: "credential.helper",
GIT_CONFIG_VALUE_0: "",
GIT_CONFIG_KEY_1: "core.hooksPath",
GIT_CONFIG_VALUE_1: hooks,
GIT_CONFIG_KEY_2: "gc.auto",
GIT_CONFIG_VALUE_2: "0",
GIT_CONFIG_KEY_3: "maintenance.auto",
GIT_CONFIG_VALUE_3: "false",
GIT_CONFIG_KEY_4: "commit.gpgsign",
GIT_CONFIG_VALUE_4: "false",
GIT_CONFIG_KEY_5: "protocol.file.allow",
GIT_CONFIG_VALUE_5: "always",
GIT_AUTHOR_NAME: "Release Fixture",
GIT_AUTHOR_EMAIL: "release-fixture@example.com",
GIT_COMMITTER_NAME: "Release Fixture",
GIT_COMMITTER_EMAIL: "release-fixture@example.com",
GITHUB_TOKEN: "synthetic-fixture-token",
};
try {
for (const dir of [checkout, bin, home, hooks]) {
mkdirSync(dir);
}
const realGit = execFileSync("bash", ["--noprofile", "--norc", "-c", "command -v git"], {
env: fixtureEnv,
encoding: "utf8",
}).trim();
const git = (cwd: string, args: string[], input?: string) =>
execFileSync(realGit, args, {
cwd,
env: fixtureEnv,
input,
encoding: "utf8",
stdio: ["pipe", "pipe", "pipe"],
timeout: 20_000,
}).trim();
git(root, ["init", "--bare", "-q", origin]);
const tree = git(origin, ["mktree"], "");
const selected = git(origin, ["commit-tree", tree, "-m", "selected"]);
const unrelated = git(origin, ["commit-tree", tree, "-m", "unrelated"]);
const count = options.many ? 4096 : 1;
const refs = Array.from({ length: count }, (_, index) => {
const suffix = options.many
? `${String(index).padStart(4, "0")}-${"a".repeat(192)}/${"b".repeat(192)}`
: "small";
return options.route === "branch"
? `refs/heads/fixture/${suffix}`
: `refs/tags/${options.route === "non-release-tag" ? "fixture" : "vfixture"}/${suffix}`;
});
git(
origin,
["update-ref", "--stdin"],
[
`create refs/heads/setup-target ${selected}`,
`create refs/heads/unrelated ${unrelated}`,
...(options.route === "orphan" ? [] : refs.map((ref) => `create ${ref} ${selected}`)),
"",
].join("\n"),
);
git(origin, ["pack-refs", "--all"]);
git(checkout, ["init", "-q"]);
git(checkout, ["remote", "add", "origin", pathToFileURL(origin).href]);
git(checkout, ["fetch", "--no-tags", "origin", "refs/heads/setup-target"]);
git(checkout, ["checkout", "-q", "--detach", "FETCH_HEAD"]);
git(origin, ["update-ref", "-d", "refs/heads/setup-target"]);
git(checkout, ["update-ref", "-d", "refs/remotes/origin/setup-target"]);
expect(
git(checkout, [
"for-each-ref",
"--format=%(objectname)",
"--contains",
selected,
"refs/remotes",
]),
).toBe("");
expect(git(checkout, ["tag", "--points-at", selected])).toBe("");
if (options.route === "tag") {
git(checkout, [
"config",
"http.https://github.com/.extraheader",
"AUTHORIZATION: basic Zml4dHVyZQ==",
]);
}
const enumerationBytes = Buffer.byteLength(
refs
.map((ref) => ref.replace(/^refs\/heads\//u, "origin/").replace(/^refs\/tags\//u, ""))
.join("\n") + "\n",
);
if (options.many && existsSync("/proc/sys/fs/pipe-max-size")) {
expect(enumerationBytes).toBeGreaterThan(
Number(readFileSync("/proc/sys/fs/pipe-max-size", "utf8").trim()),
);
}
// Enumeration inherits the real pipeline. Only verbose fetch stderr uses a regular file.
const launcher = path.join(root, "git-launcher.mjs");
writeFileSync(
launcher,
[
'import { spawnSync } from "node:child_process";',
'import { createHash } from "node:crypto";',
'import { appendFileSync, closeSync, openSync, readFileSync, statSync } from "node:fs";',
'import { constants } from "node:os";',
`const git = ${JSON.stringify(realGit)};`,
`const records = ${JSON.stringify(records)};`,
`const failure = ${JSON.stringify(options.failure ?? null)};`,
"let args = process.argv.slice(2);",
'const op = args.includes("fetch") ? (args.includes("--no-tags") ? "fetch-branches" : "fetch-tags")',
' : args[0] === "tag" ? "tag-producer" : args[0] === "for-each-ref" ? "branch-producer" : args[0];',
'if (op.startsWith("fetch-") && op === failure) {',
` args = args.map(arg => arg === "origin" ? ${JSON.stringify(pathToFileURL(path.join(root, "missing.git")).href)} : arg);`,
"}",
`const fetchPath = ${JSON.stringify(path.join(root, "fetch-"))} + op + ".stderr";`,
'const fd = op.startsWith("fetch-") ? openSync(fetchPath, "w", 0o600) : null;',
'const result = spawnSync(git, args, { stdio: ["inherit", "inherit", fd ?? "inherit"], timeout: 20_000 });',
"if (fd !== null) closeSync(fd);",
"const exitCode = result.signal ? 128 + constants.signals[result.signal] : result.status ?? 1;",
"const entry = { op, status: result.status, signal: result.signal, exitCode, error: result.error?.code };",
"if (fd !== null) {",
" const size = statSync(fetchPath).size;",
' if (size > 8 * 1024 * 1024) throw new Error("fixture fetch capture exceeded 8 MiB");',
" const bytes = readFileSync(fetchPath);",
' entry.stderr = { bytes: size, sha256: createHash("sha256").update(bytes).digest("hex") };',
" process.stderr.write(bytes.subarray(Math.max(0, bytes.length - 1024)));",
"}",
'appendFileSync(records, JSON.stringify(entry) + "\\n");',
"if (op === failure && fd === null && result.status === 0) {",
' const failed = spawnSync(git, ["rev-parse", "--verify", "refs/heads/fixture-missing"], { stdio: ["ignore", "ignore", "inherit"] });',
' appendFileSync(records, JSON.stringify({ op: "post-output-failure", status: failed.status, signal: failed.signal }) + "\\n");',
" process.exit(failed.status ?? 1);",
"}",
"process.exit(exitCode);",
"",
].join("\n"),
);
writeExecutable(path.join(bin, "git"), [
"#!/bin/bash",
`exec ${quoteShell(testNodeExecPath)} ${quoteShell(launcher)} "$@"`,
]);
const allocatedBytes = () =>
Number(
execFileSync("du", ["-sk", root], { env: fixtureEnv, encoding: "utf8" })
.trim()
.split(/\s/u)[0],
) * 1024;
const beforeBytes = allocatedBytes();
expect(beforeBytes).toBeLessThan(256 * 1024 * 1024);
console.info(
"fallback-fixture-before",
JSON.stringify({ ...options, refs: count, enumerationBytes, allocatedBytes: beforeBytes }),
);
const step = expectDefined(
readReleaseChecksWorkflow().jobs.resolve_target.steps.find(
(candidate: WorkflowStep) =>
candidate.name === "Validate selected ref belongs to this repository",
) as WorkflowStep | undefined,
"fallback history validation",
);
const result = runWorkflowShellScript(expectDefined(step.run, "fallback validation body"), {
cwd: checkout,
env: {
...fixtureEnv,
PATH: `${bin}${path.delimiter}${fixtureEnv.PATH}`,
RELEASE_REF: selected,
},
});
const events = readFileSync(records, "utf8")
.trim()
.split("\n")
.map(
(line) =>
JSON.parse(line) as {
op: string;
status: number | null;
signal: string | null;
error?: string;
},
);
const containingBranches = git(checkout, [
"for-each-ref",
"--format=%(objectname)",
"--contains",
selected,
"refs/remotes",
])
.split(/\s/u)
.filter(Boolean).length;
if (!options.failure?.startsWith("fetch-")) {
expect(containingBranches).toBe(options.route === "branch" ? count : 0);
}
const afterBytes = allocatedBytes();
expect(afterBytes).toBeLessThan(256 * 1024 * 1024);
console.info(
"fallback-fixture-result",
JSON.stringify({
...options,
status: result.status,
signal: result.signal,
error: result.error?.message,
enumerationBytes,
containingBranches,
allocatedBytes: afterBytes,
events,
rejection: result.stderr.includes("but that commit is not reachable"),
}),
);
expect(result.error).toBeUndefined();
expect(events.every((event) => event.error === undefined)).toBe(true);
return { result, events };
} finally {
ownedDirs.cleanup();
expect(existsSync(root)).toBe(false);
console.info("fallback-fixture-cleanup", JSON.stringify({ ...options, remaining: 0 }));
}
}
function runDiffBaseFixture(options: {
commitCount: 1 | 2 | 3;
eventBaseSha: string;
defaultBranch?: string;
manual?: boolean;
apiError?: "ref" | "comparison";
}) {
const root = tempDirs.make("openclaw-ci-diff-base-");
runGit(root, ["init", "-q", "-b", "main"]);
runGit(root, ["config", "commit.gpgsign", "false"]);
runGit(root, ["config", "user.email", "ci-fixture@example.com"]);
runGit(root, ["config", "user.name", "CI Fixture"]);
for (let index = 1; index <= options.commitCount; index += 1) {
writeFileSync(path.join(root, "fixture.txt"), `commit ${index}\n`, "utf8");
runGit(root, ["add", "fixture.txt"]);
runGit(root, ["commit", "-q", "-m", `fixture ${index}`]);
}
const headSha = runGit(root, ["rev-parse", "HEAD"]);
const parentSha =
options.commitCount > 1 ? runGit(root, ["rev-parse", "--verify", "HEAD^1"]) : null;
const eventBaseSha = options.eventBaseSha === "parent" ? parentSha! : options.eventBaseSha;
const outputPath = path.join(root, "github-output");
writeFileSync(outputPath, "", "utf8");
const diffBaseStep = readCiWorkflow().jobs.preflight.steps.find(
(step: WorkflowStep) => step.name === "Resolve exact diff base",
);
const defaultBranch = options.defaultBranch ?? "main";
const fixtureEnv: NodeJS.ProcessEnv = {};
if (options.manual) {
const bin = path.join(root, "bin");
mkdirSync(bin);
writeFileSync(
path.join(root, "ci-git-owner.py"),
readFileSync(".github/actions/git-owner/owner.py"),
);
writeExecutable(path.join(bin, "git"), [
"#!/bin/sh",
'if [ "$1" = -C ]; then shift 2; fi',
`[ "$*" = 'rev-parse HEAD' ] || { echo 'Anonymous Git transport is unavailable' >&2; exit 128; }`,
`printf '%s\\n' '${headSha}'`,
]);
writeExecutable(path.join(bin, "gh"), [
"#!/bin/sh",
'[ "$GH_TOKEN" = test-token ] || exit 4',
'[ "$1" = api ] || exit 64',
"shift",
'if [ "$1" = --method ]; then [ "$2" = GET ] || exit 64; shift 2; fi',
'case "$*" in',
` 'repos/openclaw/openclaw/commits/${encodeURIComponent(`refs/heads/${defaultBranch}`)} --jq .sha') kind=ref ;;`,
` 'repos/openclaw/openclaw/compare/${parentSha}...${headSha} --jq .merge_base_commit.sha') kind=comparison ;;`,
" *) exit 64 ;;",
"esac",
`printf '%s\\n' '${parentSha}'`,
'if [ "$MOCK_API_ERROR" = "$kind" ]; then echo "gh: Service Unavailable (HTTP 503)" >&2; exit 1; fi',
]);
fixtureEnv.PATH = `${bin}${path.delimiter}${process.env.PATH ?? ""}`;
fixtureEnv.GH_TOKEN = evaluateWorkflowExpression(diffBaseStep.env.GH_TOKEN, {
eventName: "workflow_dispatch",
repository: "openclaw/openclaw",
runAttempt: 1,
workflowToken: "test-token",
});
fixtureEnv.RUNNER_TEMP = root;
fixtureEnv.MOCK_API_ERROR = options.apiError ?? "";
}
const run = runWorkflowShellScript(diffBaseStep.run, {
cwd: root,
env: {
...process.env,
DEFAULT_BRANCH: defaultBranch,
EVENT_BASE_SHA: eventBaseSha,
GITHUB_EVENT_NAME: options.manual ? "workflow_dispatch" : "push",
GITHUB_OUTPUT: outputPath,
GITHUB_REPOSITORY: "openclaw/openclaw",
PULL_REQUEST_NUMBER: "",
RELEASE_GATE: "false",
...fixtureEnv,
},
});
const rawOutputs = readFileSync(outputPath, "utf8").trim();
const outputs: Record<string, string> =
rawOutputs === ""
? {}
: Object.fromEntries(
rawOutputs.split("\n").map((line) => {
const separator = line.indexOf("=");
return [line.slice(0, separator), line.slice(separator + 1)];
}),
);
const emittedBaseIsCommit =
typeof outputs.sha === "string" &&
spawnSync("git", ["cat-file", "-e", `${outputs.sha}^{commit}`], { cwd: root }).status === 0;
return {
emittedBaseIsCommit,
eventBaseSha,
headSha,
output: `${run.stdout}${run.stderr}`,
outputs,
parentSha,
status: run.status,
};
}
function writeExecutable(filePath: string, lines: string[]): void {
writeFileSync(filePath, `${lines.join("\n")}\n`, "utf8");
chmodSync(filePath, 0o755);
}
function writeProtocolDescriptor(
repo: string,
additions: Array<{
name: string;
since?: string;
compatibilityRestored?: boolean;
}> = [],
): void {
const rows = [{ name: "health", since: "2026.7" }, ...additions].map(
({ name, since, compatibilityRestored }) => {
const sinceProperty = since === undefined ? "" : `, since: ${JSON.stringify(since)}`;
const compatibilityProperty = compatibilityRestored ? ", compatibilityRestored: true" : "";
return ` { name: ${JSON.stringify(name)}${sinceProperty}${compatibilityProperty} },`;
},
);
const descriptor = path.join(repo, "src/gateway/methods/core-descriptors.ts");
mkdirSync(path.dirname(descriptor), { recursive: true });
writeFileSync(
descriptor,
`export const CORE_GATEWAY_METHOD_SPECS = [\n${rows.join("\n")}\n] as const;\n`,
);
}
function commitProtocolFixture(repo: string, message: string): string {
runGit(repo, ["add", "-A"]);
runGit(repo, ["commit", "-q", "-m", message]);
return runGit(repo, ["rev-parse", "HEAD"]);
}
function createQaProtocolTopology() {
const root = tempDirs.make("openclaw-qa-protocol-topology-");
const origin = path.join(root, "origin");
const checkout = path.join(root, "checkout");
const releaseBranch = "release/2026.8.1";
const releaseTag = "v2026.8.1";
const mainReleaseTag = "v2026.8.2";
runGit(root, ["init", "-q", "-b", "main", origin]);
runGit(origin, ["config", "commit.gpgsign", "false"]);
runGit(origin, ["config", "user.email", "qa-protocol@example.invalid"]);
runGit(origin, ["config", "user.name", "QA Protocol Fixture"]);
writeFileSync(
path.join(origin, "package.json"),
'{"name":"qa-protocol-fixture","version":"2026.8.0"}\n',
);
writeProtocolDescriptor(origin);
const mainBase = commitProtocolFixture(origin, "base protocol");
writeProtocolDescriptor(origin, [{ name: "sessions.patchMany", since: "2026.8" }]);
const mainHead = commitProtocolFixture(origin, "add main protocol method");
runGit(origin, ["tag", mainReleaseTag]);
writeFileSync(path.join(origin, "main-tip.txt"), "later main tip\n");
commitProtocolFixture(origin, "advance main");
runGit(origin, ["checkout", "-q", "-b", "compatibility/restore", mainBase]);
writeProtocolDescriptor(origin, [
{
name: "gateway.restart.preflight",
since: "<=2026.7",
compatibilityRestored: true,
},
]);
const compatibilityHead = commitProtocolFixture(origin, "restore compatibility method");
runGit(origin, ["checkout", "-q", "-b", "compatibility/invalid", mainBase]);
writeProtocolDescriptor(origin, [
{
name: "gateway.restart.invalid",
since: "2026.8",
compatibilityRestored: true,
},
]);
const invalidCompatibilityHead = commitProtocolFixture(
origin,
"mislabel new method as compatibility",
);
runGit(origin, ["checkout", "-q", "-b", releaseBranch, mainBase]);
writeProtocolDescriptor(origin, [{ name: "sessions.releaseOnly" }]);
const releaseHead = commitProtocolFixture(origin, "add release protocol method");
runGit(origin, ["checkout", "-q", "--detach", mainBase]);
writeFileSync(path.join(origin, "tag.txt"), "release tag\n");
const releaseTagHead = commitProtocolFixture(origin, "create release tag target");
runGit(origin, ["tag", releaseTag]);
runGit(origin, ["checkout", "-q", "-b", "feature/untrusted", mainBase]);
writeFileSync(path.join(origin, "feature.txt"), "untrusted\n");
const featureHead = commitProtocolFixture(origin, "add untrusted feature");
runGit(origin, ["checkout", "-q", "main"]);
runGit(root, ["clone", "-q", "--no-local", origin, checkout]);
const gitOwner = path.join(root, "ci-git-owner.py");
writeFileSync(gitOwner, readFileSync(".github/actions/git-owner/owner.py"));
return {
checkout,
compatibilityHead,
gitOwner,
featureHead,
invalidCompatibilityHead,
mainBase,
mainHead,
mainReleaseTag,
origin,
releaseBranch,
releaseHead,
releaseTag,
releaseTagHead,
};
}
function readWorkflowOutputs(outputPath: string): Record<string, string> {
if (!existsSync(outputPath)) {
return {};
}
const output = readFileSync(outputPath, "utf8").trim();
return output
? Object.fromEntries(
output.split("\n").map((line) => {
const separator = line.indexOf("=");
return [line.slice(0, separator), line.slice(separator + 1)];
}),
)
: {};
}
function runQaSelectedRefValidation(
topology: ReturnType<typeof createQaProtocolTopology>,
inputRef: string,
revision: string,
expectedSha = revision,
) {
runGit(topology.checkout, ["checkout", "-q", "--detach", revision]);
const githubOutput = path.join(topology.checkout, "github-output");
rmSync(githubOutput, { force: true });
const validateStep = expectDefined(
readQaProfileEvidenceWorkflow().jobs.validate_selected_ref.steps.find(
(step: WorkflowStep) => step.name === "Validate selected ref",
),
"QA profile selected-ref validation step",
);
const result = runWorkflowShellScript(expectDefined(validateStep.run, "validation script"), {
cwd: topology.checkout,
env: {
...process.env,
EXPECTED_SHA: expectedSha,
GITHUB_OUTPUT: githubOutput,
GITHUB_STEP_SUMMARY: path.join(topology.checkout, "github-summary"),
INPUT_REF: inputRef,
CI_GIT_OWNER: topology.gitOwner,
},
});
return { ...result, outputs: readWorkflowOutputs(githubOutput) };
}
function runProtocolSinceFixture(checkout: string, baseSha: string) {
for (const scriptPath of [
"packages/normalization-core/src/record-coerce.ts",
"scripts/check-protocol-since.mts",
"scripts/lib/repo-root.mjs",
]) {
const target = path.join(checkout, scriptPath);
mkdirSync(path.dirname(target), { recursive: true });
writeFileSync(target, readFileSync(scriptPath, "utf8"));
}
writeFileSync(
path.join(checkout, "tsconfig.json"),
JSON.stringify({
compilerOptions: {
paths: {
"@openclaw/normalization-core/record-coerce": [
"./packages/normalization-core/src/record-coerce.ts",
],
},
},
}),
);
const nodeModules = path.join(checkout, "node_modules");
if (!existsSync(nodeModules)) {
symlinkSync(TYPESCRIPT_NODE_MODULES, nodeModules, "dir");
}
return runWorkflowShellScript(
`${quoteShell(process.execPath)} --import ${quoteShell(TSX_IMPORT)} scripts/check-protocol-since.mts`,
{
cwd: checkout,
env: { ...process.env, PROTOCOL_SINCE_BASE_SHA: baseSha },
},
);
}
function runCheckShardFixture(options: {
frozenTarget: boolean;
scripts: string[];
task?: "guards" | "npm-lock" | "test-types";
checkoutBase?: string;
types?: {
compose?: boolean;
profile?: "blacksmith" | "github" | "hybrid";
eventName?: "pull_request" | "push" | "workflow_dispatch";
stripeSupport?: boolean;
hostedContract?: boolean;
failStripe?: string;
changedPathsJson?: string;
boundary?: boolean;
};
}): {
calls: string[];
output: string;
status: number | null;
typeCalls: { row: string; command: string; localCheck: string | null }[];
rows: { name: string; status: number | null }[];
} {
const root = tempDirs.make("openclaw-ci-guards-");
const fakeBin = path.join(root, "bin");
const callsPath = path.join(root, "pnpm-calls.txt");
const typeCallsPath = path.join(root, "type-calls.txt");
const typeCheck = options.task === "test-types";
mkdirSync(fakeBin);
if (typeCheck) {
mkdirSync(path.join(root, "scripts"));
writeFileSync(
path.join(root, "scripts/run-tsgo-core-test-shards.mts"),
options.types?.stripeSupport === false ? "// legacy runner\n" : "// --stripe\n",
);
writeFileSync(
path.join(root, "scripts/run-tsgo-core-test-shards.mjs"),
`import { appendFileSync } from "node:fs";
const args = process.argv.slice(2);
appendFileSync(process.env.TYPE_CALLS, [process.env.TYPE_ROW, process.env.OPENCLAW_LOCAL_CHECK ?? "<unset>", "node " + args.join(" ")].join("\\t") + "\\n");
if (args[args.indexOf("--stripe") + 1] === process.env.FAIL_TYPE_STRIPE) process.exit(17);
`,
);
}
if (options.types?.boundary) {
// Routing proof records native leaves without executing repository checks.
writeFileSync(path.join(root, "scripts/tsx.mjs"), "");
writeFileSync(
path.join(root, "scripts/check-extension-plugin-sdk-boundary.mts"),
[
'import { appendFileSync } from "node:fs";',
'const command = ["node", ...process.execArgv, "scripts/check-extension-plugin-sdk-boundary.mts", ...process.argv.slice(2)].join(" ");',
'appendFileSync(process.env.TYPE_CALLS, [process.env.TYPE_ROW, process.env.OPENCLAW_LOCAL_CHECK ?? "<unset>", command].join(String.fromCharCode(9)) + String.fromCharCode(10));',
].join(String.fromCharCode(10)),
);
writeFileSync(
path.join(root, "scripts/run-additional-boundary-checks.mts"),
readFileSync("scripts/run-additional-boundary-checks.mts"),
);
for (const directory of ["scripts/lib", "packages", "node_modules"]) {
symlinkSync(path.resolve(directory), path.join(root, directory), "dir");
}
copyFileSync("scripts/tsx.mjs", path.join(root, "scripts/tsx.mjs"));
writeFileSync(
path.join(root, "scripts/check-extension-plugin-sdk-boundary.mts"),
`
import { appendFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const imports = process.execArgv.map((arg) => arg.startsWith("file:") ? "./" + path.relative(process.cwd(), fileURLToPath(arg)) : arg);
const command = ["node", ...imports, path.relative(process.cwd(), process.argv[1]), ...process.argv.slice(2)].join(" ");
appendFileSync(process.env.TYPE_CALLS, [process.env.TYPE_ROW, process.env.OPENCLAW_LOCAL_CHECK ?? "<unset>", command].join("\\t") + "\\n");
`,
);
writeFileSync(
path.join(root, "scripts/check-native-state-schema-version.mjs"),
`
import { appendFileSync } from "node:fs";
appendFileSync(process.env.TYPE_CALLS, [process.env.TYPE_ROW, process.env.OPENCLAW_LOCAL_CHECK ?? "<unset>", "node scripts/check-native-state-schema-version.mjs"].join("\\t") + "\\n");
`,
);
}
const scripts = Object.fromEntries(options.scripts.map((name) => [name, "true"]));
if (options.types?.compose) {
// The full-path root-coverage probe must see the actual package alias.
scripts["tsgo:test"] = (
JSON.parse(readFileSync("package.json", "utf8")) as { scripts: Record<string, string> }
).scripts["tsgo:test"]!;
}
writeFileSync(path.join(root, "package.json"), `${JSON.stringify({ scripts })}\n`);
writeExecutable(path.join(fakeBin, "pnpm"), [
"#!/usr/bin/env bash",
"set -euo pipefail",
'if [ "$*" = "run --silent" ]; then exit 1; fi',
'printf "%s\\n" "$*" >> "$PNPM_CALLS"',
...(typeCheck
? [
'printf "%s\\t%s\\tpnpm %s\\n" "$TYPE_ROW" "${OPENCLAW_LOCAL_CHECK-<unset>}" "$*" >> "$TYPE_CALLS"',
]
: []),
]);
const workflow = readCiWorkflow();
const checkShardStep = workflow.jobs["check-shard"].steps.find(
(step: WorkflowStep) => step.name === "Run check shard",
);
const context: Parameters<typeof evaluateWorkflowExpression>[1] = {
eventName:
options.types?.eventName ?? (options.frozenTarget ? "workflow_dispatch" : "pull_request"),
repository: "openclaw/openclaw",
runAttempt: 1,
frozenTarget: options.frozenTarget,
hostedRunnerProfileContract: options.types?.hostedContract ?? true,
runnerProfile: options.types?.profile ?? "hybrid",
preflightOutputs: {
compatibility_target: String(options.frozenTarget),
run_format_check: "false",
changed_core_test_paths_json: options.types?.changedPathsJson ?? "",
},
};
const rows: { name: string; step: WorkflowStep; matrix: Record<string, unknown> }[] = [];
const coreJob = workflow.jobs["check-test-types-hosted-core-shard"];
if (options.types?.compose && evaluateWorkflowExpression(coreJob.if, context)) {
for (const stripe of coreJob.strategy.matrix.stripe) {
rows.push({
name: `core-${stripe}`,
step: coreJob.steps.find(
(step: WorkflowStep) => step.name === "Run hosted core test-types stripe",
),
matrix: { stripe },
});
}
}
rows.push({ name: "central", step: checkShardStep, matrix: { task: options.task ?? "guards" } });
if (options.types?.boundary) {
rows.push({
name: "boundary",
step: workflow.jobs["check-additional-shard"].steps.find(
(step: WorkflowStep) => step.name === "Run additional check shard",
),
matrix: { group: "boundaries" },
});
}
// Rows are independent (matrix fail-fast:false); each real Bash body owns its halt.
const runs = rows.map((row) => {
const resolveValue = (value: unknown) =>
typeof value === "string" && value.startsWith("${{")
? evaluateWorkflowExpression(value, { ...context, matrix: row.matrix })
: value;
const command = typeCheck
? row.step.run!.replace(/\$\{\{[\s\S]*?\}\}/gu, (expression) =>
String(resolveValue(expression)),
)
: row.step.run!;
return Object.assign(
spawnSync("bash", ["-c", command], {
cwd: root,
encoding: "utf8",
env: {
...process.env,
FROZEN_TARGET: options.frozenTarget ? "true" : "false",
FORMAT_CHECK: "false",
HISTORICAL_TARGET: options.frozenTarget ? "true" : "false",
HOSTED_RUNNER_STRIPES: "true",
CHECKOUT_BASE_SHA: options.checkoutBase ?? "",
PATH: `${fakeBin}:${process.env.PATH ?? ""}`,
PNPM_CALLS: callsPath,
TASK: options.task ?? "guards",
...(typeCheck
? {
OPENCLAW_LOCAL_CHECK: undefined,
TYPE_ROW: row.name,
TYPE_CALLS: typeCallsPath,
FAIL_TYPE_STRIPE: options.types?.failStripe,
...Object.fromEntries(
Object.entries(row.step.env ?? {}).map(([key, value]) => [
key,
String(resolveValue(value)),
]),
),
}
: {}),
},
}),
{ name: row.name },
);
});
const failed = runs.find((run) => run.status !== 0);
return {
calls: existsSync(callsPath)
? readFileSync(callsPath, "utf8").trim().split("\n").filter(Boolean)
: [],
output: runs.map((run) => `${run.stdout}${run.stderr}`).join("\n"),
status: failed ? failed.status : 0,
typeCalls: existsSync(typeCallsPath)
? readFileSync(typeCallsPath, "utf8")
.trim()
.split("\n")
.filter(Boolean)
.map((line) => {
const [row, localCheck, command] = line.split("\t");
return {
row: row!,
command: command!,
localCheck: localCheck === "<unset>" ? null : localCheck!,
};
})
: [],
rows: runs.map(({ name, status }) => ({ name, status })),
};
}
function runDependencyCheckFixture(options: {
historicalTarget: boolean;
releaseToolingEntry?: boolean;
scripts: string[];
}): {
calls: string[];
output: string;
status: number | null;
} {
const root = mkdtempSync(path.join(tmpdir(), "openclaw-ci-deadcode-"));
try {
const fakeBin = path.join(root, "bin");
const callsPath = path.join(root, "pnpm-calls.txt");
mkdirSync(fakeBin);
writeFileSync(
path.join(root, "package.json"),
`${JSON.stringify({
scripts: Object.fromEntries(options.scripts.map((name) => [name, "true"])),
})}\n`,
);
if (options.releaseToolingEntry) {
mkdirSync(path.join(root, "config"), { recursive: true });
mkdirSync(path.join(root, "scripts"), { recursive: true });
writeFileSync(
path.join(root, "config/knip.config.ts"),
"const repositoryScriptEntries = [\n] as const;\n",
);
writeFileSync(path.join(root, "scripts/generate-dependency-release-evidence.mts"), "");
}
writeExecutable(path.join(fakeBin, "pnpm"), [
"#!/usr/bin/env bash",
"set -euo pipefail",
'if [ "${EXPECT_RELEASE_TOOLING_ENTRY:-false}" = "true" ] &&',
" ! grep -Fq '\"scripts/generate-dependency-release-evidence.mts!\"' config/knip.config.ts; then",
' echo "release-only helper is missing from Knip entries" >&2',
" exit 1",
"fi",
'printf "%s\\n" "$*" >> "$PNPM_CALLS"',
]);
const checkShardRun = readCiWorkflow().jobs["check-shard"].steps.find(
(step: WorkflowStep) => step.name === "Run check shard",
).run;
const run = spawnSync("bash", ["-c", checkShardRun], {
cwd: root,
encoding: "utf8",
env: {
...process.env,
EXPECT_RELEASE_TOOLING_ENTRY: options.releaseToolingEntry ? "true" : "false",
FROZEN_TARGET: options.historicalTarget ? "true" : "false",
FORMAT_CHECK: "false",
HISTORICAL_TARGET: options.historicalTarget ? "true" : "false",
PATH: `${fakeBin}:${process.env.PATH ?? ""}`,
PNPM_CALLS: callsPath,
TASK: "dependencies",
},
});
return {
calls: existsSync(callsPath)
? readFileSync(callsPath, "utf8").trim().split("\n").filter(Boolean)
: [],
output: `${run.stdout}${run.stderr}`,
status: run.status,
};
} finally {
rmSync(root, { force: true, recursive: true });
}
}
function runControlUiI18nSourceFixture(options: {
compatibilityTarget: boolean;
hasVerifyScript: boolean;
}): { calls: string[]; output: string; summary: string; status: number | null } {
const root = mkdtempSync(path.join(tmpdir(), "openclaw-ci-control-ui-i18n-"));
try {
const fakeBin = path.join(root, "bin");
const callsPath = path.join(root, "pnpm-calls.txt");
const summaryPath = path.join(root, "summary.md");
mkdirSync(fakeBin);
writeFileSync(
path.join(root, "package.json"),
`${JSON.stringify({
scripts: options.hasVerifyScript ? { "ui:i18n:verify": "true" } : {},
})}\n`,
);
writeExecutable(path.join(fakeBin, "pnpm"), [
"#!/usr/bin/env bash",
"set -euo pipefail",
'printf "%s\\n" "$*" >> "$PNPM_CALLS"',
]);
const sourceStep = readCiWorkflow().jobs["control-ui-i18n"].steps.find(
(step: WorkflowStep) => step.name === "Verify Control UI i18n source",
);
const run = spawnSync("bash", ["-c", sourceStep.run], {
cwd: root,
encoding: "utf8",
env: {
...process.env,
COMPATIBILITY_TARGET: options.compatibilityTarget ? "true" : "false",
GITHUB_STEP_SUMMARY: summaryPath,
PATH: `${fakeBin}:${process.env.PATH ?? ""}`,
PNPM_CALLS: callsPath,
},
});
return {
calls: existsSync(callsPath)
? readFileSync(callsPath, "utf8").trim().split("\n").filter(Boolean)
: [],
output: `${run.stdout}${run.stderr}`,
status: run.status,
summary: existsSync(summaryPath) ? readFileSync(summaryPath, "utf8") : "",
};
} finally {
rmSync(root, { force: true, recursive: true });
}
}
describe("ci workflow guards", () => {
it("credits the max-lines baseline only through an emitted required ratchet guard", () => {
const manifest = runCiManifestFixture({
bundledPlanner: true,
eventName: "pull_request",
changedPaths: ["config/max-lines-baseline.txt"],
changedPlannerSource: `
export const hasBuildArtifactAffectingChange = () => false;
export const createChangedNodeTestShards = (_paths, options) => {
console.log("max-lines-guard:" + JSON.stringify(options.dedicatedMaxLinesRatchet));
return [];
};
export const createChangedExtensionFallbackShards = () => { throw new Error("Unexpected fallback"); };
`,
});
expect(manifest.status, manifest.output).toBe(0);
expect(manifest.output).toContain("max-lines-guard:true");
const tasks = JSON.parse(
expectDefined(manifest.outputs.checks_fast_core_matrix, "fast ratchet matrix"),
).include;
expect(tasks).toContainEqual({
check_name: "checks-fast-baseline-ratchets",
runtime: "node",
task: "baseline-ratchets",
});
const context = { preflightOutputs: manifest.outputs };
expect(runCiGateFixture(renderCiGateEnvironment(context)).status).toBe(0);
for (const result of ["failure", "skipped"]) {
expect(
runCiGateFixture(renderCiGateEnvironment(context, { "checks-fast-core": result })).status,
).toBe(1);
}
});
it("keeps activity unit proof without unrelated dedicated UI E2E on a PR", () => {
const manifest = runCiManifestFixture({
bundledPlanner: true,
eventName: "pull_request",
changedPaths: ["ui/src/pages/activity/activity-page.test.ts"],
scopeEnv: { OPENCLAW_CI_RUN_UI_TESTS: "true" },
changedPlannerSource: `
export const hasUiE2eAffectingChange = () => false;
export const hasBuildArtifactAffectingChange = () => false;
export const createChangedNodeTestShards = (_paths, options) => [{
checkName: "dedicated-ui-" + options.dedicatedUiE2e,
configs: [], requiresDist: false, runner: "ubuntu-24.04", shardName: "unit",
}];
export const createChangedExtensionFallbackShards = () => [];
`,
});
expect(manifest.status, manifest.output).toBe(0);
const workflow = readCiWorkflow();
const context = {
eventName: "pull_request" as const,
repository: "openclaw/openclaw",
runAttempt: 1,
preflightOutputs: manifest.outputs,
};
for (const name of ["checks-ui", "control-ui-performance"]) {
expect(evaluateWorkflowExpression(`\${{ ${workflow.jobs[name].if} }}`, context)).toBe(true);
}
for (const name of ["checks-ui-e2e", "checks-ui-e2e-real-gateway"]) {
expect(evaluateWorkflowExpression(`\${{ ${workflow.jobs[name].if} }}`, context)).toBe(false);
}
expect(JSON.stringify(manifest.outputs)).toContain("dedicated-ui-false");
expect(
runCiGateFixture(
renderCiGateEnvironment(
{ preflightOutputs: manifest.outputs },
{
"checks-ui-e2e": "skipped",
"checks-ui-e2e-real-gateway": "skipped",
},
),
).status,
).toBe(0);
expect(
runCiGateFixture(
renderCiGateEnvironment(
{ preflightOutputs: manifest.outputs },
{
"checks-ui": "skipped",
},
),
).status,
).toBe(1);
});
it.each([
{ eventName: "push" as const },
{ eventName: "workflow_dispatch" as const, historicalCompatibility: false },
{ eventName: "workflow_dispatch" as const, historicalCompatibility: true },
{ eventName: "pull_request" as const, repository: "example/openclaw" },
{ eventName: "pull_request" as const, changedPaths: null },
{ eventName: "pull_request" as const, missingSelector: true },
])("retains UI E2E outside current known unit-only PR selection %j", (options) => {
const manifest = runCiManifestFixture({
bundledPlanner: true,
changedPaths: ["ui/src/pages/activity/activity-page.test.ts"],
scopeEnv: { OPENCLAW_CI_RUN_UI_TESTS: "true" },
...options,
changedPlannerSource: `
${"missingSelector" in options && options.missingSelector ? "" : "export const hasUiE2eAffectingChange = () => false;"}
export const createChangedNodeTestShards = () => [];
export const createChangedExtensionFallbackShards = () => [];
`,
});
// Current PRs with an unusable manifest fail rather than narrowing proof.
if ("changedPaths" in options && options.changedPaths === null) {
expect(manifest.status).not.toBe(0);
} else {
expect(manifest.status, manifest.output).toBe(0);
expect(manifest.outputs.run_ui_tests).toBe("true");
expect(manifest.outputs.run_ui_e2e).toBe("true");
}
});
it("isolates mutations between workflow fixtures", () => {
const workflow = readCiWorkflow();
const expected = structuredClone(workflow);
workflow.jobs.preflight.steps[0].name = "mutated fixture";
workflow.jobs.preflight.steps.pop();
delete workflow.jobs["ci-gate"];
expect(readCiWorkflow()).toEqual(expected);
});
it.each([
["artifact", 1],
["source", 0],
["published", 0],
] as const)(
"fetches the required release preparation history for %s mode",
(packageMode, depth) => {
const prepare = readReleaseChecksWorkflow().jobs.prepare_release_package;
const checkout = prepare.steps.find(
(step: WorkflowStep) => step.name === "Checkout trusted workflow ref",
);
expect(checkout.with.ref).toBe("${{ github.sha }}");
expect(checkout.with["persist-credentials"]).toBe(false);
expect(checkout.with.filter).toBe("blob:none");
const fetchDepth = checkout.with["fetch-depth"];
expect(
typeof fetchDepth === "string"
? evaluateWorkflowExpression(fetchDepth, {
eventName: "workflow_dispatch",
repository: "openclaw/openclaw",
runAttempt: 1,
resolveTargetOutputs: { package_mode: packageMode },
})
: fetchDepth,
).toBe(depth);
},
);
it("gates frozen runtime-pair compatibility on the trusted suite outcome", () => {
const workflow = readReleaseChecksWorkflow();
const laneJob = workflow.jobs.qa_lab_runtime_pair_lane_release_checks;
const suiteValidation = laneJob.steps.find(
(step: WorkflowStep) => step.name === "Validate runtime-pair lane",
);
const reportValidation = laneJob.steps.find(
(step: WorkflowStep) => step.name === "Validate runtime-pair lane report",
);
for (const step of [suiteValidation, reportValidation]) {
expect(step?.env?.CANDIDATE_SUITE_OUTCOME).toBe(
"${{ steps.candidate_runtime_pair.outcome }}",
);
expect(step?.run).toContain('--candidate-suite-outcome "$CANDIDATE_SUITE_OUTCOME"');
expect(step?.run).toContain('--target-sha "$RELEASE_CHECK_TARGET_SHA"');
expect(step?.run).toContain('--lane "$RUNTIME_PAIR_LANE"');
}
});
it("separates release QA lanes without weakening their resource locks", () => {
const workflowPath = ".github/workflows/qa-live-transports-convex.yml";
const workflowSource = readFileSync(workflowPath, "utf8");
const workflow = parse(workflowSource);
const releaseWorkflow = readReleaseChecksWorkflow();
expect(workflow.on.workflow_call.inputs.lock_scope).toEqual({
description: "Concurrency scope for a trusted single-lane reusable call",
required: false,
default: "all",
type: "string",
});
expect(workflow.concurrency).toEqual({
group:
"qa-lab-${{ inputs.lock_scope || 'all' }}-${{ github.event_name != 'schedule' && inputs.ref || github.sha }}",
"cancel-in-progress": false,
queue: "max",
});
expect(workflow.jobs.run_live_matrix.concurrency).toEqual({
group: "qa-live-matrix-${{ needs.validate_selected_ref.outputs.selected_revision }}",
"cancel-in-progress": false,
queue: "max",
});
expect(workflow.jobs.run_live_buzz.concurrency).toEqual({
group: "qa-live-buzz-shared",
"cancel-in-progress": false,
queue: "max",
});
expect(releaseWorkflow.jobs.qa_live_release_checks.with.lock_scope).toBe("matrix");
expect(releaseWorkflow.jobs.qa_live_buzz_release_checks.with.lock_scope).toBe("buzz");
});
it("runs full-access restart proof as a failing nightly gate", () => {
const workflow = readWorkflow(".github/workflows/qa-live-transports-convex.yml");
const job = workflow.jobs.run_live_runtime_token_efficiency;
const step = job.steps.find((candidate: WorkflowStep) =>
candidate.run?.includes("--scenario gateway-restart-full-access-live"),
);
expect(workflow.on.schedule.length).toBeGreaterThan(0);
expect(job.if).toBe("github.event_name == 'schedule'");
expect(step).toBeDefined();
expect(step?.env?.OPENAI_API_KEY).toBe("${{ secrets.OPENAI_API_KEY }}");
expect(step?.["continue-on-error"]).toBeUndefined();
expect(step?.if).toBeUndefined();
expect(step?.run).toContain("--provider-mode live-frontier");
expect(step?.run).toContain("--model openai/gpt-5.6-luna");
expect(step?.run).toContain("--alt-model openai/gpt-5.6-luna");
expect(step?.run).toContain("--concurrency 1");
expect(step?.run).not.toContain("--allow-failures");
expect(step?.run).toContain(
'--output-dir "${{ steps.run_lane.outputs.output_dir }}/gateway-restart-full-access"',
);
});
it("preserves module heredocs and cleans child temporary artifacts", () => {
const parentTempDir = tmpdir();
const run = runWorkflowShellScript(
`node --input-type=module <<'NODE'
import { mkdtempSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
NODE_prefix: for (const value of ["heredoc-body-preserved"]) {
console.log(value);
break NODE_prefix;
}
console.log(mkdtempSync(join(tmpdir(), 'openclaw-workflow-child-')));
console.log(JSON.stringify(process.execArgv));
NODE
`,
{},
);
expect(run.status, run.stderr).toBe(0);
const [body, temporaryDirectory, execArgv] = run.stdout.trim().split("\n");
const childDirectory = expectDefined(temporaryDirectory, "child temporary directory");
try {
expect(body).toBe("heredoc-body-preserved");
expect(execArgv).toBe("[]");
expect(tmpdir()).toBe(parentTempDir);
expect(existsSync(childDirectory)).toBe(false);
} finally {
rmSync(childDirectory, { force: true, recursive: true });
}
});
it("routes PR edited metadata only to interested automation", () => {
const autoResponse = readWorkflow(".github/workflows/auto-response.yml");
const clawsweeperDispatch = readWorkflow(".github/workflows/clawsweeper-dispatch.yml");
const labeler = readWorkflow(".github/workflows/labeler.yml");
const realBehaviorProof = readWorkflow(".github/workflows/real-behavior-proof.yml");
for (const workflow of [autoResponse, clawsweeperDispatch, labeler, realBehaviorProof]) {
expect(workflow.on.pull_request_target.types).toContain("edited");
}
expect({
autoResponse: readPullRequestEditFields(autoResponse.jobs["auto-response"].if),
clawsweeperDispatch: readPullRequestEditFields(clawsweeperDispatch.jobs.dispatch.if),
labeler: readPullRequestEditFields(labeler.jobs.label.if),
realBehaviorProof: readPullRequestEditFields(
realBehaviorProof.jobs["real-behavior-proof"].if,
),
}).toEqual({
autoResponse: [],
clawsweeperDispatch: [],
labeler: ["title", "base"],
realBehaviorProof: ["body", "base"],
});
const labelerSteps = labeler.jobs.label.steps;
const changedFieldsForStep = (matcher: (step: WorkflowStep) => boolean) =>
readPullRequestEditFields(labelerSteps.find(matcher)?.if);
expect({
pathLabels: changedFieldsForStep(
(step) => step.uses?.startsWith("actions/labeler@") === true,
),
size: changedFieldsForStep((step) => step.name === "Apply PR size label"),
contributor: changedFieldsForStep(
(step) => step.name === "Apply maintainer or trusted-contributor label",
),
betaBlocker: changedFieldsForStep((step) => step.name === "Apply beta-blocker title label"),
activePrLimit: changedFieldsForStep((step) => step.name === "Apply too-many-prs label"),
}).toEqual({
pathLabels: ["base"],
size: ["base"],
contributor: [],
betaBlocker: ["title"],
activePrLimit: [],
});
});
it("keeps ClawSweeper dispatch events aligned with receiver workflows", () => {
const workflowPath = ".github/workflows/clawsweeper-dispatch.yml";
const source = readFileSync(workflowPath, "utf8");
const workflow = readWorkflow(workflowPath);
const steps = workflow.jobs.dispatch.steps as WorkflowStep[];
const receiverDispatchSteps = steps.filter((step) =>
step.run?.includes("repos/openclaw/clawsweeper/dispatches"),
);
const eventTypes = receiverDispatchSteps.map((step) => {
const matches = [...(step.run ?? "").matchAll(/\bevent_type\s*:\s*"([^"]+)"/gu)];
expect(matches, step.name).toHaveLength(1);
return expectDefined(matches[0]?.[1], step.name ?? "ClawSweeper dispatch event");
});
// This allowlist mirrors the target repository receiver contract; changes require coordinated receiver updates.
expect(eventTypes.toSorted()).toEqual([
"clawsweeper_comment",
"clawsweeper_item",
"github_activity",
]);
expect(source).not.toContain("clawsweeper_commit_review");
expect(source).not.toContain("CLAWSWEEPER_COMMIT_REVIEW_CREATE_CHECKS");
expect(workflow.on.push.branches).toEqual(["main"]);
const activityRun = expectDefined(
steps.find((step) => step.name === "Dispatch GitHub activity to ClawSweeper")?.run,
"ClawSweeper GitHub activity dispatch",
);
expect(activityRun).toMatch(
/push: \(if \$event_name == "push" then \{\s+before: \.before,\s+after: \.after,\s+ref: \.ref,\s+compare: \.compare,\s+head_commit: \.head_commit\.id\s+\} else null end\)/u,
);
const exactReviewStep = expectDefined(
steps.find((step) => step.name === "Dispatch exact ClawSweeper review"),
"ClawSweeper exact-review dispatch",
);
expect(exactReviewStep.env?.TARGET_BRANCH).toBe(
"${{ github.event.repository.default_branch }}",
);
expect(exactReviewStep.run).toContain('--arg target_branch "$TARGET_BRANCH"');
expect(exactReviewStep.run).toContain("target_branch:$target_branch");
expect(exactReviewStep.run).toContain('ingress_route:"target_dispatcher"');
expect(exactReviewStep.run).toContain("ingress_fingerprint:$ingress_fingerprint");
});
it("runs the PR context and evidence gate only for relevant PR changes", () => {
const workflow = readRealBehaviorProofWorkflow();
expect(workflow.name).toBe("PR context and evidence");
expect(workflow.jobs["real-behavior-proof"].name).toBe("PR context and evidence");
expect(workflow.on.pull_request_target.types).toEqual([
"opened",
"edited",
"synchronize",
"reopened",
"ready_for_review",
]);
expect(workflow.concurrency.group).toBe(
"${{ github.workflow }}-${{ github.event.pull_request.number }}",
);
expect(workflow.concurrency["cancel-in-progress"]).toBe(
"${{ github.event.action == 'synchronize' }}",
);
});
it.each([
// name, body, override, permission, issue, admitted, reacts
["plain", "Thanks", "", "write", false, false, false],
["empty", "", "", "write", false, false, false],
["multiline", "context\r\n \t/merge now \r\n", "", "write", false, true, true],
["slash nonmatch", "/landscape", "", "write", false, true, false],
["custom words", "ship it now", " ship it ,", "write", false, true, true],
["custom Unicode", " ✅ later ", "✅", "write", false, true, true],
["false override", "false", "false", "write", false, true, true],
["zero override", "0", "0", "write", false, true, true],
["empty parsed override", "Thanks", " , ", "write", false, true, false],
["non-maintainer", "/merge", "", "read", false, true, false],
["issue autoclose", "/autoclose reason", "", "write", true, true, true],
["issue non-autoclose", "/merge", "", "write", true, true, false],
] as const)(
"preserves reaction admission and actions for %s",
async (_name, body, commands, permission, issue, admitted, reacts) => {
const job = readWorkflow(".github/workflows/maintainer-command-reactions.yml").jobs.react;
const event = {
comment: { id: 456, body, user: { login: "comment-author" } },
issue: { number: 123, ...(issue ? {} : { pull_request: {} }) },
};
const context = {
repository: "openclaw/openclaw",
runAttempt: 1,
eventName: "issue_comment" as const,
actor: "event-actor",
githubEvent: event,
maintainerCommands: commands,
};
const reactions: string[] = [];
const permissionUsers: string[] = [];
const step = job.steps.find((candidate: WorkflowStep) => candidate.with?.script);
await runInNewContext(`(async () => { ${step.with.script} })()`, {
context: { payload: event, repo: { owner: "openclaw", repo: "openclaw" } },
process: {
env: {
MAINTAINER_COMMAND_REACTIONS: evaluateWorkflowExpression(
job.env.MAINTAINER_COMMAND_REACTIONS,
context,
),
},
},
core: { info() {}, warning() {} },
github: {
rest: {
repos: {
async getCollaboratorPermissionLevel({ username }: { username: string }) {
permissionUsers.push(username);
return { data: { permission } };
},
},
reactions: {
async createForIssueComment({ content }: { content: string }) {
reactions.push(content);
},
},
},
},
});
expect(reactions).toEqual(reacts ? ["eyes"] : []);
expect(permissionUsers.every((login) => login === "comment-author")).toBe(true);
expect(Boolean(evaluateWorkflowExpression(job.if, context))).toBe(admitted);
},
);
it("keeps reaction admission open for every configured default and closed for bot actors", () => {
const workflow = readWorkflow(".github/workflows/maintainer-command-reactions.yml");
const job = workflow.jobs.react;
const context = {
repository: "openclaw/openclaw",
runAttempt: 1,
eventName: "issue_comment" as const,
actor: "maintainer",
};
const defaults = String(
evaluateWorkflowExpression(job.env.MAINTAINER_COMMAND_REACTIONS, context),
);
for (const command of defaults.split(",")) {
// A future nonslash default would invalidate the necessary-character admission guard.
expect(command, "default commands must retain slash admission").toContain("/");
const githubEvent = { comment: { body: `earlier line\n ${command} args ` } };
expect(evaluateWorkflowExpression(job.if, { ...context, githubEvent })).toBe(true);
expect(
evaluateWorkflowExpression(job.if, {
...context,
githubEvent,
actor: "automation[bot]",
maintainerCommands: "ship",
}),
).toBe(false);
}
expect(workflow.on.issue_comment.types).toEqual(["created", "edited"]);
});
it.each([
["issue_comment", "created", "Bot", "human", false],
["issue_comment", "created", "User", "clawsweeper[bot]", true],
["issue_comment", "created", undefined, "human", true],
["issues", "opened", "Bot", "clawsweeper[bot]", true],
["pull_request_target", "opened", "Bot", "clawsweeper[bot]", true],
["pull_request_target", "labeled", "Bot", "clawsweeper[bot]", false],
["pull_request_target", "unlabeled", "Bot", "openclaw-clawsweeper[bot]", false],
["pull_request_target", "labeled", "Bot", "openclaw-barnacle[bot]", true],
["pull_request_target", "labeled", "User", "maintainer", true],
] as const)(
"preserves Barnacle comment admission for %s/%s/%s/%s",
(eventName, action, type, actor, admitted) => {
const job = readWorkflow(".github/workflows/auto-response.yml").jobs["auto-response"];
expect(
evaluateWorkflowExpression(job.if, {
repository: "openclaw/openclaw",
runAttempt: 1,
eventName,
actor,
githubEvent: {
action,
comment: { body: "testflight @openclaw/maintainer", user: { type } },
},
}),
).toBe(admitted);
},
);
it("isolates auto-response per item and ignores ClawSweeper PR label feedback", () => {
const workflow = readWorkflow(".github/workflows/auto-response.yml");
const guard = workflow.jobs["auto-response"].if;
expect(workflow.on.issues.types).toEqual(["opened", "edited", "labeled"]);
expect(workflow.on.issue_comment.types).toEqual(["created"]);
expect(workflow.on.pull_request_target.types).toEqual([
"opened",
"edited",
"synchronize",
"reopened",
"labeled",
"unlabeled",
]);
expect(workflow.concurrency.group).toBe(
"${{ github.workflow }}-${{ github.event.issue.number || github.event.pull_request.number }}",
);
expect(workflow.concurrency["cancel-in-progress"]).toBe(
"${{ github.event_name == 'pull_request_target' && github.event.action == 'synchronize' }}",
);
expect(guard).toContain("github.event_name != 'pull_request_target'");
expect(guard).toContain("github.event.action != 'labeled'");
expect(guard).toContain("github.event.action != 'unlabeled'");
expect(guard).toContain("github.actor != 'clawsweeper[bot]'");
expect(guard).toContain("github.actor != 'openclaw-clawsweeper[bot]'");
expect(guard).not.toContain("openclaw-barnacle[bot]");
});
it("routes stale bug issues through ClawSweeper instead of Barnacle closure", () => {
const staleWorkflow = readWorkflow(".github/workflows/stale.yml");
const staleSteps = staleWorkflow.jobs.stale.steps as WorkflowStep[];
const stepNamed = (name: string) =>
expectDefined(
staleSteps.find((step) => step.name === name),
name,
);
for (const name of [
"Mark stale unassigned issues and pull requests (primary)",
"Mark stale assigned issues (primary)",
"Mark stale unassigned issues and pull requests (fallback)",
"Mark stale assigned issues (fallback)",
]) {
const exemptLabels = String(stepNamed(name).with?.["exempt-issue-labels"])
.split(",")
.map((label) => label.trim());
expect(exemptLabels, name).toContain("bug");
}
const bugJob = staleWorkflow.jobs["stale-bug-verification"];
expect(bugJob.permissions).toEqual({ issues: "write" });
expect(bugJob["runs-on"]).toBe("ubuntu-24.04");
const bugScript = String(
(bugJob.steps as WorkflowStep[]).find(
(step) => step.name === "Mark inactive bugs for ClawSweeper verification",
)?.with?.script,
);
expect(bugScript).toContain("const maxMarks = 25;");
expect(bugScript).toContain('labels: "bug"');
expect(bugScript).toContain("github.rest.issues.addLabels");
expect(bugScript).toContain("github.rest.issues.removeLabel");
expect(bugScript).toContain("Inactivity alone will not close a bug report.");
expect(bugScript).toContain("requires separate backfill approval");
expect(bugScript).toContain("slice(staleEventIndex + 1)");
expect(bugScript).toContain("updatedAtMs > lastAutomationAtMs");
expect(bugScript).toContain('item.state !== "open"');
expect(bugScript).not.toContain("15_000");
expect(bugScript).not.toContain("github.rest.issues.update");
const backfillScript = String(
(staleWorkflow.jobs["backfill-stale-closures"].steps as WorkflowStep[]).find(
(step) => step.name === "Backfill stale closures",
)?.with?.script,
);
expect(backfillScript).toMatch(/issueExemptLabels[\s\S]*"bug"/);
const dispatchWorkflow = readWorkflow(".github/workflows/clawsweeper-dispatch.yml");
const dispatchCondition = String(dispatchWorkflow.jobs.dispatch.if);
expect(dispatchCondition).toContain("github.event.label.name == 'stale'");
expect(dispatchCondition).toContain("contains(github.event.issue.labels.*.name, 'bug')");
expect(dispatchCondition).toContain("github.actor_id == '257215752'");
expect(dispatchCondition).toContain("github.actor_id == '264559031'");
const auditJob = staleWorkflow.jobs["audit-bug-closure-reasons"];
expect(auditJob.permissions).toEqual({ issues: "read" });
const auditScript = String((auditJob.steps as WorkflowStep[])[0]?.with?.script);
expect(auditScript).toContain('item.state_reason !== "not_planned"');
expect(auditScript).toContain("github.rest.issues.listEventsForTimeline");
expect(auditScript).toContain("github.paginate.iterator(");
expect(auditScript).toContain("new Set([257215752, 264559031])");
expect(auditScript).toContain("escapeSummaryCell(violation.title)");
expect(auditScript).toContain('.replaceAll("<", "&lt;")');
expect(auditScript).toContain("core.setFailed(");
expect(auditScript).not.toContain("github.rest.issues.update");
expect(auditScript).not.toContain("github.rest.issues.createComment");
});
it("makes the hosted release-gate fallback explicit and exact-SHA only", () => {
const workflow = readCiWorkflow();
const releaseGate = workflow.on.workflow_dispatch.inputs.release_gate;
expect(releaseGate).toEqual({
description:
"Run an exact-SHA maintainer release-gate fallback when PR CI is capacity-stalled.",
required: false,
default: false,
type: "boolean",
});
expect(workflow.on.workflow_dispatch.inputs.dispatch_id).toEqual({
description: "Optional parent workflow dispatch identifier",
required: false,
default: "",
type: "string",
});
expect(workflow.on.workflow_dispatch.inputs.pull_request_number).toEqual({
description: "Pull request number required by the exact-SHA release gate.",
required: false,
default: "",
type: "string",
});
expect(workflow.on.workflow_dispatch.inputs).not.toHaveProperty("loc_base_ref");
expect(workflow.on.workflow_dispatch.inputs).not.toHaveProperty("pr_number");
expect(workflow.on.workflow_dispatch.inputs.release_scope).toMatchObject({
default: "full",
type: "choice",
options: ["full", "npm-beta", "npm-stable"],
});
expect(workflow.jobs.preflight.outputs.release_scope).toBe(
"${{ steps.manifest.outputs.release_scope }}",
);
expect(readFileSync(".github/workflows/ci.yml", "utf8")).toContain(
"run-name: ${{ github.event_name == 'workflow_dispatch' && inputs.dispatch_id != '' && format('CI {0}', inputs.dispatch_id) || (github.event_name == 'workflow_dispatch' && inputs.release_gate && format('CI release gate {0}', inputs.target_ref) || 'CI') }}",
);
const preflightSteps = workflow.jobs.preflight.steps;
expect(
preflightSteps.find((step: WorkflowStep) => step.name === "Build CI manifest").env,
).toMatchObject({
OPENCLAW_CI_RELEASE_SCOPE: "${{ inputs.release_scope || 'full' }}",
OPENCLAW_CI_PULL_REQUEST_NUMBER: "${{ inputs.pull_request_number }}",
OPENCLAW_CI_TARGET_REF: "${{ inputs.target_ref }}",
OPENCLAW_CI_TARGET_CONTEXT_REF: "${{ inputs.target_context_ref }}",
OPENCLAW_CI_HISTORICAL_TARGET_TAG: "${{ inputs.historical_target_tag }}",
});
const validationStep = preflightSteps.find(
(step: WorkflowStep) => step.name === "Validate release-gate dispatch",
);
expect(validationStep.if).toBe(
"github.event_name == 'workflow_dispatch' && inputs.release_gate",
);
expect(validationStep.run).toContain(
"release_gate requires target_ref to be a full commit SHA",
);
expect(validationStep.run).toContain("release_gate requires pull_request_number");
expect(validationStep.run).toContain("release_gate must run from the branch at target_ref");
expect(validationStep.run).toContain(
"release_gate cannot be combined with historical_target_tag",
);
const diffBaseStep = preflightSteps.find(
(step: WorkflowStep) => step.name === "Resolve exact diff base",
);
expect(diffBaseStep.env).toMatchObject({
PULL_REQUEST_NUMBER: "${{ inputs.pull_request_number }}",
RELEASE_GATE: "${{ inputs.release_gate }}",
});
expect(diffBaseStep.run).toContain("refs/pull/${PULL_REQUEST_NUMBER}/merge");
expect(diffBaseStep.run).toContain('release_gate_head="$(git rev-parse "${merge_ref}^2")"');
expect(diffBaseStep.run).toContain(
"release_gate pull request head ${release_gate_head} does not match target ${target_head}",
);
expect(diffBaseStep.run).toContain('base_sha="$(git rev-parse "${merge_ref}^1")"');
expect(diffBaseStep.run).toContain('head_sha="$(git rev-parse "$merge_ref")"');
expect(diffBaseStep.run).toContain('echo "head_sha=$head_sha" >> "$GITHUB_OUTPUT"');
const changedScopeStep = preflightSteps.find(
(step: WorkflowStep) => step.name === "Detect changed scopes",
);
expect(changedScopeStep.if).toContain(
"github.event_name == 'workflow_dispatch' && inputs.release_gate",
);
expect(changedScopeStep.env?.OPENCLAW_ALLOW_RELEASE_GENERATED_MIX).toContain(
"github.event_name == 'workflow_dispatch'",
);
expect(changedScopeStep.run).toContain('elif [ "${{ github.event_name }}" = "pull_request" ]');
expect(changedScopeStep.run).toContain('HEAD_SHA="${{ steps.diff_base.outputs.head_sha }}"');
expect(changedScopeStep.run).toContain(
'node scripts/ci-changed-scope.mjs --base "$BASE" --head "$HEAD_SHA"',
);
expect(workflow.jobs.preflight.permissions).toEqual({ contents: "read" });
expect(workflow.jobs.preflight.outputs.run_ios_screenshots).toBe(
"${{ steps.changed_scope.outputs.run_ios_screenshots }}",
);
const workflowSource = readFileSync(".github/workflows/ci.yml", "utf8");
expect(workflowSource).toContain(
"OPENCLAW_CI_RUN_MACOS: ${{ github.event_name == 'workflow_dispatch' && !inputs.release_gate && 'true' || steps.changed_scope.outputs.run_macos || 'false' }}",
);
expect(workflowSource).toContain(
"OPENCLAW_CI_RUN_IOS_BUILD: ${{ github.event_name == 'workflow_dispatch' && !inputs.release_gate && 'true' || steps.changed_scope.outputs.run_ios_build || 'false' }}",
);
expect(workflowSource).toContain(
"OPENCLAW_CI_RUN_ANDROID: ${{ github.event_name == 'workflow_dispatch' && (inputs.release_gate || inputs.include_android) && 'true' || steps.changed_scope.outputs.run_android || 'false' }}",
);
for (const [jobName, job] of Object.entries(workflow.jobs)) {
const runsOn = (job as { "runs-on"?: unknown })["runs-on"];
if (typeof runsOn !== "string" || !runsOn.includes("blacksmith-")) {
continue;
}
expect(
evaluateWorkflowExpression(runsOn, {
eventName: "workflow_dispatch",
releaseGate: true,
repository: "openclaw/openclaw",
runAttempt: 1,
runnerBackend: "hybrid",
}),
`${jobName} must use GitHub-hosted capacity for release gates`,
).toMatch(/^(?:ubuntu|windows|macos)-/u);
}
expect(
workflow.jobs["macos-node"]["runs-on"],
"macOS Node retries must escape stalled Blacksmith capacity",
).toContain("github.run_attempt > 1");
});
it.each([
["macos-swift", false, "workflow_dispatch", false, ["release", "tests"]],
["ios-build", false, "workflow_dispatch", false, ["release", "tests"]],
["ios-build", true, "workflow_dispatch", false, ["tests"]],
["ios-build", false, "pull_request", false, ["smoke"]],
["ios-build", false, "push", false, ["smoke"]],
["ios-build", false, "workflow_dispatch", true, ["smoke"]],
["ios-build", true, "workflow_dispatch", true, ["smoke"]],
] as const)(
"runs %s native phases (historical=%s, event=%s, release_gate=%s)",
(jobName, historical, eventName, releaseGate, phases) => {
const workflow = readCiWorkflow();
const job = workflow.jobs[jobName];
const context = {
eventName,
releaseGate,
repository: "openclaw/openclaw",
runAttempt: 1,
env: { HISTORICAL_TARGET: String(historical), MACOS_PRIMARY_PHASE: "release" },
fileHashes: { "apps/shared/OpenClawWatchRTC/Cargo.toml": "present" },
preflightOutputs: {
compatibility_target: String(historical),
run_openclawkit_tests: "true",
release_scope: "full",
},
};
const matrixPhases = job.strategy.matrix.phase;
expect(
Array.isArray(matrixPhases)
? matrixPhases
: evaluateWorkflowExpression(matrixPhases, context),
).toEqual(phases);
expect(job.strategy["fail-fast"]).toBe(false);
expect(job.strategy["max-parallel"]).toBe(2);
expect(job["continue-on-error"]).not.toBe(true);
expect(job.needs).toEqual(["preflight"]);
const workloads =
jobName === "macos-swift"
? {
smoke: [],
release: [
"Native state schema version contract",
"Swift lint",
"Swift build (release)",
],
tests: [
"OpenClawKit Talk-trait opt-out (no ElevenLabsKit when default traits disabled)",
"OpenClawKit tests",
"Swabble tests",
"Swift test",
],
}
: {
smoke: [
"Swift lint",
"Build iOS app",
...(historical ? [] : ["Run focused iOS voice cleanup simulator tests"]),
],
release: ["Build iOS app (Release)"],
tests: [
"Test Watch RTC engine",
"Swift lint",
"Build iOS app",
"Run focused iOS voice cleanup simulator tests",
"Run focused iOS lifecycle simulator tests",
"Run focused Apple Watch operation simulator tests",
],
};
const names = [];
for (const phase of phases) {
const phaseContext = { ...context, matrix: { phase } };
const expected =
historical && phase === "tests"
? ["Test Watch RTC engine", "Swift lint", "Build iOS app"]
: workloads[phase];
names.push(evaluateWorkflowExpression(job.name, phaseContext));
const selected = job.steps
.filter((step: WorkflowStep) =>
Object.values(workloads)
.flat()
.includes(step.name ?? ""),
)
.filter(
(step: WorkflowStep) =>
!step.if || evaluateWorkflowExpression(`\${{ ${step.if} }}`, phaseContext),
)
.map((step: WorkflowStep) => step.name);
expect(selected, phase).toEqual(expected);
if (jobName === "ios-build") {
for (const name of [
"Select Xcode 26",
"Setup Node environment",
"Install Watch Rust toolchain",
"Install iOS Swift tooling",
]) {
const setup = expectDefined(
job.steps.find((step: WorkflowStep) => step.name === name),
name,
);
expect(
!setup.if || evaluateWorkflowExpression(`\${{ ${setup.if} }}`, phaseContext),
).toBe(true);
}
}
}
// The release collector keys retained/rerun evidence by the displayed job name.
expect(new Set(names).size).toBe(phases.length);
expect(workflow.jobs["ci-gate"].needs).toContain(jobName);
const gateStep = workflow.jobs["ci-gate"].steps.find(
(step: WorkflowStep) => step.name === "Verify selected CI lanes",
);
expect(gateStep.env.JOB_RESULTS).toContain(`${jobName}=\${{ needs.${jobName}.result }}`);
for (const conclusion of ["failure", "cancelled", "skipped"]) {
expect(
runCiGateFixture(`preflight=success|true\n${jobName}=${conclusion}|true`).status,
).toBe(1);
}
},
);
it.each([
{ eventName: "pull_request", releaseGate: false, full: false },
{ eventName: "push", releaseGate: false, full: false },
{ eventName: "workflow_dispatch", releaseGate: true, full: false },
{ eventName: "workflow_dispatch", releaseGate: false, full: true },
{ eventName: "workflow_dispatch", releaseGate: false, full: true, historical: true },
] as const)(
"retains macOS tests and one guard/cache owner for %j",
({ eventName, releaseGate, full, ...target }) => {
const historical = "historical" in target && target.historical;
const job = readCiWorkflow().jobs["macos-swift"];
const manifest = runCiManifestFixture({
bundledPlanner: true,
eventName,
releaseGate,
runNode: false,
historicalCompatibility: historical,
});
expect(manifest.status, manifest.output).toBe(0);
expect(manifest.outputs.release_scope).toBe("full");
const context: Parameters<typeof evaluateWorkflowExpression>[1] = {
eventName,
releaseGate,
releaseScope: "",
repository: "openclaw/openclaw",
runAttempt: 1,
preflightOutputs: {
...manifest.outputs,
cache_write_allowed: "true",
},
fileHashes: { "scripts/test-macos-health-render.sh": "present" },
};
const phases: string[] = Array.isArray(job.strategy.matrix.phase)
? job.strategy.matrix.phase
: evaluateWorkflowExpression(job.strategy.matrix.phase, context);
expect(phases).toEqual(full ? ["release", "tests"] : ["tests"]);
const env = Object.fromEntries(
Object.entries(job.env).map(([key, value]) => [
key,
String(evaluateWorkflowExpression(value, context)),
]),
);
const selectedPhases = (name: string, overrides: Partial<typeof context> = {}) => {
const step: WorkflowStep = expectDefined(
job.steps.find((candidate: WorkflowStep) => candidate.name === name),
name,
);
expect(step["continue-on-error"] === true, name).toBe(name === "Save SwiftPM cache");
return phases.filter((phase) =>
evaluateWorkflowExpression(
step.if?.startsWith("${{") ? step.if : `\${{ ${step.if ?? "true"} }}`,
{
...context,
env,
matrix: { phase },
steps: {
"swift-test": {
outputs: { "debug-tests-built": phase === "tests" ? "true" : "" },
},
"swiftpm-cache": { outputs: { "cache-hit": "false" } },
"swift-cache-budget": { outputs: { allowed: "true" } },
},
...overrides,
},
),
);
};
for (const name of [
"Install XcodeGen / SwiftLint / SwiftFormat",
"Native state schema version contract",
"Swift lint",
"Save SwiftPM cache",
]) {
expect(selectedPhases(name), name).toEqual([full ? "release" : "tests"]);
}
for (const name of [
"OpenClawKit Talk-trait opt-out (no ElevenLabsKit when default traits disabled)",
"OpenClawKit tests",
"Swift test",
]) {
expect(selectedPhases(name), name).toEqual(["tests"]);
}
expect(selectedPhases("Swabble tests")).toEqual(historical ? [] : ["tests"]);
expect(selectedPhases("Swift build (release)")).toEqual(full ? ["release"] : []);
expect(selectedPhases("Render isolated macOS health fixtures")).toEqual(
full ? ["tests"] : [],
);
expect(selectedPhases("Render isolated macOS health fixtures", { cancelled: true })).toEqual(
[],
);
expect(
selectedPhases("Save SwiftPM cache", {
preflightOutputs: { ...context.preflightOutputs, cache_write_allowed: "false" },
}),
).toEqual([]);
expect(
selectedPhases("Save SwiftPM cache", {
steps: { "swiftpm-cache": { outputs: { "cache-hit": "true" } } },
}),
).toEqual([]);
for (const result of ["failure", "cancelled", "skipped"]) {
expect(
runCiGateFixture(renderCiGateEnvironment(context, { "macos-swift": result })).status,
).toBe(1);
}
},
);
it("starts Apple builds and screenshots directly on hosted capacity", () => {
const workflow = readCiWorkflow();
for (const jobName of ["macos-swift", "ios-build", "ios-screenshot-shard"]) {
expect(workflow.jobs[jobName]["runs-on"], jobName).toBe("macos-26");
}
expect(workflow.jobs["macos-swift"]["timeout-minutes"]).toBe(30);
});
it("serializes the shared Swift package suite on hosted macOS retries", () => {
const macosSwift = readCiWorkflow().jobs["macos-swift"];
expect(macosSwift.env.OPENCLAWKIT_TEST_EXECUTION).toContain("github.run_attempt > 1");
const openClawKitTests = macosSwift.steps.find(
(candidate: WorkflowStep) => candidate.name === "OpenClawKit tests",
);
expect(openClawKitTests?.run).toContain('if [[ "$OPENCLAWKIT_TEST_EXECUTION" == "parallel" ]]');
expect(openClawKitTests?.run).toContain("--parallel");
expect(openClawKitTests?.run).toContain("--no-parallel");
});
it("keeps Testbox pull request validation off leased runner capacity", () => {
const workflow = readTestboxWorkflow();
expect(workflow.on.pull_request).toEqual({
types: ["opened", "reopened", "synchronize", "ready_for_review"],
paths: [".github/workflows/**"],
});
expect(workflow.jobs.check.if).toBe(
"${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }}",
);
expect(workflow.jobs.check["runs-on"]).toBe(
"${{ github.event_name == 'pull_request' && 'ubuntu-24.04' || 'blacksmith-16vcpu-ubuntu-2404' }}",
);
const beginStep = workflow.jobs.check.steps.find(
(step: { name?: string }) => step.name === "Begin Testbox",
);
const runStep = workflow.jobs.check.steps.find(
(step: { name?: string }) => step.name === "Run Testbox",
);
expect(beginStep).toMatchObject({
if: "github.event_name == 'workflow_dispatch'",
with: { testbox_id: "${{ inputs.testbox_id }}" },
});
expect(runStep).toMatchObject({
if: "github.event_name == 'workflow_dispatch' && always()",
});
});
it("keeps every path-filtered hosted gate runnable on landing-relevant events", () => {
const workflows = [
[".github/workflows/ci-check-testbox.yml", "check"],
[".github/workflows/ci-check-arm-testbox.yml", "check-arm"],
[".github/workflows/ci-build-artifacts-testbox.yml", "build-artifacts"],
] as const;
for (const [workflowPath, jobName] of workflows) {
const workflow = readWorkflow(workflowPath);
expect(workflow.on.pull_request).toEqual({
types: ["opened", "reopened", "synchronize", "ready_for_review"],
paths: [".github/workflows/**"],
});
expect(workflow.jobs[jobName].if).toBe(
"${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }}",
);
}
});
it("pins every external GitHub Action reference to a full commit SHA", () => {
expect(findUnpinnedExternalActions()).toEqual([]);
});
it("schedules approved Docker refreshes from independently resolved channels", () => {
const workflow = readWorkflow(".github/workflows/docker-image-refresh.yml");
const releaseWorkflow = readWorkflow(".github/workflows/docker-release.yml");
const plan = workflow.jobs.plan;
const publish = workflow.jobs.publish;
const planSteps = plan.steps as WorkflowStep[];
const mainGuard = expectDefined(
planSteps.find((step) => step.name === "Require a main-branch run"),
"Docker refresh main-branch guard",
);
const resolve = expectDefined(
planSteps.find((step) => step.name === "Resolve refresh plan"),
"Docker refresh plan step",
);
expect(workflow.on.schedule).toEqual([{ cron: "17 3 * * 1" }]);
expect(workflow.on.workflow_dispatch.inputs.channel).toEqual({
description: "Release channel to rebuild",
required: false,
default: "both",
type: "choice",
options: ["stable", "extended-stable", "both"],
});
expect(workflow.on.workflow_dispatch.inputs.dry_run).toEqual({
description: "Resolve and summarize without publishing",
required: false,
default: false,
type: "boolean",
});
expect(plan.permissions).toEqual({ contents: "read" });
expect(mainGuard.run).toContain('[[ "${WORKFLOW_REF}" != "refs/heads/main" ]]');
expect(resolve.run).toContain("docker-release-policy.mjs --current");
expect(resolve.run).toContain('git rev-parse "refs/tags/${stable_tag}^{commit}"');
expect(resolve.run).toContain('git rev-parse "refs/tags/${extended_stable_tag}^{commit}"');
expect(resolve.run).toContain('suffix="-r$(date -u +%Y%m%d)"');
expect(resolve.run).toContain('echo "matrix=${matrix}"');
expect(resolve.run).toContain('} >> "${GITHUB_OUTPUT}"');
expect(plan.environment).toBeUndefined();
expect(publish.environment).toBeUndefined();
expect(publish.needs).toBe("plan");
expect(publish.if).toBe("needs.plan.outputs.dry_run != 'true'");
expect(publish.strategy).toEqual({
"fail-fast": false,
matrix: { include: "${{ fromJSON(needs.plan.outputs.matrix) }}" },
});
expect(publish.uses).toBe("./.github/workflows/docker-release.yml");
expect(publish.with).toEqual({
tag: "${{ matrix.tag }}",
release_sha: "${{ matrix.release_sha }}",
image_tag_suffix: "${{ needs.plan.outputs.image_tag_suffix }}",
});
expect(publish.secrets).toEqual({
DOCKERHUB_USERNAME: "${{ secrets.DOCKERHUB_USERNAME }}",
DOCKERHUB_TOKEN: "${{ secrets.DOCKERHUB_TOKEN }}",
});
expect(publish.permissions).toEqual({
actions: "read",
attestations: "read",
contents: "read",
packages: "write",
});
expect(releaseWorkflow.jobs.approve.environment).toBe("docker-release");
expect(releaseWorkflow.jobs.publish.environment).toBeUndefined();
expect(releaseWorkflow.jobs.publish.needs).toContain("approve");
});
it("forbids moving reusable workflow references", () => {
expect([...OIDC_BOUND_MAIN_REUSABLE_WORKFLOWS]).toEqual([]);
});
it("keeps locale refresh matrices alive and publishes each aggregate through a PR", () => {
const controlUiWorkflow = parse(readFileSync(CONTROL_UI_LOCALE_REFRESH_WORKFLOW, "utf8"));
const workflow = parse(readFileSync(NATIVE_APP_LOCALE_REFRESH_WORKFLOW, "utf8"));
const controlUiResolveBase = controlUiWorkflow.jobs["resolve-base"];
const nativeResolveBase = workflow.jobs["resolve-base"];
const controlUiPreflight = controlUiWorkflow.jobs["publisher-preflight"];
const nativePreflight = workflow.jobs["publisher-preflight"];
const refresh = workflow.jobs.refresh;
const nativeFinalize = workflow.jobs.finalize;
const controlUiFinalize = controlUiWorkflow.jobs.finalize;
const refreshStep = refresh.steps.find(
(step: { name?: string }) => step.name === "Refresh locale translations",
);
const nativeArtifactStep = refresh.steps.find(
(step: { name?: string }) => step.name === "Prepare locale artifact",
);
const nativeGeneratedStep = nativeFinalize.steps.find(
(step: { name?: string }) => step.name === "Refresh native generated artifacts",
);
const nativeValidationStep = nativeFinalize.steps.find(
(step: { name?: string }) => step.name === "Validate native locale refresh",
);
const nativePublishStep = nativeFinalize.steps.find(
(step: { name?: string }) => step.name === "Open or update generated locale PR",
);
const controlUiRefreshStep = controlUiWorkflow.jobs.refresh.steps.find(
(step: { name?: string }) => step.name === "Refresh locale translations",
);
const controlUiAggregateStep = controlUiFinalize.steps.find(
(step: { name?: string }) => step.name === "Finalize control UI generated artifacts",
);
const controlUiValidationStep = controlUiFinalize.steps.find(
(step: { name?: string }) => step.name === "Validate control UI locale refresh",
);
expect(refresh.if).toBe(
"needs.resolve-base.result == 'success' && needs.publisher-preflight.result == 'success'",
);
expect(refresh.strategy.matrix.locale).toEqual(NATIVE_I18N_LOCALES);
expect(controlUiWorkflow.concurrency["cancel-in-progress"]).toBe(false);
expect(controlUiWorkflow.concurrency.group.replace(/\s+/gu, " ")).toBe(
"${{ github.event_name == 'workflow_dispatch' && inputs.token_preflight_only && format('control-ui-locale-token-preflight-{0}', github.ref) || 'control-ui-locale-refresh' }}",
);
expect(controlUiWorkflow.jobs.plan).toBeUndefined();
expect(controlUiResolveBase.outputs.locales).toBe("${{ steps.base.outputs.locales }}");
expect(controlUiWorkflow.jobs.refresh.if).toBe(
"needs.resolve-base.result == 'success' && needs.publisher-preflight.result == 'success' && !(github.event_name == 'workflow_dispatch' && inputs.token_preflight_only)",
);
expect(controlUiWorkflow.jobs.refresh.strategy.matrix.locale).toBe(
"${{ fromJSON(needs.resolve-base.outputs.locales) }}",
);
expect(workflow.concurrency["cancel-in-progress"]).toBe(false);
expect(workflow.concurrency.group).toBe("native-app-locale-refresh");
expect(controlUiResolveBase.if).not.toContain("chore(ui): refresh control ui locales");
const controlResolveCondition = controlUiResolveBase.if.replace(/\s+/gu, " ");
expect(controlResolveCondition).toBe(
"github.repository == 'openclaw/openclaw' && (github.event_name != 'workflow_dispatch' || github.ref == 'refs/heads/main')",
);
expect(controlResolveCondition).not.toContain("inputs.token_preflight_only");
expect(controlResolveCondition).not.toContain("github.ref_type");
expect(nativeResolveBase.if).toBe(
"github.repository == 'openclaw/openclaw' && (github.event_name != 'workflow_dispatch' || github.ref == 'refs/heads/main')",
);
expect(controlUiWorkflow.on.workflow_dispatch.inputs.token_preflight_only).toEqual({
description: "Verify generated PR App permissions without running locale generation.",
required: false,
default: false,
type: "boolean",
});
for (const owner of [workflow, controlUiWorkflow]) {
expect(owner.on.workflow_dispatch.inputs.full_refresh).toMatchObject({
default: false,
type: "boolean",
});
}
expect(workflow.on.push.paths).toContain("ui/src/i18n/.i18n/glossary.*.json");
expect(workflow.on.push.paths).toContain("apps/.i18n/native/**");
expect(workflow.on.push.paths).toContain("apps/.i18n/native-source.json");
expect(workflow.on.push.paths).toContain("apps/android/app/src/play/**");
expect(workflow.on.push.paths).toContain("apps/android/app/src/thirdParty/**");
expect(workflow.on.push.paths).toContain("apps/android/wear/src/main/**");
for (const generatorInput of [
"scripts/android-app-i18n.ts",
"scripts/apple-app-i18n.ts",
"scripts/native-app-i18n.ts",
"scripts/native-i18n-locales.ts",
]) {
expect(workflow.on.push.paths).toContain(generatorInput);
expect(nativePublishStep.with["invalidation-paths"].trim().split("\n")).toContain(
generatorInput,
);
}
expect(refreshStep.env.OPENAI_API_KEY).toBe(
"${{ secrets.OPENCLAW_DOCS_I18N_OPENAI_API_KEY || secrets.OPENAI_API_KEY }}",
);
expect(refreshStep.env.OPENCLAW_CONTROL_UI_I18N_MODEL).toBe(
"${{ secrets.OPENCLAW_I18N_MODEL }}",
);
expect(refreshStep.env.OPENCLAW_I18N_FALLBACK_MODEL).toBe(
"${{ secrets.OPENCLAW_I18N_FALLBACK_MODEL }}",
);
expect(refreshStep.env.FULL_REFRESH).toBe("${{ inputs.full_refresh || false }}");
expect(refreshStep.run).toContain("args+=(--force)");
expect(nativeArtifactStep.run).toContain("git add -A apps/.i18n/native");
expect(nativeArtifactStep.run).not.toContain("native-source.json");
expect(nativeGeneratedStep.run).toBe(
"node --import tsx scripts/native-app-i18n.ts sync --write",
);
expect(nativeValidationStep.run).toBe("node --import tsx scripts/native-app-i18n.ts check");
expect(nativeFinalize.steps.map((step: { name?: string }) => step.name)).not.toContain(
"Refresh Android native resources",
);
expect(nativeFinalize.steps.map((step: { name?: string }) => step.name)).not.toContain(
"Refresh Apple native resources",
);
expect(nativePublishStep.with["generated-paths"].trim().split("\n")).toEqual([
"apps/.i18n/native",
"apps/android/app/src/main/java/ai/openclaw/app/i18n/NativeStringResources.kt",
"apps/android/app/src/main/res/values*/assistant.xml",
"apps/android/app/src/main/res/values*/strings.xml",
"apps/android/app/src/thirdParty/res/values*/accessibility_strings.xml",
"apps/android/wear/src/main/res/values*/strings.xml",
"apps/ios/Resources/Localizable.xcstrings",
"apps/macos/Sources/OpenClaw/Resources/Localizable.xcstrings",
"apps/ios/Sources/*.lproj/InfoPlist.strings",
"apps/ios/WatchApp/*.lproj/InfoPlist.strings",
"apps/ios/ShareExtension/*.lproj/InfoPlist.strings",
"apps/ios/ActivityWidget/*.lproj/InfoPlist.strings",
]);
expect(nativePublishStep.with["invalidation-paths"]).toContain("apps/.i18n/native-source.json");
expect(nativePublishStep.with["invalidation-paths"]).toContain("apps/android/app/src/play");
expect(nativePublishStep.with["invalidation-paths"]).toContain(
"apps/android/app/src/thirdParty",
);
expect(nativePublishStep.with["auto-merge"]).toBe("true");
expect(controlUiRefreshStep.env.OPENAI_API_KEY).toBe(
"${{ secrets.OPENCLAW_DOCS_I18N_OPENAI_API_KEY || secrets.OPENAI_API_KEY }}",
);
expect(controlUiRefreshStep.env.OPENCLAW_CONTROL_UI_I18N_MODEL).toBe(
"${{ secrets.OPENCLAW_I18N_MODEL }}",
);
expect(controlUiRefreshStep.env.OPENCLAW_I18N_FALLBACK_MODEL).toBe(
"${{ secrets.OPENCLAW_I18N_FALLBACK_MODEL }}",
);
expect(controlUiRefreshStep.env.FULL_REFRESH).toBe("${{ inputs.full_refresh || false }}");
expect(controlUiRefreshStep.run).toContain("args+=(--force)");
expect(controlUiRefreshStep.env.OPENCLAW_CONTROL_UI_I18N_AUTH_OPTIONAL).toBe("0");
const controlUiArtifactStep = controlUiWorkflow.jobs.refresh.steps.find(
(step: { name?: string }) => step.name === "Prepare locale artifact",
);
expect(controlUiArtifactStep.run).toContain(
":(exclude)ui/src/i18n/.i18n/catalog-fallbacks.json",
);
expect(controlUiArtifactStep.run).toContain("ui/src/i18n/.i18n/${LOCALE}.tm.jsonl");
expect(controlUiArtifactStep.run).toContain("ui/src/i18n/.i18n/${LOCALE}.meta.json");
expect(controlUiArtifactStep.run).not.toContain("git add -A ui/src/i18n");
expect(controlUiAggregateStep.run).toBe(
"node --import tsx scripts/control-ui-i18n.ts sync --write",
);
const controlUiPublishStep = controlUiFinalize.steps.find(
(step: { name?: string }) => step.name === "Open or update generated locale PR",
);
const sharedCatalogInputs = [
"scripts/lib/control-ui-i18n-catalog.ts",
"scripts/lib/control-ui-i18n-catalog-values.ts",
"ui/src/i18n/lib/config-hint-translation.ts",
"ui/src/lib/fnv1a.ts",
"src/config/schema*.ts",
"src/config/zod-schema*.ts",
"src/config/media-audio-field-metadata.ts",
"src/config/talk-defaults.ts",
"src/config/channel-config-keys.ts",
];
const sharedCatalogInputOwners = [
workflow.on.push.paths,
nativePublishStep.with["invalidation-paths"].trim().split("\n"),
controlUiWorkflow.on.push.paths,
controlUiPublishStep.with["invalidation-paths"].trim().split("\n"),
];
for (const catalogInput of sharedCatalogInputs) {
for (const ownerPaths of sharedCatalogInputOwners) {
expect(ownerPaths).toContain(catalogInput);
}
}
expect(controlUiPublishStep.with["generated-paths"].trim().split("\n")).toEqual([
"ui/src/i18n/.i18n/*.tm.jsonl",
"ui/src/i18n/.i18n/*.meta.json",
"ui/src/i18n/.i18n/catalog-fallbacks.json",
]);
expect(controlUiPublishStep.with["invalidation-paths"]).toContain(
"scripts/lib/control-ui-i18n-sync-plan.ts",
);
expect(controlUiPublishStep.with["invalidation-paths"]).toContain("ui/src/i18n/locales/*.ts");
expect(controlUiPublishStep.with["invalidation-paths"]).toContain(
"ui/src/i18n/locales/en-agents.ts",
);
expect(controlUiPublishStep.with["invalidation-paths"]).toContain(
"scripts/control-ui-i18n-verify.ts",
);
expect(controlUiPublishStep.with["invalidation-paths"]).toContain(
"scripts/lib/control-ui-i18n-raw-copy.ts",
);
expect(controlUiFinalize.steps.indexOf(controlUiAggregateStep)).toBeLessThan(
controlUiFinalize.steps.indexOf(controlUiValidationStep),
);
for (const ownerWorkflow of [controlUiWorkflow, workflow]) {
expect(ownerWorkflow.on.push.paths).toContain(CREATE_GENERATED_PR_TOKENS_ACTION);
expect(ownerWorkflow.on.push.paths).toContain(PUBLISH_GENERATED_PR_ACTION);
const resolveBase = ownerWorkflow.jobs["resolve-base"];
const resolveStep = resolveBase.steps.find(
(step: { name?: string }) =>
step.name ===
(ownerWorkflow === controlUiWorkflow
? "Resolve source commit"
: "Resolve default branch head"),
);
expect(resolveBase.outputs.sha).toBe("${{ steps.base.outputs.sha }}");
expect(resolveStep.env.GH_TOKEN).toBe("${{ github.token }}");
if (ownerWorkflow === controlUiWorkflow) {
expect(resolveStep.run.match(/gh api/gu)).toHaveLength(1);
expect(resolveStep.run).toContain("gh api graphql");
} else {
expect(resolveStep.run).toContain(
'gh api --method GET "repos/${REPOSITORY}/commits/${DEFAULT_BRANCH}" --jq .sha',
);
}
expect(resolveStep.run).toContain('[[ ! "${sha}" =~ ^[0-9a-f]{40}$ ]]');
const checkoutSteps = (
Object.values(ownerWorkflow.jobs) as Array<{
steps?: Array<{ uses?: string; with?: Record<string, unknown> }>;
}>
).flatMap((job: { steps?: Array<{ uses?: string; with?: Record<string, unknown> }> }) =>
(job.steps ?? []).filter((step: WorkflowStep) => step.uses === CHECKOUT_V6),
);
expect(checkoutSteps.length).toBeGreaterThan(0);
for (const checkoutStep of checkoutSteps) {
expect(checkoutStep.with?.ref).toBe("${{ needs.resolve-base.outputs.sha }}");
expect(checkoutStep.with?.["persist-credentials"]).toBe(false);
}
}
const controlUiResolveStep = controlUiResolveBase.steps.find(
(step: { name?: string }) => step.name === "Resolve source commit",
);
expect(controlUiResolveStep.env.TOKEN_PREFLIGHT_ONLY).toContain("inputs.token_preflight_only");
expect(controlUiResolveStep.env.WORKFLOW_SHA).toBe("${{ github.workflow_sha }}");
expect(controlUiResolveStep.run).toContain(
'if [[ "${TOKEN_PREFLIGHT_ONLY}" == "true" ]]; then',
);
expect(controlUiResolveStep.run).toContain('source_ref="${WORKFLOW_SHA}"');
expect(controlUiResolveStep.run).toContain(
'-F configRef="${source_ref}:scripts/lib/control-ui-i18n-config.json"',
);
expect(controlUiResolveStep.run).toContain(
"jq -ce '.data.repository.config.text | fromjson | [.[].locale]",
);
for (const preflight of [controlUiPreflight, nativePreflight]) {
expect(preflight.needs).toBe("resolve-base");
expect(preflight.if).toBe("needs.resolve-base.result == 'success'");
expect(preflight.strategy).toBeUndefined();
expect(preflight.steps).toHaveLength(3);
const checkoutStep = preflight.steps.find(
(step: { uses?: string }) => step.uses === CHECKOUT_V6,
);
const tokensStep = preflight.steps.find(
(step: { name?: string }) => step.name === "Create generated PR tokens",
);
expect(checkoutStep.with).toMatchObject({
ref: "${{ needs.resolve-base.outputs.sha }}",
"persist-credentials": false,
});
expect(tokensStep.uses).toBe("./.github/actions/create-generated-pr-tokens");
expect(tokensStep.with).toEqual({
"contents-client-id": "Iv23liOECG0slfuhz093",
"contents-private-key": "${{ secrets.CLAWSWEEPER_APP_PRIVATE_KEY }}",
"pull-request-client-id": MANTIS_GITHUB_APP_CLIENT_ID,
"pull-request-contents-permission": "write",
"pull-request-private-key": "${{ secrets.MANTIS_GITHUB_APP_PRIVATE_KEY }}",
});
}
for (const preflight of [controlUiPreflight, nativePreflight]) {
const tokensStep = preflight.steps.find(
(step: { name?: string }) => step.name === "Create generated PR tokens",
);
const autoMergeSettingStep = preflight.steps.find(
(step: { name?: string }) => step.name === "Verify repository auto-merge setting",
);
expect(tokensStep.id).toBe("tokens");
expect(autoMergeSettingStep.env.GH_TOKEN).toBe(
"${{ steps.tokens.outputs.pull-request-token }}",
);
expect(autoMergeSettingStep.run).toContain("autoMergeAllowed");
expect(autoMergeSettingStep.run).toContain("Repository auto-merge must be enabled");
}
const tokenAction = parse(readFileSync(CREATE_GENERATED_PR_TOKENS_ACTION, "utf8"));
const tokenActionSource = readFileSync(CREATE_GENERATED_PR_TOKENS_ACTION, "utf8");
const contentsTokenStep = tokenAction.runs.steps.find(
(step: { name?: string }) => step.name === "Create generated branch app token",
);
const pullRequestTokenStep = tokenAction.runs.steps.find(
(step: { name?: string }) => step.name === "Create generated PR app token",
);
const publishAction = parse(readFileSync(PUBLISH_GENERATED_PR_ACTION, "utf8"));
const publishActionSource = readFileSync(PUBLISH_GENERATED_PR_ACTION, "utf8");
const createTokensStep = publishAction.runs.steps.find(
(step: { name?: string }) => step.name === "Create generated PR tokens",
);
const actionPublishStep = publishAction.runs.steps.find(
(step: { name?: string }) => step.name === "Publish generated pull request",
);
expect(tokenAction.runs.steps).toHaveLength(2);
for (const input of [
"contents-client-id",
"contents-private-key",
"pull-request-client-id",
"pull-request-private-key",
]) {
expect(tokenAction.inputs[input].required).toBe(true);
expect(publishAction.inputs[input].required).toBe(true);
}
expect(`${tokenActionSource}\n${publishActionSource}`).not.toMatch(
/2729701|2971289|primary-private-key|fallback-private-key/u,
);
expect(contentsTokenStep).toEqual({
name: "Create generated branch app token",
id: "contents-token",
uses: CREATE_GITHUB_APP_TOKEN_V3,
with: {
"client-id": "${{ inputs.contents-client-id }}",
"private-key": "${{ inputs.contents-private-key }}",
owner: "${{ github.repository_owner }}",
repositories: "${{ github.event.repository.name }}",
"permission-contents": "write",
},
});
expect(pullRequestTokenStep).toEqual({
name: "Create generated PR app token",
id: "pull-request-token",
uses: CREATE_GITHUB_APP_TOKEN_V3,
with: {
"client-id": "${{ inputs.pull-request-client-id }}",
"private-key": "${{ inputs.pull-request-private-key }}",
owner: "${{ github.repository_owner }}",
repositories: "${{ github.event.repository.name }}",
"permission-contents": "${{ inputs.pull-request-contents-permission }}",
"permission-pull-requests": "write",
},
});
expect(tokenAction.inputs["pull-request-contents-permission"].required).toBe(false);
expect(tokenAction.outputs["contents-token"].value).toBe(
"${{ steps.contents-token.outputs.token }}",
);
expect(tokenAction.outputs["pull-request-token"].value).toBe(
"${{ steps.pull-request-token.outputs.token }}",
);
expect(createTokensStep).toMatchObject({
id: "tokens",
uses: "./.github/actions/create-generated-pr-tokens",
with: {
"contents-client-id": "${{ inputs.contents-client-id }}",
"contents-private-key": "${{ inputs.contents-private-key }}",
"pull-request-client-id": "${{ inputs.pull-request-client-id }}",
"pull-request-contents-permission": "${{ inputs.auto-merge == 'true' && 'write' || '' }}",
"pull-request-private-key": "${{ inputs.pull-request-private-key }}",
},
});
expect(
publishAction.runs.steps.filter(
(step: { uses?: string }) => step.uses === CREATE_GITHUB_APP_TOKEN_V3,
),
).toEqual([]);
expect(actionPublishStep.env.CONTENTS_TOKEN).toBe("${{ steps.tokens.outputs.contents-token }}");
expect(actionPublishStep.env.GH_TOKEN).toBe("${{ steps.tokens.outputs.pull-request-token }}");
expect(actionPublishStep.env.INVALIDATION_PATHS).toBe("${{ inputs.invalidation-paths }}");
expect(publishAction.inputs["invalidation-paths"]).toEqual({
description: "Newline-delimited generator input paths that make an older run stale.",
required: false,
default: "",
});
expect(publishAction.inputs["working-directory"]).toEqual({
description: "Repository root containing the generated files.",
required: false,
default: ".",
});
expect(actionPublishStep["working-directory"]).toBe("${{ inputs.working-directory }}");
expect(publishAction.inputs["overlap-policy"]).toEqual({
description: "Whether stale inputs or owned-path overlap defer to a successor run or fail.",
required: false,
default: "defer",
});
expect(publishAction.inputs["auto-merge"]).toEqual({
description: "Enable squash auto-merge; false rejects an inherited auto-merge request.",
required: false,
default: "false",
});
expect(actionPublishStep.env.OVERLAP_POLICY).toBe("${{ inputs.overlap-policy }}");
expect(actionPublishStep.env.AUTO_MERGE).toBe("${{ inputs.auto-merge }}");
const publishPolicy = readFileSync(".github/actions/publish-generated-pr/policy.py", "utf8");
expect(actionPublishStep.run).toContain('case "${OVERLAP_POLICY}" in');
expect(actionPublishStep.run).toContain("defer | fail");
expect(actionPublishStep.run).toContain("GIT_TERMINAL_PROMPT=0");
expect(
actionPublishStep.run.match(/timeout --signal=TERM --kill-after=10s 60s/gu),
).toHaveLength(6);
expect(actionPublishStep.env.PUBLISH_ACTION_PATH).toBe("${{ github.action_path }}");
expect(actionPublishStep.run).toContain(
'exec python3 -I -S "$CI_GIT_OWNER" --policy "$PUBLISH_ACTION_PATH/policy.py"',
);
expect(actionPublishStep.run).not.toMatch(
/(?:^|[\s;])git (?:config|fetch|push|diff|ls-tree|ls-remote|rev-parse|merge-base|add|commit|switch|restore|rm)\b/mu,
);
expect(publishPolicy).not.toMatch(
/except (?:Exception|BaseException|SystemExit|RuntimeError)|backoff\(|subprocess\.(?:run|Popen)\([^\n]*["']git/u,
);
expect(publishPolicy.match(/timeout=\d+/gu)).toEqual([
"timeout=60",
"timeout=120",
"timeout=60",
]);
for (const contract of [
'auth_key = "http.https://github.com/.extraheader"',
'f"AUTHORIZATION: basic {git_auth}"',
'print(f"::add-mask::{git_auth}"',
'git("config", "--local", "--unset-all", auth_key)',
"except GitFailure:",
"except PublicationFailure as error:",
"finally:\n cleanup_git_auth()",
"--force-with-lease=refs/heads/{head_branch}:{expected_head}",
"GH013|repository rule violations|required status check",
"bool(remote_head) and not current_remote_head",
'push_generated_branch("")',
]) {
expect(publishPolicy).toContain(contract);
}
// The real repository scenarios below own overlap, invalidation, tree/lease,
// reconciliation and auto-merge behavior; spelling is no longer Bash policy.
for (const contract of [
'gh api --method GET "repos/${GITHUB_REPOSITORY}/pulls"',
'-f "head=${GITHUB_REPOSITORY_OWNER}:${HEAD_BRANCH}"',
".head.repo.full_name == env.GITHUB_REPOSITORY",
".head.ref == env.HEAD_BRANCH",
".head.sha",
"gh pr edit",
"gh pr create",
'--base "${BASE_BRANCH}"',
'--head "${HEAD_BRANCH}"',
'--body-file "${body_file}"',
"--json autoMergeRequest",
'--auto --squash --match-head-commit "${published_commit}"',
]) {
expect(actionPublishStep.run).toContain(contract);
}
for (const forbidden of [
"gh auth setup-git",
"gh pr list",
"gh pr close",
'GH_TOKEN="${CONTENTS_TOKEN}"',
'HEAD:"${BASE_BRANCH}"',
]) {
expect(actionPublishStep.run).not.toContain(forbidden);
}
expect(readFileSync(".github/workflows/ci.yml", "utf8")).toContain(
"OPENCLAW_ALLOW_RELEASE_GENERATED_MIX",
);
for (const [
ownerWorkflow,
refreshJob,
finalizeJob,
artifactPattern,
commitMessage,
automationBranch,
] of [
[
workflow,
refresh,
nativeFinalize,
"native-locale-*",
"chore(i18n): refresh native locales",
"automation/native-app-locale-refresh",
],
[
controlUiWorkflow,
controlUiWorkflow.jobs.refresh,
controlUiFinalize,
"control-ui-locale-*",
"chore(ui): refresh control ui locales",
"automation/control-ui-locale-refresh",
],
] as const) {
const uploadStep = refreshJob.steps.find(
(step: { name?: string }) => step.name === "Upload locale artifact",
);
const downloadStep = finalizeJob.steps.find(
(step: { name?: string }) => step.name === "Download locale artifacts",
);
const checkoutStep = finalizeJob.steps.find(
(step: { uses?: string }) => step.uses === CHECKOUT_V6,
);
const publishStep = finalizeJob.steps.find(
(step: { name?: string }) => step.name === "Open or update generated locale PR",
);
expect(ownerWorkflow.permissions.contents).toBe("read");
expect(refreshJob.needs).toEqual(["resolve-base", "publisher-preflight"]);
expect(finalizeJob.needs).toEqual(["resolve-base", "publisher-preflight", "refresh"]);
const isNative = automationBranch.includes("native");
expect(finalizeJob.if).toBe(
isNative
? "needs.resolve-base.result == 'success' && needs.publisher-preflight.result == 'success' && needs.refresh.result == 'success'"
: "needs.resolve-base.result == 'success' && needs.publisher-preflight.result == 'success' && needs.refresh.result == 'success' && !(github.event_name == 'workflow_dispatch' && inputs.token_preflight_only)",
);
expect(uploadStep.uses).toBe(UPLOAD_ARTIFACT_V7);
expect(downloadStep.uses).toBe(DOWNLOAD_ARTIFACT_V8);
expect(downloadStep.with.pattern).toBe(artifactPattern);
expect(downloadStep.with["merge-multiple"]).toBe(true);
expect(checkoutStep.with["persist-credentials"]).toBe(false);
expect(checkoutStep.with["fetch-depth"]).toBe(0);
expect(publishStep.uses).toBe("./.github/actions/publish-generated-pr");
expect(publishStep.with).toMatchObject({
"contents-client-id": "Iv23liOECG0slfuhz093",
"contents-private-key": "${{ secrets.CLAWSWEEPER_APP_PRIVATE_KEY }}",
"pull-request-client-id": MANTIS_GITHUB_APP_CLIENT_ID,
"pull-request-private-key": "${{ secrets.MANTIS_GITHUB_APP_PRIVATE_KEY }}",
"base-branch": "${{ github.event.repository.default_branch }}",
"head-branch": automationBranch,
"commit-message": commitMessage,
"pr-title": commitMessage,
});
expect(publishStep.with["generated-paths"]).toContain(
automationBranch.includes("native") ? "apps/.i18n/native" : "ui/src/i18n",
);
expect(publishStep.with["invalidation-paths"]).toContain(
automationBranch.includes("native")
? "apps/android/app/src/main"
: "ui/src/i18n/locales/en.ts",
);
expect(publishStep.with["invalidation-paths"]).toContain(
".github/actions/create-generated-pr-tokens/action.yml",
);
expect(publishStep.with["invalidation-paths"]).toContain(
".github/actions/publish-generated-pr/action.yml",
);
expect(publishStep.with).not.toHaveProperty("overlap-policy");
expect(publishStep.with["auto-merge"]).toBe("true");
expect(publishStep.with["pr-body"]).toContain("## What Problem This Solves");
expect(publishStep.with["pr-body"]).toContain("## Evidence");
expect(publishStep.with["pr-body"]).toContain("${{ needs.resolve-base.outputs.sha }}");
expect(publishStep.with["pr-body"]).not.toContain("${{ github.sha }}");
}
});
it.skipIf(process.platform === "win32")(
"enables auto-merge for the exact generated pull request head",
() => {
const result = runGeneratedPublisherScenario(null, { autoMerge: true });
expect(result.branchExists).toBe(true);
expect(result.mergeCalls).toContain("pr merge https://github.com/openclaw/openclaw/pull/1");
expect(result.mergeCalls).toContain("--auto --squash --match-head-commit");
expect(result.summary).toContain("Enabled squash auto-merge for exact generated head");
},
);
it.skipIf(process.platform === "win32")(
"waits for the published pull request head before enabling auto-merge",
() => {
const result = runGeneratedPublisherScenario(null, {
autoMerge: true,
stalePrViewHeadOnce: true,
});
expect(result.mergeCalls).toContain("--auto --squash --match-head-commit");
expect(result.publishOutput).toContain(
"Generated pull request head has not converged yet; rechecking",
);
},
);
it.skipIf(process.platform === "win32")(
"preserves inherited auto-merge while replacing a generated pull request head",
() => {
const result = runGeneratedPublisherScenario(null, {
autoMerge: true,
existingAutoMergeMethod: "SQUASH",
existingPr: true,
});
expect(result.generatedA).toBe("desired-a");
expect(result.mergeCalls).toBe("");
expect(result.summary).toContain(
"Squash auto-merge already enabled for generated pull request",
);
},
);
it.skipIf(process.platform === "win32")(
"accepts inherited auto-merge completing immediately after publication",
() => {
const result = runGeneratedPublisherScenario(null, {
autoMerge: true,
existingAutoMergeMethod: "SQUASH",
existingPr: true,
mergeGeneratedPush: true,
});
expect(result.branchExists).toBe(false);
expect(result.mainGeneratedA).toBe("desired-a");
expect(result.mergeCalls).toBe("");
expect(result.summary).toContain(
"Generated output was merged before pull request reconciliation",
);
},
);
it.skipIf(process.platform === "win32")(
"waits for the existing pull request head before replacing it",
() => {
const result = runGeneratedPublisherScenario(null, {
autoMerge: true,
existingAutoMergeMethod: "SQUASH",
existingPr: true,
stalePrHeadOnce: true,
});
expect(result.generatedA).toBe("desired-a");
expect(result.publishOutput).toContain(
"Generated pull request head has not converged yet; rechecking",
);
},
);
it.skipIf(process.platform === "win32")(
"refuses to replace an auto-merge-enabled head when publication opts out",
() => {
const result = runGeneratedPublisherScenario(null, {
autoMerge: false,
existingAutoMergeMethod: "SQUASH",
existingPr: true,
expectFailure: true,
});
expect(result.generatedA).toBe("stale-pr-a");
expect(result.mergeCalls).toBe("");
expect(result.publishOutput).toContain("auto-merge enabled while publication opted out");
},
);
it.skipIf(process.platform === "win32")(
"does not mutate inherited auto-merge when generated publication fails",
() => {
const result = runGeneratedPublisherScenario(null, {
autoMerge: true,
existingAutoMergeMethod: "SQUASH",
existingPr: true,
expectFailure: true,
failGeneratedPush: true,
});
expect(result.generatedA).toBe("stale-pr-a");
expect(result.mergeCalls).toBe("");
expect(result.summary).not.toContain("auto-merge");
},
);
it.skipIf(process.platform === "win32")(
"rejects an incompatible inherited auto-merge method without mutating it",
() => {
const result = runGeneratedPublisherScenario(null, {
autoMerge: true,
existingAutoMergeMethod: "MERGE",
existingPr: true,
expectFailure: true,
});
expect(result.generatedA).toBe("stale-pr-a");
expect(result.mergeCalls).toBe("");
expect(result.publishOutput).toContain(
"Generated pull request already uses incompatible MERGE auto-merge",
);
},
);
it.skipIf(process.platform === "win32")(
"defers a newer owned snapshot even when the desired diff is disjoint",
() => {
const result = runGeneratedPublisherScenario("b");
expect(result.branchExists).toBe(false);
expect(result.summary).toContain(
"Deferred stale generated output because owned generated paths changed on main.",
);
},
);
it.skipIf(process.platform === "win32")(
"defers stale generator inputs and preserves an existing pull request and disarms auto-merge",
() => {
const result = runGeneratedPublisherScenario(null, {
existingPr: true,
autoMerge: true,
existingAutoMergeMethod: "SQUASH",
updateSource: true,
});
expect(result.branchHead).not.toBe(result.mainHead);
expect(result.generatedA).toBe("stale-pr-a");
expect(result.summary).toContain(
"Deferred stale generated output because generator inputs changed on main.",
);
expect(result.mergeCalls).toContain("--disable-auto");
expect(result.summary).toContain("Preserved stale generated pull request");
},
);
it.skipIf(process.platform === "win32")(
"defers timing refits when only the runtime group codec changes on main",
() => {
const workflow = readWorkflow(".github/workflows/ci-test-timings-refit.yml");
const publisher = expectDefined(
workflow.jobs.refit.steps.find(
(step: WorkflowStep) => step.uses === "./.github/actions/publish-generated-pr",
),
"timing refit publisher",
);
const result = runGeneratedPublisherScenario(null, {
invalidationPaths: publisher.with["invalidation-paths"],
updateSource: "scripts/lib/ci-node-test-groups-codec.mts",
});
expect(result.branchExists).toBe(false);
expect(result.mainGeneratedA).toBe("old-a");
expect(result.mergeCalls).toBe("");
expect(result.summary).toContain(
"Deferred stale generated output because generator inputs changed on main.",
);
},
);
it.skipIf(process.platform === "win32")(
"publishes after unrelated source changes when input invalidation is disabled",
() => {
const result = runGeneratedPublisherScenario(null, {
invalidationPaths: "",
overlapPolicy: "fail",
updateSource: true,
});
expect(result.branchExists).toBe(true);
expect(result.generatedA).toBe("desired-a");
expect(result.publishOutput).not.toContain("Refusing stale generated output");
},
);
it.skipIf(process.platform === "win32")(
"preserves an existing pull request when a no-change run becomes stale",
() => {
const result = runGeneratedPublisherScenario("b", {
existingPr: true,
noGeneratedChange: true,
});
expect(result.branchHead).toBe(result.initialBranch);
expect(result.generatedA).toBe("stale-pr-a");
expect(result.generatedB).toBe("old-b");
expect(result.summary).toContain(
"Deferred stale generated output because owned generated paths changed on main.",
);
expect(result.summary).toContain("Preserved stale generated pull request");
},
);
it.skipIf(process.platform === "win32").each([false, true])(
"disarms stale output when inputs advance during PR publication (inherited=%s)",
(inherited) => {
const result = runGeneratedPublisherScenario(null, {
autoMerge: true,
existingPr: inherited,
existingAutoMergeMethod: inherited ? "SQUASH" : undefined,
updateSourceBeforeAutoMerge: true,
});
expect(result.generatedA).toBe("desired-a");
expect(result.branchHead).not.toBe(result.mainHead);
expect(result.mergeCalls).not.toContain("--auto --squash");
expect(result.mergeCalls.includes("--disable-auto")).toBe(inherited);
expect(result.summary).toContain("Deferred stale generated output");
},
);
it.skipIf(process.platform === "win32")(
"leaves a current no-change run's existing pull request and auto-merge unchanged",
() => {
const result = runGeneratedPublisherScenario(null, {
existingPr: true,
noGeneratedChange: true,
autoMerge: true,
existingAutoMergeMethod: "SQUASH",
});
expect(result.branchHead).toBe(result.initialBranch);
expect(result.generatedA).toBe("stale-pr-a");
expect(result.mergeCalls).toBe("");
expect(result.summary).toBe("");
},
);
it.skipIf(process.platform === "win32")(
"does not overwrite a successor that moves while stale auto-merge is disabled",
() => {
const result = runGeneratedPublisherScenario(null, {
existingPr: true,
updateSource: true,
existingAutoMergeMethod: "SQUASH",
autoMerge: true,
disarmRace: true,
expectFailure: true,
});
expect(result.branchHead).not.toBe(result.initialBranch);
expect(result.generatedA).toBe("old-a");
expect(result.mergeCalls).toContain("--disable-auto");
expect(result.mergeCalls).not.toContain("--auto --squash");
expect(result.summary).not.toContain("Preserved stale");
},
);
it.skipIf(process.platform === "win32")(
"fails stale generated publication when no successor run is guaranteed",
() => {
const overlap = runGeneratedPublisherScenario("a", {
expectFailure: true,
overlapPolicy: "fail",
});
expect(overlap.branchExists).toBe(false);
expect(overlap.publishOutput).toContain(
"::error::Refusing stale generated output because owned generated paths changed on main.",
);
const stalePr = runGeneratedPublisherScenario(null, {
existingPr: true,
expectFailure: true,
noGeneratedChange: true,
overlapPolicy: "fail",
updateSource: true,
});
expect(stalePr.branchHead).toBe(stalePr.initialBranch);
expect(stalePr.summary).toContain("Preserved stale generated pull request");
expect(stalePr.publishOutput).toContain(
"::error::Refusing stale generated output because generator inputs changed on main.",
);
const publishRun = parse(readFileSync(PUBLISH_GENERATED_PR_ACTION, "utf8")).runs.steps.find(
(step: { name?: string }) => step.name === "Publish generated pull request",
).run;
const invalidPolicy = spawnSync("bash", ["-c", publishRun], {
encoding: "utf8",
env: {
...process.env,
AUTO_MERGE: "false",
CONTENTS_TOKEN: "contents-token",
GH_TOKEN: "pull-request-token",
OVERLAP_POLICY: "continue",
},
});
expect(invalidPolicy.status).not.toBe(0);
expect(`${invalidPolicy.stdout}${invalidPolicy.stderr}`).toContain(
"Generated PR publication overlap policy must be 'defer' or 'fail'.",
);
},
);
it("fails OpenGrep SARIF artifact uploads when reports are missing", () => {
const cases = [
{
workflowPath: OPENGREP_PR_DIFF_WORKFLOW,
artifactName: "opengrep-pr-diff-sarif",
},
{
workflowPath: OPENGREP_FULL_WORKFLOW,
artifactName: "opengrep-full-sarif",
},
];
for (const item of cases) {
const workflow = parse(readFileSync(item.workflowPath, "utf8"));
const uploadStep = workflow.jobs.scan.steps.find(
(step: WorkflowStep) => step.name === "Upload SARIF as workflow artifact",
);
expect(uploadStep.if, item.workflowPath).toBe("always()");
expect(uploadStep.uses, item.workflowPath).toBe(UPLOAD_ARTIFACT_V7);
expect(uploadStep.with, item.workflowPath).toMatchObject({
name: item.artifactName,
path: ".opengrep-out/precise.sarif",
"if-no-files-found": "error",
});
}
});
it("verifies the pinned OpenGrep release binary before installing it", () => {
for (const workflowPath of [OPENGREP_PR_DIFF_WORKFLOW, OPENGREP_FULL_WORKFLOW]) {
const workflow = parse(readFileSync(workflowPath, "utf8"));
const installStep = expectDefined(
workflow.jobs.scan.steps.find((step: WorkflowStep) => step.name === "Install opengrep"),
`Install opengrep step in ${workflowPath}`,
);
const run = expectDefined(installStep.run, `Install opengrep script in ${workflowPath}`);
expect(installStep.env, workflowPath).toMatchObject({
OPENGREP_VERSION: "v1.30.0",
OPENGREP_LINUX_X64_SHA256:
"35779bdd72e92129c8df2a77f0c55e8c08356801ea92591ef32108d6b28d564c",
});
expect(run, workflowPath).toContain('binary="$(mktemp "${RUNNER_TEMP}/opengrep.XXXXXX")"');
expect(run, workflowPath).toContain("trap 'rm -f \"$binary\"' EXIT");
expect(run, workflowPath).toContain(
"curl -fsSL --retry 4 --retry-all-errors --retry-delay 2",
);
expect(run, workflowPath).toContain("--connect-timeout 10 --max-time 300");
expect(run, workflowPath).toContain('-o "$binary"');
expect(run, workflowPath).toContain(
"https://github.com/opengrep/opengrep/releases/download/${OPENGREP_VERSION}/opengrep_manylinux_x86",
);
expect(run, workflowPath).toContain(
'printf \'%s %s\\n\' "$OPENGREP_LINUX_X64_SHA256" "$binary" | sha256sum --check',
);
expect(run, workflowPath).toContain('install -m 0755 "$binary" "$install_dir/opengrep"');
expect(run.indexOf('-o "$binary"'), workflowPath).toBeLessThan(
run.indexOf("sha256sum --check"),
);
expect(run.indexOf("sha256sum --check"), workflowPath).toBeLessThan(
run.indexOf('install -m 0755 "$binary"'),
);
expect(run, workflowPath).not.toMatch(/\|\s*bash/u);
}
});
it("runs real behavior proof from the trusted workflow revision", () => {
const workflow = readRealBehaviorProofWorkflow();
const source = readFileSync(".github/workflows/real-behavior-proof.yml", "utf8");
const checkout = workflow.jobs["real-behavior-proof"].steps.find(
(step: WorkflowStep) => step.uses === CHECKOUT_V6,
);
expect(checkout.with.ref).toBe("${{ github.workflow_sha }}");
expect(checkout.with.ref).not.toBe("${{ github.event.pull_request.base.sha }}");
expect(source).toContain("Old PR events can carry a stale base SHA");
});
it("keeps docs-change detection fail-safe and fixture-aware", () => {
const action = readFileSync(".github/actions/detect-docs-changes/action.yml", "utf8");
expect(action).toContain("base-sha:");
expect(action).toContain("docs_only:");
expect(action).toContain("docs_changed:");
expect(action).toContain("BASE_SHA: ${{ inputs.base-sha }}");
expect(action).toContain('BASE="$BASE_SHA"');
expect(action).toContain(
'CHANGED=$(git diff --no-renames --name-only "$BASE" HEAD 2>/dev/null || echo "UNKNOWN")',
);
expect(action).toContain('if [ "$CHANGED" = "UNKNOWN" ] || [ -z "$CHANGED" ]; then');
expect(action).toContain("docs_only=false");
expect(action).toContain("docs_changed=false");
expect(action).toContain("test/fixtures/*)");
expect(action).toContain("docs/* | *.md | *.mdx | config/markdownlint*.jsonc)");
const run = parse(action).runs.steps[0].run as string;
for (const [source, destination, docsChanged, docsOnly] of [
["src/old.ts", "docs/new.md", "true", "false"],
["docs/old.md", "src/new.ts", "true", "false"],
["docs/old.md", "docs/new.md", "true", "true"],
["docs/old.md", "docs/.generated/config-baseline.counts.json", "true", "true"],
["docs/old.md", "docs/plugins/plugin-inventory.md", "true", "true"],
["src/old.ts", "src/new.ts", "false", "false"],
["test/fixtures/old.md", "docs/new.md", "true", "false"],
["docs/removed.md", null, "true", "true"],
] as const) {
const root = tempDirs.make("openclaw-docs-diff-");
const origin = path.join(root, "origin");
const checkout = path.join(root, "checkout");
mkdirSync(path.dirname(path.join(origin, source)), { recursive: true });
const content = Array.from({ length: 100 }, (_, index) => `line ${index}\n`).join("");
writeFileSync(path.join(origin, source), content);
runGit(origin, ["init", "-q", "-b", "main"]);
for (const [name, value] of [
["user.name", "CI Fixture"],
["user.email", "ci-fixture@example.invalid"],
["commit.gpgsign", "false"],
["uploadpack.allowFilter", "true"],
] as const) {
runGit(origin, ["config", name, value]);
}
runGit(origin, ["add", "."]);
runGit(origin, ["commit", "-qm", "base"]);
const base = runGit(origin, ["rev-parse", "HEAD"]);
const sourceBlob = runGit(origin, ["rev-parse", `HEAD:${source}`]);
rmSync(path.join(origin, source));
if (destination) {
mkdirSync(path.dirname(path.join(origin, destination)), { recursive: true });
writeFileSync(path.join(origin, destination), `${content}edited after rename\n`);
}
runGit(origin, ["add", "-A"]);
runGit(origin, ["commit", "-qm", "change"]);
runGit(root, [
"clone",
"-q",
"--no-local",
"--filter=blob:none",
"--depth=2",
origin,
checkout,
]);
runGit(checkout, ["config", "diff.renames", "true"]);
const localObjects = () =>
runGit(checkout, ["cat-file", "--batch-all-objects", "--batch-check=%(objectname)"]);
expect(localObjects()).toContain(base);
expect(localObjects()).not.toContain(sourceBlob);
const output = path.join(root, "output");
const trace = path.join(root, "trace");
const result = runWorkflowShellScript(run, {
cwd: checkout,
env: { ...process.env, BASE_SHA: base, GITHUB_OUTPUT: output, GIT_TRACE2_EVENT: trace },
});
expect(result.status, result.stderr).toBe(0);
expect(readWorkflowOutputs(output), `${source} -> ${destination}`).toEqual({
docs_changed: docsChanged,
docs_only: docsOnly,
});
expect(readFileSync(trace, "utf8")).not.toContain('"fetch"');
expect(localObjects()).not.toContain(sourceBlob);
}
});
it("runs generated docs checks in the docs-only job", () => {
const job = readCiWorkflow().jobs["check-docs"];
const configDocsCheck = job.steps.find(
(step: WorkflowStep) => step.name === "Check config docs baseline",
);
const pluginInventoryCheck = job.steps.find(
(step: WorkflowStep) => step.name === "Check plugin inventory",
);
expect(job.if).toBe("needs.preflight.outputs.run_check_docs == 'true'");
expect(configDocsCheck?.run).toBe("pnpm config:docs:check");
expect(pluginInventoryCheck?.run).toBe("pnpm plugins:inventory:check");
});
describe("bounded hybrid hosted offload", () => {
function emittedHostedRows(
outputs: Record<string, string>,
overrides: Partial<Parameters<typeof evaluateWorkflowExpression>[1]> = {},
) {
const context = {
eventName: "push" as const,
repository: "openclaw/openclaw",
runAttempt: 1,
runnerBackend: "hybrid" as const,
...overrides,
preflightOutputs: outputs,
};
const evaluate = (value: unknown, matrix: Record<string, unknown> = {}) =>
typeof value === "string" && value.startsWith("${{")
? evaluateWorkflowExpression(value.replaceAll("fromJson(", "fromJSON("), {
...context,
matrix,
})
: value;
const rows: string[] = [];
for (const [name, job] of Object.entries(readCiWorkflow().jobs)) {
const definition = job as {
if?: string;
"runs-on": string;
strategy?: {
matrix: string | { include?: Record<string, unknown>[]; [key: string]: unknown };
};
};
const condition = definition.if;
if (
condition &&
!evaluate(condition.startsWith("${{") ? condition : `\${{ ${condition} }}`)
) {
continue;
}
const matrix = evaluate(definition.strategy?.matrix) as
| {
include?: Record<string, unknown>[];
[key: string]: unknown;
}
| undefined;
let selectedRows: Record<string, unknown>[] = [{}];
if (matrix?.include) {
selectedRows = matrix.include;
} else if (matrix) {
for (const [key, value] of Object.entries(matrix)) {
const values = evaluate(value) as unknown[];
selectedRows = selectedRows.flatMap((row) =>
values.map((entry) => Object.assign({}, row, { [key]: entry })),
);
}
}
for (const row of selectedRows) {
if (!String(evaluate(definition["runs-on"], row)).startsWith("blacksmith-")) {
rows.push(name);
}
}
}
return rows;
}
function manifestWithHostedNodeRows(
count: number,
options: Partial<Parameters<typeof runCiManifestFixture>[0]> = {},
) {
return runCiManifestFixture({
bundledPlanner: true,
eventName: "push",
historicalCompatibility: false,
runnerBackend: "hybrid",
nodeTestShards: Array.from({ length: count }, (_, index) => ({
checkName: `hosted-node-${index}`,
configs: [],
requiresDist: false,
runner: "ubuntu-24.04",
shardName: `hosted-node-${index}`,
})),
...options,
scopeEnv: { OPENCLAW_CI_RUN_UI_TESTS: "true", ...options.scopeEnv },
});
}
it("counts the actual workflow rows and admits five offloads only below the base threshold", () => {
const planner = readCiWorkflow().jobs.preflight.steps.find(
(step: WorkflowStep) => step.name === "Build CI manifest",
);
expect(planner.run).toContain("const HYBRID_HOSTED_ROW_LIMIT = 45;");
expect(planner.run).toContain("const HYBRID_HOSTED_BASE_ROW_LIMIT = 40;");
const baseline = manifestWithHostedNodeRows(0);
expect(baseline.status, baseline.output).toBe(0);
const originalBase = emittedHostedRows({
...baseline.outputs,
hybrid_hosted_offload: "false",
}).length;
expect(Number(baseline.outputs.hybrid_hosted_base_rows)).toBe(originalBase);
for (const baseRows of [40, 41, 45, 46]) {
const manifest = manifestWithHostedNodeRows(baseRows - originalBase);
expect(manifest.status, manifest.output).toBe(0);
if (baseRows === 46) {
expect(manifest.output).toContain(
"::warning::Hybrid base manifest has 46 hosted jobs, above the 45-row offload budget; keeping optional offloads on Blacksmith.",
);
}
const hosted = emittedHostedRows(manifest.outputs);
expect(manifest.outputs.hybrid_hosted_offload).toBe(String(baseRows <= 40));
expect(Number(manifest.outputs.hybrid_hosted_base_rows)).toBe(baseRows);
expect(Number(manifest.outputs.hybrid_hosted_total_rows)).toBe(hosted.length);
expect(hosted.length).toBe(baseRows <= 40 ? baseRows + 5 : baseRows);
for (const name of ["security-fast", "checks-ui", "checks-ui-e2e"]) {
expect(hosted.includes(name), name).toBe(baseRows <= 40);
}
for (const name of [
"qa-smoke-ci-profile",
"checks-ui-e2e-real-gateway",
"build-artifacts",
"android",
"check-test-types-hosted-core-shard",
]) {
expect(hosted, name).not.toContain(name);
}
expect(hosted.filter((name) => name === "checks-ui")).toHaveLength(baseRows <= 40 ? 3 : 0);
expect(hosted.filter((name) => name === "checks-ui-e2e")).toHaveLength(
baseRows <= 40 ? 1 : 0,
);
}
});
it.each([
{
label: "same-repo PR",
eventName: "pull_request" as const,
runnerProfile: "hybrid" as const,
headRepository: "openclaw/openclaw",
},
{
label: "trusted fork PR",
eventName: "pull_request" as const,
runnerProfile: "github" as const,
headRepository: "contributor/openclaw",
},
{
label: "main",
eventName: "push" as const,
runnerProfile: "hybrid" as const,
headRepository: "openclaw/openclaw",
},
])("counts all selected rows for $label", ({ eventName, runnerProfile, headRepository }) => {
const manifest = manifestWithHostedNodeRows(0, {
eventName,
runnerProfile,
changedPaths: [".github/workflows/ci.yml"],
scopeEnv: {
OPENCLAW_CI_HEAD_REPOSITORY: headRepository,
OPENCLAW_CI_RUN_MACOS_NODE: "true",
},
});
expect(manifest.status, manifest.output).toBe(0);
expect(manifest.outputs.hybrid_hosted_offload).toBe("true");
const context = { eventName, runnerProfile, headRepository };
const base = emittedHostedRows(
{ ...manifest.outputs, hybrid_hosted_offload: "false" },
context,
);
expect(Number(manifest.outputs.hybrid_hosted_base_rows)).toBe(base.length);
expect(Number(manifest.outputs.hybrid_hosted_total_rows)).toBe(
emittedHostedRows(manifest.outputs, context).length,
);
expect(base.filter((name) => name === "macos-node")).toHaveLength(3);
});
it.each<{ label: string } & Partial<Parameters<typeof runCiManifestFixture>[0]>>([
{ label: "retry", scopeEnv: { GITHUB_RUN_ATTEMPT: "2" } },
{ label: "missing attempt", scopeEnv: { GITHUB_RUN_ATTEMPT: "" } },
{
label: "untrusted author",
eventName: "pull_request" as const,
changedPaths: [".github/workflows/ci.yml"],
scopeEnv: { OPENCLAW_CI_AUTHOR_ASSOCIATION: "NONE" },
},
{ label: "manual", eventName: "workflow_dispatch" as const },
{ label: "frozen", eventName: "workflow_dispatch" as const, historicalCompatibility: true },
{ label: "noncanonical", repository: "contributor/openclaw" },
{ label: "GitHub backend", runnerBackend: "github" as const },
{ label: "Blacksmith backend", runnerBackend: "blacksmith" as const },
])(
"leaves $label routing outside optional offload admission",
({ label: _label, ...options }) => {
const manifest = manifestWithHostedNodeRows(0, options);
expect(manifest.status, manifest.output).toBe(0);
expect(manifest.outputs.hybrid_hosted_offload).toBe("false");
},
);
it("runs security after preflight failures and skips a cancelled workflow", () => {
const job = readCiWorkflow().jobs["security-fast"];
expect(job.needs).toEqual(["preflight"]);
const context = {
eventName: "push" as const,
repository: "openclaw/openclaw",
runAttempt: 1,
runnerBackend: "hybrid" as const,
failed: true,
};
expect(evaluateWorkflowExpression(job.if, context)).toBe(true);
expect(evaluateWorkflowExpression(job["runs-on"], context)).toBe(
"blacksmith-4vcpu-ubuntu-2404",
);
expect(evaluateWorkflowExpression(job.if, { ...context, cancelled: true })).toBe(false);
});
});
it("bounds matrix fan-out for runner-registration pressure", () => {
const workflow = readCiWorkflow();
expect(workflow.concurrency.group).toContain("github.event.pull_request.number");
expect(workflow.concurrency["cancel-in-progress"]).toContain(
"github.event_name == 'pull_request'",
);
expect(workflow.jobs["checks-fast-core"].strategy["max-parallel"]).toBe(12);
expect(workflow.jobs["checks-node-core-test-nondist-shard"].strategy["max-parallel"]).toBe(96);
expect(workflow.jobs["checks-fast-plugin-contracts-shard"].strategy["max-parallel"]).toBe(12);
expect(workflow.jobs["checks-fast-channel-contracts-shard"].strategy["max-parallel"]).toBe(12);
expect(workflow.jobs["check-shard"].strategy["max-parallel"]).toBe(12);
expect(workflow.jobs["check-additional-shard"].strategy["max-parallel"]).toBe(12);
expect(workflow.jobs["checks-windows"].strategy["max-parallel"]).toBe(2);
expect(workflow.jobs.android.strategy["max-parallel"]).toBe(2);
});
it("runs changed Docker seed owners in one gated scheduler job", () => {
const source = readFileSync(".github/workflows/ci.yml", "utf8");
const jobs = readCiWorkflow().jobs;
const job = jobs["docker-seed-e2e"];
expect(source).toContain("docker-seed-e2e-contract-v1");
expect(source).toContain(
'typeof changedNodeTestPlan.resolveChangedDockerSeedLanes === "function"',
);
expect(jobs.preflight.outputs).toMatchObject({
docker_seed_lanes: "${{ steps.manifest.outputs.docker_seed_lanes }}",
run_docker_seed_e2e: "${{ steps.manifest.outputs.run_docker_seed_e2e }}",
});
expect(job.if).toBe("needs.preflight.outputs.run_docker_seed_e2e == 'true'");
expect(job.needs).toEqual(["preflight"]);
expect(job["timeout-minutes"]).toBe(60);
expect(job.permissions).toEqual({ contents: "read" });
expect(job.strategy).toBeUndefined();
expect(job.steps[0]).toEqual(jobs["build-artifacts"].steps[0]);
expect(job.steps[1].uses).toBe("./.ci-harness/.github/actions/setup-node-env");
expect(job.steps[1].with).toMatchObject({
"build-all-cache-scope": "full",
"cache-mode": "${{ needs.preflight.outputs.cache_mode }}",
});
const run = job.steps.find(
(step: WorkflowStep) => step.name === "Run changed Docker seed owner lanes",
) as WorkflowStep;
const parallelism = run.env?.OPENCLAW_DOCKER_ALL_PARALLELISM;
expect(run).toMatchObject({
run: "pnpm test:docker:all",
env: {
OPENCLAW_DOCKER_ALL_LANES: "${{ needs.preflight.outputs.docker_seed_lanes }}",
OPENCLAW_DOCKER_ALL_LIVE_MODE: "skip",
OPENCLAW_DOCKER_E2E_ALLOW_UNRELEASED_CHANGELOG: "1",
OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS: "legacy-operator-state",
OPENCLAW_UPGRADE_SURVIVOR_UPDATE_RESTART_MODE: "auto-auth",
OPENCLAW_DOCKER_ALL_TAIL_PARALLELISM: parallelism,
},
});
expect(parallelism).toContain("&& 3 || 1");
expect(run.env).not.toHaveProperty("OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC");
const baseline = job.steps.find(
(step: WorkflowStep) => step.name === "Resolve published Docker seed upgrade baseline",
) as WorkflowStep;
expect(baseline.if).toBe(
"contains(format(' {0} ', needs.preflight.outputs.docker_seed_lanes), ' published-upgrade-survivor ')",
);
expect(baseline.env).toEqual({
TARGET_CONTEXT_REF: "${{ inputs.target_context_ref || github.base_ref || github.ref_name }}",
});
expect(job.steps.indexOf(baseline)).toBeLessThan(job.steps.indexOf(run));
});
it.each([
{ candidate: "2026.9.3", expected: "openclaw@2026.9.2" },
{ candidate: "2026.9.2", expected: "openclaw@2026.9.1" },
{ candidate: "2026.9.4-beta.1", expected: "openclaw@2026.9.3" },
{ candidate: "2026.9.3-beta.1", expected: "openclaw@2026.9.2" },
{
candidate: "2026.6.35",
context: "extended-stable/2026.6.33",
expected: "openclaw@2026.6.34",
},
{ candidate: "2026.6.34", expected: undefined },
])("hands Docker seed a strictly older published baseline for $candidate", (fixture) => {
const job = readCiWorkflow().jobs["docker-seed-e2e"];
const baseline = job.steps.find(
(step: WorkflowStep) => step.name === "Resolve published Docker seed upgrade baseline",
) as WorkflowStep | undefined;
const run = job.steps.find(
(step: WorkflowStep) => step.name === "Run changed Docker seed owner lanes",
) as WorkflowStep;
const root = mkdtempSync(path.join(tmpdir(), "openclaw-ci-upgrade-baseline-"));
try {
const bin = path.join(root, "bin");
const tooling = path.join(root, ".ci-harness", "scripts", "lib");
mkdirSync(bin);
mkdirSync(tooling, { recursive: true });
for (const name of ["release-upgrade-baseline.mjs", "release-version.mjs"]) {
copyFileSync(path.resolve("scripts", "lib", name), path.join(tooling, name));
}
writeFileSync(
path.join(root, "package.json"),
JSON.stringify({ version: fixture.candidate }),
);
writeFileSync(
path.join(root, ".ci-harness", "package.json"),
JSON.stringify({ version: "2026.10.1" }),
);
symlinkSync(process.execPath, path.join(bin, "node"));
writeFileSync(
path.join(bin, "npm"),
`#!${process.execPath}
const args = process.argv.slice(2);
if (JSON.stringify(args) !== JSON.stringify(["view", "openclaw", "versions", "--json", "--silent", "--prefer-online"])) process.exit(2);
console.log(JSON.stringify(["2026.6.34", "2026.6.35", "2026.9.1", "2026.9.2", "2026.9.3", "2026.9.4-beta.1"]));
`,
{ mode: 0o755 },
);
writeFileSync(
path.join(bin, "pnpm"),
`#!${process.execPath}
if (process.argv.slice(2).join(" ") !== "test:docker:all") process.exit(2);
require("node:fs").writeFileSync("scheduler-baseline", process.env.OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC ?? "missing");
`,
{ mode: 0o755 },
);
writeFileSync(path.join(root, "github-env"), "");
const inheritedBaseline = run.env?.OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC;
const result = runWorkflowShellScript(
`set -euo pipefail\n${baseline?.run ?? ""}\nset -a\nsource "$GITHUB_ENV"\nset +a\n${run.run}`,
{
cwd: root,
env: {
...process.env,
PATH: `${bin}${path.delimiter}${process.env.PATH ?? ""}`,
GITHUB_ENV: path.join(root, "github-env"),
OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC:
typeof inheritedBaseline === "string" ? inheritedBaseline : "",
TARGET_CONTEXT_REF: fixture.context ?? "main",
},
},
);
const receipt = path.join(root, "scheduler-baseline");
if (fixture.expected) {
expect(result.status, result.stderr).toBe(0);
expect(readFileSync(receipt, "utf8")).toBe(fixture.expected);
} else {
expect(result.status).not.toBe(0);
expect(result.stderr).toContain("no published stable OpenClaw baseline predates candidate");
expect(existsSync(receipt)).toBe(false);
}
} finally {
rmSync(root, { recursive: true, force: true });
}
});
it.each([
{ eventName: "pull_request" as const, production: false, expected: false },
{ eventName: "pull_request" as const, production: true, expected: true },
{ eventName: "push" as const, production: false, expected: false },
{ eventName: "push" as const, production: true, expected: true },
{ eventName: "workflow_dispatch" as const, production: false, expected: true },
])("routes published-upgrade proof for $eventName (production=$production)", (options) => {
const changedPaths = [
"src/commands/doctor-config-preflight.admission.process.test.ts",
"src/commands/doctor-config-runtime.test-support.ts",
...(options.production ? ["src/commands/doctor-config-preflight.ts"] : []),
];
expect(resolveChangedDockerSeedLanes(changedPaths)).toEqual(
options.production ? ["published-upgrade-survivor"] : [],
);
const result = runCiManifestFixture({
bundledPlanner: true,
runNode: false,
changedPaths,
eventName: options.eventName,
scopeEnv: { GITHUB_REF: "refs/heads/main" },
changedPlannerSource: `export { resolveChangedDockerSeedLanes } from "./ci-docker-seed-plan.mts";`,
changedPlannerDependencies: [
"scripts/lib/ci-docker-seed-plan.mts",
"scripts/lib/changed-path-facts.mjs",
],
});
expect(result.status, result.output).toBe(0);
expect(result.outputs.run_docker_seed_e2e).toBe(String(options.expected));
expect(result.outputs.docker_seed_lanes).toBe(
options.expected ? "published-upgrade-survivor" : "",
);
});
it.each([
{ repository: "openclaw/openclaw", ref: "refs/heads/main", expected: true },
{ repository: "openclaw/openclaw", ref: "refs/heads/feature", expected: false },
{ repository: "fork/openclaw", ref: "refs/heads/main", expected: false },
])(
"gates only canonical main pushes admitted by CI ($repository $ref)",
({ repository, ref, expected }) => {
const push = readCiWorkflow().on.push;
expect(push.branches).toEqual(["main"]);
expect(push).not.toHaveProperty("paths");
expect(push["paths-ignore"]).toEqual(["**/*.md", "docs/**"]);
const result = runCiManifestFixture({
bundledPlanner: true,
eventName: "push",
repository,
changedPaths: ["scripts/e2e/docker-openai-seed.ts"],
scopeEnv: { GITHUB_REF: ref },
});
expect(result.status, result.output).toBe(0);
expect(result.outputs.run_docker_seed_e2e).toBe(String(expected));
expect(result.outputs.docker_seed_lanes).toBe(
expected ? "mcp-channels cron-mcp-cleanup" : "",
);
},
);
it.each([
{ paths: ["README.md"], admitted: false },
{ paths: [".agents/skills/example/SKILL.md"], admitted: false },
{ paths: ["docs/ci.md", "docs/images/diagram.svg"], admitted: false },
{ paths: ["docs/reference/schema.json"], admitted: false },
{ paths: ["src/ordinary.ts"], admitted: true },
{ paths: ["assets/logo.svg"], admitted: true },
{ paths: ["README.md", "src/ordinary.ts"], admitted: true },
{ paths: ["docs/ci.md", "assets/logo.svg"], admitted: true },
])("admits main push paths $paths to CI: $admitted", ({ paths, admitted }) => {
const ignored: string[] = readCiWorkflow().on.push["paths-ignore"] ?? [];
// GitHub skips paths-ignore only when every changed path matches a pattern.
const runsCi = paths.some(
(changedPath) => !ignored.some((pattern) => minimatch(changedPath, pattern, { dot: true })),
);
expect(runsCi).toBe(admitted);
if (!runsCi) {
return;
}
const result = runCiManifestFixture({
bundledPlanner: true,
eventName: "push",
repository: "openclaw/openclaw",
changedPaths: paths,
scopeEnv: { GITHUB_REF: "refs/heads/main" },
});
expect(result.status, result.output).toBe(0);
expect(result.outputs.run_docker_seed_e2e).toBe("false");
expect(result.outputs.docker_seed_lanes).toBe("");
});
it.each([
{
eventName: "push" as const,
changedPaths: ["src/cli/cron-cli/shared.ts"],
qa: false,
performance: false,
},
{
eventName: "pull_request" as const,
changedPaths: ["src/cli/cron-cli/shared.ts"],
qa: false,
performance: false,
},
{
eventName: "push" as const,
changedPaths: ["extensions/qa-lab/src/ci-smoke-plan.ts"],
qa: true,
performance: false,
},
{
eventName: "pull_request" as const,
changedPaths: ["extensions/telegram/src/index.ts"],
qa: true,
performance: false,
},
{ eventName: "push" as const, changedPaths: ["ui/src/main.ts"], qa: false, performance: true },
{
eventName: "push" as const,
changedPaths: ["src/gateway/control-ui-asset-manifest.ts"],
qa: false,
performance: true,
},
{ eventName: "push" as const, changedPaths: null, qa: true, performance: true },
{
eventName: "workflow_dispatch" as const,
changedPaths: ["src/cli/cron-cli/shared.ts"],
qa: true,
performance: true,
},
])("selects owner/release coverage for $eventName $changedPaths", (fixture) => {
const plannerUrl = pathToFileURL(
path.resolve("scripts/lib/ci-changed-node-test-plan.mts"),
).href;
const result = runCiManifestFixture({
...fixture,
bundledPlanner: true,
historicalCompatibility: false,
changedPlannerSource: `
import { hasQaSmokeAffectingChange, hasControlUiPerformanceAffectingChange as performance } from ${JSON.stringify(plannerUrl)};
export { hasQaSmokeAffectingChange };
export const hasControlUiPerformanceAffectingChange = paths => performance(paths, { cwd: ${JSON.stringify(process.cwd())} });
export const createChangedNodeTestShards = () => null;
export const createChangedExtensionFallbackShards = () => [];
`,
});
expect(result.status, result.output).toBe(0);
expect(result.outputs.run_qa_smoke_ci).toBe(String(fixture.qa));
expect(result.outputs.run_control_ui_performance).toBe(String(fixture.performance));
});
it("splits Windows tests two ways on every runner backend", () => {
const workflow = readCiWorkflow();
const runStep = workflow.jobs["checks-windows"].steps.find(
(step: WorkflowStep) => step.name === "Run ${{ matrix.task }} (${{ matrix.runtime }})",
);
const blacksmith = runCiManifestFixture({
bundledPlanner: true,
eventName: "push",
historicalCompatibility: false,
runnerBackend: "blacksmith",
});
const github = runCiManifestFixture({
bundledPlanner: true,
eventName: "push",
historicalCompatibility: false,
runnerBackend: "github",
});
const hybrid = runCiManifestFixture({
bundledPlanner: true,
eventName: "push",
historicalCompatibility: false,
runnerBackend: "hybrid",
});
const hybridDispatch = runCiManifestFixture({
bundledPlanner: true,
eventName: "workflow_dispatch",
historicalCompatibility: false,
runnerBackend: "hybrid",
});
expect(blacksmith.status, blacksmith.output).toBe(0);
expect(github.status, github.output).toBe(0);
expect(hybrid.status, hybrid.output).toBe(0);
expect(hybridDispatch.status, hybridDispatch.output).toBe(0);
// Blacksmith's Windows class admits exactly 2 concurrent jobs (run
// 31865243804), so every backend uses the same 2-part split: a 3rd part
// queues behind a finished one and a single lane serializes the whole body.
const expectedWindowsMatrix = [
{ check_name: "checks-windows-node-test-1", runtime: "node", task: "test-1" },
{ check_name: "checks-windows-node-test-2", runtime: "node", task: "test-2" },
];
for (const [label, manifest] of [
["Blacksmith", blacksmith],
["GitHub", github],
["hybrid", hybrid],
["hybrid dispatch", hybridDispatch],
] as const) {
expect(
JSON.parse(expectDefined(manifest.outputs.checks_windows_matrix, `${label} Windows matrix`))
.include,
label,
).toEqual(expectedWindowsMatrix);
}
expect(runStep.run).toContain('scripts?.["test:windows:ci:1"]');
expect(runStep.run).toContain('scripts?.["test:windows:ci:2"]');
expect(runStep.run).toContain("pnpm test:windows:ci");
expect(runStep.run).toContain("target's combined Windows suite ran in test-1");
expect(runStep.run).not.toContain("pnpm test:windows:ci:3");
});
it.skipIf(process.platform === "win32").for(["blacksmith", "github", "hybrid"] as const)(
"executes each Mac partition once and keeps historical coverage on %s",
(runnerBackend) => {
const workflow = readCiWorkflow();
const job = workflow.jobs["macos-node"];
const runStep = job.steps.find((step: WorkflowStep) => step.name === "TS tests (macOS)");
const cwd = tempDirs.make("macos-partition-routing-");
const bin = path.join(cwd, "bin");
mkdirSync(bin);
writeFileSync(path.join(bin, "pnpm"), '#!/bin/sh\nprintf "selected=%s\\n" "$*"\n');
chmodSync(path.join(bin, "pnpm"), 0o755);
for (const partsSupported of [true, false]) {
const manifest = runCiManifestFixture({
bundledPlanner: true,
eventName: "workflow_dispatch",
historicalCompatibility: !partsSupported,
macosNodeParts: partsSupported,
runnerBackend,
});
expect(manifest.status, manifest.output).toBe(0);
const rows = JSON.parse(
expectDefined(manifest.outputs.macos_node_matrix, "Mac Node matrix"),
).include as Array<{ task: string }>;
expect(rows.map(({ task }) => task)).toEqual(
partsSupported ? ["test-1", "test-2", "test-3"] : ["test"],
);
const commands = rows.map(({ task }) => {
const result = runWorkflowShellScript(runStep.run, {
cwd,
env: { ...process.env, PATH: `${bin}${path.delimiter}${process.env.PATH}`, TASK: task },
});
expect(result.status, result.stdout + result.stderr).toBe(0);
return result.stdout.match(/^selected=(.*)$/m)?.[1];
});
expect(commands).toEqual(
partsSupported
? ["test:macos:ci:1", "test:macos:ci:2", "test:macos:ci:3"]
: ["test:macos:ci"],
);
}
expect(job.strategy["max-parallel"]).toBe(3);
expect(runStep.env.OPENCLAW_VITEST_MAX_WORKERS).toBe(2);
},
);
it.skipIf(process.platform === "win32")(
"keeps Windows projects serial on each runner while both jobs remain parallel",
() => {
const workflow = readCiWorkflow();
const job = workflow.jobs["checks-windows"];
const runStep = job.steps.find(
(step: WorkflowStep) => step.name === "Run ${{ matrix.task }} (${{ matrix.runtime }})",
);
const cwd = tempDirs.make("windows-project-budget-");
const bin = path.join(cwd, "bin");
mkdirSync(bin);
writeFileSync(
path.join(cwd, "package.json"),
JSON.stringify({
scripts: { "test:windows:ci:1": "fixture", "test:windows:ci:2": "fixture" },
}),
);
const pnpm = path.join(bin, "pnpm");
writeFileSync(
pnpm,
'#!/bin/sh\nprintf "project_parallelism=%s\\n" "${OPENCLAW_TEST_PROJECTS_PARALLEL:-1}"\n',
);
chmodSync(pnpm, 0o755);
for (const task of ["test-1", "test-2"]) {
for (const runner of ["github-hosted", "self-hosted"]) {
const result = runWorkflowShellScript(runStep.run, {
cwd,
env: {
...process.env,
PATH: `${bin}${path.delimiter}${process.env.PATH}`,
TASK: task,
RUNNER_ENVIRONMENT: runner,
OPENCLAW_TEST_PROJECTS_PARALLEL: undefined,
},
});
expect(result.status, result.stdout + result.stderr).toBe(0);
expect(result.stdout).toContain("project_parallelism=1");
}
}
expect(job.strategy["max-parallel"]).toBe(2);
expect(job.env.OPENCLAW_VITEST_MAX_WORKERS).toBe(1);
},
);
it("installs the Android SDK platform used by Gradle", () => {
const workflow = readCiWorkflow();
const releaseWorkflow = readAndroidReleaseWorkflow();
const action = readAndroidToolchainAction();
const appCompileSdk = readAndroidCompileSdk("apps/android/app/build.gradle.kts");
const benchmarkCompileSdk = readAndroidCompileSdk("apps/android/benchmark/build.gradle.kts");
const packageId = `platforms;android-${appCompileSdk}.0`;
expect(appCompileSdk).toBe(benchmarkCompileSdk);
expect(
workflow.jobs.android.steps.filter(
(step: WorkflowStep) =>
step.uses === "./.ci-harness/.github/actions/setup-android-toolchain",
),
).toHaveLength(1);
expect(
releaseWorkflow.jobs.publish_signed_android_apk.steps.filter(
(step: WorkflowStep) => step.uses === "./.github/actions/setup-android-toolchain",
),
).toHaveLength(1);
const sdkRestoreStep = expectDefined(
action.runs.steps.find((step: WorkflowStep) => step.name === "Restore Android SDK cache"),
"Android SDK cache restore step",
);
const sdkSaveStep = expectDefined(
action.runs.steps.find((step: WorkflowStep) => step.name === "Save Android SDK cache"),
"Android SDK cache save step",
);
const gradleCacheStep = expectDefined(
action.runs.steps.find((step: WorkflowStep) => step.name === "Setup Gradle cache"),
"Gradle cache setup step",
);
const javaStep = expectDefined(
action.runs.steps.find((step: WorkflowStep) => step.name === "Setup Java"),
"Android Java setup step",
);
const installStep = expectDefined(
action.runs.steps.find((step: WorkflowStep) => step.name === "Install Android SDK packages"),
"Android SDK package install step",
);
expect(javaStep.uses).toBe("actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c");
expect(javaStep.with).toMatchObject({
distribution: "temurin",
"java-version": 17,
});
expect(javaStep.with?.["set-default"]).not.toBe(false);
const gradleJavaStep = expectDefined(
action.runs.steps.find((step: WorkflowStep) => step.name === "Setup Gradle Java"),
"Gradle Java setup step",
);
expect(gradleJavaStep).toMatchObject({
id: "gradle-java",
uses: javaStep.uses,
with: {
distribution: "temurin",
"java-version": "21.0.12+101.0.LTS",
"set-default": false,
},
});
expect(action.outputs["gradle-java-home"].value).toBe("${{ steps.gradle-java.outputs.path }}");
expect(action.outputs["gradle-java-version"].value).toBe(
"${{ steps.gradle-java.outputs.version }}",
);
expect(action.inputs["cache-mode"].default).toBe("off");
expect(sdkRestoreStep.if).toBe("inputs.cache-mode != 'off'");
expect(sdkRestoreStep.uses).toBe(CACHE_V5);
expect(sdkRestoreStep.with?.key).toContain(`platform-${appCompileSdk}.0-`);
expect(sdkRestoreStep.with?.key).toContain(
"${{ inputs.install-screenshot-emulators == 'true' && 'screenshot-emulators' || 'base' }}",
);
expect(String(sdkRestoreStep.with?.["restore-keys"])).toContain(
"inputs.install-screenshot-emulators == 'true'",
);
expect(sdkSaveStep.if).toContain("inputs.cache-mode == 'read-write'");
expect(sdkSaveStep.if).toContain("steps.android-sdk-cache.outputs.cache-hit != 'true'");
expect(sdkSaveStep.uses).toBe(CACHE_SAVE_V5);
expect(sdkSaveStep.with?.key).toBe("${{ steps.android-sdk-cache.outputs.cache-primary-key }}");
expect(gradleCacheStep).toMatchObject({
if: "inputs.cache-mode != 'off'",
uses: SETUP_GRADLE_V6,
with: {
"add-job-summary": "never",
"cache-provider": "basic",
"cache-read-only": "${{ inputs.cache-mode != 'read-write' }}",
},
});
expect(installStep.run).toContain(`"${packageId}"`);
expect(installStep.run).toContain(
'yes | sdkmanager --sdk_root="${ANDROID_SDK_ROOT}" --licenses >/dev/null || [[ "${PIPESTATUS[1]}" -eq 0 ]]',
);
});
it("binds frozen target context to the declared live release branch", () => {
const workflow = readCiWorkflow();
const input = workflow.on.workflow_dispatch.inputs.target_context_ref;
const step = expectDefined(
workflow.jobs.preflight.steps.find(
(candidate: WorkflowStep) => candidate.name === "Validate target context",
),
"target context validation step",
);
const targetSha = "a".repeat(40);
expect(input).toEqual({
description:
"Canonical release branch context authorizing compatibility fallbacks for an exact-SHA target",
required: false,
default: "",
type: "string",
});
expect(step.if).toBe("inputs.target_context_ref != ''");
for (const contextRef of [
"release/2026.8.1",
"release/2026.8.1-1",
"extended-stable/2026.8.33",
]) {
for (const comparisonStatus of ["ahead", "identical"]) {
const result = runCiReleaseRefValidation({
ref: contextRef,
targetSha,
resolvedSha: comparisonStatus === "identical" ? targetSha : "b".repeat(40),
comparisonStatus,
});
expect(result.status, `${contextRef}: ${result.output}`).toBe(0);
expect(result.outputs.eligible).toBe("true");
}
}
for (const contextRef of [
"v2026.8.1",
"main",
"release-ci/2026.8.1-beta.2-frozen",
"release/2026.8",
"refs/heads/release/2026.8.1",
]) {
const result = runCiReleaseRefValidation({ ref: contextRef, targetSha });
expect(result.status, contextRef).toBe(1);
expect(result.output).toContain(
"target_context_ref must be a canonical OpenClaw release branch.",
);
}
for (const targetRef of ["main", "a".repeat(39)]) {
const result = runCiReleaseRefValidation({ ref: "release/2026.8.1", targetSha: targetRef });
expect(result.status, targetRef).toBe(1);
expect(result.output).toContain(
"target_context_ref requires target_ref to be a full commit SHA.",
);
}
for (const comparisonStatus of ["behind", "diverged"]) {
const result = runCiReleaseRefValidation({
ref: "release/2026.8.1",
targetSha,
comparisonStatus,
});
expect(result.status, comparisonStatus).toBe(1);
expect(result.output).toContain(
"target_ref must be the declared release branch head or one of its ancestors.",
);
}
});
it.each([
{ kind: "historical", ref: "v2026.8.1" },
{ kind: "historical", ref: "v2026.8.1-beta.2" },
{ kind: "historical", ref: "v2026.8.1-1" },
{ kind: "candidate", ref: "release/2026.8.1" },
{ kind: "candidate", ref: "release/2026.8.1-1" },
{ kind: "candidate", ref: "extended-stable/2026.8.33" },
] as const)("binds authenticated $kind ref $ref to its exact commit", (identity) => {
const targetSha = "a".repeat(40);
const accepted = runCiReleaseRefValidation({ ...identity, targetSha, resolvedSha: targetSha });
expect(accepted.status, accepted.output).toBe(0);
expect(accepted.outputs.eligible).toBe("true");
const mismatched = runCiReleaseRefValidation({ ...identity, targetSha });
expect(mismatched.status).not.toBe(0);
expect(mismatched.output).toContain(`does not resolve to ${targetSha}`);
expect(mismatched.outputs).not.toHaveProperty("eligible");
});
it.each([
{ kind: "context", ref: "release/2026.8.1", apiError: "ref" },
{ kind: "context", ref: "release/2026.8.1", apiError: "comparison" },
{ kind: "historical", ref: "v2026.8.1", apiError: "ref" },
{ kind: "candidate", ref: "release/2026.8.1", apiError: "ref" },
] as const)("rejects unavailable authenticated $kind $apiError evidence", (identity) => {
const targetSha = "a".repeat(40);
const result = runCiReleaseRefValidation({
...identity,
targetSha,
resolvedSha: targetSha,
});
expect(result.status).not.toBe(0);
expect(result.output).toContain("HTTP 503");
expect(result.outputs).not.toHaveProperty("eligible");
});
it.each([
{ kind: "historical", ref: "refs/heads/v2026.8.1" },
{ kind: "historical", ref: "release/2026.8.1" },
{ kind: "candidate", ref: "refs/tags/release/2026.8.1" },
{ kind: "candidate", ref: "v2026.8.1" },
] as const)("rejects wrong-namespace $kind ref $ref before remote admission", (identity) => {
const result = runCiReleaseRefValidation({ ...identity, targetSha: "a".repeat(40) });
expect(result.status).not.toBe(0);
expect(result.output).toContain("must be a canonical OpenClaw release");
expect(result.outputs).not.toHaveProperty("eligible");
});
// Native Windows Node cannot execute this fixture's POSIX gh child shim.
it.skipIf(process.platform === "win32")("protects correction credentials", () => {
const root = tempDirs.make("openclaw-ci-correction-order-");
const trusted = path.join(root, ".ci-harness/scripts/lib");
const eventsPath = path.join(root, "events");
const outputPath = path.join(root, "output");
const bin = path.join(root, "bin");
mkdirSync(trusted, { recursive: true });
mkdirSync(path.join(root, "scripts"));
mkdirSync(bin);
writeFileSync(eventsPath, "");
writeFileSync(outputPath, "");
writeFileSync(path.join(root, "package.json"), JSON.stringify({ version: "2026.9.1" }));
for (const name of ["release-context.mjs", "release-version.mjs"]) {
writeFileSync(path.join(trusted, name), readFileSync(`scripts/lib/${name}`));
}
writeFileSync(
path.join(trusted, "release-context-original.mjs"),
readFileSync("scripts/lib/release-context.mjs"),
);
const poisonedHelper = `
import { appendFileSync } from 'node:fs';
if (process.env.GH_TOKEN) appendFileSync(${JSON.stringify(eventsPath)}, 'token-exposed\\n');
export { resolveReleaseContextIdentity } from './release-context-original.mjs';
`;
writeFileSync(
path.join(root, "scripts/ci-changed-scope.mjs"),
`import { appendFileSync, writeFileSync } from 'node:fs';
appendFileSync(${JSON.stringify(eventsPath)}, 'candidate\\n');
writeFileSync(${JSON.stringify(path.join(trusted, "release-context.mjs"))}, ${JSON.stringify(poisonedHelper)});`,
);
writeExecutable(path.join(bin, "gh"), [
"#!/bin/sh",
'[ "$GH_TOKEN" = test-token ] || exit 4',
`[ "$*" = 'api repos/openclaw/openclaw/commits/refs%2Ftags%2Fv2026.9.1 --jq .sha' ] || exit 64`,
`printf 'lookup\\n' >> ${quoteShell(eventsPath)}`,
`printf '%s\\n' '${"a".repeat(40)}'`,
]);
const context = {
eventName: "workflow_dispatch" as const,
releaseGate: true,
releaseScope: "npm-stable",
repository: "openclaw/openclaw",
runAttempt: 1,
targetContextRef: "release/2026.9.1-1",
workflowToken: "test-token",
steps: {
diff_base: { outputs: { sha: "b".repeat(40), head_sha: "a".repeat(40) } },
target_context_target: { outputs: { eligible: "true" } },
},
};
const steps = readCiWorkflow().jobs.preflight.steps.filter((step: WorkflowStep) =>
["Resolve release correction base", "Detect changed scopes"].includes(step.name ?? ""),
);
expect(steps).toHaveLength(2);
for (const step of steps) {
const evaluate = (expression: string) => evaluateWorkflowExpression(expression, context);
expect(evaluate(`\${{ ${step.if} }}`), step.name).toBe(true);
const run = runWorkflowShellScript(
step.run.replace(/\$\{\{[\s\S]*?\}\}/gu, (expression: string) =>
String(evaluate(expression)),
),
{
cwd: root,
env: {
PATH: `${bin}${path.delimiter}${process.env.PATH ?? ""}`,
GITHUB_REPOSITORY: context.repository,
GITHUB_OUTPUT: outputPath,
...Object.fromEntries(
Object.entries(step.env ?? {}).map(([name, value]) => [
name,
String(evaluate(String(value))),
]),
),
},
},
);
expect(run.status, `${step.name}: ${run.stdout}${run.stderr}`).toBe(0);
}
expect(readFileSync(eventsPath, "utf8").trim().split("\n")).toEqual(["lookup", "candidate"]);
expect(readWorkflowOutputs(outputPath).sha).toBe("a".repeat(40));
});
it("pins Swift 6.3 workflow jobs to Xcode 26.6-capable runners", () => {
const codeql = parse(
readFileSync(".github/workflows/codeql-macos-critical-security.yml", "utf8"),
);
const codeqlJob = codeql.jobs.macos;
const codeqlSelect = expectDefined(
codeqlJob.steps.find((step: WorkflowStep) => step.name === "Select Xcode"),
"CodeQL macOS Xcode selection",
);
const codeqlInitializeIndex = codeqlJob.steps.findIndex(
(step: WorkflowStep) => step.name === "Initialize CodeQL",
);
const codeqlBuildIndex = codeqlJob.steps.findIndex(
(step: WorkflowStep) => step.name === "Build macOS for CodeQL",
);
const codeqlAnalyzeIndex = codeqlJob.steps.findIndex(
(step: WorkflowStep) => step.name === "Analyze",
);
const codeqlBuild = expectDefined(codeqlJob.steps[codeqlBuildIndex], "CodeQL macOS build");
const codeqlPrepare = codeql.jobs["prepare-mermaid"];
const codeqlPrepareSteps = codeqlPrepare.steps as WorkflowStep[];
const codeqlPrepareCheckout = expectDefined(
codeqlPrepareSteps.find((step) => step.name === "Checkout"),
"CodeQL Mermaid checkout",
);
const codeqlPrepareSetup = expectDefined(
codeqlPrepareSteps.find((step) => step.name === "Setup Node environment"),
"CodeQL Mermaid Node setup",
);
const codeqlPrepareInstall = expectDefined(
codeqlPrepareSteps.find((step) => step.name === "Install Mermaid renderer dependencies"),
"CodeQL Mermaid dependency install",
);
const codeqlPrepareAssets = expectDefined(
codeqlPrepareSteps.find((step) => step.name === "Prepare Apple Mermaid assets"),
"CodeQL Mermaid asset preparation",
);
const codeqlUpload = expectDefined(
codeqlPrepareSteps.find((step) => step.name === "Upload Mermaid assets"),
"CodeQL Mermaid artifact upload",
);
const codeqlDownloadIndex = codeqlJob.steps.findIndex(
(step: WorkflowStep) => step.name === "Download Mermaid assets",
);
const codeqlDownload = expectDefined(
codeqlJob.steps[codeqlDownloadIndex],
"CodeQL Mermaid artifact download",
);
expect(codeqlPrepare["runs-on"]).toBe("ubuntu-24.04");
expect(codeqlPrepare["timeout-minutes"]).toBe(10);
expect(codeqlPrepare.permissions).toEqual({ contents: "read" });
expect(codeqlPrepare.outputs["artifact-id"]).toBe("${{ steps.upload.outputs.artifact-id }}");
expect(codeqlPrepareCheckout.with).toMatchObject({
"fetch-depth": 1,
"persist-credentials": false,
ref: "${{ github.sha }}",
submodules: false,
});
expect(codeqlPrepareSetup.with).toMatchObject({
"cache-mode": "restore",
"install-bun": "false",
"install-deps": "false",
});
expect(codeqlPrepareInstall.env).toEqual({ CI: "true" });
expect(codeqlPrepareInstall.run).toContain(
"pnpm install --frozen-lockfile --prefer-offline --optional",
);
expect(codeqlPrepareInstall.run).toContain("--filter '@openclaw/mermaid-renderer...'");
expect(codeqlPrepareInstall.run).toContain("--config.ignore-scripts=false");
expect(codeqlPrepareInstall.run).toContain("--config.engine-strict=false");
expect(codeqlPrepareInstall.run).toContain("--config.enable-pre-post-scripts=true");
expect(codeqlPrepareInstall.run).toContain("--config.side-effects-cache=true");
expect(codeqlPrepareAssets.run).toBe("node scripts/prepare-apple-mermaid.mjs");
expect(codeqlUpload).toMatchObject({
id: "upload",
uses: UPLOAD_ARTIFACT_V7,
with: {
name: "codeql-macos-mermaid-${{ github.run_id }}-${{ github.run_attempt }}",
path: "apps/shared/OpenClawKit/Sources/OpenClawChatUI/Resources/Mermaid",
"if-no-files-found": "error",
"retention-days": 1,
},
});
expect(codeqlPrepareSteps.indexOf(codeqlPrepareInstall)).toBeLessThan(
codeqlPrepareSteps.indexOf(codeqlPrepareAssets),
);
expect(codeqlPrepareSteps.indexOf(codeqlPrepareAssets)).toBeLessThan(
codeqlPrepareSteps.indexOf(codeqlUpload),
);
expect(codeqlJob.needs).toBe("prepare-mermaid");
expect(codeqlJob["runs-on"]).toBe("macos-26-intel");
expect(codeqlJob["timeout-minutes"]).toBe(90);
const codeqlCheckout = expectDefined(
codeqlJob.steps.find((step: WorkflowStep) => step.name === "Checkout"),
"CodeQL macOS checkout",
);
expect(codeqlCheckout.with).toMatchObject({
"fetch-depth": 1,
"persist-credentials": false,
ref: "${{ github.sha }}",
submodules: false,
});
expect(
codeqlJob.steps.some(
(step: WorkflowStep) => step.uses === "./.github/actions/setup-node-env",
),
).toBe(false);
expect(
codeqlJob.steps.some((step: WorkflowStep) => step.run?.includes("prepare-apple-mermaid.mjs")),
).toBe(false);
expect(codeqlDownload).toMatchObject({
uses: DOWNLOAD_ARTIFACT_V8,
with: {
"artifact-ids": "${{ needs.prepare-mermaid.outputs.artifact-id }}",
path: "apps/shared/OpenClawKit/Sources/OpenClawChatUI/Resources/Mermaid",
},
});
expect(codeqlDownloadIndex).toBeLessThan(codeqlInitializeIndex);
expect(codeqlInitializeIndex).toBeGreaterThanOrEqual(0);
expect(codeqlInitializeIndex).toBeLessThan(codeqlBuildIndex);
expect(codeqlBuildIndex).toBeLessThan(codeqlAnalyzeIndex);
expect(codeqlBuild.run).toBe(
"swift build --package-path apps/macos --product OpenClaw --arch arm64 --disable-index-store -debug-info-format none",
);
expect(codeqlSelect.run).toContain("/Applications/Xcode_26.6.app/Contents/Developer");
expect(codeqlSelect.run).toContain('if [[ "$xcode_version" != 26.6* ]]; then');
for (const [workflowPath, selectorCount] of [
[".github/workflows/ci.yml", 2],
[".github/workflows/ios-periphery.yml", 1],
[".github/workflows/macos-periphery.yml", 1],
[".github/workflows/shared-openclawkit-periphery.yml", 2],
] as const) {
const source = readFileSync(workflowPath, "utf8");
expect(source.match(/\/Applications\/Xcode_26\.6\.app/gu), workflowPath).toHaveLength(
selectorCount,
);
expect(source.match(/expected Xcode 26\.6/gu), workflowPath).toHaveLength(selectorCount);
expect(source, workflowPath).not.toContain("Xcode_26.5.app");
}
});
it("loads Android CI setup from the workflow revision for frozen targets", () => {
const steps = readCiWorkflow().jobs.android.steps as WorkflowStep[];
const checkoutIndex = steps.findIndex((step) => step.name === "Checkout");
const actionCheckoutIndex = steps.findIndex(
(step) => step.name === "Checkout CI Android toolchain action",
);
const setupIndex = steps.findIndex((step) => step.name === "Setup Android toolchain");
const actionCheckout = expectDefined(steps[actionCheckoutIndex], "Android action checkout");
expect(actionCheckout.uses).toBe(CHECKOUT_V6);
expect(actionCheckout.with).toMatchObject({
path: ".ci-harness",
"persist-credentials": false,
ref: "${{ github.workflow_sha }}",
"sparse-checkout": ".github/actions",
});
expect(checkoutIndex).toBeLessThan(actionCheckoutIndex);
expect(actionCheckoutIndex).toBeLessThan(setupIndex);
});
it("bounds Android SDK command-line tools downloads", () => {
const action = readAndroidToolchainAction();
const restoreStep = expectDefined(
action.runs.steps.find((step: WorkflowStep) => step.name === "Restore Android SDK cache"),
"Android SDK cache restore step",
);
const setupStep = expectDefined(
action.runs.steps.find((step: WorkflowStep) =>
step.run?.includes("commandlinetools-linux-${CMDLINE_TOOLS_VERSION}_latest.zip"),
),
"Android SDK setup step",
);
expect(restoreStep.with?.key).toBe(
"${{ runner.os }}-android-sdk-v2-cmdline-15859902-platform-37.0-build-tools-36.0.0-${{ inputs.install-screenshot-emulators == 'true' && 'screenshot-emulators' || 'base' }}",
);
expect(String(restoreStep.with?.["restore-keys"]).trim().split("\n")).toEqual([
"${{ inputs.install-screenshot-emulators == 'true' && format('{0}-android-sdk-v2-cmdline-15859902-platform-37.0-build-tools-36.0.0-base', runner.os) || '' }}",
"${{ runner.os }}-android-sdk-v1-cmdline-15859902-platform-37.0-build-tools-36.0.0",
"${{ runner.os }}-android-sdk-v1-cmdline-15859902-",
]);
expect(setupStep.run).toContain('CMDLINE_TOOLS_VERSION="15859902"');
expect(setupStep.run).toContain(
'CMDLINE_TOOLS_SHA256="4e4c464f145a7512b57d088ac6c278c03c9eea610886b35a5e0804e74eedf583"',
);
expect(setupStep.run).toContain("curl -fsSL --connect-timeout 10 --max-time 300");
expect(setupStep.run).toContain("sha256sum --check -");
});
it("covers Android app variants, lint, and benchmark compilation", () => {
const workflow = readCiWorkflow();
const source = readFileSync(".github/workflows/ci.yml", "utf8");
const androidJob = workflow.jobs.android;
const runStep = expectDefined(
androidJob.steps.find((step: WorkflowStep) => step.name === "Run Android ${{ matrix.task }}"),
"Android task runner",
);
const nativeResourcesSetup = expectDefined(
androidJob.steps.find(
(step: WorkflowStep) => step.name === "Setup Node environment for native resources",
),
"Android native resources Node setup",
);
const buildPlayCase = expectDefined(
runStep.run?.match(/^\s*build-play\)\n([\s\S]*?)^\s*;;$/mu)?.[1],
"Android build-play case",
);
const buildPlayBranches = expectDefined(
buildPlayCase.match(
/if \[ "\$CI_RUNNER_BACKEND" = "github" \] \|\| \[ "\$GITHUB_EVENT_NAME" = "workflow_dispatch" \]; then\n([\s\S]*?)\n\s*else\n([\s\S]*?)\n\s*fi/u,
),
"Android build-play runner branches",
);
const dispatchBuild = expectDefined(buildPlayBranches[1], "hosted dispatch build branch");
const blacksmithBuild = expectDefined(buildPlayBranches[2], "Blacksmith build branch");
const readTasks = (script: string) =>
[...script.matchAll(/^\s+(:[a-z][A-Za-z0-9:-]*)\s*\\?$/gmu)].map((match) => match[1]);
const dispatchTasks = readTasks(dispatchBuild);
const blacksmithTasks = readTasks(blacksmithBuild);
expect(source).toContain('task: useCompatibleAndroidCi ? "test-play-compat" : "test-play"');
expect(source).toContain('task: "test-third-party"');
expect(source.match(/check_name: "android-build-play"/gu)).toHaveLength(1);
expect(source).toContain('task: useCompatibleAndroidCi ? "build-play-compat" : "build-play"');
expect(androidJob.name).toBe("${{ matrix.check_name || 'android' }}");
expect(runStep.env.CI_RUNNER_BACKEND).toContain(
"vars.OPENCLAW_CI_RUNNER_BACKEND == 'hybrid' && github.run_attempt > 1",
);
expect(runStep.run).toContain(":app:testPlayDebugUnitTest");
expect(runStep.run).toContain(":app:testThirdPartyDebugUnitTest");
expect(dispatchBuild.match(/^\s*\.\/gradlew\b/gmu)).toHaveLength(3);
expect(dispatchTasks).toEqual([
":app:assemblePlayDebug",
":app:lintPlayDebug",
":app:assembleThirdPartyDebug",
":app:lintThirdPartyDebug",
":benchmark:assembleDebug",
":wear-shared:assembleDebug",
":wear-shared:lintDebug",
]);
expect(new Set(dispatchTasks).size).toBe(dispatchTasks.length);
expect(blacksmithBuild.match(/^\s*\.\/gradlew\b/gmu)).toHaveLength(1);
expect(blacksmithTasks).toEqual([
":app:assemblePlayDebug",
":app:assembleThirdPartyDebug",
":app:lintPlayDebug",
":app:lintThirdPartyDebug",
":benchmark:assembleDebug",
":wear-shared:assembleDebug",
":wear-shared:lintDebug",
]);
expect(nativeResourcesSetup.uses).toBe("./.ci-harness/.github/actions/setup-node-env");
expect(nativeResourcesSetup.if).toBe(
"needs.preflight.outputs.use_compatible_android_ci != 'true'",
);
expect(nativeResourcesSetup.with).toMatchObject({
"install-bun": "false",
"install-deps": "false",
});
const nativeResourcesInstall = expectDefined(
androidJob.steps.find(
(step: WorkflowStep) => step.name === "Install Mermaid renderer dependencies",
),
"Android native resources dependency install",
);
expect(nativeResourcesInstall.if).toBe(nativeResourcesSetup.if);
expect(nativeResourcesInstall.env).toEqual({ CI: "true" });
expect(nativeResourcesInstall.run.trim().split(/\s+/u)).toEqual([
"pnpm",
"install",
"--frozen-lockfile",
"--prefer-offline",
"--optional",
"--filter",
"'@openclaw/mermaid-renderer...'",
"--config.ignore-scripts=false",
"--config.engine-strict=false",
"--config.enable-pre-post-scripts=true",
"--config.side-effects-cache=true",
]);
expect(androidJob.steps.indexOf(nativeResourcesSetup)).toBeLessThan(
androidJob.steps.indexOf(nativeResourcesInstall),
);
expect(androidJob.steps.indexOf(nativeResourcesInstall)).toBeLessThan(
androidJob.steps.indexOf(runStep),
);
});
describe("Android validation tiers", () => {
function runAndroidTask(
row: Record<string, unknown>,
context: Parameters<typeof evaluateWorkflowExpression>[1],
failTask = "",
) {
const step: WorkflowStep = expectDefined(
readCiWorkflow().jobs.android.steps.find(
(candidate: WorkflowStep) => candidate.name === "Run Android ${{ matrix.task }}",
),
"Android task runner",
);
const root = tempDirs.make("openclaw-android-tier-");
const callsPath = path.join(root, "gradle-calls.jsonl");
const clockPath = path.join(root, "clock-reads.jsonl");
const gradleJavaHome = path.join(root, "gradle jdk");
const action = readAndroidToolchainAction();
const setupStep: WorkflowStep = expectDefined(
readCiWorkflow().jobs.android.steps.find(
(candidate: WorkflowStep) => candidate.name === "Setup Android toolchain",
),
"Android toolchain setup",
);
const gradleJavaOutputs = {
path: gradleJavaHome,
version: "21.0.12+101.0.LTS",
};
const setupOutputs = Object.fromEntries(
["gradle-java-home", "gradle-java-version"].map((key) => [
key,
String(
evaluateWorkflowExpression(action.outputs[key].value, {
...context,
steps: { "gradle-java": { outputs: gradleJavaOutputs } },
}),
),
]),
);
writeExecutable(path.join(root, "date"), [
"#!/usr/bin/env node",
'const fs = require("node:fs");',
'const previous = fs.existsSync(process.env.CLOCK_READS) ? fs.readFileSync(process.env.CLOCK_READS, "utf8").trim().split("\\n") : [];',
"const instant = new Date(1700000000000 + previous.length * 1000).toISOString();",
'fs.appendFileSync(process.env.CLOCK_READS, instant + "\\n");',
"console.log(instant);",
]);
writeExecutable(path.join(root, "gradlew"), [
"#!/usr/bin/env node",
'require("node:fs").appendFileSync(process.env.GRADLE_CALLS, JSON.stringify({ args: process.argv.slice(2), javaHome: process.env.JAVA_HOME }) + "\\n");',
"if (process.argv.includes(process.env.FAIL_GRADLE_TASK)) process.exit(23);",
]);
const result = runWorkflowShellScript(expectDefined(step.run, "Android commands"), {
linuxWorkflow: true,
cwd: root,
env: {
...process.env,
JAVA_HOME: path.join(root, "sdk jdk 17"),
...Object.fromEntries(
Object.entries(step.env ?? {}).map(([key, value]) => [
key,
String(
evaluateWorkflowExpression(value, {
...context,
matrix: row,
steps: {
...context.steps,
[expectDefined(setupStep.id, "Android toolchain step id")]: {
outputs: setupOutputs,
},
},
}),
),
]),
),
GITHUB_EVENT_NAME: context.eventName,
OPENCLAW_ROBOLECTRIC_INIT: "robolectric.gradle",
GRADLE_CALLS: callsPath,
FAIL_GRADLE_TASK: failTask,
CLOCK_READS: clockPath,
PATH: `${root}${path.delimiter}${process.env.PATH}`,
},
});
const invocations: Array<{ args: string[]; javaHome: string }> = existsSync(callsPath)
? readFileSync(callsPath, "utf8")
.trim()
.split("\n")
.map((line) => JSON.parse(line))
: [];
const calls = invocations.map(({ args }) => args);
expect(invocations.map(({ javaHome }) => javaHome)).toEqual(calls.map(() => gradleJavaHome));
const clockReads = existsSync(clockPath)
? readFileSync(clockPath, "utf8").trim().split("\n")
: [];
return {
...result,
calls,
clockReads,
tasks: calls.flat().filter((arg) => arg.startsWith(":")),
};
}
it.each(["test-play", "test-third-party"])(
"reuses one build instant across the %s unit and lint commands",
(task) => {
const result = runAndroidTask(
{ task, lint: true },
{ eventName: "pull_request", repository: "openclaw/openclaw", runAttempt: 1 },
);
expect(result.status, `${result.stdout}${result.stderr}`).toBe(0);
expect(result.calls).toHaveLength(2);
const metadata = result.calls.map((call) =>
call.filter((arg) => arg.startsWith("-PopenclawBuildTimestamp=")),
);
expect(metadata[0]).toHaveLength(1);
expect(metadata[1]).toEqual(metadata[0]);
expect(result.clockReads).toHaveLength(1);
expect(metadata[0]).toEqual([`-PopenclawBuildTimestamp=${result.clockReads[0]}`]);
},
);
it.each([
{ eventName: "pull_request", releaseGate: false, full: false, legacy: false },
{ eventName: "push", releaseGate: false, full: false, legacy: false },
{ eventName: "workflow_dispatch", releaseGate: true, full: false, legacy: false },
{ eventName: "workflow_dispatch", releaseGate: false, full: true, legacy: false },
{ eventName: "workflow_dispatch", releaseGate: false, full: true, legacy: true },
] as const)(
"executes Android test/lint and retained full-tier commands for %j",
({ eventName, releaseGate, full, legacy }) => {
const manifest = runCiManifestFixture({
bundledPlanner: !legacy,
eventName,
releaseGate,
historicalCompatibility: true,
changedPaths: ["apps/android/app/src/main/java/ai/openclaw/app/Example.kt"],
});
expect(manifest.status, manifest.output).toBe(0);
const rows: Record<string, unknown>[] = JSON.parse(
expectDefined(manifest.outputs.android_matrix, "Android matrix"),
).include;
expect(rows.map(({ task }) => task)).toEqual(
legacy
? ["test-play-compat", "test-third-party", "build-play-compat"]
: [
"test-play",
"test-third-party",
"test-wear",
...(full ? ["build-play", "build-wear"] : []),
"ktlint",
],
);
const context = {
eventName,
releaseGate,
repository: "openclaw/openclaw",
runAttempt: 1,
preflightOutputs: manifest.outputs,
};
const tasks = rows.flatMap((row) => {
const result = runAndroidTask(row, context);
expect(result.status, `${result.stdout}${result.stderr}`).toBe(0);
for (const call of result.calls) {
expect(call).toContain("--no-daemon");
expect(call).toContain("--build-cache");
const testCall = call.some((arg) => arg.endsWith("UnitTest"));
expect(call.includes("--init-script")).toBe(testCall);
}
if ((row.task !== "test-play" && row.task !== "test-third-party") || row.lint !== true) {
expect(result.clockReads).toEqual([]);
expect(
result.calls.flat().filter((arg) => arg.startsWith("-PopenclawBuildTimestamp=")),
).toEqual([]);
}
if (row.task === "build-play") {
expect(result.calls.map((call) => call.filter((arg) => arg.startsWith(":")))).toEqual([
[":app:assemblePlayDebug", ":app:lintPlayDebug"],
[":app:assembleThirdPartyDebug", ":app:lintThirdPartyDebug"],
[":benchmark:assembleDebug", ":wear-shared:assembleDebug", ":wear-shared:lintDebug"],
]);
}
return result.tasks;
});
expect(tasks.toSorted()).toEqual(
(legacy
? [
":app:testPlayDebugUnitTest",
":app:testThirdPartyDebugUnitTest",
":app:assemblePlayDebug",
]
: [
":app:testPlayDebugUnitTest",
":wear-shared:testDebugUnitTest",
":app:testThirdPartyDebugUnitTest",
":wear:testDebugUnitTest",
":app:lintPlayDebug",
":app:lintThirdPartyDebug",
":wear-shared:lintDebug",
":wear:lintDebug",
":app:ktlintCheck",
":benchmark:ktlintCheck",
":wear:ktlintCheck",
":wear-shared:ktlintCheck",
...(full
? [
":app:assemblePlayDebug",
":app:assembleThirdPartyDebug",
":benchmark:assembleDebug",
":wear-shared:assembleDebug",
":wear:assembleDebug",
]
: []),
]
).toSorted(),
);
expect(new Set(tasks).size, "no duplicate lint or build invocations").toBe(tasks.length);
for (const result of ["failure", "cancelled", "skipped"]) {
expect(
runCiGateFixture(renderCiGateEnvironment(context, { android: result })).status,
).toBe(1);
}
},
);
it.each([
{ paths: ["apps/android/benchmark/src/main/Example.kt"], build: true },
{ paths: ["apps/android/build.gradle.kts"], build: true },
{ paths: ["apps/android/app/build.gradle.kts"], build: true },
{ paths: ["apps/android/settings.gradle.kts"], build: true },
{ paths: ["apps/android/gradle.properties"], build: true },
{ paths: ["apps/android/gradle/libs.versions.toml"], build: true },
{ paths: ["apps/android/gradle/wrapper/gradle-wrapper.properties"], build: true },
{ paths: ["apps/android/gradlew"], build: true },
{ paths: ["apps/android/Config/Version.properties"], build: true },
{ paths: ["apps/android/app/src/main/java/ai/openclaw/app/Example.kt"], build: false },
{ paths: ["apps/android/wear/src/main/Example.kt"], build: false },
{ paths: ["docs/ci.md"], build: false },
{ paths: [], build: true },
{ paths: [""], build: true },
{ paths: null, build: true },
{ paths: undefined, build: true },
{ paths: undefined, json: "{", build: true },
{ paths: undefined, json: "[42]", build: true },
])("retains benchmark assembly for changed or unknown inputs: %j", ({ paths, json, build }) => {
const manifest = runCiManifestFixture({
bundledPlanner: true,
eventName: "pull_request",
runNode: false,
changedPaths: paths,
...(json ? { scopeEnv: { OPENCLAW_CI_CHANGED_PATHS_JSON: json } } : {}),
});
expect(manifest.status, manifest.output).toBe(0);
const rows: Record<string, unknown>[] = JSON.parse(
expectDefined(manifest.outputs.android_matrix, "Android matrix"),
).include;
expect(rows).toHaveLength(4);
const result = runAndroidTask(
expectDefined(
rows.find(({ task }) => task === "ktlint"),
"Kotlin lint row",
),
{
eventName: "pull_request",
repository: "openclaw/openclaw",
runAttempt: 1,
},
);
expect(result.status, result.stderr).toBe(0);
expect(result.tasks.includes(":benchmark:assembleDebug")).toBe(build);
});
it.each([
["test-play", ":app:testPlayDebugUnitTest"],
["test-play", ":app:lintPlayDebug"],
["test-third-party", ":app:testThirdPartyDebugUnitTest"],
["test-third-party", ":app:lintThirdPartyDebug"],
["test-wear", ":wear:lintDebug"],
["ktlint", ":benchmark:assembleDebug"],
])("propagates %s failure from %s", (task, failTask) => {
const result = runAndroidTask(
{ task, lint: true, build_benchmark: true },
{ eventName: "pull_request", repository: "openclaw/openclaw", runAttempt: 1 },
failTask,
);
expect(result.status).toBe(23);
if (failTask.endsWith("UnitTest")) {
expect(result.calls).toHaveLength(1);
}
});
});
describe("CI workflow admission", () => {
type EventContext = Parameters<typeof evaluateWorkflowExpression>[1];
type AdmissionRun = {
context: EventContext;
group: string;
state: "pending" | "running" | "cancelling" | "cancelled" | "completed" | "skipped";
eligibleJobs?: string[];
};
const guardedJobs = ["preflight", "security-fast", "ci-gate"];
const event = (runId: number, overrides: Partial<EventContext> = {}): EventContext => ({
eventName: "pull_request",
action: "ready_for_review",
draft: false,
pullRequestNumber: 7,
headSha: "a".repeat(40),
sha: "b".repeat(40),
ref: "refs/pull/7/merge",
repository: "openclaw/openclaw",
workflow: "CI",
runAttempt: 1,
runId,
runNumber: runId,
...overrides,
});
function admissionDriver() {
const workflow = readCiWorkflow();
const runs: AdmissionRun[] = [];
const active = (run: AdmissionRun) => run.state === "running" || run.state === "cancelling";
return {
admit(context: EventContext): AdmissionRun {
if (context.eventName === "pull_request") {
expect(workflow.on.pull_request.types).toContain(context.action);
}
const group: string = evaluateWorkflowExpression(workflow.concurrency.group, context);
const cancel = evaluateWorkflowExpression(
workflow.concurrency["cancel-in-progress"],
context,
);
// GitHub replaces pending work even without active cancellation:
// https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/control-workflow-concurrency
for (const previous of runs.filter(
(run) => run.group.toLowerCase() === group.toLowerCase(),
)) {
if (previous.state === "pending") {
previous.state = "cancelled";
}
if (active(previous) && cancel) {
previous.state = "cancelling";
}
}
const run: AdmissionRun = { context, group, state: "pending" };
runs.push(run);
return run;
},
start(run: AdmissionRun) {
if (
run.state !== "pending" ||
runs.some((other) => other.group === run.group && active(other))
) {
return;
}
// Admission precedes job conditions. This probes eligibility, not the job DAG.
run.eligibleJobs = guardedJobs.filter((job) => {
const condition: string = workflow.jobs[job].if;
return evaluateWorkflowExpression(
condition.startsWith("${{") ? condition : `\${{ ${condition} }}`,
run.context,
);
});
run.state = run.eligibleJobs.length ? "running" : "skipped";
},
finish(run: AdmissionRun) {
expect(active(run)).toBe(true);
run.state = run.state === "cancelling" ? "cancelled" : "completed";
},
cancel(run: AdmissionRun) {
run.state = "cancelled";
},
};
}
// Synthetic admission orders, not recovered webhook payloads.
it.each(
["opened", "reopened", "synchronize"].flatMap((action) =>
["pending", "running"].map((state) => ({ action, state })),
),
)("preserves $state ready CI after a delayed draft $action", ({ action, state }) => {
const scheduler = admissionDriver();
const predecessor = scheduler.admit(event(1, { action: "opened" }));
scheduler.start(predecessor);
const ready = scheduler.admit(event(2));
if (state === "running") {
scheduler.finish(predecessor);
scheduler.start(ready);
}
expect(ready.state).toBe(state);
const lateDraft = scheduler.admit(event(3, { action, draft: true }));
expect(ready.state, "late draft displaced runnable ready CI").toBe(state);
scheduler.start(lateDraft);
expect(lateDraft.state).toBe("skipped");
expect(lateDraft.eligibleJobs).toEqual([]);
const anotherDraft = scheduler.admit(event(4, { action, draft: true }));
expect(anotherDraft.group).not.toBe(lateDraft.group);
expect(lateDraft.group).not.toBe(ready.group);
if (state === "pending") {
expect(ready.eligibleJobs).toBeUndefined();
scheduler.start(ready);
expect(ready.state).toBe("pending");
scheduler.finish(predecessor);
scheduler.start(ready);
}
expect(ready.state).toBe("running");
expect(ready.eligibleJobs).toEqual(guardedJobs);
});
it("admits ready CI after the forward draft-to-ready sequence", () => {
const scheduler = admissionDriver();
const draft = scheduler.admit(event(1, { action: "opened", draft: true }));
scheduler.start(draft);
expect(draft.eligibleJobs).toEqual([]);
expect(draft.state).toBe("skipped");
const ready = scheduler.admit(event(2));
scheduler.start(ready);
expect(ready.group).toBe("CI-v7-7");
expect(ready.eligibleJobs).toEqual(guardedJobs);
});
it.each(["pending", "running"])(
"converted_to_draft cancels %s CI and skips its jobs",
(state) => {
const scheduler = admissionDriver();
const previous = scheduler.admit(event(1));
scheduler.start(previous);
const ready = state === "pending" ? scheduler.admit(event(2)) : previous;
const converted = scheduler.admit(event(3, { action: "converted_to_draft", draft: true }));
expect(converted.group).toBe("CI-v7-7");
expect(ready.state).toBe(state === "pending" ? "cancelled" : "cancelling");
expect(previous.state).toBe("cancelling");
scheduler.finish(previous);
scheduler.start(converted);
expect(converted.state).toBe("skipped");
expect(converted.eligibleJobs).toEqual([]);
},
);
it.each(["pending", "running"])(
"a newer non-draft head supersedes %s CI only for its PR",
(state) => {
const scheduler = admissionDriver();
const otherPr = scheduler.admit(
event(1, { pullRequestNumber: 8, ref: "refs/pull/8/merge" }),
);
scheduler.start(otherPr);
const old = scheduler.admit(event(2));
if (state === "running") {
scheduler.start(old);
}
const next = scheduler.admit(
event(3, {
action: "synchronize",
headSha: "c".repeat(40),
sha: "d".repeat(40),
}),
);
expect(next.group).toBe(old.group);
expect(old.state).toBe(state === "pending" ? "cancelled" : "cancelling");
expect(otherPr.state).toBe("running");
if (state === "running") {
scheduler.finish(old);
}
scheduler.start(next);
expect(next.eligibleJobs).toEqual(guardedJobs);
},
);
it("isolates manual dispatches on the same target from each other and PR CI", () => {
const scheduler = admissionDriver();
const ready = scheduler.admit(event(1));
scheduler.start(ready);
const manual = [2, 3].map((runId) =>
scheduler.admit(
event(runId, {
eventName: "workflow_dispatch",
targetRef: "a".repeat(40),
}),
),
);
for (const run of manual) {
scheduler.start(run);
expect(run.state).toBe("running");
expect(run.eligibleJobs).toEqual(guardedJobs);
}
expect(manual.map((run) => run.group)).toEqual(["CI-manual-v1-2", "CI-manual-v1-3"]);
expect(ready.state).toBe("running");
});
it.each(["pending", "running"])(
"passive drafts do not resurrect explicitly cancelled %s CI",
(state) => {
const scheduler = admissionDriver();
const ready = scheduler.admit(event(1));
if (state === "running") {
scheduler.start(ready);
}
scheduler.cancel(ready);
const draft = scheduler.admit(event(2, { action: "synchronize", draft: true }));
scheduler.start(draft);
scheduler.start(ready);
expect(ready.state).toBe("cancelled");
expect(draft.state).toBe("skipped");
expect(draft.eligibleJobs).toEqual([]);
expect(
evaluateWorkflowExpression(readCiWorkflow().jobs["ci-gate"].if, {
...ready.context,
cancelled: ready.state === "cancelled",
}),
).toBe(false);
},
);
it("pipelines canonical main across two non-canceling slots with coalesced pending work", () => {
const workflow = readCiWorkflow();
const scheduler = admissionDriver();
const push = (runId: number) =>
event(runId, {
eventName: "push",
ref: "refs/heads/main",
sha: runId.toString(16).padStart(40, "0"),
});
for (let digit = 0; digit < 10; digit++) {
expect(evaluateWorkflowExpression(workflow.concurrency.group, push(100 + digit))).toBe(
`CI-v8-refs/heads/main-${digit % 2 === 0 ? "a" : "b"}`,
);
expect(
evaluateWorkflowExpression(workflow.concurrency["cancel-in-progress"], push(100 + digit)),
).toBe(false);
}
const active = [20, 21].map((id) => scheduler.admit(push(id)));
active.forEach((run) => scheduler.start(run));
const pending = [22, 23].map((id) => scheduler.admit(push(id)));
const newest = [24, 25].map((id) => scheduler.admit(push(id)));
expect(active.map((run) => run.state)).toEqual(["running", "running"]);
expect(pending.map((run) => run.state)).toEqual(["cancelled", "cancelled"]);
for (const run of newest) {
scheduler.start(run);
expect(run.state).toBe("pending");
expect(run.eligibleJobs).toBeUndefined();
}
scheduler.finish(active[0]!);
newest.forEach((run) => scheduler.start(run));
expect(newest.map((run) => run.state)).toEqual(["running", "pending"]);
scheduler.finish(active[1]!);
scheduler.start(newest[1]!);
expect(newest.map((run) => run.state)).toEqual(["running", "running"]);
expect(workflow.jobs["runner-admission"]).toBeUndefined();
expect(workflow.jobs.preflight.needs).toBeUndefined();
expect(workflow.jobs["security-fast"].needs).toEqual(["preflight"]);
});
it.each([
["openclaw/openclaw", "refs/heads/topic", "CI-v7-refs/heads/topic"],
["contributor/fork", "refs/heads/main", `CI-v7-refs/heads/main-${"b".repeat(40)}`],
["contributor/fork", "refs/heads/topic", `CI-v7-refs/heads/topic-${"b".repeat(40)}`],
])("preserves push grouping for %s on %s", (repository, ref, group) => {
const workflow = readCiWorkflow();
const context = event(1, { eventName: "push", repository, ref });
expect(evaluateWorkflowExpression(workflow.concurrency.group, context)).toBe(group);
expect(evaluateWorkflowExpression(workflow.concurrency["cancel-in-progress"], context)).toBe(
false,
);
});
});
it.each([
{ buildImpact: false, uiE2e: false, distRequired: false },
{ buildImpact: true, uiE2e: true, distRequired: false },
{ buildImpact: false, uiE2e: false, distRequired: true },
])(
"composes dedicated suite coverage before precise planning (build=$buildImpact, UI=$uiE2e, dist=$distRequired)",
({ buildImpact, uiE2e, distRequired }) => {
const runnerProfile = distRequired ? "hybrid" : "blacksmith";
const manifest = runCiManifestFixture({
runnerProfile,
bundledPlanner: true,
eventName: "pull_request",
changedPaths: [buildImpact ? "src/fixture.ts" : "src/plugins/contracts/fixture-a.test.ts"],
scopeEnv: { OPENCLAW_CI_RUN_UI_TESTS: String(uiE2e) },
changedPlannerSource: `
export const createChangedNodeTestShards = (_paths, options = {}) => {
console.log("dedicated-coverage:" + JSON.stringify(options));
return ${
buildImpact
? "[]"
: `[{ checkName: "changed-boundary", shardName: "changed-boundary",
configs: ["test/vitest/vitest.boundary.config.ts"], requiresDist: ${distRequired},
runner: "ubuntu-24.04" }]`
};
};
export const createChangedExtensionFallbackShards = () => { throw new Error("Unexpected broad fallback"); };
export const hasBuildArtifactAffectingChange = () => ${buildImpact};
export const hasSqliteSessionLifecycleAffectingChange = () => false;
`,
});
expect(manifest.status, manifest.output).toBe(0);
const dedicated = ["plugin", "channel"].flatMap((family) => {
expect(manifest.outputs[`run_${family}_contracts_shards`]).toBe("true");
const rows = JSON.parse(
expectDefined(manifest.outputs[`${family}_contracts_matrix`], family),
).include;
expect(rows).toHaveLength(1);
return rows.flatMap((row: { groups: unknown[] }) => row.groups);
});
expect(dedicated).toHaveLength(4);
const coverage = expectDefined(
manifest.output.split("\n").find((line) => line.startsWith("dedicated-coverage:")),
"precise planner coverage input",
);
expect(JSON.parse(coverage.slice("dedicated-coverage:".length))).toEqual({
runnerBackend: runnerProfile,
dedicatedContractShards: dedicated,
dedicatedUiE2e: uiE2e,
dedicatedMaxLinesRatchet: true,
});
for (const job of ["checks-ui-e2e", "checks-ui-e2e-real-gateway"]) {
expect(
evaluateWorkflowExpression(`\${{ ${readCiWorkflow().jobs[job].if} }}`, {
eventName: "pull_request",
repository: "openclaw/openclaw",
runAttempt: 1,
preflightOutputs: manifest.outputs,
}),
job,
).toBe(uiE2e);
}
const nodeRows = JSON.parse(
expectDefined(manifest.outputs.checks_node_core_nondist_matrix, "precise matrix"),
).include;
expect(nodeRows).toEqual(
buildImpact || distRequired
? []
: [expect.objectContaining({ shard_name: "changed-boundary" })],
);
expect(manifest.outputs.run_build_artifacts).toBe(String(buildImpact || distRequired));
expect(manifest.outputs.run_checks_node_core_dist).toBe(String(buildImpact || distRequired));
},
);
it.each([
["push", "blacksmith", false],
["pull_request", "github", false],
["pull_request", "hybrid", false],
["workflow_dispatch", "blacksmith", false],
["workflow_dispatch", "blacksmith", true],
["workflow_dispatch", "github", true],
] as const)(
"shares contract setup while retaining process envelopes (%s, %s, frozen=%s)",
(eventName, runnerProfile, frozenTarget) => {
const manifest = runCiManifestFixture({
bundledPlanner: true,
changedPaths: ["package.json"],
eventName,
runnerProfile,
scopeEnv: {
OPENCLAW_CI_WORKFLOW_REVISION: (frozenTarget ? "b" : "a").repeat(40),
},
});
expect(manifest.status, manifest.output).toBe(0);
for (const family of ["plugin", "channel"] as const) {
const outputName = `${family}_contracts_matrix`;
const rows = JSON.parse(expectDefined(manifest.outputs[outputName], outputName)).include;
const expected = ["a", "b"].map((suffix) => ({
checkName: `${family}-contracts-${suffix}`,
includePatterns: [
`${family === "plugin" ? "src/plugins" : "src/channels/plugins"}/contracts/fixture-${suffix}.test.ts`,
],
runtime: "node",
task: `contracts-${family}s`,
}));
expect(rows).toHaveLength(frozenTarget ? 2 : 1);
expect(rows.flatMap((row: { groups: unknown[] }) => row.groups)).toEqual(expected);
expect(rows.map((row: { checkName: string }) => row.checkName)).toEqual(
frozenTarget
? expected.map((shard) => shard.checkName)
: [`checks-fast-contracts-${family}s`],
);
}
},
);
it.each(["plugin", "channel"] as const)(
"joins %s contract envelopes and stops admission on any failure",
(family) => {
const workflow = readCiWorkflow();
const job = workflow.jobs[`checks-fast-${family}-contracts-shard`];
const step = job.steps.find(
(candidate: WorkflowStep) => candidate.name === `Run ${family} contract shard`,
);
expect(step.env.OPENCLAW_CONTRACT_INCLUDE_PATTERNS_JSON).toBe("${{ toJson(matrix) }}");
expect(step.env.OPENCLAW_TEST_PROJECTS_PARALLEL).toBe(family === "channel" ? "4" : undefined);
const fixture = tempDirs.make("openclaw-contract-groups-");
const binDir = path.join(fixture, "bin");
mkdirSync(binDir);
const commandLog = path.join(fixture, "commands.jsonl");
const pnpm = path.join(binDir, "pnpm");
writeFileSync(
pnpm,
String.raw`#!${process.execPath}
const fs = require("node:fs");
const files = JSON.parse(fs.readFileSync(process.env.OPENCLAW_VITEST_INCLUDE_FILE, "utf8"));
const record = { args: process.argv.slice(2), files, parallel: process.env.OPENCLAW_TEST_PROJECTS_PARALLEL ?? null };
fs.appendFileSync(process.env.CONTRACT_COMMAND_LOG, JSON.stringify({ ...record, phase: "start" }) + "\n");
setImmediate(() => {
fs.appendFileSync(process.env.CONTRACT_COMMAND_LOG, JSON.stringify({ ...record, phase: "end" }) + "\n");
process.exitCode = files[0] === "first.test.ts" ? Number(process.env.CONTRACT_FIRST_EXIT) : 0;
});
`,
);
chmodSync(pnpm, 0o755);
for (const firstExit of [0, 7, 143]) {
writeFileSync(commandLog, "");
const run = runWorkflowShellScript(step.run, {
cwd: fixture,
env: {
PATH: `${binDir}${path.delimiter}${process.env.PATH ?? ""}`,
RUNNER_TEMP: fixture,
CONTRACT_COMMAND_LOG: commandLog,
CONTRACT_FIRST_EXIT: String(firstExit),
OPENCLAW_TEST_PROJECTS_PARALLEL: step.env.OPENCLAW_TEST_PROJECTS_PARALLEL,
OPENCLAW_CONTRACT_INCLUDE_PATTERNS_JSON: JSON.stringify({
task: `contracts-${family}s`,
groups: [
{ checkName: "first-envelope", includePatterns: ["first.test.ts"] },
{ checkName: "second-envelope", includePatterns: ["second.test.ts"] },
],
}),
},
});
expect(run.status, `${run.stdout}${run.stderr}`).toBe(firstExit);
const files = firstExit === 0 ? ["first.test.ts", "second.test.ts"] : ["first.test.ts"];
expect(
readFileSync(commandLog, "utf8")
.trim()
.split("\n")
.map((line) => JSON.parse(line)),
).toEqual(
files.flatMap((file) =>
["start", "end"].map((phase) => ({
args: [`test:contracts:${family}s`],
files: [file],
parallel: family === "channel" ? "4" : null,
phase,
})),
),
);
}
},
);
it("keeps CodeQL critical quality scans off Blacksmith registrations", () => {
const source = readCriticalQualityWorkflow();
const workflow = parse(source);
const blacksmithJobs = Object.entries(workflow.jobs)
.filter(([, job]) => job && typeof job === "object")
.filter(([, job]) => (job as Record<string, unknown>)["runs-on"] !== "ubuntu-24.04")
.map(([name]) => name);
expect(blacksmithJobs).toEqual([]);
expect(source).not.toContain("blacksmith-");
});
it("keeps trusted hybrid controls on Blacksmith when optional hosted admission is closed", () => {
const workflow = readCiWorkflow();
expect(workflow.jobs["ci-gate"]["runs-on"]).toBe("ubuntu-24.04");
const context = {
eventName: "pull_request",
repository: "openclaw/openclaw",
runAttempt: 1,
runnerBackend: "hybrid",
} as const;
for (const jobName of ["preflight", "security-fast"]) {
const expression = workflow.jobs[jobName]["runs-on"];
for (const eventName of ["pull_request", "push"] as const) {
expect(evaluateWorkflowExpression(expression, { ...context, eventName }), jobName).toBe(
jobName === "preflight"
? "blacksmith-16vcpu-ubuntu-2404"
: "blacksmith-4vcpu-ubuntu-2404",
);
}
for (const override of [
{ runAttempt: 0 },
{ runAttempt: 2 },
{ runAttempt: 2, headRepository: "contributor/openclaw" },
{ runnerBackend: "github" },
{ eventName: "workflow_dispatch" },
{ repository: "contributor/openclaw" },
{ authorAssociation: "NONE", headRepository: "contributor/openclaw" },
] as const) {
expect(evaluateWorkflowExpression(expression, { ...context, ...override }), jobName).toBe(
"ubuntu-24.04",
);
}
for (const runnerBackend of ["", "blacksmith"] as const) {
for (const eventName of ["pull_request", "push"] as const) {
expect(
evaluateWorkflowExpression(expression, { ...context, eventName, runnerBackend }),
jobName,
).toBe(jobName === "security-fast" ? "ubuntu-24.04" : "blacksmith-4vcpu-ubuntu-2404");
}
}
}
for (const [jobName, task, expected] of [
["preflight", undefined, "blacksmith-16vcpu-ubuntu-2404"],
["security-fast", undefined, "ubuntu-24.04"],
["checks-ui", undefined, "ubuntu-24.04"],
["checks-ui-e2e", "browser-extension", "ubuntu-24.04"],
["checks-ui-e2e", "control-ui", "blacksmith-16vcpu-ubuntu-2404"],
["checks-ui-e2e-real-gateway", undefined, "blacksmith-32vcpu-ubuntu-2404"],
] as const) {
expect(
evaluateWorkflowExpression(workflow.jobs[jobName]["runs-on"], {
...context,
matrix: { task },
preflightOutputs: { hybrid_hosted_offload: "true" },
}),
`${jobName}: ${task ?? "default"}`,
).toBe(expected);
}
for (const authorAssociation of ["OWNER", "MEMBER", "COLLABORATOR", "CONTRIBUTOR"]) {
for (const headRepository of ["openclaw/openclaw", "contributor/openclaw"]) {
expect(
evaluateWorkflowExpression(workflow.jobs.preflight["runs-on"], {
...context,
authorAssociation,
headRepository,
}),
`${authorAssociation}: ${headRepository}`,
).toBe("blacksmith-16vcpu-ubuntu-2404");
}
}
});
it.each(
[
{
file: "full-release-validation.yml",
runner: "blacksmith-4vcpu-ubuntu-2404",
jobs: [
"release_checks_independent",
"release_checks_candidate",
"performance",
"release_execution_plan",
"release_decision",
"diagnostic_drain",
],
},
{
file: "openclaw-npm-preflight.yml",
runner: "blacksmith-32vcpu-ubuntu-2404",
jobs: [
"check_openclaw_npm",
"prepare_openclaw_npm",
"check_sdk_npm",
"check_dependencies_npm",
"check_contents_npm",
"verify_openclaw_npm",
],
},
{
file: "qa-live-transports-convex.yml",
runner: "blacksmith-8vcpu-ubuntu-2404",
jobs: ["authorize_actor", "validate_selected_ref"],
},
{
file: "qa-live-transports-convex.yml",
runner: "blacksmith-16vcpu-ubuntu-2404",
jobs: [
"run_mock_parity",
"run_live_runtime_token_efficiency",
"run_live_matrix",
"run_live_buzz",
"run_live_telegram",
"run_live_discord",
"run_live_whatsapp",
"run_live_slack",
],
},
{
file: "openclaw-performance.yml",
runner: "blacksmith-16vcpu-ubuntu-2404",
jobs: ["kova", "source_performance"],
},
{
file: "npm-telegram-beta-e2e.yml",
runner: "blacksmith-32vcpu-ubuntu-2404",
jobs: ["run_package_telegram_e2e"],
},
{
file: "openclaw-live-and-e2e-checks-reusable.yml",
runner: "blacksmith-32vcpu-ubuntu-2404",
jobs: ["validate_docker_openwebui"],
},
{
file: "openclaw-release-checks.yml",
runner: "blacksmith-8vcpu-ubuntu-2404",
jobs: ["qa_lab_runtime_pair_lane_release_checks"],
},
].flatMap(({ file, runner, jobs }) => jobs.map((job) => ({ file, runner, job }))),
)("honors the global hosted runner override for $file/$job", ({ file, runner, job }) => {
const workflow = parse(readFileSync(`.github/workflows/${file}`, "utf8"));
const runsOn = workflow.jobs[job]["runs-on"];
const supportsHostedInput =
file === "openclaw-npm-preflight.yml" || file === "openclaw-live-and-e2e-checks-reusable.yml";
for (const runnerBackend of ["github", "", "blacksmith", "hybrid"] as const) {
for (const useGithubHostedRunners of [false, true]) {
const expectedRunner =
runnerBackend === "github" || (supportsHostedInput && useGithubHostedRunners)
? "ubuntu-24.04"
: runner;
const actualRunner =
typeof runsOn === "string" && runsOn.startsWith("${{")
? evaluateWorkflowExpression(runsOn, {
eventName: "workflow_dispatch",
repository: "openclaw/openclaw",
runAttempt: 1,
runnerBackend,
useGithubHostedRunners,
})
: runsOn;
expect(
actualRunner,
`${runnerBackend || "unset"}, use_github_hosted_runners=${useGithubHostedRunners}`,
).toBe(expectedRunner);
}
}
});
it("resolves one event-aware logical runner profile without changing physical routing", () => {
const scenarios = [
{
expected: "github",
name: "current manual dispatch ignores configured Blacksmith",
options: {
configuredProfile: "blacksmith",
eventName: "workflow_dispatch" as const,
targetSupportsContract: true,
},
},
{
expected: "blacksmith",
name: "canonical trusted push keeps the default",
options: {
eventName: "push" as const,
targetSupportsContract: true,
},
},
{
expected: "github",
name: "canonical trusted push keeps configured GitHub",
options: {
configuredProfile: "github",
eventName: "push" as const,
targetSupportsContract: true,
},
},
{
expected: "hybrid",
name: "canonical trusted hybrid retry keeps the hybrid workload shape",
options: {
authorAssociation: "CONTRIBUTOR",
configuredProfile: "hybrid",
eventName: "pull_request" as const,
runAttempt: 2,
targetSupportsContract: true,
},
},
{
expected: "github",
name: "fork pull request is hosted",
options: {
configuredProfile: "hybrid",
eventName: "pull_request" as const,
headRepository: "contributor/openclaw",
targetSupportsContract: true,
},
},
{
expected: "github",
name: "untrusted same-repository pull request is hosted",
options: {
authorAssociation: "NONE",
configuredProfile: "blacksmith",
eventName: "pull_request" as const,
targetSupportsContract: true,
},
},
{
expected: "github",
name: "noncanonical repository is hosted",
options: {
configuredProfile: "blacksmith",
eventName: "push" as const,
repository: "fork/openclaw",
targetSupportsContract: true,
},
},
{
expected: "blacksmith",
name: "frozen target without the marker keeps legacy dispatch behavior",
options: {
configuredProfile: "blacksmith",
eventName: "workflow_dispatch" as const,
targetSupportsContract: false,
},
},
{
expected: "github",
name: "frozen target with the marker uses event-aware dispatch behavior",
options: {
configuredProfile: "blacksmith",
eventName: "workflow_dispatch" as const,
targetSupportsContract: true,
},
},
];
for (const { expected, name, options } of scenarios) {
const result = runRunnerProfileFixture(options);
expect(result.status, `${name}: ${result.output}`).toBe(0);
expect(result.outputs.runner_profile, name).toBe(expected);
expect(result.outputs.hosted_runner_profile_contract, name).toBe(
String(options.targetSupportsContract),
);
}
const invalid = runRunnerProfileFixture({
configuredProfile: "other",
eventName: "push",
targetSupportsContract: true,
});
expect(invalid.status).toBe(1);
expect(invalid.output).toContain(
"OPENCLAW_CI_RUNNER_BACKEND must be github, hybrid, or blacksmith",
);
const workflow = readCiWorkflow();
expect(workflow.jobs.preflight.outputs.runner_profile).toBe(
"${{ steps.runner_profile.outputs.runner_profile }}",
);
expect(workflow.jobs.preflight["runs-on"]).toContain("vars.OPENCLAW_CI_RUNNER_BACKEND");
const dispatchManifest = runCiManifestFixture({
bundledPlanner: true,
eventName: "workflow_dispatch",
historicalCompatibility: false,
runnerBackend: "blacksmith",
runnerProfile: "github",
});
expect(dispatchManifest.status, dispatchManifest.output).toBe(0);
expect(
JSON.parse(expectDefined(dispatchManifest.outputs.ui_e2e_matrix, "dispatch UI E2E matrix"))
.include,
).toHaveLength(13);
expect(
JSON.parse(
expectDefined(dispatchManifest.outputs.qa_smoke_ci_matrix, "dispatch QA smoke matrix"),
).include,
).toHaveLength(6);
});
it.each(["", "release/2026.9.1"])(
"honors trusted dispatch runner selection for check shards with context %j",
(targetContextRef) => {
const runsOn = readCiWorkflow().jobs["check-shard"]["runs-on"];
const lintMatrix = {
runner: "blacksmith-32vcpu-ubuntu-2404",
task: "lint",
};
const evaluateDispatch = (
runnerBackend: "blacksmith" | "github" | "hybrid",
overrides: {
dispatchId?: string;
frozenTarget?: boolean;
matrix?: Record<string, unknown>;
releaseGate?: boolean;
repository?: string;
targetContextRef?: string;
} = {},
) =>
evaluateWorkflowExpression(runsOn, {
eventName: "workflow_dispatch",
matrix: lintMatrix,
repository: "openclaw/openclaw",
runAttempt: 1,
runnerBackend,
...overrides,
});
expect(evaluateDispatch("blacksmith")).toBe("blacksmith-32vcpu-ubuntu-2404");
expect(evaluateDispatch("blacksmith", { releaseGate: true })).toBe("ubuntu-24.04");
expect(evaluateDispatch("github")).toBe("ubuntu-24.04");
expect(evaluateDispatch("hybrid")).toBe("ubuntu-24.04");
const frozenFrv = {
dispatchId: "full-release-validation-33128772779-ci",
frozenTarget: true,
targetContextRef,
};
expect(evaluateDispatch("hybrid", frozenFrv)).toBe("blacksmith-32vcpu-ubuntu-2404");
expect(evaluateDispatch("github", frozenFrv)).toBe("ubuntu-24.04");
expect(evaluateDispatch("hybrid", { ...frozenFrv, frozenTarget: false })).toBe(
"ubuntu-24.04",
);
expect(evaluateDispatch("hybrid", { ...frozenFrv, dispatchId: "manual-ci-proof" })).toBe(
"ubuntu-24.04",
);
expect(evaluateDispatch("hybrid", { ...frozenFrv, releaseGate: true })).toBe("ubuntu-24.04");
expect(
evaluateDispatch("hybrid", {
...frozenFrv,
matrix: { runner: "blacksmith-16vcpu-ubuntu-2404", task: "test-types" },
}),
).toBe("ubuntu-24.04");
expect(evaluateDispatch("hybrid", { ...frozenFrv, repository: "fork/openclaw" })).toBe(
"ubuntu-24.04",
);
expect(
evaluateWorkflowExpression(runsOn, {
authorAssociation: "NONE",
eventName: "pull_request",
headRepository: "openclaw/openclaw",
matrix: lintMatrix,
repository: "openclaw/openclaw",
runAttempt: 1,
runnerBackend: "blacksmith",
}),
).toBe("ubuntu-24.04");
},
);
it("encodes GitHub, Blacksmith, and hybrid runner-backend shapes", () => {
const workflow = readCiWorkflow();
const jobs = workflow.jobs as Record<string, { "runs-on": unknown }>;
const expectedHostedRunners = {
android: "ubuntu-24.04",
"build-artifacts": "ubuntu-24.04",
"check-additional-shard": "ubuntu-24.04",
"check-shard": "ubuntu-24.04",
"checks-fast-channel-contracts-shard": "ubuntu-24.04",
"checks-fast-core": "ubuntu-24.04",
"checks-fast-plugin-contracts-shard": "ubuntu-24.04",
"checks-node-compat": "ubuntu-24.04",
"checks-node-core-test-nondist-shard": "ubuntu-24.04",
"checks-ui": "ubuntu-24.04",
"checks-ui-e2e": "ubuntu-24.04",
"checks-ui-e2e-real-gateway": "ubuntu-24.04",
"control-ui-i18n": "ubuntu-24.04",
"control-ui-performance": "ubuntu-24.04",
"docker-seed-e2e": "ubuntu-24.04",
"macos-node": "macos-15",
"native-i18n": "ubuntu-24.04",
preflight: "ubuntu-24.04",
"security-fast": "ubuntu-24.04",
"qa-smoke-ci-profile": "ubuntu-24.04",
"skills-python": "ubuntu-24.04",
"check-test-types-hosted-core-shard": "ubuntu-24.04",
"checks-windows": "windows-2025",
} as const;
const expectedHybridFirstAttemptRunners = {
...expectedHostedRunners,
preflight: "blacksmith-16vcpu-ubuntu-2404",
"security-fast": "blacksmith-4vcpu-ubuntu-2404",
android: "blacksmith-8vcpu-ubuntu-2404",
"build-artifacts": "blacksmith-16vcpu-ubuntu-2404",
"checks-node-core-test-nondist-shard": "blacksmith-32vcpu-ubuntu-2404",
"checks-ui-e2e": "blacksmith-8vcpu-ubuntu-2404",
"checks-ui-e2e-real-gateway": "blacksmith-32vcpu-ubuntu-2404",
"docker-seed-e2e": "blacksmith-16vcpu-ubuntu-2404",
"qa-smoke-ci-profile": "blacksmith-16vcpu-ubuntu-2404",
"check-test-types-hosted-core-shard": "blacksmith-16vcpu-ubuntu-2404",
"checks-ui": "blacksmith-8vcpu-ubuntu-2404",
"checks-windows": "blacksmith-8vcpu-windows-2025",
} as const;
const expectedHybridForkRunners = {
...expectedHybridFirstAttemptRunners,
"docker-seed-e2e": "ubuntu-24.04",
} as const;
const configurableJobs = Object.entries(jobs)
.filter(([, job]) => String(job["runs-on"]).startsWith("${{"))
.map(([jobName]) => jobName)
.toSorted();
const canonicalPullRequest = {
eventName: "pull_request",
headRepository: "openclaw/openclaw",
matrix: { runner: "blacksmith-32vcpu-ubuntu-2404" },
repository: "openclaw/openclaw",
runAttempt: 1,
} as const;
expect(configurableJobs).toEqual(Object.keys(expectedHostedRunners).toSorted());
expect(jobs["check-lint-hosted-core-shard"]?.["runs-on"]).toBe("ubuntu-24.04");
// check-docs stays hosted in every mode: its ClawHub clone is unauthenticated by design.
expect(jobs["check-docs"]?.["runs-on"]).toBe("ubuntu-24.04");
for (const [jobName, hostedRunner] of Object.entries(expectedHostedRunners)) {
const expression = jobs[jobName]?.["runs-on"];
for (const [label, overrides, expectedRunner] of [
["github backend", { runnerBackend: "github" }, hostedRunner],
[
"hybrid first attempt",
{ runnerBackend: "hybrid" },
expectedHybridFirstAttemptRunners[jobName as keyof typeof expectedHostedRunners],
],
["hybrid retry", { runnerBackend: "hybrid", runAttempt: 2 }, hostedRunner],
[
"explicit Blacksmith matches default",
{ runnerBackend: "blacksmith" },
evaluateWorkflowExpression(expression, canonicalPullRequest),
],
// New contributors stay hosted. GitHub can also report maintainers as
// CONTRIBUTOR when organization membership is concealed.
[
"untrusted fork",
{
authorAssociation: "NONE",
headRepository: "contributor/openclaw",
runnerBackend: "hybrid",
},
hostedRunner,
],
[
"returning-contributor fork",
{
authorAssociation: "CONTRIBUTOR",
headRepository: "contributor/openclaw",
runnerBackend: "hybrid",
},
expectedHybridForkRunners[jobName as keyof typeof expectedHostedRunners],
],
] as const) {
expect(
evaluateWorkflowExpression(expression, { ...canonicalPullRequest, ...overrides }),
`${jobName}: ${label}`,
).toBe(expectedRunner);
}
for (const runnerBackend of ["", "blacksmith", "hybrid"] as const) {
expect(
evaluateWorkflowExpression(expression, {
...canonicalPullRequest,
authorAssociation: "CONTRIBUTOR",
headRepository: "contributor/openclaw",
runnerBackend,
runAttempt: 2,
}),
`${jobName}: returning-contributor fork retry (${runnerBackend || "unset"})`,
).toBe(hostedRunner);
}
}
const widenedHybridMatrixRows = [
...["lint", "test-types", "dependencies"].map((task) => ({
jobName: "check-shard",
matrix: { runner: "blacksmith-32vcpu-ubuntu-2404", task },
runner: "blacksmith-32vcpu-ubuntu-2404",
})),
...["extension-package-boundary", "runtime-topology-architecture"].map((group) => ({
jobName: "check-additional-shard",
matrix: { group, runner: "blacksmith-32vcpu-ubuntu-2404" },
runner: "blacksmith-32vcpu-ubuntu-2404",
})),
{
jobName: "check-additional-shard",
matrix: { group: "plugin-sdk-api-diff", runner: "blacksmith-4vcpu-ubuntu-2404" },
runner: "blacksmith-4vcpu-ubuntu-2404",
},
...[4, 8].map((size) => ({
jobName: "checks-node-core-test-nondist-shard",
matrix: { runner: `blacksmith-${size}vcpu-ubuntu-2404` },
runner: `blacksmith-${size}vcpu-ubuntu-2404`,
})),
// Preserve planner capacity for parallel, tooling and explicit large owners.
...["small-3", "small-4", "small-7", "small-10", "large-10", "large32-1"].map((bin) => ({
jobName: "checks-node-core-test-nondist-shard",
matrix: {
check_name: `checks-node-compact-${bin}`,
runner: "blacksmith-32vcpu-ubuntu-2404",
},
runner: "blacksmith-32vcpu-ubuntu-2404",
})),
...[
"changed-extensions-bundle-16",
"changed-extensions-bundle-25",
"compact-large-5",
"compact-large-9",
].map((bin) => ({
jobName: "checks-node-core-test-nondist-shard",
matrix: { check_name: `checks-node-${bin}`, runner: "blacksmith-8vcpu-ubuntu-2404" },
runner: "blacksmith-16vcpu-ubuntu-2404",
})),
] as const;
for (const { jobName, matrix, runner } of widenedHybridMatrixRows) {
const expression = jobs[jobName]?.["runs-on"];
for (const [label, overrides, expectedRunner] of [
["hybrid attempt 1", { runnerBackend: "hybrid" }, runner],
["hybrid main", { eventName: "push", runnerBackend: "hybrid" }, runner],
["Blacksmith main", { eventName: "push", runnerBackend: "blacksmith" }, runner],
["hybrid retry", { runnerBackend: "hybrid", runAttempt: 2 }, "ubuntu-24.04"],
["github backend", { runnerBackend: "github" }, "ubuntu-24.04"],
[
"untrusted fork pull request",
{
authorAssociation: "NONE",
headRepository: "contributor/openclaw",
runnerBackend: "hybrid",
},
"ubuntu-24.04",
],
[
"workflow dispatch",
{ eventName: "workflow_dispatch", runnerBackend: "hybrid" },
"ubuntu-24.04",
],
] as const) {
expect(
evaluateWorkflowExpression(expression, { ...canonicalPullRequest, matrix, ...overrides }),
`${jobName}: ${label}`,
).toBe(expectedRunner);
}
}
});
it("gives breaker-routed hosted jobs their hosted timeout budgets", () => {
const workflow = readCiWorkflow();
const jobs = workflow.jobs as Record<string, { "timeout-minutes": unknown }>;
const expectedHostedTimeouts = {
android: 35,
"build-artifacts": 35,
"checks-ui-e2e-real-gateway": 40,
} as const;
const routeDependentTimeoutJobs = Object.entries(jobs)
.filter(([, job]) => {
const timeout = job["timeout-minutes"];
return typeof timeout === "string" && timeout.includes("github.");
})
.map(([jobName]) => jobName)
.toSorted();
const canonicalPullRequest = {
eventName: "pull_request",
headRepository: "openclaw/openclaw",
matrix: { task: "build-play" },
repository: "openclaw/openclaw",
runAttempt: 1,
} as const;
const evaluateTimeout = (
jobName: string,
context: Parameters<typeof evaluateWorkflowExpression>[1],
) => {
const value = jobs[jobName]?.["timeout-minutes"];
return typeof value === "number" ? value : evaluateWorkflowExpression(value, context);
};
for (const [jobName, hostedTimeout] of Object.entries(expectedHostedTimeouts)) {
for (const [overrides, expectedTimeout] of [
[{ runnerBackend: "github" }, hostedTimeout],
[{ runnerBackend: "blacksmith" }, 20],
[{ runnerBackend: "hybrid" }, 20],
[{ runnerBackend: "hybrid", runAttempt: 2 }, hostedTimeout],
] as const) {
expect(evaluateTimeout(jobName, { ...canonicalPullRequest, ...overrides }), jobName).toBe(
expectedTimeout,
);
}
expect(jobs[jobName]?.["timeout-minutes"], jobName).toContain(
"vars.OPENCLAW_CI_RUNNER_BACKEND == 'github'",
);
}
expect(routeDependentTimeoutJobs).toEqual(Object.keys(expectedHostedTimeouts).toSorted());
const androidRoutes = [
["GitHub override", { runnerBackend: "github" }, "ubuntu-24.04"],
["hybrid retry", { runnerBackend: "hybrid", runAttempt: 2 }, "ubuntu-24.04"],
["manual dispatch", { eventName: "workflow_dispatch" }, "ubuntu-24.04"],
["non-canonical repository", { repository: "contributor/openclaw" }, "ubuntu-24.04"],
["untrusted author", { authorAssociation: "NONE" }, "ubuntu-24.04"],
[
"untrusted fork",
{ authorAssociation: "FIRST_TIME_CONTRIBUTOR", headRepository: "contributor/openclaw" },
"ubuntu-24.04",
],
[
"trusted fork first attempt",
{ headRepository: "contributor/openclaw" },
"blacksmith-8vcpu-ubuntu-2404",
],
[
"trusted fork retry",
{ headRepository: "contributor/openclaw", runAttempt: 2 },
"ubuntu-24.04",
],
["same-repository Blacksmith retry", { runAttempt: 2 }, "blacksmith-8vcpu-ubuntu-2404"],
] as const;
for (const [label, overrides, runner] of androidRoutes) {
const context = { ...canonicalPullRequest, ...overrides };
expect(evaluateWorkflowExpression(workflow.jobs.android["runs-on"], context), label).toBe(
runner,
);
for (const task of [
"build-play",
"build-play-compat",
"build-wear",
"ktlint",
"test-play",
"test-play-compat",
"test-third-party",
"test-wear",
]) {
for (const lint of [undefined, false, true]) {
const extendedBudget =
((task === "test-play" || task === "test-third-party") && lint === true) ||
(task === "build-play" && runner === "ubuntu-24.04");
expect(
evaluateTimeout("android", { ...context, matrix: { task, lint } }),
`${label}: ${task}, lint=${lint}`,
).toBe(extendedBudget ? 35 : 20);
}
}
}
const realGateway = workflow.jobs["checks-ui-e2e-real-gateway"];
for (const eventName of ["pull_request", "push", "workflow_dispatch"] as const) {
for (const repository of ["openclaw/openclaw", "contributor/openclaw"]) {
for (const authorAssociation of ["CONTRIBUTOR", "NONE"]) {
for (const runnerBackend of ["", "blacksmith", "github", "hybrid"] as const) {
for (const runAttempt of [1, 2]) {
const context = {
...canonicalPullRequest,
eventName,
repository,
authorAssociation,
runnerBackend,
runAttempt,
};
const runner = evaluateWorkflowExpression(realGateway["runs-on"], context);
expect(
evaluateTimeout("checks-ui-e2e-real-gateway", context),
JSON.stringify(context),
).toBe(runner === "ubuntu-24.04" ? 40 : 20);
}
}
}
}
}
});
it("resolves the pull request base and changed files from the shallow security checkout", () => {
const securitySteps = readCiWorkflow().jobs["security-fast"].steps as WorkflowStep[];
const checkoutIndex = securitySteps.findIndex((step) => step.name === "Checkout");
const checkout = expectDefined(securitySteps[checkoutIndex], "security checkout");
const root = tempDirs.make("openclaw-security-checkout-");
const depth = checkout.with?.["fetch-depth"];
expect(Number.isInteger(Number(depth)) && Number(depth) > 0).toBe(true);
expect(checkout.with?.["persist-credentials"]).toBe(false);
const source = path.join(root, "source");
const selected = path.join(root, "selected");
mkdirSync(source);
mkdirSync(selected);
const git = (cwd: string, ...args: string[]) =>
execFileSync(
"git",
[
"-C",
cwd,
"-c",
"user.name=CI Fixture",
"-c",
"user.email=ci@example.invalid",
"-c",
"commit.gpgsign=false",
...args,
],
{
encoding: "utf8",
timeout: 5_000,
env: {
...process.env,
GIT_ALLOW_PROTOCOL: "file",
GIT_CONFIG_GLOBAL: devNull,
GIT_CONFIG_NOSYSTEM: "1",
GIT_TERMINAL_PROMPT: "0",
},
},
).trim();
git(source, "init", "--initial-branch=main");
writeFileSync(path.join(source, "base.txt"), "base\n");
git(source, "add", ".");
git(source, "commit", "-m", "base");
git(source, "checkout", "-b", "pull-request");
for (let index = 0; index < 3; index++) {
writeFileSync(path.join(source, "change.txt"), `change ${index}\n`);
git(source, "add", ".");
git(source, "commit", "-m", `change ${index}`);
}
git(source, "checkout", "main");
writeFileSync(path.join(source, "base.txt"), "advanced base\n");
git(source, "commit", "-am", "advance main");
const base = git(source, "rev-parse", "HEAD");
git(source, "merge", "--no-ff", "pull-request", "-m", "synthetic merge");
const merge = git(source, "rev-parse", "HEAD");
git(selected, "init");
git(
selected,
"fetch",
"--no-tags",
`--depth=${String(depth)}`,
pathToFileURL(source).href,
merge,
);
git(selected, "checkout", "--detach", "FETCH_HEAD");
const resolveBase = expectDefined(
securitySteps.find((step) => step.id === "diff_base"),
"security diff base",
);
const output = path.join(root, "base-output");
const result = spawnSync("bash", ["-e", "-c", expectDefined(resolveBase.run, "base script")], {
cwd: selected,
encoding: "utf8",
timeout: 5_000,
env: {
...process.env,
GITHUB_EVENT_NAME: "pull_request",
EVENT_BASE_SHA: "stale-event-base",
GITHUB_OUTPUT: output,
},
});
expect(result.status, result.stderr).toBe(0);
expect(readFileSync(output, "utf8").trim()).toBe(`sha=${base}`);
expect(git(selected, "diff", "--name-only", base, "HEAD")).toBe("change.txt");
});
it("keeps setup cache access explicit and isolates every cache write", () => {
const setupActionPaths = [
".github/actions/setup-node-env/action.yml",
".github/actions/setup-pnpm-store-cache/action.yml",
];
const legacyInputs = [
"save-actions-cache",
"save-dependency-cache",
"save-node-compile-cache",
"save-vitest-fs-cache",
"use-actions-cache",
];
for (const actionPath of setupActionPaths) {
const action = parse(readFileSync(actionPath, "utf8"));
const steps = action.runs.steps as WorkflowStep[];
expect(action.inputs["cache-mode"].default, actionPath).toBe("off");
for (const legacyInput of legacyInputs) {
expect(action.inputs, `${actionPath}: ${legacyInput}`).not.toHaveProperty(legacyInput);
}
expect(
steps.filter(
(step) =>
step.uses?.startsWith("actions/cache@") || step.uses?.startsWith("actions/cache/save@"),
),
actionPath,
).toEqual([]);
expect(
steps.filter((step) => step.uses?.startsWith("actions/cache/restore@")).length,
actionPath,
).toBeGreaterThan(0);
const validation = expectDefined(
steps.find((step) => step.run?.includes("off|restore|read-write")),
`${actionPath} cache-mode validation`,
);
expect(validation.run).toContain("Invalid cache-mode input");
}
const callers: Array<{ file: string; mode: unknown; step: WorkflowStep }> = [];
const directCaches: Array<{
file: string;
step: WorkflowStep;
jobId?: string;
jobCondition?: string;
}> = [];
const rubySetups: Array<{ file: string; step: WorkflowStep }> = [];
for (const file of [
...findYamlFiles(".github/workflows"),
...findYamlFiles(".github/actions"),
]) {
const parsed = parse(readFileSync(file, "utf8"));
const stepLists = [
...Object.entries(parsed?.jobs ?? {}).map(([jobId, job]) => {
const owner = job as { steps?: WorkflowStep[]; if?: string };
return { jobId, jobCondition: owner.if, steps: owner.steps ?? [] };
}),
{
jobId: undefined,
jobCondition: undefined,
steps: (parsed?.runs?.steps ?? []) as WorkflowStep[],
},
];
for (const { jobId, jobCondition, steps } of stepLists) {
for (const step of steps) {
if (step.uses?.startsWith("actions/cache")) {
directCaches.push({ file, step, jobId, jobCondition });
}
if (step.uses?.startsWith("ruby/setup-ruby@")) {
rubySetups.push({ file, step });
}
if (
step.uses === "./.github/actions/setup-node-env" ||
step.uses?.endsWith("/.github/actions/setup-node-env") ||
step.uses === "./.github/actions/setup-pnpm-store-cache" ||
step.uses?.endsWith("/.github/actions/setup-pnpm-store-cache")
) {
callers.push({ file, mode: step.with?.["cache-mode"], step });
}
}
}
}
expect(rubySetups.length).toBeGreaterThan(0);
for (const { file, step } of rubySetups) {
const bundlerCache = step.with?.["bundler-cache"] ?? false;
expect([false, true, "false", "true"], `${file}: ${step.name}`).toContain(bundlerCache);
if (bundlerCache === true || bundlerCache === "true") {
expect(String(step.if), `${file}: ${step.name}`).toContain("cache_write_allowed == 'true'");
}
}
expect(callers.length).toBeGreaterThan(0);
for (const caller of callers) {
const staticMode = ["off", "restore", "read-write"].includes(String(caller.mode));
const conditionalMode =
typeof caller.mode === "string" &&
caller.mode.startsWith("${{") &&
(caller.mode.includes("needs.preflight.outputs.cache_mode") ||
caller.mode.includes("steps.candidate_trust.outputs.cache_mode") ||
(caller.mode.includes("'restore'") &&
(caller.mode.includes("'off'") || caller.mode.includes("'read-write'"))));
expect(staticMode || conditionalMode, `${caller.file}: ${caller.step.name}`).toBe(true);
for (const legacyInput of legacyInputs) {
expect(caller.step.with, `${caller.file}: ${legacyInput}`).not.toHaveProperty(legacyInput);
}
}
const writeAuthorizedCallers = callers.filter(
(caller) =>
caller.mode === "read-write" ||
(typeof caller.mode === "string" && caller.mode.includes("'read-write'")),
);
expect(writeAuthorizedCallers).toHaveLength(3);
expect(writeAuthorizedCallers).toEqual(
expect.arrayContaining([
{
file: ".github/workflows/ci-build-artifacts-testbox.yml",
mode: expect.stringContaining("'read-write'"),
step: expect.objectContaining({ name: "Setup Node environment" }),
},
{
file: ".github/workflows/openclaw-npm-preflight.yml",
mode: "read-write",
step: expect.objectContaining({ name: "Setup Node environment" }),
},
{
file: ".github/workflows/vitest-cache-warm.yml",
mode: "read-write",
step: expect.objectContaining({ name: "Setup Node environment" }),
},
]),
);
const nodeCachePathPattern =
/(?:^|\n)\s*(?:\.artifacts\/build-all-cache|dist\/|dist-runtime\/|packages\/\*\/dist\/|extensions\/\*\/dist\/|~\/\.cache\/ms-playwright|~\/\.local\/share\/pnpm|~\/\.cache\/pnpm|node_modules)(?:\n|$)/u;
for (const { file, step, jobId, jobCondition } of directCaches) {
if (step.uses?.startsWith("actions/cache/save@")) {
if (step.with?.path === "full-release-execution-plan") {
expect(file).toBe(".github/workflows/full-release-validation.yml");
expect(jobId).toBe("release_execution_plan");
expect(step.with).toEqual({
path: "full-release-execution-plan",
key: "full-release-execution-plan-v2-${{ github.run_id }}",
});
expect(step.if).toBe(
"${{ always() && github.run_attempt == 1 && steps.plan_witness.outcome == 'success' }}",
);
continue;
}
if (step.with?.path === ".cache/openclaw-cross-os-npm-cache/_cacache") {
expect([
".github/workflows/openclaw-cross-os-release-checks-reusable.yml",
".github/workflows/release-npm-cache-warm.yml",
]).toContain(file);
const authority = `${jobCondition ?? ""} ${step.if ?? ""}`;
expect(authority).toContain("github.repository == 'openclaw/openclaw'");
expect(authority).toContain("github.event_name == 'workflow_dispatch'");
continue;
}
const condition = String(step.if);
expect(
condition.includes(".outputs.cache-mode == 'read-write'") ||
condition.includes("inputs.cache-mode == 'read-write'") ||
condition.includes("needs.preflight.outputs.cache_write_allowed == 'true'"),
`${file}: ${step.name}`,
).toBe(true);
}
if (step.uses?.startsWith("actions/cache@")) {
expect(nodeCachePathPattern.test(String(step.with?.path)), `${file}: ${step.name}`).toBe(
false,
);
}
}
});
it("owns one exact immutable semantic dependency cache", () => {
const actionSource = readFileSync(".github/actions/setup-node-env/action.yml", "utf8");
const ciSource = readFileSync(".github/workflows/ci.yml", "utf8");
const action = parse(actionSource);
const workflow = parse(ciSource);
const actionSteps = action.runs.steps as WorkflowStep[];
const step = (name: string) =>
expectDefined(
actionSteps.find((candidate) => candidate.name === name),
name,
);
const configureStore = step("Configure dependency cache store");
const resolve = step("Resolve dependency cache key");
const prepare = step("Prepare dependency cache restore");
const restore = step("Restore exact dependency cache");
const prepareFallback = step("Prepare dependency cache miss fallback");
const setupPnpm = step("Setup pnpm");
const install = step("Install dependencies");
const installScript = readFileSync(
".github/actions/setup-node-env/install-dependencies.sh",
"utf8",
);
const cachePaths =
"node_modules\nui/node_modules\npackages/*/node_modules\nextensions/*/node_modules\nexamples/*/node_modules\n.cache/openclaw-pnpm-store\n";
expect(action.inputs["cache-mode"].default).toBe("off");
expect(action.inputs["dependency-cache"].default).toBe("false");
expect(action.inputs).not.toHaveProperty("save-dependency-cache");
expect(action.inputs).not.toHaveProperty("save-actions-cache");
expect(action.inputs).not.toHaveProperty("use-actions-cache");
expect(action.inputs).not.toHaveProperty("sticky-disk");
expect(action.inputs).not.toHaveProperty("save-sticky-disk");
expect(actionSource).not.toContain("useblacksmith/stickydisk");
for (const mode of ["off", "restore", "read-write"]) {
for (const exact of ["false", "true"]) {
expect(
runInNewContext(
expectDefined(configureStore.if, "store configuration condition").replace(
/inputs\.([a-z-]+)/gu,
'inputs["$1"]',
),
{ inputs: { "cache-mode": mode, "dependency-cache": exact }, runner: { os: "Linux" } },
),
`store-only and exact consumers share the publisher path: ${mode}/${exact}`,
).toBe(mode !== "off");
}
}
expect(configureStore.run).toContain(
'echo "PNPM_CONFIG_STORE_DIR=$GITHUB_WORKSPACE/.cache/openclaw-pnpm-store"',
);
expect(resolve.if).toBe("inputs.cache-mode != 'off' && inputs.dependency-cache == 'true'");
expect(resolve.run).toContain('node "$GITHUB_ACTION_PATH/dependency-fingerprint.mjs"');
expect(resolve.run).toContain("${GITHUB_REPOSITORY:?}-node-deps-v4");
expect(resolve.run).toContain("${RUNNER_OS:?}-arch-${RUNNER_ARCH:?}");
expect(resolve.run).toContain("node-$(node --version)-${deps_input_fingerprint:?}");
expect(resolve.run).not.toMatch(/GITHUB_(?:REF|SHA|RUN_ID)|RUN_(?:ID|ATTEMPT)/u);
expect(actionSteps.indexOf(resolve)).toBeLessThan(actionSteps.indexOf(restore));
for (const cleanup of [prepare, prepareFallback]) {
expect(cleanup.run).toContain('rm -rf "$GITHUB_WORKSPACE/node_modules"');
expect(cleanup.run).toContain('"$GITHUB_WORKSPACE/.cache/openclaw-pnpm-store"');
expect(cleanup.run).toContain('"$GITHUB_WORKSPACE/packages"');
expect(cleanup.run).toContain("-name node_modules");
}
expect(actionSteps.indexOf(prepare)).toBeLessThan(actionSteps.indexOf(restore));
expect(restore).toMatchObject({
if: "inputs.cache-mode != 'off' && inputs.dependency-cache == 'true'",
uses: CACHE_V5,
with: { key: "${{ steps.dependency-cache-key.outputs.key }}", path: cachePaths },
});
expect((restore as WorkflowStep & { "continue-on-error"?: boolean })["continue-on-error"]).toBe(
true,
);
expect(restore.with).not.toHaveProperty("restore-keys");
expect(prepareFallback.if).toContain("steps.dependency-cache.outputs.cache-hit != 'true'");
expect(prepareFallback.run).toContain(
"actions/cache treats service, download, and extraction failures as",
);
expect(actionSteps.indexOf(restore)).toBeLessThan(actionSteps.indexOf(prepareFallback));
expect(actionSteps.indexOf(prepareFallback)).toBeLessThan(actionSteps.indexOf(setupPnpm));
expect(setupPnpm.with?.["cache-mode"]).toContain(
"steps.dependency-cache.outputs.cache-hit != 'true'",
);
expect(setupPnpm.with?.["cache-mode"]).toContain("inputs.cache-mode != 'off'");
expect(setupPnpm.with?.["cache-mode"]).toContain("'restore' || 'off'");
expect(actionSteps.indexOf(restore)).toBeLessThan(actionSteps.indexOf(setupPnpm));
expect(install.run).toBe('bash "$GITHUB_ACTION_PATH/install-dependencies.sh"');
expect(installScript).toContain("export PNPM_CONFIG_PACKAGE_IMPORT_METHOD=hardlink");
expect(installScript).toContain("run_pnpm_install --offline");
expect(installScript).toContain("run_pnpm_install --prefer-offline");
expect(installScript).toContain('[ "$DEPENDENCY_CACHE_HIT" = "true" ]');
expect(installScript).toContain('rm -rf "$GITHUB_WORKSPACE/node_modules"');
expect(installScript).toContain('"$GITHUB_WORKSPACE/packages"');
expect(installScript).toContain("-name node_modules");
expect(installScript).toContain('"${PNPM_CONFIG_STORE_DIR:?}"');
expect(installScript.match(/run_pnpm_install/g)).toHaveLength(5);
expect(installScript).toContain('echo "OPENCLAW_BUILD_ALL_NO_PNPM=1" >> "$GITHUB_ENV"');
expect(installScript).toContain(
'echo "pnpm_config_verify_deps_before_run=false" >> "$GITHUB_ENV"',
);
expect(
actionSteps.some(
(candidate) =>
candidate.uses?.startsWith("actions/cache@") ||
candidate.uses?.startsWith("actions/cache/save@"),
),
).toBe(false);
const dependencySetups = Object.entries(workflow.jobs).flatMap(([jobName, job]) =>
((job as { steps?: WorkflowStep[] }).steps ?? []).flatMap((candidate) =>
candidate.uses?.endsWith("/.github/actions/setup-node-env") &&
candidate.with?.["dependency-cache"] !== undefined
? [{ jobName, step: candidate }]
: [],
),
);
const preflightRestore = dependencySetups.find(({ jobName }) => jobName === "preflight");
expect(preflightRestore?.step).toMatchObject({
if: expect.stringContaining("steps.manifest.outputs.run_node == 'true'"),
with: {
"cache-mode": "${{ steps.candidate_trust.outputs.cache_mode }}",
"dependency-cache": "true",
"install-bun": "false",
},
});
expect(preflightRestore?.step.if).toContain("github.ref == 'refs/heads/main'");
expect(preflightRestore?.step.if).toContain("github.event_name == 'pull_request'");
expect(preflightRestore?.step.if).toContain("vars.OPENCLAW_CI_RUNNER_BACKEND != 'github'");
expect(preflightRestore?.step.if).toContain("vars.OPENCLAW_CI_RUNNER_BACKEND != 'hybrid'");
const consumers = dependencySetups.filter(({ jobName }) => jobName !== "preflight");
expect(consumers.map(({ jobName }) => jobName).toSorted()).toEqual([
"build-artifacts",
"check-additional-shard",
"check-docs",
"check-lint-hosted-core-shard",
"check-shard",
"check-test-types-hosted-core-shard",
"checks-fast-channel-contracts-shard",
"checks-fast-core",
"checks-fast-plugin-contracts-shard",
"checks-node-core-test-nondist-shard",
"checks-ui",
"checks-ui-e2e",
"checks-ui-e2e-real-gateway",
"control-ui-i18n",
"control-ui-performance",
"docker-seed-e2e",
"native-i18n",
"qa-smoke-ci-profile",
]);
for (const { jobName, step: consumer } of consumers) {
const needs = workflow.jobs[jobName].needs;
expect(Array.isArray(needs) ? needs : [needs], jobName).toContain("preflight");
expect(consumer.with, jobName).not.toHaveProperty("save-dependency-cache");
expect(consumer.with?.["dependency-cache"], jobName).toContain("'true' || 'false'");
expect(consumer.with?.["cache-mode"], jobName).toBe(
"${{ needs.preflight.outputs.cache_mode }}",
);
const canonical = {
eventName: "push",
matrix: {
group: "extension-package-boundary",
node_version: "24.x",
runner: "blacksmith-32vcpu-ubuntu-2404",
task: "lint",
},
repository: "openclaw/openclaw",
runAttempt: 1,
} as const;
const scenarios = [
{ eventName: "push", trusted: true },
{ eventName: "pull_request", headRepository: "openclaw/openclaw", trusted: true },
{ eventName: "pull_request", headRepository: "contributor/openclaw", trusted: false },
{
eventName: "pull_request",
headRepository: "contributor/openclaw",
authorAssociation: "NONE",
trusted: false,
},
{ eventName: "workflow_dispatch", trusted: false },
{ eventName: "push", repository: "contributor/openclaw", trusted: false },
] as const;
for (const runnerBackend of ["", "blacksmith", "github", "hybrid"] as const) {
for (const runAttempt of [1, 2]) {
for (const { trusted, ...scenario } of scenarios) {
const context = { ...canonical, ...scenario, runnerBackend, runAttempt };
const runsOn = workflow.jobs[jobName]["runs-on"] as string;
const routedRunner = runsOn.startsWith("${{")
? evaluateWorkflowExpression(runsOn, context)
: runsOn;
const selfHosted = String(routedRunner).startsWith("blacksmith-");
expect(
evaluateWorkflowExpression(consumer.with?.["dependency-cache"], {
...context,
runnerEnvironment: selfHosted ? "self-hosted" : "github-hosted",
}),
`${jobName} ${JSON.stringify(context)} on ${routedRunner}`,
).toBe(trusted && selfHosted ? "true" : "false");
}
}
}
// The actual runner must fence restores even when the configured backend
// still names Blacksmith or hybrid (including hosted retry routing).
for (const runnerEnvironment of ["", "github-hosted"] as const) {
expect(
evaluateWorkflowExpression(consumer.with?.["dependency-cache"], {
...canonical,
runnerBackend: "hybrid",
runnerEnvironment,
}),
`${jobName} actual runner ${runnerEnvironment}`,
).toBe("false");
}
if (jobName === "checks-node-core-test-nondist-shard") {
expect(
evaluateWorkflowExpression(consumer.with?.["dependency-cache"], {
...canonical,
matrix: { ...canonical.matrix, node_version: "22.x" },
runnerBackend: "hybrid",
runnerEnvironment: "self-hosted",
}),
).toBe("false");
}
}
for (const { jobName: setupJobName, step: setup } of Object.entries(workflow.jobs).flatMap(
([jobName, job]) =>
((job as { steps?: WorkflowStep[] }).steps ?? [])
.filter((candidate) => candidate.uses?.endsWith("/.github/actions/setup-node-env"))
.map((candidate) => ({ jobName, step: candidate })),
)) {
expect(setup.with, setupJobName).not.toHaveProperty("sticky-disk");
expect(setup.with, setupJobName).not.toHaveProperty("save-sticky-disk");
expect(
[
"off",
"restore",
"read-write",
"${{ needs.preflight.outputs.cache_mode }}",
"${{ steps.candidate_trust.outputs.cache_mode }}",
],
setupJobName,
).toContain(setup.with?.["cache-mode"]);
}
const warmer = parse(readFileSync(".github/workflows/vitest-cache-warm.yml", "utf8"));
const dependencySave = warmer.jobs.warm.steps.find(
(candidate: WorkflowStep) => candidate.name === "Save exact dependency cache",
);
expect(dependencySave).toMatchObject({
uses: "actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9",
with: {
key: "${{ steps.setup-node-env.outputs.dependency-cache-key }}",
path: cachePaths,
},
});
expect(dependencySave.if).toContain("steps.setup-node-env.outputs.cache-mode == 'read-write'");
});
it.skipIf(process.platform === "win32").each([
{
name: "uncached frozen",
cache: false,
frozen: "true",
exits: [0],
modes: ["--prefer-offline"],
status: 0,
},
{
name: "uncached mutable",
cache: false,
frozen: "false",
exits: [0],
modes: ["--prefer-offline"],
status: 0,
},
{
name: "invalid frozen policy",
cache: false,
frozen: "invalid",
exits: [],
modes: [],
status: 2,
},
{
name: "uncached failure",
cache: false,
frozen: "true",
exits: [23],
modes: ["--prefer-offline"],
status: 23,
},
{
name: "cached success",
cache: true,
frozen: "true",
exits: [0],
modes: ["--offline"],
status: 0,
},
{
name: "cached relink",
cache: true,
frozen: "true",
exits: [23, 0],
modes: ["--offline", "--offline"],
status: 0,
},
{
name: "cached store rebuild",
cache: true,
frozen: "true",
exits: [23, 23, 0],
modes: ["--offline", "--offline", "--prefer-offline"],
status: 0,
},
{
name: "cached terminal failure",
cache: true,
frozen: "true",
exits: [23, 23, 23],
modes: ["--offline", "--offline", "--prefer-offline"],
status: 23,
},
])("executes the dependency install recipe: $name", ({ cache, frozen, exits, modes, status }) => {
const root = tempDirs.make("openclaw-install-recipe-");
const workspace = path.join(root, "workspace");
const bin = path.join(root, "bin");
const store = path.join(root, "store");
const log = path.join(root, "calls.jsonl");
const githubEnv = path.join(root, "github.env");
const payload = path.join(root, "payload");
for (const directory of [
bin,
store,
...["", "ui", "packages", "extensions", "examples"].map((entry) =>
path.join(workspace, entry),
),
]) {
mkdirSync(directory, { recursive: true });
}
mkdirSync(path.join(workspace, "node_modules"));
writeFileSync(path.join(workspace, "node_modules", "before"), "");
writeFileSync(path.join(store, "before"), "");
mkdirSync(path.join(store, "toolchain"));
writeFileSync(path.join(store, "toolchain", "pnpm.tgz"), "authenticated archive");
symlinkSync(testNodeExecPath, path.join(bin, "node"));
const pnpm = path.join(bin, "pnpm");
writeFileSync(
pnpm,
"#!" +
testNodeExecPath +
"\n" +
String.raw`
const fs = require("node:fs");
const args = process.argv.slice(2);
if (args[0] === "-v") { console.log("fixture"); process.exit(0); }
const log = process.env.RECIPE_LOG;
const count = fs.existsSync(log) ? fs.readFileSync(log, "utf8").trim().split("\n").length : 0;
fs.appendFileSync(log, JSON.stringify({ args, cwd: process.cwd(), importMethod: process.env.PNPM_CONFIG_PACKAGE_IMPORT_METHOD }) + "\n");
process.exit(JSON.parse(process.env.RECIPE_EXITS)[count] ?? 99);
`,
);
chmodSync(pnpm, 0o755);
const action = parse(readFileSync(".github/actions/setup-node-env/action.yml", "utf8"));
const step: WorkflowStep = expectDefined(
action.runs.steps.find(
(candidate: WorkflowStep) => candidate.name === "Install dependencies",
),
"Install dependencies",
);
const run = expectDefined(step.run, "Install dependencies script");
const config = {
PNPM_CONFIG_CACHE_DIR: path.join(root, "metadata"),
PNPM_CONFIG_CHILD_CONCURRENCY: "3",
PNPM_CONFIG_NETWORK_CONCURRENCY: "4",
PNPM_CONFIG_PACKAGE_IMPORT_METHOD: "copy",
PNPM_CONFIG_STORE_DIR: store,
PNPM_CONFIG_VIRTUAL_STORE_DIR: path.join(root, "virtual"),
};
const result = spawnSync(
"bash",
["-c", run.trimEnd() + ' && printf reached > "$RECIPE_PAYLOAD"'],
{
cwd: workspace,
encoding: "utf8",
env: {
PATH: process.env.PATH,
NODE_BIN: bin,
GITHUB_ACTION_PATH: path.resolve(".github/actions/setup-node-env"),
GITHUB_WORKSPACE: workspace,
GITHUB_ENV: githubEnv,
CI: "true",
DEPENDENCY_CACHE: String(cache),
DEPENDENCY_CACHE_HIT: String(cache),
FROZEN_LOCKFILE: frozen,
RECIPE_LOG: log,
RECIPE_PAYLOAD: payload,
RECIPE_EXITS: JSON.stringify(exits),
...config,
},
},
);
expect(result.error).toBeUndefined();
expect(result.status, result.stdout + result.stderr).toBe(status);
expect(existsSync(payload)).toBe(status === 0);
const calls: Array<{ args: string[]; cwd: string; importMethod: string }> = existsSync(log)
? readFileSync(log, "utf8")
.trim()
.split("\n")
.map((line) => JSON.parse(line))
: [];
const expectedArgs = [
"install",
"--config.ignore-scripts=false",
"--config.engine-strict=false",
"--config.enable-pre-post-scripts=true",
"--config.side-effects-cache=true",
...(frozen === "true" ? ["--frozen-lockfile"] : []),
"--config.cache-dir=" + config.PNPM_CONFIG_CACHE_DIR,
"--config.child-concurrency=3",
"--config.network-concurrency=4",
"--config.package-import-method=" + (cache ? "hardlink" : "copy"),
"--config.store-dir=" + store,
"--config.virtual-store-dir=" + config.PNPM_CONFIG_VIRTUAL_STORE_DIR,
];
expect(calls).toEqual(
modes.map((mode) => ({
args: [...expectedArgs, mode],
cwd: workspace,
importMethod: cache ? "hardlink" : "copy",
})),
);
expect(existsSync(path.join(workspace, "node_modules", "before"))).toBe(modes.length < 2);
expect(existsSync(path.join(store, "before"))).toBe(modes.length < 3);
expect(readFileSync(path.join(store, "toolchain", "pnpm.tgz"), "utf8")).toBe(
"authenticated archive",
);
expect(existsSync(githubEnv)).toBe(cache && status === 0);
if (cache && status === 0) {
expect(readFileSync(githubEnv, "utf8")).toBe(
"OPENCLAW_BUILD_ALL_NO_PNPM=1\npnpm_config_verify_deps_before_run=false\n",
);
}
});
it.skipIf(process.platform === "win32")(
"preserves pnpm hard links and validates cached importers and supply-chain policy offline",
async ({ onTestFinished, signal }) => {
const fixtureDirs = createTempDirTracker();
// oxlint-disable-next-line prefer-const -- Failure cleanup can run before the registry is started.
let stopRegistry: (() => Promise<void>) | undefined;
let readyTimeout: NodeJS.Timeout | undefined;
// Timeout does not join the test body. Keep close and deletion in one hook,
// outside afterEach, so a failed join cannot release the registry's files.
onTestFinished(async () => {
clearTimeout(readyTimeout);
await stopRegistry?.();
fixtureDirs.cleanup();
});
const root = fixtureDirs.make("openclaw-dependency-cache-");
const source = path.join(root, "source");
const registry = path.join(root, "registry");
const workspace = path.join(root, "workspace");
const consumer = path.join(workspace, "packages", "consumer");
const store = path.join(workspace, ".cache", "openclaw-pnpm-store");
let userHome = path.join(root, "producer-home");
mkdirSync(userHome, { recursive: true });
mkdirSync(source, { recursive: true });
mkdirSync(registry, { recursive: true });
mkdirSync(consumer, { recursive: true });
writeFileSync(
path.join(source, "package.json"),
JSON.stringify({
files: ["index.js"],
name: "cache-proof-dep",
packageManager: rootPackageManager,
scripts: { "pnpm-path": "node -p process.env.npm_execpath" },
version: "1.0.0",
}),
);
writeFileSync(path.join(source, "index.js"), 'module.exports = "cache-proof-v1";\n');
// Both projects own the pinned environment before any command runs; otherwise
// pnpm resolves its own metadata from the public registry during bootstrap.
const { environment } = pnpmLockfileDocuments(readFileSync("pnpm-lock.yaml", "utf8"));
if (environment !== null) {
for (const directory of [source, workspace]) {
writeFileSync(path.join(directory, "pnpm-lock.yaml"), `---\n${environment}\n---\n`);
}
}
// Capture the pinned CLI before switching to the fixture-only registry/store.
const nodeExecPath = resolveTestNodeExecPath();
const bootstrap = resolvePnpmRunner({ nodeExecPath });
const npmExecPath = execFileSync(
bootstrap.command,
[...bootstrap.args, "--silent", "run", "pnpm-path"],
{ cwd: source, encoding: "utf8", env: { ...process.env, CI: "true" } },
).trim();
const pnpm = resolvePnpmRunner({ nodeExecPath, npmExecPath });
const action = parse(readFileSync(".github/actions/setup-node-env/action.yml", "utf8"));
const configureCache = expectDefined(
action.runs.steps.find(
(step: WorkflowStep) => step.name === "Configure dependency cache store",
)?.run,
"Configure dependency cache store script",
);
const envFile = path.join(root, "dependency-cache.env");
execFileSync("bash", ["-c", configureCache], {
env: { ...process.env, GITHUB_WORKSPACE: workspace, GITHUB_ENV: envFile },
});
const dependencyEnvironment = Object.fromEntries(
readFileSync(envFile, "utf8")
.trim()
.split("\n")
.map((line) => {
const separator = line.indexOf("=");
return [line.slice(0, separator), line.slice(separator + 1)];
}),
);
const runPnpm = (args: string[], cwd: string) =>
spawnSync(pnpm.command, [...pnpm.args, ...args], {
cwd,
encoding: "utf8",
env: {
PATH: process.env.PATH,
HOME: userHome,
XDG_CACHE_HOME: path.join(userHome, ".cache"),
CI: "true",
PNPM_CONFIG_PACKAGE_IMPORT_METHOD: "hardlink",
...dependencyEnvironment,
},
});
const version = runPnpm(["--version"], source);
expect(version.status, version.stderr).toBe(0);
expect(`pnpm@${version.stdout.trim()}`).toBe(rootPackageManager.split("+")[0]);
const packed = runPnpm(["pack", "--pack-destination", registry], source);
expect(packed.status, `${packed.stdout}${packed.stderr}`).toBe(0);
const tarball = path.join(registry, "cache-proof-dep-1.0.0.tgz");
const registryScript = String.raw`
const { createHash } = require("node:crypto");
const { readFileSync } = require("node:fs");
const { createServer } = require("node:http");
const tarballPath = process.argv[1];
const tarball = readFileSync(tarballPath);
const server = createServer((request, response) => {
if (request.url === "/cache-proof-dep") {
const port = server.address().port;
const metadata = {
name: "cache-proof-dep",
"dist-tags": { latest: "1.0.0" },
time: {
"1.0.0": new Date(Date.now() - 14 * 24 * 60 * 60 * 1000).toISOString(),
modified: new Date().toISOString(),
},
versions: {
"1.0.0": {
name: "cache-proof-dep",
version: "1.0.0",
dist: {
tarball: "http://127.0.0.1:" + port + "/cache-proof-dep-1.0.0.tgz",
shasum: createHash("sha1").update(tarball).digest("hex"),
integrity: "sha512-" + createHash("sha512").update(tarball).digest("base64"),
},
},
},
};
const abbreviated = request.headers.accept?.includes("application/vnd.npm.install-v1+json");
if (abbreviated) {
delete metadata.time;
}
response.setHeader("content-type", abbreviated ? "application/vnd.npm.install-v1+json" : "application/json");
response.end(JSON.stringify(metadata));
return;
}
if (request.url === "/cache-proof-dep-1.0.0.tgz") {
response.setHeader("content-type", "application/octet-stream");
response.end(tarball);
return;
}
response.statusCode = 404;
response.end();
});
server.listen(0, "127.0.0.1", () => {
process.send(server.address().port);
});
`;
const registryServer = spawn(process.execPath, ["-e", registryScript, tarball], {
stdio: ["ignore", "ignore", "ignore", "ipc"],
});
let registryDidClose = false;
// Retain actual close from launch, including failed spawn; readiness must not own this join.
const registryClosed = new Promise<void>((resolve) => {
registryServer.once("close", () => {
registryDidClose = true;
resolve();
});
});
const failures: unknown[] = [];
registryServer.on("error", (error) => failures.push(error));
stopRegistry = async () => {
if (!registryDidClose) {
registryServer.kill("SIGTERM");
}
await registryClosed;
};
try {
const port = await new Promise<number>((resolve, reject) => {
readyTimeout = setTimeout(() => reject(new Error("fixture registry not ready")), 2_000);
registryServer.once("message", (message) => {
if (typeof message !== "number") {
reject(new Error("fixture registry sent an invalid port"));
return;
}
resolve(message);
});
registryServer.once("error", reject);
void registryClosed.then(() => reject(new Error("fixture registry closed before ready")));
});
clearTimeout(readyTimeout);
signal.throwIfAborted();
const registryUrl = `http://127.0.0.1:${port}`;
writeFileSync(
path.join(workspace, "package.json"),
JSON.stringify({
dependencies: { "cache-proof-dep": "1.0.0" },
name: "cache-proof-root",
packageManager: rootPackageManager,
private: true,
}),
);
const workspaceConfig =
"packages:\n - packages/*\nminimumReleaseAge: 10080\nminimumReleaseAgeStrict: true\n";
writeFileSync(path.join(workspace, "pnpm-workspace.yaml"), workspaceConfig);
const writeConsumerManifest = (dependencyVersion: string) =>
writeFileSync(
path.join(consumer, "package.json"),
JSON.stringify({
dependencies: { "cache-proof-dep": dependencyVersion },
name: "cache-proof-consumer",
private: true,
}),
);
writeConsumerManifest("1.0.0");
// The fixture registry serves only its test package, not the preserved project pnpm pin.
const installArgs = [
"install",
"--ignore-scripts",
"--config.engine-strict=false",
"--pm-on-fail=ignore",
];
const onlineArgs = [...installArgs, `--registry=${registryUrl}`];
const seeded = runPnpm([...onlineArgs, "--lockfile-only"], workspace);
expect(seeded.status, `${seeded.stdout}${seeded.stderr}`).toBe(0);
// CI publishes a frozen install, without the lockfile generator's caches.
rmSync(userHome, { force: true, recursive: true });
rmSync(store, { force: true, recursive: true });
mkdirSync(userHome, { recursive: true });
const installed = runPnpm([...onlineArgs, "--frozen-lockfile"], workspace);
expect(installed.status, `${installed.stdout}${installed.stderr}`).toBe(0);
const findSameFile = (directory: string, referencePath: string): string | undefined => {
const reference = statSync(referencePath);
for (const entry of readdirSync(directory, { withFileTypes: true })) {
const entryPath = path.join(directory, entry.name);
if (entry.isDirectory()) {
const nested = findSameFile(entryPath, referencePath);
if (nested) {
return nested;
}
} else if (entry.isFile()) {
const candidate = statSync(entryPath);
if (candidate.dev === reference.dev && candidate.ino === reference.ino) {
return entryPath;
}
}
}
return undefined;
};
const rootPackageFile = path.join(workspace, "node_modules", "cache-proof-dep", "index.js");
expect(findSameFile(store, rootPackageFile)).toBeDefined();
const archive = path.join(root, "dependency-cache.tar");
execFileSync(
"tar",
[
"-cf",
archive,
"-C",
workspace,
"node_modules",
"packages/consumer/node_modules",
".cache/openclaw-pnpm-store",
],
{ stdio: "pipe" },
);
rmSync(path.join(workspace, "node_modules"), { force: true, recursive: true });
rmSync(path.join(consumer, "node_modules"), { force: true, recursive: true });
rmSync(store, { force: true, recursive: true });
rmSync(userHome, { force: true, recursive: true });
userHome = path.join(root, "consumer-home");
mkdirSync(userHome, { recursive: true });
execFileSync("tar", ["-xf", archive, "-C", workspace], { stdio: "pipe" });
const restoredPackageFile = path.join(
workspace,
"node_modules",
"cache-proof-dep",
"index.js",
);
expect(findSameFile(store, restoredPackageFile)).toBeDefined();
expect(
readFileSync(path.join(consumer, "node_modules", "cache-proof-dep", "index.js"), "utf8"),
).toBe('module.exports = "cache-proof-v1";\n');
await stopRegistry();
signal.throwIfAborted();
expect(registryDidClose, "registry closed before source deletion/offline install").toBe(
true,
);
await expect(
new Promise<void>((resolve, reject) => {
const socket = connect({ host: "127.0.0.1", port, signal });
socket.once("error", reject);
socket.once("connect", () => {
socket.destroy();
resolve();
});
}),
).rejects.toMatchObject({ code: "ECONNREFUSED" });
signal.throwIfAborted();
rmSync(registry, { force: true, recursive: true });
const cachedIdentity = statSync(restoredPackageFile);
const cachedLockfile = readFileSync(path.join(workspace, "pnpm-lock.yaml"), "utf8");
const offlineArgs = [...onlineArgs, "--offline", "--frozen-lockfile"];
const reconciliation = runPnpm(offlineArgs, workspace);
expect(reconciliation.status, `${reconciliation.stdout}${reconciliation.stderr}`).toBe(0);
expect(statSync(restoredPackageFile)).toMatchObject({
dev: cachedIdentity.dev,
ino: cachedIdentity.ino,
});
expect(readFileSync(path.join(workspace, "pnpm-lock.yaml"), "utf8")).toBe(cachedLockfile);
expect(
readFileSync(path.join(consumer, "node_modules", "cache-proof-dep", "index.js"), "utf8"),
).toBe('module.exports = "cache-proof-v1";\n');
// A stricter policy invalidates pnpm's saved verification and reads the
// restored registry metadata. A 14-day-old release fails a 21-day gate.
writeFileSync(
path.join(workspace, "pnpm-workspace.yaml"),
workspaceConfig.replace("minimumReleaseAge: 10080", "minimumReleaseAge: 30240"),
);
const stricterPolicy = runPnpm(offlineArgs, workspace);
expect(stricterPolicy.status).toBe(1);
expect(`${stricterPolicy.stdout}${stricterPolicy.stderr}`).toContain(
"ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION",
);
writeFileSync(path.join(workspace, "pnpm-workspace.yaml"), workspaceConfig);
writeConsumerManifest("2.0.0");
const drift = runPnpm(offlineArgs, workspace);
expect(drift.status).toBe(1);
expect(`${drift.stdout}${drift.stderr}`).toContain('Cannot install with "frozen-lockfile"');
expect(`${drift.stdout}${drift.stderr}`).toContain('in importers["packages/consumer"]');
expect(`${drift.stdout}${drift.stderr}`).toContain(
"cache-proof-dep (lockfile: 1.0.0, manifest: 2.0.0)",
);
} catch (error) {
if (failures[0] !== error) {
failures.unshift(error);
}
} finally {
clearTimeout(readyTimeout);
try {
await stopRegistry();
} catch (error) {
failures.push(error);
}
}
if (failures.length === 1) {
throw failures[0];
}
if (failures.length > 1) {
throw new AggregateError(failures, "dependency cache fixture failed");
}
},
);
it("persists content-validated public full-build declarations", () => {
const action = parse(readFileSync(".github/actions/setup-node-env/action.yml", "utf8"));
const installStep = action.runs.steps.find(
(step: WorkflowStep) => step.name === "Install dependencies",
);
const cacheStep = action.runs.steps.find(
(step: WorkflowStep) => step.name === "Restore build-all cache",
);
expect(action.inputs["build-all-cache-scope"].default).toBe("");
expect(cacheStep).toMatchObject({
if: "inputs.cache-mode != 'off' && inputs.build-all-cache-scope != ''",
uses: CACHE_V5,
with: { path: ".artifacts/build-all-cache" },
});
expect(cacheStep.with.key).toContain("build-all-v1-${{ inputs.build-all-cache-scope }}");
expect(cacheStep.with.key).toContain("${{ runner.os }}-${{ runner.arch }}");
const renderCacheKey = (template: string, runId: number, runAttempt: number) =>
template.replace(/\$\{\{([\s\S]*?)\}\}/gu, (_, expression: string) =>
String(
runInNewContext(expression.replace(/inputs\.([a-z-]+)/gu, 'inputs["$1"]'), {
github: { repository: "openclaw/openclaw", run_id: runId, run_attempt: runAttempt },
inputs: { "build-all-cache-scope": "full", "node-version": "24.x" },
runner: { os: "Linux", arch: "X64" },
hashFiles: () => "unchanged-source",
}),
),
);
// A new warmer or rerun must publish rebuilt groups even when an outer input
// fingerprint would be unchanged; per-group signatures own content validity.
const keys = (
[
[10, 1],
[11, 1],
[11, 2],
] as const
).map(([runId, runAttempt]) => renderCacheKey(cacheStep.with.key, runId, runAttempt));
expect(new Set(keys).size).toBe(3);
for (const key of keys) {
expect(key.startsWith(renderCacheKey(cacheStep.with["restore-keys"], 11, 2).trim())).toBe(
true,
);
}
expect(cacheStep.with["restore-keys"]).not.toContain("hashFiles");
expect(action.runs.steps.indexOf(installStep)).toBeLessThan(
action.runs.steps.indexOf(cacheStep),
);
const warmer = parse(readFileSync(".github/workflows/vitest-cache-warm.yml", "utf8"));
const buildSave = warmer.jobs.warm.steps.find(
(step: WorkflowStep) => step.name === "Save build-all cache",
);
expect(buildSave).toMatchObject({
uses: "actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9",
with: {
key: "${{ steps.setup-node-env.outputs.build-all-cache-key }}",
path: ".artifacts/build-all-cache",
},
});
expect(buildSave.if).toContain("steps.setup-node-env.outputs.cache-mode == 'read-write'");
const privateQaWorkflows = [
".github/workflows/mantis-discord-smoke.yml",
".github/workflows/mantis-discord-status-reactions.yml",
".github/workflows/mantis-discord-thread-attachment.yml",
".github/workflows/mantis-slack-desktop-smoke.yml",
".github/workflows/qa-live-transports-convex.yml",
];
for (const workflowPath of privateQaWorkflows) {
const source = readFileSync(workflowPath, "utf8");
expect(source, workflowPath).not.toContain("build-all-cache-scope:");
}
const releaseChecks = parse(
readFileSync(".github/workflows/openclaw-live-and-e2e-checks-reusable.yml", "utf8"),
);
const repoE2eWorkflow = readWorkflow(".github/workflows/openclaw-repo-e2e-reusable.yml");
const pipelines = [
releaseChecks.jobs.validate_repo_e2e_gateway,
releaseChecks.jobs.validate_repo_e2e_runtime,
];
expect(releaseChecks.jobs.validate_live_docker_provider_suites.env).toMatchObject({
OPENCLAW_SELECTED_SHA: "${{ needs.validate_selected_ref.outputs.selected_sha }}",
OPENCLAW_TOOLING_SHA: "${{ needs.validate_selected_ref.outputs.workflow_sha }}",
});
const repoE2eRows = pipelines.flatMap((pipeline) => JSON.parse(pipeline.with.suites)) as Array<{
name: string;
command: string;
target_script?: string;
}>;
expect(pipelines.map((pipeline) => pipeline.with.build_profile)).toEqual([
"full",
"ciArtifacts",
]);
for (const pipeline of pipelines) {
// Each profile starts independently; a slow/full declaration build cannot hold up UI readers.
expect(pipeline.needs).toBe("validate_selected_ref");
expect(pipeline.if).toBe(
"(!inputs.prepare_only) && inputs.include_repo_e2e && inputs.live_suite_filter == ''",
);
expect(pipeline.uses).toBe("./.github/workflows/openclaw-repo-e2e-reusable.yml");
expect(pipeline.with.ref).toBe("${{ needs.validate_selected_ref.outputs.selected_sha }}");
expect(pipeline.with.advisory).toBe("${{ inputs.advisory }}");
expect(pipeline.with.allow_frozen_target_scenario_omissions).toBe(
"${{ inputs.allow_frozen_target_scenario_omissions }}",
);
}
expect(repoE2eRows.map((row) => row.command)).toEqual([
...Array.from({ length: 4 }, (_, index) => `pnpm test:e2e:gateway --shard=${index + 1}/4`),
...Array.from({ length: 4 }, (_, index) => `pnpm test:ui:e2e --shard=${index + 1}/4`),
"pnpm test:e2e:agent-plugin-gateway",
]);
expect(new Set(repoE2eRows.map((row) => row.name)).size).toBe(9);
expect(repoE2eRows.find((row) => row.name === "Agent plugin Gateway")).toMatchObject({
target_script: "test:e2e:agent-plugin-gateway",
});
expect(repoE2eWorkflow.env).toMatchObject({
OPENCLAW_BUILD_PRIVATE_QA: "1",
OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1",
OPENCLAW_VITEST_MAX_WORKERS: "2",
OPENCLAW_SELECTED_SHA: "${{ inputs.ref }}",
OPENCLAW_TOOLING_SHA: "${{ inputs.workflow_sha }}",
OPENCLAW_DOCKER_E2E_REPO_ROOT: "${{ github.workspace }}",
});
const producer = repoE2eWorkflow.jobs.build;
const repoE2e = repoE2eWorkflow.jobs.test;
expect(repoE2e.needs).toBe("build");
expect(repoE2e.name).toBe("Repo E2E (${{ matrix.name }})");
expect(repoE2e["timeout-minutes"]).toBe(90);
expect(repoE2e.strategy).toMatchObject({ "fail-fast": false, "max-parallel": 4 });
expect(repoE2e["continue-on-error"]).toBe("${{ inputs.advisory }}");
const producerSteps = producer.steps as WorkflowStep[];
expect(producerSteps.find((step) => step.name === "Build dist for repo E2E")?.run).toContain(
"full) pnpm build",
);
expect(producerSteps.find((step) => step.name === "Build dist for repo E2E")?.run).toContain(
"ciArtifacts) pnpm build:ci-artifacts",
);
expect(producerSteps.find((step) => step.uses === UPLOAD_ARTIFACT_V7)?.with?.name).toContain(
"${{ github.run_attempt }}",
);
const repoE2eSteps = repoE2e.steps as WorkflowStep[];
expect(repoE2eSteps.find((step) => step.name === "Checkout selected ref")?.with?.ref).toBe(
"${{ inputs.ref }}",
);
expect(repoE2eSteps.find((step) => step.uses === DOWNLOAD_ARTIFACT_V8)?.with).toMatchObject({
"artifact-ids": "${{ needs.build.outputs.artifact_id }}",
"run-id": "${{ needs.build.outputs.artifact_run_id }}",
"github-token": "${{ github.token }}",
});
expect(repoE2eSteps.some((step) => step.run?.includes("pnpm build"))).toBe(false);
const restoreIndex = repoE2eSteps.findIndex((step) => step.name === "Restore repo E2E build");
const sandboxSetupIndex = repoE2eSteps.findIndex(
(step) => step.run === "scripts/sandbox-setup.sh",
);
const repoE2eIndex = repoE2eSteps.findIndex((step) => step.name === "Run repo E2E suite");
expect(restoreIndex).toBeGreaterThanOrEqual(0);
expect(sandboxSetupIndex).toBeGreaterThan(restoreIndex);
expect(repoE2eIndex).toBeGreaterThan(sandboxSetupIndex);
expect(repoE2eSteps[repoE2eIndex]).toMatchObject({
env: {
OPENCLAW_E2E_WORKERS: "2",
OPENCLAW_E2E_USE_PREBUILT_DIST: "1",
TARGET_REQUIRED_SCRIPT: "${{ matrix.target_script || '' }}",
},
});
const repoE2eRun = repoE2eSteps[repoE2eIndex]?.run;
expect(repoE2eRun).toContain("OPENCLAW_ALLOW_FROZEN_TARGET_SCENARIO_OMISSIONS");
expect(repoE2eRun).toContain("Selected target does not provide required repo E2E capability");
expect(repoE2eRun).toContain("selected target does not provide this newer repo E2E capability");
expect(repoE2eRun).toContain("${{ matrix.command }}");
const targetedGroupStep = releaseChecks.jobs.plan_docker_lane_groups.steps.find(
(step: WorkflowStep) => step.name === "Build targeted Docker lane groups",
);
expect(targetedGroupStep.env.OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS).toBe(
"${{ inputs.published_upgrade_survivor_scenarios }}",
);
expect(releaseChecks.jobs.validate_docker_lanes["timeout-minutes"]).toBe(
"${{ matrix.group.timeout_minutes || 60 }}",
);
expect(releaseChecks.jobs.validate_docker_lanes.strategy["max-parallel"]).toBe(32);
expect(releaseChecks.jobs.validate_docker_lanes.env.OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS).toBe(
"${{ matrix.group.published_upgrade_survivor_scenarios || inputs.published_upgrade_survivor_scenarios }}",
);
});
it("persists Node 26 minimum declarations through trusted bounded artifacts", () => {
const workflow = parse(readFileSync(".github/workflows/node-runtime-compat.yml", "utf8"));
const steps = workflow.jobs.compat.steps as WorkflowStep[];
const setupStep = steps.find((step) => step.name === "Setup Node environment");
const resolveStep = steps.find(
(step) => step.name === "Resolve trusted declaration cache artifact",
);
const downloadStep = steps.find(
(step) => step.name === "Restore trusted declaration cache artifact",
);
const uploadStep = steps.find(
(step) => step.name === "Publish trusted declaration cache artifact",
);
expect(workflow.permissions).toMatchObject({ actions: "read", contents: "read" });
expect(setupStep?.with).not.toHaveProperty("build-all-cache-scope");
expect(resolveStep?.run).toContain('.head_branch == "main"');
expect(resolveStep?.run).toContain('(.path | split("@")[0])');
expect(resolveStep?.run).toContain('.conclusion == "success"');
expect(resolveStep?.run).toContain("status=success&per_page=5");
expect(resolveStep?.run).toContain("artifacts?per_page=10");
expect(resolveStep?.run).not.toContain("--paginate");
expect(downloadStep).toMatchObject({
if: "steps.declaration_cache.outputs.artifact_id != ''",
uses: DOWNLOAD_ARTIFACT_V8,
with: {
path: ".artifacts/build-all-cache",
repository: "${{ github.repository }}",
},
});
expect(uploadStep).toMatchObject({
if: "success() && github.repository == 'openclaw/openclaw' && github.ref == 'refs/heads/main'",
uses: UPLOAD_ARTIFACT_V7,
with: {
"if-no-files-found": "error",
"include-hidden-files": true,
overwrite: true,
path: ".artifacts/build-all-cache",
"retention-days": 14,
},
});
});
it("fingerprints dependency install inputs without ordinary script churn", () => {
const root = mkdtempSync(path.join(tmpdir(), "openclaw-dependency-fingerprint-"));
try {
const helper = path.resolve(".github/actions/setup-node-env/dependency-fingerprint.mjs");
const writeManifest = (manifest: Record<string, unknown>) => {
writeFileSync(path.join(root, "package.json"), `${JSON.stringify(manifest, null, 2)}\n`);
};
const fingerprint = (frozenLockfile = true) =>
execFileSync(
process.execPath,
[helper, "--workspace", root, "--frozen-lockfile", frozenLockfile ? "true" : "false"],
{ encoding: "utf8" },
).trim();
execFileSync("git", ["init", "-q"], { cwd: root });
writeManifest({
name: "fixture",
openclaw: { schemaVersions: { agent: 17, state: 6 } },
scripts: {
"pnpm:devPreinstall": "node scripts/check-install-dependency-ownership.mjs",
postinstall: "node scripts/postinstall-bundled-plugins.mjs",
preinstall: "node scripts/preinstall-package-manager-warning.mjs",
prepare: "node scripts/prepare-git-hooks.mjs",
test: "vitest run",
},
devDependencies: { vitest: "1.0.0" },
});
writeFileSync(path.join(root, "pnpm-lock.yaml"), "lockfileVersion: '9.0'\n");
execFileSync("git", ["add", "package.json", "pnpm-lock.yaml"], { cwd: root });
const baseline = fingerprint();
expect(baseline).toMatch(/^v2-[a-f0-9]{64}$/);
// Presence is part of the record type, so a real file cannot collide
// with the representation of an absent optional install input.
writeFileSync(path.join(root, ".pnpmfile.cjs"), "<missing>");
expect(fingerprint()).not.toBe(baseline);
rmSync(path.join(root, ".pnpmfile.cjs"));
expect(fingerprint()).toBe(baseline);
writeFileSync(path.join(root, ".pnpmfile.mjs"), "export const hooks = {};\n");
const mjsHookFingerprint = fingerprint();
expect(mjsHookFingerprint).not.toBe(baseline);
writeFileSync(
path.join(root, ".pnpmfile.mjs"),
"export const hooks = { readPackage: (pkg) => pkg };\n",
);
expect(fingerprint()).not.toBe(mjsHookFingerprint);
rmSync(path.join(root, ".pnpmfile.mjs"));
expect(fingerprint()).toBe(baseline);
for (const relativePath of [
"node-version.mjs",
".github/actions/setup-node-env/install-dependencies.sh",
"scripts/check-install-dependency-ownership.mjs",
"scripts/prepare-git-hooks.mjs",
"scripts/lib/package-lifecycle-marker.mjs",
]) {
const inputPath = path.join(root, relativePath);
mkdirSync(path.dirname(inputPath), { recursive: true });
writeFileSync(inputPath, "fixture\n");
expect(fingerprint(), relativePath).not.toBe(baseline);
rmSync(inputPath);
expect(fingerprint(), relativePath).toBe(baseline);
}
// Formatting, key order, and scripts that pnpm install never executes
// should keep the existing dependency snapshot warm.
writeManifest({
devDependencies: { vitest: "1.0.0" },
scripts: {
test: "vitest run --reporter=dot",
prepare: "node scripts/prepare-git-hooks.mjs",
"pnpm:devPreinstall": "node scripts/check-install-dependency-ownership.mjs",
postinstall: "node scripts/postinstall-bundled-plugins.mjs",
preinstall: "node scripts/preinstall-package-manager-warning.mjs",
},
name: "fixture",
});
expect(fingerprint()).toBe(baseline);
// Repository-owned package metadata does not affect pnpm's install tree
// or any audited install hook, so schema churn must stay warm.
writeManifest({
name: "fixture",
openclaw: { schemaVersions: { agent: 17, state: 7 } },
scripts: {
"pnpm:devPreinstall": "node scripts/check-install-dependency-ownership.mjs",
postinstall: "node scripts/postinstall-bundled-plugins.mjs",
preinstall: "node scripts/preinstall-package-manager-warning.mjs",
prepare: "node scripts/prepare-git-hooks.mjs",
test: "vitest run",
},
devDependencies: { vitest: "1.0.0" },
});
expect(fingerprint()).toBe(baseline);
writeManifest({
name: "fixture",
scripts: {
"pnpm:devPreinstall": "node scripts/check-install-dependency-ownership.mjs",
postinstall: "node scripts/postinstall-bundled-plugins.mjs",
preinstall: "node scripts/preinstall-package-manager-warning.mjs",
prepare: "node scripts/prepare-git-hooks.mjs",
test: "vitest run",
},
devDependencies: { vitest: "2.0.0" },
});
expect(fingerprint()).not.toBe(baseline);
writeManifest({
name: "fixture",
scripts: { postinstall: "node install-v2.mjs", test: "vitest run" },
devDependencies: { vitest: "1.0.0" },
});
expect(() => fingerprint()).toThrow(/unaudited install lifecycle scripts in package\.json/);
mkdirSync(path.join(root, "packages", "worker"), { recursive: true });
writeManifest({
name: "fixture",
scripts: {
"pnpm:devPreinstall": "node scripts/check-install-dependency-ownership.mjs",
postinstall: "node scripts/postinstall-bundled-plugins.mjs",
preinstall: "node scripts/preinstall-package-manager-warning.mjs",
prepare: "node scripts/prepare-git-hooks.mjs",
},
devDependencies: { vitest: "1.0.0" },
});
const workerManifest = path.join(root, "packages", "worker", "package.json");
writeFileSync(
workerManifest,
`${JSON.stringify({ name: "worker", scripts: { prepare: "node build.mjs" } })}\n`,
);
execFileSync("git", ["add", "packages/worker/package.json"], { cwd: root });
expect(() => fingerprint()).toThrow(
/unaudited install lifecycle scripts in packages\/worker\/package\.json/,
);
writeFileSync(
workerManifest,
`${JSON.stringify({ name: "worker", scripts: { build: "node build.mjs" } })}\n`,
);
writeManifest({
name: "fixture",
scripts: {
"pnpm:devPreinstall": "node scripts/check-install-dependency-ownership.mjs",
postinstall: "node scripts/postinstall-bundled-plugins.mjs",
preinstall: "node scripts/preinstall-package-manager-warning.mjs",
prepare: "node scripts/prepare-git-hooks.mjs",
test: "vitest run",
},
devDependencies: { vitest: "1.0.0" },
});
writeFileSync(path.join(root, "pnpm-lock.yaml"), "lockfileVersion: '9.1'\n");
expect(fingerprint()).not.toBe(baseline);
expect(fingerprint(false)).not.toBe(baseline);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
it("hashes transform inputs once per enabled setup and never for skipped caches", () => {
const action = parse(readFileSync(".github/actions/setup-node-env/action.yml", "utf8"));
const transformSteps = (action.runs.steps as WorkflowStep[]).filter((step) =>
step.name?.includes("Vitest transform cache"),
);
const output = path.join(tempDirs.make("openclaw-transform-generation-"), "output");
for (const os of ["Linux", "macOS", "Windows"]) {
for (const mode of ["off", "restore", "read-write"]) {
for (const flags of [
["false", "false"],
["true", "false"],
["false", "true"],
["true", "true"],
]) {
for (const generation of ["a".repeat(64), "b".repeat(64)]) {
const hashes: string[][] = [];
const steps: Record<string, { outputs: Record<string, string> }> = {};
const context = {
github: { repository: "openclaw/openclaw", run_id: 10, run_attempt: 2 },
inputs: {
"cache-mode": mode,
"vitest-fs-cache": flags[0],
"restore-test-caches": flags[1],
"node-version": "24.x",
},
runner: { os, arch: "X64" },
steps,
hashFiles: (...patterns: string[]) => {
hashes.push(patterns);
return generation;
},
};
const evaluate = (expression: string): unknown =>
runInNewContext(
expression.replace(/(inputs|steps)\.([a-z-]+)/gu, '$1["$2"]'),
context,
);
const render = (value: unknown) =>
String(value).replace(/\$\{\{([\s\S]*?)\}\}/gu, (_, expression: string) => {
const result = evaluate(expression);
if (result == null) {
return "";
}
if (
typeof result === "string" ||
typeof result === "number" ||
typeof result === "boolean"
) {
return String(result);
}
throw new TypeError(`non-scalar workflow interpolation: ${expression}`);
});
let cacheInputs: Record<string, string> | undefined;
let configuredGeneration: string | undefined;
for (const step of transformSteps) {
// Runner v2.336.0 evaluates embedded env before if; run/with inputs
// are evaluated only after admission (CompositeActionHandler/ActionRunner).
const env = Object.fromEntries(
Object.entries(step.env ?? {}).map(([key, value]) => [key, render(value)]),
);
if (!evaluate(step.if ?? "true")) {
if (step.id) {
steps[step.id] = { outputs: {} };
}
continue;
}
if (step.name === "Resolve Vitest transform cache generation") {
writeFileSync(output, "");
execFileSync("bash", ["-e", "-c", render(step.run)], {
env: { ...process.env, GITHUB_OUTPUT: output },
});
steps[expectDefined(step.id, "transform generation step id")] = {
outputs: Object.fromEntries(
readFileSync(output, "utf8")
.trim()
.split("\n")
.map((line) => line.split("=")),
),
};
} else if (step.uses) {
cacheInputs = Object.fromEntries(
Object.entries(step.with ?? {}).map(([key, value]) => [key, render(value)]),
);
} else {
configuredGeneration = env.CACHE_GENERATION;
}
}
const enabled = os !== "Windows" && mode !== "off" && flags.includes("true");
expect(hashes, JSON.stringify({ os, mode, flags, generation })).toHaveLength(
enabled ? 1 : 0,
);
if (enabled) {
expect(hashes[0]).toEqual([
"pnpm-lock.yaml",
"pnpm-workspace.yaml",
"**/package.json",
"**/tsconfig*.json",
"vitest.config.*",
"test/vitest/**",
"src/state/*.sql",
"!**/node_modules/**",
]);
const prefix = `openclaw/openclaw-vitest-fs-v3-protected-${os}-X64-node-24.x-${generation}-`;
expect(cacheInputs).toEqual({
path: "/var/tmp/openclaw-vitest-fs-cache",
key: `${prefix}10-2`,
"restore-keys": `${prefix}\n`,
});
expect(configuredGeneration).toBe(generation);
} else {
expect(cacheInputs).toBeUndefined();
expect(configuredGeneration).toBeUndefined();
}
}
}
}
}
});
it("persists isolated transform and compile caches through immutable protected archives", () => {
const workflow = readCiWorkflow();
const nodeTestJob = workflow.jobs["checks-node-core-test-nondist-shard"];
const setupNodeStep = nodeTestJob.steps.find(
(step: WorkflowStep) => step.name === "Setup Node environment",
);
const action = parse(readFileSync(".github/actions/setup-node-env/action.yml", "utf8"));
const readerStep = action.runs.steps.find(
(step: WorkflowStep) => step.name === "Restore Vitest transform cache",
);
const configureStep = action.runs.steps.find(
(step: WorkflowStep) => step.name === "Configure Vitest transform cache",
);
const compileEpochStep = action.runs.steps.find(
(step: WorkflowStep) => step.name === "Select Node compile cache epoch",
);
const compileReaderStep = action.runs.steps.find(
(step: WorkflowStep) => step.name === "Restore Node compile cache",
);
const compileConfigureStep = action.runs.steps.find(
(step: WorkflowStep) => step.name === "Configure Node compile cache",
);
const buildSetupNodeStep = workflow.jobs["build-artifacts"].steps.find(
(step: WorkflowStep) => step.name === "Setup Node environment",
);
const hostedTestCacheInput =
"${{ (needs.preflight.outputs.runner_profile == 'github' || needs.preflight.outputs.runner_profile == 'hybrid') && 'true' || 'false' }}";
const hostedTestCacheJobs = [
"checks-ui",
"checks-ui-e2e",
"checks-fast-plugin-contracts-shard",
"checks-fast-channel-contracts-shard",
];
const hostedFastCoreTestCacheInput =
"${{ (needs.preflight.outputs.runner_profile == 'github' || needs.preflight.outputs.runner_profile == 'hybrid') && (matrix.task == 'bundled-protocol' || matrix.task == 'contracts-plugins-ci-routing' || matrix.task == 'ci-routing' || matrix.task == 'bun-launcher') && 'true' || 'false' }}";
expect(setupNodeStep.with).toMatchObject({
"cache-mode": "${{ needs.preflight.outputs.cache_mode }}",
"node-compile-cache": "true",
"node-compile-cache-scope": "test",
"vitest-fs-cache": "true",
});
expect(setupNodeStep.with).not.toHaveProperty("save-node-compile-cache");
expect(setupNodeStep.with).not.toHaveProperty("runtime-cache-sticky-disk");
expect(action.inputs).not.toHaveProperty("runtime-cache-sticky-disk");
expect(action.inputs["vitest-fs-cache"].default).toBe("false");
expect(action.inputs["restore-test-caches"].default).toBe("false");
expect(action.inputs).not.toHaveProperty("save-vitest-fs-cache");
expect(action.inputs["node-compile-cache"].default).toBe("false");
expect(action.inputs["node-compile-cache-scope"].default).toBe("test");
expect(action.inputs).not.toHaveProperty("save-node-compile-cache");
expect(
action.runs.steps.some((step: WorkflowStep) =>
step.name?.includes("transform cache sticky disk"),
),
).toBe(false);
expect(
action.runs.steps.some((step: WorkflowStep) =>
step.name?.includes("compile cache sticky disk"),
),
).toBe(false);
expect(readerStep.uses).toBe(CACHE_V5);
expect(readerStep.if).toContain("inputs.cache-mode != 'off'");
expect(readerStep.if).toContain("inputs.restore-test-caches == 'true'");
expect(readerStep.if).toContain("runner.os != 'Windows'");
expect(readerStep.if).not.toMatch(/runner\.(?:environment|labels|name)/u);
expect(readerStep.with.key).toContain("vitest-fs-v3-protected-");
expect(readerStep.with.key).toContain("github.run_id");
expect(readerStep.with.key).toContain("github.run_attempt");
expect(configureStep.if).toContain("inputs.restore-test-caches == 'true'");
expect(configureStep.run).toContain("OPENCLAW_VITEST_FS_MODULE_CACHE_PATH=$cache_root");
expect(configureStep.run).toContain(".openclaw-transform-generation");
expect(configureStep.run).not.toContain("protected Vitest transform seed");
expect(configureStep.env.CACHE_WRITER).toBe("0");
expect(configureStep.run).toContain("OPENCLAW_VITEST_FS_MODULE_CACHE_WRITER=");
expect(compileEpochStep.run).toContain('if [ "$CACHE_SCOPE" = "build" ]');
expect(compileEpochStep.run).toContain("date -u +%Y%m%d");
expect(compileEpochStep.run).toContain("GITHUB_RUN_ID");
expect(compileReaderStep.with.key).toContain(
"node-compile-v3-${{ inputs.node-compile-cache-scope }}-protected-",
);
expect(compileReaderStep.with.key).toContain("steps.node-compile-cache-epoch.outputs.value");
expect(compileReaderStep.with.key).not.toContain("pull_request");
expect(compileEpochStep.if).toContain("inputs.restore-test-caches == 'true'");
expect(compileReaderStep.if).toContain("inputs.cache-mode != 'off'");
expect(compileReaderStep.if).toContain("inputs.restore-test-caches == 'true'");
expect(compileConfigureStep.if).toContain("inputs.restore-test-caches == 'true'");
expect(compileConfigureStep.run).toContain("NODE_COMPILE_CACHE=$cache_root");
expect(compileConfigureStep.run).toContain("NODE_COMPILE_CACHE_PORTABLE=1");
expect(compileConfigureStep.run).toContain("OPENCLAW_NODE_COMPILE_CACHE_WRITER=0");
expect(buildSetupNodeStep.with).toMatchObject({
"cache-mode": "${{ needs.preflight.outputs.cache_mode }}",
"node-compile-cache": "true",
"node-compile-cache-scope": "build",
"build-all-cache-scope": "full",
});
expect(buildSetupNodeStep.with["node-compile-cache-scope"]).not.toBe(
setupNodeStep.with["node-compile-cache-scope"],
);
for (const jobName of hostedTestCacheJobs) {
const setup = workflow.jobs[jobName].steps.find(
(step: WorkflowStep) => step.name === "Setup Node environment",
);
expect(setup.with["restore-test-caches"], jobName).toBe(hostedTestCacheInput);
expect(
evaluateWorkflowExpression(setup.with["restore-test-caches"], {
eventName: "push",
repository: "openclaw/openclaw",
runnerBackend: "github",
runAttempt: 1,
}),
jobName,
).toBe("true");
expect(
evaluateWorkflowExpression(setup.with["restore-test-caches"], {
eventName: "push",
repository: "openclaw/openclaw",
runnerBackend: "blacksmith",
runAttempt: 1,
}),
jobName,
).toBe("false");
expect(setup.with, jobName).not.toHaveProperty("save-node-compile-cache");
expect(setup.with, jobName).not.toHaveProperty("save-vitest-fs-cache");
}
const fastCoreSetup = workflow.jobs["checks-fast-core"].steps.find(
(step: WorkflowStep) => step.name === "Setup Node environment",
);
expect(fastCoreSetup.with["restore-test-caches"]).toBe(hostedFastCoreTestCacheInput);
for (const task of [
"bundled-protocol",
"contracts-plugins-ci-routing",
"ci-routing",
"bun-launcher",
]) {
expect(
evaluateWorkflowExpression(fastCoreSetup.with["restore-test-caches"], {
eventName: "push",
matrix: { task },
repository: "openclaw/openclaw",
runnerBackend: "github",
runAttempt: 1,
}),
task,
).toBe("true");
}
for (const task of ["baseline-ratchets", "coercion-helpers"]) {
expect(
evaluateWorkflowExpression(fastCoreSetup.with["restore-test-caches"], {
eventName: "push",
matrix: { task },
repository: "openclaw/openclaw",
runnerBackend: "github",
runAttempt: 1,
}),
task,
).toBe("false");
}
expect(
evaluateWorkflowExpression(fastCoreSetup.with["restore-test-caches"], {
eventName: "push",
matrix: { task: "bundled-protocol" },
repository: "openclaw/openclaw",
runnerBackend: "blacksmith",
runAttempt: 1,
}),
).toBe("false");
expect(fastCoreSetup.with).not.toHaveProperty("save-node-compile-cache");
expect(fastCoreSetup.with).not.toHaveProperty("save-vitest-fs-cache");
for (const jobName of ["checks-ui-e2e-real-gateway", "native-i18n", "control-ui-i18n"]) {
const setup = workflow.jobs[jobName].steps.find(
(step: WorkflowStep) => step.name === "Setup Node environment",
);
expect(setup.with, jobName).not.toHaveProperty("restore-test-caches");
}
});
it("warms protected caches without main-run cancellation", () => {
const warmerSource = readFileSync(".github/workflows/vitest-cache-warm.yml", "utf8");
const warmer = parse(warmerSource);
const warmerSetup = warmer.jobs.warm.steps.find(
(step: WorkflowStep) => step.name === "Setup Node environment",
);
const checkoutStep = warmer.jobs.warm.steps.find(
(step: WorkflowStep) => step.name === "Checkout",
);
const seedStep = warmer.jobs.warm.steps.find(
(step: WorkflowStep) => step.name === "Select broad cache seed",
);
const warmStep = warmer.jobs.warm.steps.find(
(step: WorkflowStep) => step.name === "Warm transform and compile caches",
);
const warmerSteps = warmer.jobs.warm.steps as WorkflowStep[];
const buildStep = expectDefined(
warmerSteps.find((step) => step.name === "Warm build cache"),
"cache warm build",
);
const boundaryRestoreStep = expectDefined(
warmerSteps.find((step) => step.name === "Restore native SDK boundary cache"),
"native SDK boundary cache restore",
);
const boundaryPrepareStep = expectDefined(
warmerSteps.find((step) => step.name === "Prepare native SDK boundary cache"),
"native SDK boundary cache preparation",
);
const boundarySaveStep = expectDefined(
warmerSteps.find((step) => step.name === "Save native SDK boundary cache"),
"native SDK boundary cache publication",
);
const boundaryCleanupStep = expectDefined(
warmerSteps.find((step) => step.name === "Clear native SDK boundary output before build"),
"native SDK boundary output cleanup",
);
const warmAssertionStep = expectDefined(
warmerSteps.find((step) => step.name === "Assert cache warming succeeded"),
"final cache warming assertion",
);
expect(warmer.concurrency["cancel-in-progress"]).toBe(false);
expect(warmer.concurrency.group).toBe("vitest-cache-warm-${{ github.ref }}");
// hosted-mode cache recovery needs a maintainer-operated fallback when the
// scheduled seed is missing or stale.
expect(warmer.on).toHaveProperty("workflow_dispatch");
expect(warmer.on.push.branches).toEqual(["main"]);
expect(warmer.on.repository_dispatch.types).toEqual(["vitest-cache-warm"]);
expect(warmer.jobs.warm.if).toContain("github.repository == 'openclaw/openclaw'");
expect(warmer.jobs.warm.strategy["fail-fast"]).toBe(false);
expect(warmer.on).not.toHaveProperty("pull_request");
expect(warmer.on).not.toHaveProperty("pull_request_target");
for (const eventName of ["push", "workflow_dispatch"] as const) {
for (const runnerBackend of ["blacksmith", "hybrid", "github"] as const) {
const configuredPlatforms = warmer.jobs.warm.strategy.matrix.platform;
const platforms =
typeof configuredPlatforms === "string"
? evaluateWorkflowExpression(configuredPlatforms, {
eventName,
repository: "openclaw/openclaw",
runAttempt: 1,
runnerBackend,
})
: configuredPlatforms;
expect(platforms).toEqual(
runnerBackend === "hybrid" ? ["linux", "linux-hosted", "macos"] : ["linux", "macos"],
);
for (const platform of platforms as string[]) {
const context = {
eventName,
matrix: { platform },
repository: "openclaw/openclaw",
runAttempt: 1,
runnerBackend,
};
const full = platform === "linux";
const expectedRunner =
platform === "macos"
? "macos-15"
: platform === "linux-hosted" || runnerBackend === "github"
? "ubuntu-24.04"
: "blacksmith-8vcpu-ubuntu-2404";
expect(evaluateWorkflowExpression(warmer.jobs.warm["runs-on"], context)).toBe(
expectedRunner,
);
const setupInputs = Object.fromEntries(
Object.entries(warmerSetup.with).map(([key, value]) => [
key,
typeof value === "string" && value.startsWith("${{")
? evaluateWorkflowExpression(value, context)
: value,
]),
);
expect(setupInputs).toMatchObject({
"build-all-cache-scope": full ? "full" : "",
"cache-mode": "read-write",
"dependency-cache": String(full),
"install-bun": "false",
"node-compile-cache-scope": "test",
"node-compile-cache": String(full),
"vitest-fs-cache": String(full),
});
for (const step of [
buildStep,
boundaryPrepareStep,
boundaryCleanupStep,
seedStep,
warmStep,
warmAssertionStep,
]) {
expect(evaluateWorkflowExpression(step.if, context), step.name).toBe(full);
}
}
}
}
expect(warmer.on).not.toHaveProperty("workflow_run");
expect(checkoutStep.with).toBeUndefined();
expect(warmerSource).toContain('cron: "17 8 * * *"');
expect(seedStep.run).toContain(
'import { createVitestCacheWarmGroups } from "./scripts/lib/ci-node-test-plan.mts";',
);
expect(seedStep.run).toMatch(
/const groups = createVitestCacheWarmGroups\(\);[\s\S]*appendFileSync\(\s*process\.env\.GITHUB_ENV,[\s\S]*OPENCLAW_NODE_TEST_GROUPS_JSON=\$\{JSON\.stringify\(groups\)\}/u,
);
expect(warmerSource).not.toContain("OPENCLAW_NODE_TEST_CONFIGS_JSON");
expect(warmerSource).toContain('"OPENCLAW_NODE_TEST_PLAN_CONCURRENCY=1"');
expect(seedStep.run).toContain('"OPENCLAW_NODE_TEST_PLAN_CONTINUE_ON_FAILURE=1"');
expect(warmStep.id).toBe("warm-caches");
expect(warmStep["continue-on-error"]).toBe(true);
expect(warmStep.env).toMatchObject({
OPENCLAW_VITEST_FS_MODULE_CACHE_WRITER: "1",
OPENCLAW_NODE_COMPILE_CACHE_WRITER: "1",
});
expect(warmerSetup["continue-on-error"]).not.toBe(true);
for (const legacyInput of [
"save-actions-cache",
"save-dependency-cache",
"save-node-compile-cache",
"save-vitest-fs-cache",
"use-actions-cache",
]) {
expect(warmerSetup.with).not.toHaveProperty(legacyInput);
}
const saveSteps = warmerSteps.filter((step) => step.uses?.startsWith("actions/cache/save@"));
expect(saveSteps.map((step) => step.name)).toEqual([
"Save Node toolchain cache",
"Save exact dependency cache",
"Save pnpm store cache",
"Save native SDK boundary cache",
"Save build-all cache",
"Save dist build cache",
"Save Vitest transform cache",
"Save Node compile cache",
]);
for (const saveStep of saveSteps) {
expect(saveStep.if, saveStep.name).toContain(
"steps.setup-node-env.outputs.cache-mode == 'read-write'",
);
expect(warmerSteps.indexOf(saveStep), saveStep.name).toBeGreaterThan(
warmerSteps.indexOf(warmerSetup),
);
if (
saveStep.name === "Save Node toolchain cache" ||
saveStep.name === "Save exact dependency cache" ||
saveStep.name === "Save pnpm store cache"
) {
expect(warmerSteps.indexOf(saveStep), saveStep.name).toBeLessThan(
warmerSteps.indexOf(buildStep),
);
// A normal step condition retains Actions' implicit success() gate,
// so failed setup cannot publish even if it produced cache outputs.
expect(saveStep.if, saveStep.name).not.toMatch(/\b(?:always|failure|cancelled)\(/u);
} else if (
saveStep.name === "Save build-all cache" ||
saveStep.name === "Save dist build cache"
) {
expect(warmerSteps.indexOf(saveStep), saveStep.name).toBeGreaterThan(
warmerSteps.findIndex((step) => step.name === "Warm build cache"),
);
expect(warmerSteps.indexOf(saveStep), saveStep.name).toBeLessThan(
warmerSteps.indexOf(seedStep),
);
expect(saveStep.if).not.toMatch(/always\(|failure\(/u);
} else if (saveStep.name === "Save native SDK boundary cache") {
expect(saveStep.if).toContain(
"steps.extension-package-boundary-cache.outputs.cache-hit != 'true'",
);
expect(saveStep.if).not.toMatch(/always\(|failure\(|cancelled\(/u);
} else {
expect(warmerSteps.indexOf(saveStep), saveStep.name).toBeGreaterThan(
warmerSteps.indexOf(warmStep),
);
}
expect(warmerSteps.indexOf(saveStep), saveStep.name).toBeLessThan(
warmerSteps.indexOf(warmAssertionStep),
);
}
expect(warmAssertionStep.if).toBe("${{ always() && matrix.platform == 'linux' }}");
expect(warmAssertionStep.run).toContain("steps.warm-caches.outcome");
expect(warmAssertionStep.run).toContain("exit 1");
expect(warmerSteps.at(-1)).toBe(warmAssertionStep);
// No close-time cleanup workflow is needed; Actions cache LRU/TTL expires
// old hosted-writer and warmer generations.
expect(existsSync(".github/workflows/pr-cache-cleanup.yml")).toBe(false);
expect(seedStep.if).toBe("${{ matrix.platform == 'linux' }}");
expect(warmStep.if).toBe("${{ matrix.platform == 'linux' }}");
const distSave = expectDefined(
saveSteps.find((step) => step.name === "Save dist build cache"),
"Linux dist publication",
);
expect(distSave.if).toBe(
"${{ matrix.platform == 'linux' && steps.setup-node-env.outputs.cache-mode == 'read-write' }}",
);
expect(boundaryRestoreStep.uses).toBe(CACHE_V5);
expect(boundarySaveStep.uses).toBe(CACHE_SAVE_V5);
const boundaryRestoreInputs = expectDefined(
boundaryRestoreStep.with,
"native SDK boundary cache inputs",
);
expect(boundaryRestoreInputs.key).toBe(
"${{ runner.os }}-extension-package-boundary-v4-${{ github.sha }}",
);
expect(boundarySaveStep.with).toEqual({
path: boundaryRestoreInputs.path,
key: boundaryRestoreInputs.key,
});
expect(boundaryRestoreInputs["restore-keys"]).toBe(
"${{ runner.os }}-extension-package-boundary-v4-\n",
);
expect(boundaryPrepareStep.run).toBe(
"node --import ./scripts/tsx.mjs scripts/prepare-extension-package-boundary-artifacts.mts --mode=package-boundary",
);
expect(boundaryPrepareStep["continue-on-error"]).not.toBe(true);
expect(warmerSteps.indexOf(boundaryRestoreStep)).toBeLessThan(warmerSteps.indexOf(buildStep));
expect(warmerSteps.indexOf(boundaryPrepareStep)).toBeGreaterThan(
warmerSteps.indexOf(boundaryRestoreStep),
);
expect(warmerSteps.indexOf(boundarySaveStep)).toBeGreaterThan(
warmerSteps.indexOf(boundaryPrepareStep),
);
expect(warmerSteps.indexOf(boundarySaveStep)).toBeLessThan(warmerSteps.indexOf(buildStep));
expect(warmerSteps.indexOf(boundaryCleanupStep)).toBeGreaterThan(
warmerSteps.indexOf(boundarySaveStep),
);
expect(warmerSteps.indexOf(boundaryCleanupStep)).toBeLessThan(warmerSteps.indexOf(buildStep));
const cleanupRoot = tempDirs.make("openclaw-native-sdk-cleanup-");
const sdkOutput = path.join(cleanupRoot, "packages/plugin-sdk/dist/native.d.ts");
const sdkSource = path.join(cleanupRoot, "packages/plugin-sdk/src/core.ts");
const siblingOutput = path.join(cleanupRoot, "packages/normalization-core/dist/index.js");
const boundaryReceipt = path.join(
cleanupRoot,
".artifacts/extension-package-boundary/plugin-sdk.json",
);
for (const file of [sdkOutput, sdkSource, siblingOutput, boundaryReceipt]) {
mkdirSync(path.dirname(file), { recursive: true });
writeFileSync(file, "sentinel\n");
}
const cleanupResult = runWorkflowShellScript(
expectDefined(boundaryCleanupStep.run, "cleanup"),
{
cwd: cleanupRoot,
env: process.env,
},
);
expect(cleanupResult.status, `${cleanupResult.stdout}${cleanupResult.stderr}`).toBe(0);
expect(existsSync(sdkOutput)).toBe(false);
expect(existsSync(sdkSource)).toBe(true);
expect(existsSync(siblingOutput)).toBe(true);
expect(existsSync(boundaryReceipt)).toBe(true);
const storeSave = expectDefined(
saveSteps.find((step) => step.name === "Save pnpm store cache"),
"platform pnpm store publication",
);
expect(storeSave.if).not.toContain("matrix.platform");
expect(storeSave.if).toContain("steps.setup-node-env.outputs.pnpm-store-cache-hit != 'true'");
expect(storeSave.if).not.toMatch(/\b(?:always|failure|cancelled)\(/u);
expect(storeSave.with).toEqual({
path: "${{ steps.setup-node-env.outputs.pnpm-store-cache-path }}",
key: "${{ steps.setup-node-env.outputs.pnpm-store-cache-key }}",
});
});
it("publishes a portable release npm seed without hooks or push-time downloads", () => {
const warmer = parse(readFileSync(".github/workflows/release-npm-cache-warm.yml", "utf8"));
expect(warmer.on).not.toHaveProperty("push");
expect(warmer.on).toHaveProperty("schedule");
expect(warmer.on).toHaveProperty("workflow_dispatch");
expect(warmer.concurrency.group).not.toBe(
parse(readFileSync(".github/workflows/vitest-cache-warm.yml", "utf8")).concurrency.group,
);
const seed = warmer.jobs["warm-release-npm"];
for (const repository of ["openclaw/openclaw", "example/fork"]) {
for (const eventName of [
"push",
"pull_request",
"repository_dispatch",
"schedule",
"workflow_dispatch",
] as const) {
expect(evaluateWorkflowExpression(seed.if, { repository, eventName, runAttempt: 1 })).toBe(
repository === "openclaw/openclaw" &&
(eventName === "schedule" || eventName === "workflow_dispatch"),
);
}
}
expect(seed["runs-on"]).toBe("ubuntu-24.04");
const steps = seed.steps as WorkflowStep[];
const install = expectDefined(
steps.find((entry) => entry.run),
"npm seed install",
);
expect(install.run).toContain("openclaw@latest --ignore-scripts --omit=dev");
expect(install.env).toEqual({
NPM_CONFIG_CACHE: "${{ github.workspace }}/.cache/openclaw-cross-os-npm-cache",
});
const save = expectDefined(
steps.find((entry) => entry.uses?.startsWith("actions/cache/save@")),
"npm seed publication",
);
expect(save.with).toMatchObject({
path: ".cache/openclaw-cross-os-npm-cache/_cacache",
enableCrossOsArchive: true,
});
expect(save.with?.key).toMatch(/^openclaw-cross-os-npm-v1-seed-/u);
expect(save["continue-on-error"]).toBe(true);
expect(save.if ?? "").not.toMatch(/always\(|failure\(|cancelled\(/u);
expect(steps.indexOf(save)).toBeGreaterThan(steps.indexOf(install));
expect(steps.some((entry) => entry.uses?.startsWith("actions/cache/restore@"))).toBe(false);
});
it("uses bundled Node shards and telemetry-backed runner sizes", () => {
const workflow = readCiWorkflow();
const buildArtifactsTestbox = readBuildArtifactsTestboxWorkflow();
const source = readFileSync(".github/workflows/ci.yml", "utf8");
expect(source).toContain("createNodeTestShardBundles");
const artifactRunner = workflow.jobs["build-artifacts"]["runs-on"];
for (const [frozenTarget, expected] of [
["false", "blacksmith-16vcpu-ubuntu-2404"],
["true", "blacksmith-16vcpu-ubuntu-2404"],
["", "blacksmith-16vcpu-ubuntu-2404"],
] as const) {
const context = {
eventName: "push",
repository: "openclaw/openclaw",
runAttempt: 1,
preflightOutputs: { frozen_target: frozenTarget },
} as const;
for (const runnerBackend of ["", "blacksmith", "hybrid"] as const) {
for (const eventName of ["push", "pull_request"] as const) {
expect(
evaluateWorkflowExpression(artifactRunner, { ...context, runnerBackend, eventName }),
`build-artifacts: ${runnerBackend || "default"}/${eventName}/frozen=${frozenTarget}`,
).toBe(expected);
}
}
for (const override of [
{ runnerBackend: "github" },
{ runnerBackend: "hybrid", runAttempt: 2 },
{ eventName: "workflow_dispatch" },
{ eventName: "pull_request", authorAssociation: "NONE", headRepository: "fork/openclaw" },
] as const) {
expect(evaluateWorkflowExpression(artifactRunner, { ...context, ...override })).toBe(
"ubuntu-24.04",
);
}
}
expect(workflow.jobs["build-artifacts"]["timeout-minutes"]).toBe(
"${{ (vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' || (vars.OPENCLAW_CI_RUNNER_BACKEND == 'hybrid' && github.run_attempt > 1) || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository)) && 35 || 20 }}",
);
// PR events validate the artifact build on hosted runners (landing gate
// stays satisfiable during Blacksmith outages); Testbox leases are
// dispatch-only, mirroring ci-check-testbox.yml.
expect(buildArtifactsTestbox.jobs["build-artifacts"]["runs-on"]).toBe(
"${{ github.event_name == 'pull_request' && 'ubuntu-24.04' || 'blacksmith-16vcpu-ubuntu-2404' }}",
);
for (const stepName of ["Begin Testbox", "Run Testbox"]) {
expect(
buildArtifactsTestbox.jobs["build-artifacts"].steps.find(
(step: { name?: string }) => step.name === stepName,
).if,
).toContain("github.event_name == 'workflow_dispatch'");
}
expect(
buildArtifactsTestbox.jobs["build-artifacts"].steps.find(
(step: { name?: string }) => step.name === "Build dist on cache miss",
).env.NODE_OPTIONS,
).toBe(
"${{ github.event_name == 'pull_request' && '--max-old-space-size=8192' || '--max-old-space-size=16384' }}",
);
expect(workflow.jobs["checks-node-core-test-nondist-shard"]["runs-on"]).toContain(
"blacksmith-4vcpu-ubuntu-2404",
);
for (const task of ["dependencies", "test-types"]) {
expect(workflow.jobs["check-shard"].strategy.matrix.include).toContainEqual({
check_name: `check-${task}`,
task,
runner: "blacksmith-16vcpu-ubuntu-2404",
});
}
expect(workflow.jobs["check-additional-shard"]["runs-on"]).toContain("matrix.runner");
expect(readFrozenAdditionalCheckRows()).toContainEqual({
check_name: "check-additional-runtime-topology-architecture",
group: "runtime-topology-architecture",
runner: "blacksmith-16vcpu-ubuntu-2404",
});
expect(readFrozenAdditionalCheckRows()).toContainEqual({
check_name: "check-session-accessor-boundary",
group: "session-accessor-boundary",
runner: "blacksmith-4vcpu-ubuntu-2404",
});
expect(readFrozenAdditionalCheckRows()).toContainEqual({
check_name: "check-export-name-collisions",
group: "export-name-collisions",
runner: "blacksmith-4vcpu-ubuntu-2404",
});
expect(readFrozenAdditionalCheckRows()).toContainEqual({
check_name: "check-sqlite-session-schema-baseline",
group: "sqlite-session-schema-baseline",
runner: "blacksmith-4vcpu-ubuntu-2404",
});
// The Windows matrix carries no per-row runner: both parts share one class.
expect(workflow.jobs["checks-windows"]["runs-on"]).not.toContain("matrix.runner");
expect(source).toContain("blacksmith-8vcpu-windows-2025");
});
it("keeps the extension boundary sticky disk on one protected key", () => {
const workflow = readCiWorkflow();
const warmer = parse(readFileSync(".github/workflows/vitest-cache-warm.yml", "utf8"));
const additionalJob = workflow.jobs["check-additional-shard"];
const checkShardJob = workflow.jobs["check-shard"];
const hostedCoreJob = workflow.jobs["check-lint-hosted-core-shard"];
// Cold SDK preparation and plugin compilation need CPU and memory headroom.
expect(readFrozenAdditionalCheckRows()).toContainEqual({
check_name: "check-additional-extension-package-boundary",
group: "extension-package-boundary",
runner: "blacksmith-16vcpu-ubuntu-2404",
});
const runStep = additionalJob.steps.find(
(step: WorkflowStep) => step.name === "Run additional check shard",
);
expect(runStep.env.OPENCLAW_EXTENSION_BOUNDARY_CONCURRENCY).toBe(16);
// O(1) disks: Blacksmith caps sticky disks per installation, and the old
// per-PR/per-config keys minted new disks until every mount 429-failed
// fleet-wide. Snapshot validity lives in the in-job marker, not the key.
const boundaryMount = additionalJob.steps.find(
(step: WorkflowStep) => step.name === "Mount extension boundary sticky disk",
);
const lintMount = checkShardJob.steps.find(
(step: WorkflowStep) => step.name === "Mount extension boundary sticky disk",
);
const boundaryCache = expectDefined(
additionalJob.steps.find(
(step: WorkflowStep) => step.name === "Cache extension package boundary artifacts",
),
"extension package boundary cache",
);
const hostedLintCache = expectDefined(
checkShardJob.steps.find(
(step: WorkflowStep) =>
step.name === "Cache extension package boundary artifacts for hosted lint",
),
"hosted lint extension package boundary cache",
);
const hostedCoreCache = expectDefined(
hostedCoreJob.steps.find(
(step: WorkflowStep) =>
step.name === "Cache extension package boundary artifacts for hosted core lint",
),
"hosted core extension package boundary cache",
);
expect(boundaryMount.with.key).toBe("${{ github.repository }}-ext-boundary-v2");
expect(lintMount.with.key).toBe(boundaryMount.with.key);
for (const gate of [boundaryMount, lintMount]) {
expect(gate.if).toContain("vars.OPENCLAW_CI_RUNNER_BACKEND != 'github'");
}
expect(hostedLintCache.if).toBe(
"needs.preflight.outputs.cache_mode != 'off' && matrix.task == 'lint' && steps.extension-boundary-inputs.outputs.enabled == 'true' && (needs.preflight.outputs.runner_profile == 'github' || needs.preflight.outputs.runner_profile == 'hybrid')",
);
expect(boundaryCache.if).toBe(
"needs.preflight.outputs.cache_mode != 'off' && matrix.group == 'extension-package-boundary' && steps.extension-boundary-inputs.outputs.enabled == 'true'",
);
expect(hostedCoreCache.if).toBe(
"needs.preflight.outputs.cache_mode != 'off' && needs.preflight.outputs.runner_profile == 'github' && !inputs.release_gate && steps.extension-boundary-inputs.outputs.enabled == 'true'",
);
for (const cache of [hostedLintCache, hostedCoreCache]) {
expect(cache.uses).toBe(CACHE_V5);
expect(cache.with).toEqual(boundaryCache.with);
}
const fingerprintReference = "${{ steps.extension-boundary-inputs.outputs.fingerprint }}";
expect(boundaryCache.with.key).toBe(
"${{ runner.os }}-extension-package-boundary-v4-${{ steps.extension-boundary-inputs.outputs.fingerprint }}",
);
expect(boundaryCache.with.path.trim().split("\n")).toEqual([
"packages/plugin-sdk/dist",
".artifacts/extension-package-boundary/plugins",
".artifacts/extension-package-boundary/*.json",
".artifacts/extension-package-boundary/compile",
]);
const fingerprintSteps = [additionalJob, checkShardJob, hostedCoreJob].map((job) =>
expectDefined(
job.steps.find(
(step: WorkflowStep) => step.name === "Compute extension boundary input fingerprint",
),
"extension boundary input fingerprint step",
),
);
for (const step of fingerprintSteps) {
expect(step.id).toBe("extension-boundary-inputs");
expect(step.run).toContain('fingerprint="$(git rev-parse HEAD)"');
expect(step.run).toContain('echo "enabled=false" >> "$GITHUB_OUTPUT"');
}
expect(fingerprintSteps[0]?.run).toBe(fingerprintSteps[1]?.run);
expect(fingerprintSteps[1]?.run).toBe(fingerprintSteps[2]?.run);
expect(fingerprintSteps[2]?.if).toBe(
"needs.preflight.outputs.runner_profile == 'github' && !inputs.release_gate",
);
expect(hostedCoreJob.steps.indexOf(fingerprintSteps[2])).toBeLessThan(
hostedCoreJob.steps.indexOf(hostedCoreCache),
);
expect(hostedCoreJob.steps.indexOf(hostedCoreCache)).toBeLessThan(
hostedCoreJob.steps.findIndex(
(step: WorkflowStep) => step.name === "Run hosted core lint stripe",
),
);
expect(
hostedCoreJob.steps.some((step: WorkflowStep) =>
step.uses?.startsWith("actions/cache/save@"),
),
).toBe(false);
const warmerBoundaryRestore = expectDefined(
warmer.jobs.warm.steps.find(
(step: WorkflowStep) => step.name === "Restore native SDK boundary cache",
),
"warmer boundary restore",
);
const warmerBoundarySave = expectDefined(
warmer.jobs.warm.steps.find(
(step: WorkflowStep) => step.name === "Save native SDK boundary cache",
),
"warmer boundary save",
);
expect(warmerBoundaryRestore.with.path).toBe(boundaryCache.with.path);
expect(warmerBoundaryRestore.with["restore-keys"]).toBe(boundaryCache.with["restore-keys"]);
expect(warmerBoundarySave.with.path).toBe(boundaryCache.with.path);
// Single semantic writer: protected pushes commit explicitly (not
// on-change/if-missing, whose allocated-byte heuristic can strand a stale
// marker); PR clones and the lint consumer stay read-only.
expect(boundaryMount.with.commit).toBe(
"${{ github.event_name != 'pull_request' && 'true' || 'false' }}",
);
expect(lintMount.with.commit).toBe("false");
// Transport keys use the same commit; native owner records independently
// validate source content and output integrity after restoration.
const restoreStep = additionalJob.steps.find(
(step: WorkflowStep) => step.name === "Restore extension boundary artifacts from sticky disk",
);
const lintRestoreStep = checkShardJob.steps.find(
(step: WorkflowStep) => step.name === "Restore extension boundary artifacts from sticky disk",
);
const seedStep = additionalJob.steps.find(
(step: WorkflowStep) => step.name === "Seed extension boundary sticky disk",
);
for (const gate of [restoreStep, lintRestoreStep, seedStep]) {
expect(gate.run).toContain(fingerprintReference);
expect(gate.run).toContain(".source-fingerprint");
expect(gate.run).not.toContain("git rev-parse HEAD:");
expect(gate.run).not.toContain("BOUNDARY_CONFIG_HASH");
expect(gate.if).toContain("vars.OPENCLAW_CI_RUNNER_BACKEND != 'github'");
}
// Seeding is writer-only work: PR mounts never commit, so seeding there
// would burn wall clock on a discarded clone.
expect(seedStep.if).toContain("github.event_name != 'pull_request'");
expect(seedStep.if).toContain("steps.boundary-sticky-restore.outputs.restored == 'false'");
expect(seedStep.run).toContain(
"rsync -aR --exclude='*.lock*' .artifacts/extension-package-boundary",
);
for (const step of [restoreStep, lintRestoreStep]) {
expect(step.run).toContain("for payload in packages .artifacts;");
}
});
it("keeps the Gradle sticky disk on O(1) per-task protected keys", () => {
const workflow = readCiWorkflow();
const androidSteps = workflow.jobs.android.steps as WorkflowStep[];
const mountWith = expectDefined(
androidSteps.find((step) => step.name === "Mount Gradle sticky disk")?.with,
"Gradle sticky mount step",
);
const pointStep = expectDefined(
androidSteps.find((step) => step.name === "Point Gradle at the sticky disk"),
"Gradle sticky point step",
);
const pointEnv = expectDefined(pointStep.env, "Gradle sticky point step env");
// Task scope stays in the key (a light task like ktlint must never seed
// heavy build lanes), but PR number and dependency hash must not: those
// minted a backing disk per PR/bump until Blacksmith's installation-wide
// budget 429-failed every mount fleet-wide.
expect(mountWith.key).toBe("${{ github.repository }}-gradle-v2-${{ matrix.task }}");
expect(androidSteps.find((step) => step.name === "Mount Gradle sticky disk")?.if).toContain(
"vars.OPENCLAW_CI_RUNNER_BACKEND != 'github'",
);
expect(pointStep.if).toContain("vars.OPENCLAW_CI_RUNNER_BACKEND != 'github'");
// Single semantic writer: protected pushes commit explicitly (on-change's
// allocated-byte heuristic can miss a same-size refresh and strand the
// fingerprint marker); PR clones stay read-only.
expect(mountWith.commit).toBe(
"${{ github.event_name != 'pull_request' && 'true' || 'false' }}",
);
// The dependency hash moved from the key into a runtime fingerprint that
// bounds disk growth: the writer rebuilds cold when inputs change so
// retired artifacts do not accumulate on the O(1) key forever.
expect(pointEnv.GRADLE_DEPS_FINGERPRINT).toContain("hashFiles(");
expect(pointEnv.GRADLE_DEPS_FINGERPRINT).toContain("apps/android/gradle/libs.versions.toml");
expect(pointEnv.STICKY_WRITER).toContain("github.event_name != 'pull_request'");
expect(pointStep.run).toContain(".openclaw-gradle-deps-fingerprint");
expect(pointStep.run).toContain('rm -rf "$sticky_root/gradle-user-home"');
});
it("caches Robolectric SDK artifacts for Android test tasks only", () => {
const workflowSource = readFileSync(".github/workflows/ci.yml", "utf8");
const androidSteps = readCiWorkflow().jobs.android.steps as WorkflowStep[];
const restoreIndex = androidSteps.findIndex(
(step) => step.name === "Restore Robolectric Maven cache",
);
const configureIndex = androidSteps.findIndex(
(step) => step.name === "Configure Robolectric Maven cache",
);
const runIndex = androidSteps.findIndex(
(step) => step.name === "Run Android ${{ matrix.task }}",
);
const saveIndex = androidSteps.findIndex(
(step) => step.name === "Save Robolectric Maven cache",
);
const restoreStep = expectDefined(androidSteps[restoreIndex], "Robolectric cache restore");
const configureStep = expectDefined(
androidSteps[configureIndex],
"Robolectric cache configuration",
);
const runStep = expectDefined(androidSteps[runIndex], "Android task runner");
const saveStep = expectDefined(androidSteps[saveIndex], "Robolectric cache save");
expect([restoreIndex, configureIndex, runIndex, saveIndex]).toEqual(
[restoreIndex, configureIndex, runIndex, saveIndex].toSorted((a, b) => a - b),
);
expect(restoreStep).toMatchObject({
id: "robolectric-cache",
if: "startsWith(matrix.task, 'test-') && needs.preflight.outputs.cache_mode != 'off'",
uses: CACHE_V5,
with: {
path: "/var/tmp/openclaw-robolectric-m2",
},
});
const cacheKey = String(restoreStep.with?.key);
expect(cacheKey).toContain("${{ github.repository }}-robolectric-m2-v1-");
expect(cacheKey).toContain("${{ runner.os }}-${{ runner.arch }}-${{ matrix.task }}-");
expect(cacheKey).toContain("apps/android/**/*.gradle*");
expect(cacheKey).toContain("apps/android/**/gradle-wrapper.properties");
expect(cacheKey).toContain("apps/android/gradle/libs.versions.toml");
expect(cacheKey).toContain("apps/android/**/src/test*/**");
for (const forbiddenDimension of [
"github.run_id",
"github.sha",
"github.ref",
"github.event.pull_request.number",
]) {
expect(cacheKey).not.toContain(forbiddenDimension);
}
expect(String(restoreStep.with?.["restore-keys"]).trim()).toBe(
"${{ github.repository }}-robolectric-m2-v1-${{ runner.os }}-${{ runner.arch }}-${{ matrix.task }}-",
);
expect(configureStep.if).toBe("startsWith(matrix.task, 'test-')");
expect(configureStep.run).toContain("OPENCLAW_ROBOLECTRIC_M2");
expect(configureStep.run).toContain("OPENCLAW_ROBOLECTRIC_INIT");
expect(configureStep.run).toContain(
'systemProperty "maven.repo.local", System.getenv("OPENCLAW_ROBOLECTRIC_M2")',
);
expect(workflowSource).not.toContain("robolectric.dependency.repo.url");
expect(saveStep).toMatchObject({
if: "success() && startsWith(matrix.task, 'test-') && needs.preflight.outputs.cache_write_allowed == 'true' && steps.robolectric-cache.outputs.cache-hit != 'true'",
uses: CACHE_SAVE_V5,
with: {
key: "${{ steps.robolectric-cache.outputs.cache-primary-key }}",
path: "/var/tmp/openclaw-robolectric-m2",
},
});
const taskCases = new Map(
[...String(runStep.run).matchAll(/^\s{2}([a-z-]+)\)\n([\s\S]*?)^\s{4};;$/gmu)].map(
(match) => [match[1], match[2]],
),
);
for (const task of ["test-play", "test-play-compat", "test-third-party", "test-wear"]) {
expect(taskCases.get(task), task).toContain('--init-script "$OPENCLAW_ROBOLECTRIC_INIT"');
}
for (const task of ["build-play", "build-wear", "build-play-compat", "ktlint"]) {
expect(taskCases.get(task), task).not.toContain("--init-script");
}
expect(runStep.run).not.toMatch(/\bsleep\b/u);
expect(runStep.run).not.toMatch(/\bretry\b/iu);
});
it("retains Android test XML and JVM diagnostics after failures without collecting caches or canceled jobs", () => {
const steps = readCiWorkflow().jobs.android.steps as WorkflowStep[];
const runIndex = steps.findIndex((step) => step.name === "Run Android ${{ matrix.task }}");
const uploadIndex = steps.findIndex((step) => step.name === "Upload Android test reports");
const upload = expectDefined(steps[uploadIndex], "Android test reports");
expect(uploadIndex).toBeGreaterThan(runIndex);
expect(upload.with?.["retention-days"]).toBe(14);
// A status function prevents Actions' implicit success() from hiding failed-test evidence.
expect(upload.if).toMatch(/\b(?:always|cancelled|failure|success)\(\)/u);
for (const [task, failed, cancelled, expected] of [
["test-play", false, false, true],
["test-play", true, false, true],
["test-play-compat", true, false, true],
["test-third-party", true, false, true],
["test-wear", false, false, true],
["test-wear", true, true, false],
["test-play", false, true, false],
["build-play", false, false, false],
["build-wear", true, false, false],
["ktlint", false, false, false],
] as const) {
expect(
evaluateWorkflowExpression(upload.if, {
eventName: "push",
repository: "openclaw/openclaw",
runAttempt: 1,
matrix: { task },
failed,
cancelled,
}),
`${task}: failed=${failed}, cancelled=${cancelled}`,
).toBe(expected);
}
const root = tempDirs.make("openclaw-android-test-reports-");
const reports = [
"apps/android/app/build/test-results/testPlayDebugUnitTest/TEST-Play.xml",
"apps/android/app/build/test-results/testThirdPartyDebugUnitTest/TEST-ThirdParty.xml",
"apps/android/wear/build/test-results/testDebugUnitTest/TEST-Wear.xml",
"apps/android/wear-shared/build/test-results/testDebugUnitTest/TEST-Shared.xml",
"apps/android/hs_err_pid101.log",
"apps/android/replay_pid101.log",
"apps/android/app/hs_err_pid202.log",
"apps/android/app/replay_pid202.log",
"apps/android/wear/hs_err_pid303.log",
"apps/android/wear-shared/replay_pid404.log",
];
const unrelated = [
"apps/android/app/build/test-results/testPlayDebugUnitTest/binary/results.bin",
"apps/android/app/build/reports/lint-results-playDebug.xml",
"apps/android/app/build/outputs/apk/play/debug/app.apk",
"apps/android/benchmark/build/test-results/testDebugUnitTest/TEST-Benchmark.xml",
".gradle/caches/TEST-cached.xml",
".gradle/caches/hs_err_pid505.log",
"hs_err_pid606.log",
"replay_pid606.log",
"apps/android/app/core.202",
"apps/android/app/java_pid202.hprof",
];
for (const file of [...reports, ...unrelated]) {
mkdirSync(path.dirname(path.join(root, file)), { recursive: true });
writeFileSync(path.join(root, file), "synthetic report fixture");
}
const patterns = String(upload.with?.path).trim().split("\n");
expect(globSync(patterns, { cwd: root }).toSorted()).toEqual(reports.toSorted());
});
it("never keys a Blacksmith sticky disk by unbounded run dimensions", () => {
// Blacksmith caps backing disks per installation; per-PR, per-commit,
// per-run, or per-hash key segments mint disks until every mount 429s.
// Snapshot validity belongs in in-job fingerprints/markers, never the key.
const workflowFiles = readdirSync(".github/workflows")
.filter((name) => name.endsWith(".yml"))
.map((name) => `.github/workflows/${name}`);
const actionFiles = readdirSync(".github/actions").map(
(name) => `.github/actions/${name}/action.yml`,
);
const stickyKeys: Array<{ file: string; key: string }> = [];
for (const file of [...workflowFiles, ...actionFiles]) {
if (!existsSync(file)) {
continue;
}
const parsed = parse(readFileSync(file, "utf8"));
const jobs = parsed?.jobs ? Object.values(parsed.jobs) : [];
const stepLists = [
...jobs.map((job) => (job as { steps?: WorkflowStep[] }).steps ?? []),
(parsed?.runs?.steps ?? []) as WorkflowStep[],
];
for (const step of stepLists.flat()) {
if (typeof step?.uses !== "string" || !step.uses.startsWith("useblacksmith/stickydisk@")) {
continue;
}
const key = step.with?.key;
stickyKeys.push({ file, key: typeof key === "string" ? key : "" });
}
}
expect(stickyKeys.length).toBeGreaterThan(0);
for (const { file, key } of stickyKeys) {
expect(key, file).not.toContain("github.event.pull_request.number");
expect(key, file).not.toContain("github.sha");
expect(key, file).not.toContain("github.ref");
expect(key, file).not.toContain("github.run_");
expect(key, file).not.toContain("hashFiles(");
}
});
it("deletes only exact allowlisted retired sticky disks from protected main", () => {
const cleanupSource = readFileSync(".github/workflows/sticky-disk-cleanup.yml", "utf8");
const cleanup = parse(cleanupSource);
const job = cleanup.jobs.delete;
const checkoutStep = job.steps.find(
(step: WorkflowStep) => step.name === "Checkout protected manifest",
);
const validateStep = job.steps.find(
(step: WorkflowStep) => step.name === "Validate exact retired key",
);
const deleteStep = job.steps.find(
(step: WorkflowStep) => step.name === "Delete retired sticky disk",
);
const retiredDisks = JSON.parse(
readFileSync(".github/retired-sticky-disks.json", "utf8"),
) as Array<{ architecture?: unknown; key?: unknown; region?: unknown }>;
expect(Array.isArray(retiredDisks)).toBe(true);
expect(
retiredDisks.every(
(disk) =>
typeof disk.key === "string" &&
disk.key.length > 0 &&
disk.key === disk.key.trim() &&
(disk.architecture === "amd64" || disk.architecture === "arm64") &&
typeof disk.region === "string" &&
disk.region.length > 0 &&
disk.region === disk.region.trim(),
),
).toBe(true);
expect(
new Set(
retiredDisks.map(
(disk) => `${disk.key as string}:${disk.architecture as string}:${disk.region as string}`,
),
).size,
).toBe(retiredDisks.length);
expect(cleanup.on).toHaveProperty("workflow_dispatch");
expect(cleanup.permissions).toEqual({ contents: "read" });
expect(cleanup.concurrency).toEqual({
group: "sticky-disk-cleanup",
"cancel-in-progress": false,
});
expect(job.if).toContain("github.ref == 'refs/heads/main'");
expect(job.if).toContain("inputs.confirm");
expect(checkoutStep.with.ref).toBe("refs/heads/main");
expect(job["runs-on"]).toContain("inputs.architecture == 'arm64'");
expect(validateStep.env.RETIRED_ARCHITECTURE).toBe("${{ inputs.architecture }}");
expect(validateStep.env.RETIRED_KEY).toBe("${{ inputs.retired_key }}");
expect(validateStep.env.RETIRED_REGION).toBe("${{ inputs.region }}");
expect(validateStep.run).toContain('process.env.BLACKSMITH_ENV?.includes("arm")');
expect(validateStep.run).toContain("requestedRegion !== process.env.BLACKSMITH_REGION");
expect(validateStep.run).toContain("requestedKey !== requestedKey.trim()");
expect(validateStep.run).toContain("disk?.key === requestedKey");
const rejectedKey = runWorkflowShellScript(validateStep.run, {
env: {
...process.env,
BLACKSMITH_ENV: "production-amd64",
BLACKSMITH_REGION: "us-test-1",
RETIRED_ARCHITECTURE: "amd64",
RETIRED_KEY: "openclaw/openclaw-not-retired",
RETIRED_REGION: "us-test-1",
},
});
expect(rejectedKey.status).not.toBe(0);
expect(rejectedKey.stderr).toContain("identity is not allowlisted for retirement");
const paddedKey = runWorkflowShellScript(validateStep.run, {
env: {
...process.env,
BLACKSMITH_ENV: "production-amd64",
BLACKSMITH_REGION: "us-test-1",
RETIRED_ARCHITECTURE: "amd64",
RETIRED_KEY: " openclaw/openclaw-active-key ",
RETIRED_REGION: "us-test-1",
},
});
expect(paddedKey.status).not.toBe(0);
expect(paddedKey.stderr).toContain("key must be non-empty and canonical");
expect(deleteStep).toMatchObject({
uses: "useblacksmith/stickydisk-delete@3bd8d43f9da764c6b80c2cd6db129bdb568c79b6",
with: {
"delete-docker-cache": "false",
"delete-key": "${{ inputs.retired_key }}",
},
});
// A retired-key entry must never match any disk family still mounted by
// the repository. Expressions stand for one non-empty resolved segment.
const workflowFiles = readdirSync(".github/workflows")
.filter((name) => name.endsWith(".yml"))
.map((name) => `.github/workflows/${name}`);
const actionFiles = readdirSync(".github/actions").map(
(name) => `.github/actions/${name}/action.yml`,
);
const activeKeyPatterns: RegExp[] = [];
for (const file of [...workflowFiles, ...actionFiles]) {
if (!existsSync(file)) {
continue;
}
const parsed = parse(readFileSync(file, "utf8"));
const jobs = parsed?.jobs ? Object.values(parsed.jobs) : [];
const stepLists = [
...jobs.map((candidate) => (candidate as { steps?: WorkflowStep[] }).steps ?? []),
(parsed?.runs?.steps ?? []) as WorkflowStep[],
];
for (const step of stepLists.flat()) {
if (typeof step?.uses !== "string" || !step.uses.startsWith("useblacksmith/stickydisk@")) {
continue;
}
const key = step.with?.key;
if (typeof key !== "string") {
continue;
}
const escapedParts = key
.split(/\$\{\{[^}]+\}\}/u)
.map((part) => part.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"));
activeKeyPatterns.push(new RegExp(`^${escapedParts.join(".+")}$`, "u"));
}
}
for (const retiredDisk of retiredDisks) {
expect(
activeKeyPatterns.some((pattern) => pattern.test(retiredDisk.key as string)),
`${retiredDisk.key as string} is still an active sticky-disk key`,
).toBe(false);
}
});
it("selects every supplemental boundary check exactly once across the CI matrix", () => {
const job = readCiWorkflow().jobs["check-additional-shard"];
const step = job.steps.find(
(entry: WorkflowStep) => entry.name === "Run additional check shard",
);
const selector = String(step?.env?.OPENCLAW_ADDITIONAL_BOUNDARY_SHARD ?? "");
const rows = readFrozenAdditionalCheckRows();
const selected = rows
.filter((row) => row.group === "boundaries")
.flatMap(() => selectChecksForShard(BOUNDARY_CHECKS, selector));
expect(selected.toSorted((left, right) => left.label.localeCompare(right.label))).toEqual(
BOUNDARY_CHECKS.toSorted((left, right) => left.label.localeCompare(right.label)),
);
});
it("runs all source checks serially and preserves individual failures", () => {
const additionalJob = readCiWorkflow().jobs["check-additional-shard"];
const runStep = additionalJob.steps.find(
(step: WorkflowStep) => step.name === "Run additional check shard",
);
const sessionCommands = [
"lint:tmp:session-accessor-boundary",
"lint:tmp:sqlite-transaction-boundary",
"lint:tmp:session-transcript-reader-boundary",
];
const root = tempDirs.make("openclaw-session-boundary-workflow-");
const binDir = path.join(root, "bin");
const callsPath = path.join(root, "pnpm-calls.txt");
mkdirSync(binDir);
const pnpmPath = path.join(binDir, "pnpm");
writeFileSync(
pnpmPath,
'#!/usr/bin/env bash\nset -euo pipefail\nprintf \'%s\\n\' "$*" >> "$PNPM_CALLS"\nif [[ "${2:-}" == "${PNPM_FAIL:-}" ]]; then exit 1; fi\n',
"utf8",
);
chmodSync(pnpmPath, 0o755);
const exportScript = path.join(root, "scripts/check-export-name-collisions.mts");
mkdirSync(path.dirname(exportScript));
for (const [group, commands] of [
[
"source-contracts",
["lint:tmp:export-name-collisions", ...sessionCommands, "sqlite:sessions-schema:check"],
],
["session-accessor-boundary", sessionCommands],
["export-name-collisions", ["lint:tmp:export-name-collisions"]],
["sqlite-session-schema-baseline", ["sqlite:sessions-schema:check"]],
] as const) {
for (const scenario of [
{ failed: "", missing: "", missingFile: false },
...commands.map((failed) => ({ failed, missing: "", missingFile: false })),
...(group === "source-contracts"
? [
...commands.map((missing) => ({ failed: "", missing, missingFile: false })),
{ failed: "", missing: "", missingFile: true },
]
: []),
]) {
const present = commands.filter(
(command) =>
command !== scenario.missing &&
!(scenario.missingFile && command === "lint:tmp:export-name-collisions"),
);
if (scenario.missingFile) {
rmSync(exportScript, { force: true });
} else {
writeFileSync(exportScript, "");
}
writeFileSync(
path.join(root, "package.json"),
JSON.stringify({
scripts: Object.fromEntries(
commands
.filter((command) => command !== scenario.missing)
.map((command) => [command, "fixture"]),
),
}),
);
writeFileSync(callsPath, "");
const result = runWorkflowShellScript(runStep.run, {
cwd: root,
env: {
...process.env,
ADDITIONAL_CHECK_GROUP: group,
PATH: `${binDir}:${process.env.PATH ?? ""}`,
PNPM_CALLS: callsPath,
PNPM_FAIL: scenario.failed,
},
});
const context = `${group} ${JSON.stringify(scenario)}\n${result.stdout}${result.stderr}`;
expect(readFileSync(callsPath, "utf8").trim().split("\n").filter(Boolean), context).toEqual(
present.map((command) => `run ${command}`),
);
expect(result.status, context).toBe(scenario.failed ? 1 : 0);
expect(result.stdout.match(/^::error .+$/gmu) ?? [], context).toEqual(
scenario.failed
? [`::error title=${scenario.failed} failed::${scenario.failed} failed`]
: [],
);
for (const command of present.filter((entry) => entry !== scenario.failed)) {
expect(result.stdout, context).toContain(`[ok] ${command}`);
}
expect(result.stdout.match(/^\[skip\].+$/gmu) ?? [], context).toHaveLength(
scenario.missing || scenario.missingFile ? 1 : 0,
);
}
}
});
it("groups current source checks and allocates SDK reports only for dispatch", () => {
const workflow = readCiWorkflow();
const additionalJob = workflow.jobs["check-additional-shard"];
expect(additionalJob.strategy.matrix).toBe(
"${{ fromJSON(needs.preflight.outputs.check_additional_matrix) }}",
);
expect(workflow.jobs.preflight.outputs.check_additional_matrix).toBe(
"${{ steps.manifest.outputs.check_additional_matrix }}",
);
const frozenGroups = [
"boundaries",
"prompt-snapshots",
"export-name-collisions",
"session-accessor-boundary",
"sqlite-session-schema-baseline",
"plugin-sdk-api-diff",
"extension-package-boundary",
"runtime-topology-architecture",
];
for (const [eventName, frozen] of [
["push", false],
["pull_request", false],
["workflow_dispatch", false],
["workflow_dispatch", true],
] as const) {
const manifest = runCiManifestFixture({
bundledPlanner: true,
eventName,
historicalCompatibility: frozen,
changedPaths: [],
scopeEnv: {
OPENCLAW_CI_CHECKOUT_REVISION: "a".repeat(40),
OPENCLAW_CI_WORKFLOW_REVISION: (frozen ? "b" : "a").repeat(40),
},
});
expect(manifest.status, manifest.output).toBe(0);
const rows = JSON.parse(
expectDefined(manifest.outputs.check_additional_matrix, "additional check matrix"),
).include;
const expectedGroups = frozen
? frozenGroups
: [
"boundaries",
"prompt-snapshots",
"source-contracts",
...(eventName === "workflow_dispatch" ? ["plugin-sdk-api-diff"] : []),
"extension-package-boundary",
"runtime-topology-architecture",
];
expect(rows.map((row: { group: string }) => row.group)).toEqual(expectedGroups);
expect(manifest.outputs.run_check_additional).toBe("true");
for (const row of rows) {
if (row.group === "source-contracts") {
expect(row).toEqual({
check_name: "check-source-contracts",
group: "source-contracts",
runner: "blacksmith-4vcpu-ubuntu-2404",
});
} else {
expect(readFrozenAdditionalCheckRows()).toContainEqual(row);
}
}
}
for (const selection of [{ runNode: false }, { nodeFastOnly: true }]) {
const manifest = runCiManifestFixture({
bundledPlanner: true,
eventName: "pull_request",
changedPaths: [],
...selection,
});
expect(manifest.status, manifest.output).toBe(0);
expect(manifest.outputs.run_check_additional).toBe("false");
expect(
JSON.parse(
expectDefined(manifest.outputs.check_additional_matrix, "additional check matrix"),
).include,
).toEqual([]);
}
expect(workflow.jobs.preflight.outputs.diff_head_revision).toBe(
"${{ steps.diff_base.outputs.head_sha }}",
);
const ensureHeadStep = additionalJob.steps.find(
(step: WorkflowStep) => step.name === "Ensure Plugin SDK API diff head commit",
);
expect(ensureHeadStep.with["base-sha"]).toBe(
"${{ needs.preflight.outputs.diff_head_revision }}",
);
expect(ensureHeadStep.with["fetch-ref"]).toContain("refs/pull/{0}/merge");
for (const revision of ["base", "head"]) {
const ensureRevisionStep = additionalJob.steps.find(
(step: WorkflowStep) => step.name === `Ensure Plugin SDK API diff ${revision} commit`,
);
for (const [eventName, group, eligible] of [
["pull_request", "plugin-sdk-api-diff", false],
["push", "plugin-sdk-api-diff", false],
["workflow_dispatch", "plugin-sdk-api-diff", true],
["workflow_dispatch", "boundaries", false],
] as const) {
expect(
evaluateWorkflowExpression(`\${{ ${ensureRevisionStep.if} }}`, {
eventName,
matrix: { group },
repository: "openclaw/openclaw",
runAttempt: 1,
}),
`${revision} preparation for ${eventName}/${group}`,
).toBe(eligible);
}
}
const runStep = additionalJob.steps.find(
(step: WorkflowStep) => step.name === "Run additional check shard",
);
expect(runStep.run).toContain("plugin-sdk-api-diff)");
expect(runStep.run).toContain('run_check "plugin-sdk:api:diff" pnpm run plugin-sdk:api:diff');
expect(runStep.run).toContain('--base "${{ needs.preflight.outputs.diff_base_revision }}"');
expect(runStep.run).toContain('--head "${{ needs.preflight.outputs.diff_head_revision }}"');
expect(runStep.run).not.toContain('--head "${{ needs.preflight.outputs.checkout_revision }}"');
});
it("uses the current SDK diff and preserves the historical baseline check", () => {
const workflow = readCiWorkflow();
const runStep = workflow.jobs["check-additional-shard"].steps.find(
(step: WorkflowStep) => step.name === "Run additional check shard",
);
const runCase = (
scripts: Record<string, string>,
compatibilityTarget: boolean,
eventName = "workflow_dispatch",
fail = false,
) => {
const root = tempDirs.make("openclaw-plugin-sdk-api-workflow-");
const binDir = path.join(root, "bin");
const callsPath = path.join(root, "pnpm-calls.txt");
const summaryPath = path.join(root, "summary.md");
mkdirSync(binDir);
writeFileSync(path.join(root, "package.json"), JSON.stringify({ scripts }), "utf8");
const pnpmPath = path.join(binDir, "pnpm");
writeFileSync(
pnpmPath,
'#!/usr/bin/env bash\nset -euo pipefail\nprintf \'%s\\n\' "$*" >> "$PNPM_CALLS"\nexit "$PNPM_RESULT"\n',
"utf8",
);
chmodSync(pnpmPath, 0o755);
const script = runStep.run
.replaceAll("${{ needs.preflight.outputs.diff_base_revision }}", "base-sha")
.replaceAll("${{ needs.preflight.outputs.diff_head_revision }}", "synthetic-head-sha");
const result = runWorkflowShellScript(script, {
cwd: root,
env: {
...process.env,
ADDITIONAL_CHECK_GROUP: "plugin-sdk-api-diff",
COMPATIBILITY_TARGET: compatibilityTarget ? "true" : "false",
GITHUB_EVENT_NAME: eventName,
GITHUB_STEP_SUMMARY: summaryPath,
PATH: `${binDir}:${process.env.PATH ?? ""}`,
PNPM_CALLS: callsPath,
PNPM_RESULT: fail ? "1" : "0",
RUN_PROMPT_SNAPSHOTS: "false",
},
});
return {
calls: existsSync(callsPath) ? readFileSync(callsPath, "utf8").trim().split("\n") : [],
result,
summaryPath,
};
};
// Pure reporting: pushes and PRs skip the diff; dispatches (including
// release validation) still produce it.
for (const eventName of ["push", "pull_request"]) {
const skipped = runCase({ "plugin-sdk:api:diff": "mock" }, false, eventName);
expect(skipped.result.status, skipped.result.stderr).toBe(0);
expect(skipped.calls).toEqual([]);
expect(skipped.result.stdout).toContain("manual and release dispatches only");
}
const current = runCase({ "plugin-sdk:api:diff": "mock" }, false);
expect(current.result.status, current.result.stderr).toBe(0);
expect(current.calls).toEqual([
"run plugin-sdk:api:diff -- --base base-sha --head synthetic-head-sha --json .artifacts/plugin-sdk-api-diff.json --summary " +
current.summaryPath,
]);
const failed = runCase({ "plugin-sdk:api:diff": "mock" }, false, "workflow_dispatch", true);
expect(failed.result.status, failed.result.stderr).toBe(1);
expect(failed.calls).toHaveLength(1);
expect(failed.result.stdout).toContain(
"::error title=plugin-sdk:api:diff failed::plugin-sdk:api:diff failed",
);
const historical = runCase({ "plugin-sdk:api:check": "mock" }, true);
expect(historical.result.status, historical.result.stderr).toBe(0);
expect(historical.calls).toEqual(["run plugin-sdk:api:check"]);
const missingCurrent = runCase({ "plugin-sdk:api:check": "mock" }, false);
expect(missingCurrent.result.status).toBe(1);
expect(missingCurrent.calls).toEqual([]);
expect(missingCurrent.result.stdout).toContain(
"Current CI targets must provide plugin-sdk:api:diff.",
);
});
it("retains fetch deadlines in other standalone workflows", () => {
const workflowPaths = [[".github/workflows/crabbox-hydrate.yml", "30s"]] as const;
for (const [workflowPath, timeoutSeconds] of workflowPaths) {
const workflow = readFileSync(workflowPath, "utf8");
const fetchTimeouts = workflow.match(
new RegExp(
`timeout --signal=TERM[^\\n]* ${timeoutSeconds} git(?: -C "(?:\\$workdir|\\$GITHUB_WORKSPACE|clawhub-source)")?`,
"g",
),
);
expect(fetchTimeouts?.length, workflowPath).toBeGreaterThan(0);
expect(
fetchTimeouts?.every((line) =>
line.startsWith(`timeout --signal=TERM --kill-after=10s ${timeoutSeconds} git`),
),
workflowPath,
).toBe(true);
}
});
it("owns Docs Agent Git without changing cadence, deadlines, or action authority", () => {
const source = readFileSync(".github/workflows/docs-agent.yml", "utf8");
const workflow = parse(source);
const job = workflow.jobs["update-docs"];
const steps = job.steps as WorkflowStep[];
expect(steps.map(({ name }) => name)).toEqual([
"Checkout",
"Prepare Git owner",
"Gate trusted main activity and hourly cadence",
"Setup Node environment",
"Ensure docs agent key exists",
"Run Codex docs agent",
"Enforce existing-docs-only patch",
"Restore Node 24 path",
"Check docs",
"Commit docs updates",
]);
expect(steps[1]).toEqual({
name: "Prepare Git owner",
uses: "openclaw/openclaw/.github/actions/git-owner@dd4528b6393e7d00063067a080ca7241b48ce475",
});
expect(steps[0]).toMatchObject({
uses: CHECKOUT_V6,
with: {
ref: "main",
"fetch-depth": 0,
"persist-credentials": false,
submodules: false,
},
});
expect(job["timeout-minutes"]).toBe(30);
expect(workflow.permissions).toEqual({ actions: "read", contents: "write" });
expect(workflow.concurrency).toEqual({ group: "docs-agent-main", "cancel-in-progress": false });
expect(steps[5]).toEqual({
name: "Run Codex docs agent",
if: "steps.gate.outputs.run_agent == 'true'",
uses: "openai/codex-action@52fe01ec70a42f454c9d2ebd47598f9fd6893d56",
env: {
DOCS_AGENT_BASE_SHA: "${{ steps.gate.outputs.review_base_sha }}",
DOCS_AGENT_HEAD_SHA: "${{ steps.gate.outputs.review_head_sha }}",
},
with: {
"openai-api-key":
"${{ secrets.OPENCLAW_DOCS_AGENT_OPENAI_API_KEY || secrets.OPENAI_API_KEY }}",
"prompt-file": ".github/codex/prompts/docs-agent.md",
model: "${{ vars.OPENCLAW_CI_OPENAI_MODEL_BARE }}",
effort: "medium",
sandbox: "workspace-write",
"safety-strategy": "drop-sudo",
"codex-args": '["--full-auto"]',
},
});
const gate = expectDefined(steps[2]?.run, "gate policy");
const commit = expectDefined(steps[9]?.run, "commit policy");
const enforce = expectDefined(steps[6]?.run, "enforcement producers");
expect(gate.match(/python3 -I -S "\$CI_GIT_OWNER" --policy -/gu)).toHaveLength(2);
expect(gate.indexOf("--policy -")).toBeLessThan(gate.indexOf("gh api"));
expect(gate.lastIndexOf("--policy -")).toBeGreaterThan(gate.indexOf("gh api"));
expect(commit).toContain('exec python3 -I -S "$CI_GIT_OWNER" --policy -');
for (const policy of [gate, commit]) {
expect(policy.match(/for attempt in range\(1, 6\):/gu)).toHaveLength(1);
expect(policy).toContain("except (GitFailure, FetchTimeout):");
expect(policy).not.toMatch(
/except (?:Exception|BaseException)|except:|error\.code|\$\?|\|\| true/u,
);
}
expect(gate).toContain(
'if attempt == 5:\n print("Failed to fetch main after retries.", file=sys.stderr)\n raise SystemExit(1)',
);
expect(gate.match(/backoff\(attempt \* 2\)/gu)).toHaveLength(1);
expect(commit.match(/backoff\(attempt \* 2\)/gu)).toHaveLength(2);
const calls = [
...`${gate}\n${commit}`.matchAll(/(?:run_git|git_output)\(([\s\S]*?)\)(?=\.rstrip|\n|$)/gu),
].map((match) => match[1]!);
const fetches = calls.filter((call) => call.startsWith('workspace, "fetch"'));
expect(fetches).toEqual([
'workspace, "fetch", "--no-tags", "origin", "main", timeout=120, reclaim_locks=True',
'workspace, "fetch", "--no-tags", "origin", target, timeout=120, reclaim_locks=True',
]);
expect(calls.filter((call) => call.includes("timeout="))).toEqual(fetches);
expect(enforce.match(/--checkout-git 0 (?:ls-files|diff)/gu)).toHaveLength(5);
expect(`${gate}\n${commit}\n${enforce}`).not.toMatch(
/\btimeout --|\bgit (?:fetch|rev-parse|cat-file|diff|ls-files|config|add|commit|push)\b/u,
);
// The corrected REST cadence contract is deliberately byte-stable across Git migration.
const cadence = source.slice(
source.indexOf(" runs_json="),
source.indexOf(' python3 -I -S "$CI_GIT_OWNER" --policy - "$remote_main"'),
);
expect(createHash("sha256").update(cadence).digest("hex")).toBe(
"f130607e377acff6983fc2efaa015025ae2865d340dfad1fb865ee61e081f83e",
);
});
it("owns docs mirror Git lifecycle without changing transport or stale-source policy", () => {
const source = readFileSync(".github/workflows/docs-sync-publish.yml", "utf8");
const workflow = parse(source);
const steps = workflow.jobs["sync-publish-repo"].steps as WorkflowStep[];
expect(steps.map(({ name }) => name)).toEqual([
"Skip publish sync without token",
"Checkout source repo",
"Checkout ClawHub docs source",
"Prepare Git owner",
"Setup Node",
"Clone publish repo",
"Sync docs into publish repo",
"Cache successful docs validation",
"Commit publish repo sync",
]);
expect(steps[3]).toEqual({
name: "Prepare Git owner",
if: "env.OPENCLAW_DOCS_SYNC_TOKEN != ''",
uses: "openclaw/openclaw/.github/actions/git-owner@dd4528b6393e7d00063067a080ca7241b48ce475",
});
expect(steps[1]).toMatchObject({ with: { "fetch-depth": 0 } });
expect(steps[2]).toMatchObject({
with: {
repository: "openclaw/clawhub",
ref: "main",
path: "clawhub-source",
"fetch-depth": 1,
"persist-credentials": false,
},
});
for (const step of steps.slice(1)) {
expect(step.if).toBe(
step.name === "Cache successful docs validation"
? "env.OPENCLAW_DOCS_SYNC_TOKEN != '' && github.repository == 'openclaw/openclaw' && github.ref == 'refs/heads/main'"
: "env.OPENCLAW_DOCS_SYNC_TOKEN != ''",
);
}
expect(source).not.toContain("setup-python");
expect(workflow.concurrency).toEqual({
group:
"docs-sync-publish-${{ github.event_name == 'workflow_dispatch' && format('manual-{0}', github.run_id) || github.ref }}",
"cancel-in-progress": false,
});
const clone = expectDefined(steps[5]?.run, "clone policy");
const sync = expectDefined(steps[6]?.run, "sync body");
const publish = expectDefined(steps[8]?.run, "publication policy");
expect(steps[8]?.["working-directory"]).toBe("publish");
for (const policy of [clone, publish]) {
expect(
policy.startsWith(
"set -euo pipefail\nexec python3 -I -S \"$CI_GIT_OWNER\" --policy - <<'PYTHON'\n",
),
).toBe(true);
expect(policy.match(/for attempt in range\(1, 6\):/gu)).toHaveLength(1);
expect(policy.match(/backoff\(attempt \* 2\)/gu)).toHaveLength(1);
expect(policy).toContain("except (GitFailure, FetchTimeout):");
expect(policy).not.toMatch(
/except (?:Exception|BaseException)|except:|error\.code|\$\?|\|\| true/u,
);
}
expect(clone).toContain('publish = os.path.join(workspace, "publish")');
expect(clone).toContain('subprocess.run(["rm", "-rf", publish], check=True)');
expect(clone).toContain(
"https://x-access-token:{os.environ['OPENCLAW_DOCS_SYNC_TOKEN']}@github.com/openclaw/docs.git",
);
const calls = [...`${clone}\n${publish}`.matchAll(/run_git\(([\s\S]*?)\)(?=\n|$)/gu)].map(
(match) => match[1]!,
);
const transports = calls.filter((call) => /^\w+, "(?:clone|fetch)"/u.test(call));
expect(transports).toHaveLength(3);
expect(transports.every((call) => call.includes("timeout=120"))).toBe(true);
expect(transports.slice(1)).toEqual(
Array(2).fill(
'publish, "fetch", "origin", "main:refs/remotes/origin/main", timeout=120, reclaim_locks=True',
),
);
expect(calls.filter((call) => call.includes("timeout="))).toEqual(transports);
expect(calls.filter((call) => /^publish, "(?:rebase|push)"/u.test(call))).toHaveLength(3);
expect(
calls
.filter((call) => /^publish, "(?:config|add|commit|rebase|push)"/u.test(call))
.every((call) => call.includes("reclaim_locks=True")),
).toBe(true);
expect(publish).toContain("if not current_source_sha or current_source_sha == source_sha:");
expect(publish).toContain(
'run_git(workspace, "merge-base", "--is-ancestor", source_sha, current_source_sha)',
);
expect(publish).toContain("except (GitFailure, json.JSONDecodeError):");
expect(sync.startsWith("set -euo pipefail\n")).toBe(true);
expect(sync).toContain(
'clawhub_sha="$(cd "$GITHUB_WORKSPACE/clawhub-source" && python3 -I -S "$CI_GIT_OWNER" --checkout-git 0 rev-parse HEAD)"\nnode scripts/docs-sync-publish.mjs',
);
expect([clone, sync, publish].join("\n")).not.toMatch(
/\btimeout --|\bgit (?:clone|fetch|show|merge-base|diff|config|add|commit|rebase|push|rev-parse)\b|--depth|--no-tags/u,
);
});
it("pins plugin publication owners before selected checkout and preserves Git deadlines", () => {
const owner = {
name: "Prepare Git owner",
uses: "openclaw/openclaw/.github/actions/git-owner@dd4528b6393e7d00063067a080ca7241b48ce475",
};
const clawhub = parse(readFileSync(".github/workflows/plugin-clawhub-release.yml", "utf8"));
const npm = parse(readFileSync(".github/workflows/plugin-npm-release.yml", "utf8"));
for (const [workflow, jobName, checkoutName] of [
[clawhub, "preview_plugins_clawhub", "Checkout"],
[npm, "preview_plugins_npm", "Checkout"],
[npm, "verify_plugin_npm_preflight", "Checkout trusted npm preflight tooling"],
[npm, "publish_plugins_npm", "Checkout trusted publication tooling"],
] as const) {
const steps = workflow.jobs[jobName].steps as WorkflowStep[];
expect(steps[0], jobName).toEqual(owner);
expect(steps[1]?.name, jobName).toBe(checkoutName);
const body = steps
.map(({ run }) => run ?? "")
.join("\n")
.split("\n")
.filter((line) => !line.trimStart().startsWith("#"))
.join("\n");
const calls = [...body.matchAll(/(?:run_git|git_output)\(([\s\S]*?)\)(?=\.|\n|$)/gu)].map(
(match) => match[1]!,
);
const transports = calls.filter((call) => /^\s*workspace,\s*"(?:fetch|show)",/u.test(call));
expect(transports.length, jobName).toBeGreaterThan(0);
for (const call of transports) {
expect(call, jobName).toMatch(/\btimeout\s*=\s*120\b/u);
}
expect(body, jobName).not.toMatch(
/timeout[^\n]*git|(?:^|\s)git (?:fetch|rev-parse|merge-base|for-each-ref|checkout|show)\b/mu,
);
expect(body, jobName).not.toMatch(/backoff\(|for attempt in range/u);
}
for (const stepName of [
"Read exact npm preflight source package",
"Read exact npm publication source package",
]) {
const step = [
...npm.jobs.verify_plugin_npm_preflight.steps,
...npm.jobs.publish_plugins_npm.steps,
].find(({ name }: WorkflowStep) => name === stepName) as WorkflowStep;
expect(step.run, stepName).toContain("git_output(");
expect(step.run, stepName).toContain('errors="surrogateescape"');
}
});
it("pins the Mantis Git owner and preserves distinct terminal ref-validation contracts", () => {
const action = parse(
readFileSync(".github/actions/mantis-validate-trusted-ref/action.yml", "utf8"),
);
const workflow = parse(readFileSync(".github/workflows/mantis-discord-smoke.yml", "utf8"));
const owner = {
name: "Prepare Git owner",
uses: "openclaw/openclaw/.github/actions/git-owner@dd4528b6393e7d00063067a080ca7241b48ce475",
};
const actionSteps = action.runs.steps as WorkflowStep[];
const discordSteps = workflow.jobs.validate_selected_ref.steps as WorkflowStep[];
expect(actionSteps.map(({ name }) => name)).toEqual([
"Prepare Git owner",
"Validate refs are trusted",
]);
expect(actionSteps[0]).toEqual(owner);
expect(discordSteps.map(({ name }) => name)).toEqual([
"Prepare Git owner",
"Checkout selected ref",
"Validate selected ref",
]);
expect(discordSteps[0]).toEqual(owner);
expect(discordSteps[1]).toMatchObject({
uses: CHECKOUT_V6,
with: { "persist-credentials": false, ref: "${{ inputs.ref }}", "fetch-depth": 0 },
});
expect(Object.keys(action.inputs)).toEqual(["candidate-ref", "baseline-ref"]);
expect(Object.keys(action.outputs)).toEqual(["candidate-revision", "baseline-revision"]);
for (const [steps, shared] of [
[actionSteps, true],
[discordSteps, false],
] as const) {
const run = expectDefined(steps.at(-1)?.run, "Mantis validation body");
const revision = shared ? "revision" : "selected_revision";
const prefix = `python3 -I -S "$CI_GIT_OWNER" --checkout-git ${shared ? 0 : 120} fetch --no-tags origin `;
expect(run.startsWith("set -euo pipefail\n")).toBe(true);
expect(
run
.split("\n")
.map((line) => line.trim())
.filter((line) => /\bfetch\b/u.test(line)),
).toEqual([
`${prefix}+refs/heads/main:refs/remotes/origin/main`,
...(shared
? []
: [`${prefix}"+refs/heads/\${INPUT_REF}:refs/remotes/origin/\${INPUT_REF}"`]),
]);
expect(run).not.toMatch(/\bgit fetch\b|^\s*(?:timeout|for|while|until)\b|\$\?/mu);
expect(
run.match(
/(?:reason|trusted_reason)="(?:main-ancestor|release-tag|release-branch-head|open-pr-head)"/gu,
),
).toEqual(
[
"main-ancestor",
"release-tag",
...(shared ? [] : ["release-branch-head"]),
"open-pr-head",
].map((reason) => `${shared ? "reason" : "trusted_reason"}="${reason}"`),
);
expect(run).toContain(`git tag --points-at "$${revision}" | grep -Eq '^v'`);
expect(run).toContain("gh api \\\n");
expect(run).toContain('-H "Accept: application/vnd.github+json"');
expect(run).toContain(`"repos/\${GITHUB_REPOSITORY}/commits/\${${revision}}/pulls"`);
expect(run).toContain(
`select(.state == "open" and .head.repo.full_name == "'"\${GITHUB_REPOSITORY}"'" and .head.sha == "'"\${${revision}}"'")] | length`,
);
if (shared) {
expect(run).toContain('echo "${label}_revision=${revision}" >> "$GITHUB_OUTPUT"');
expect(run).toContain(
'validate_ref baseline "$BASELINE_REF"\nfi\nvalidate_ref candidate "$CANDIDATE_REF"',
);
} else {
expect(run).toContain(
'elif [[ "$INPUT_REF" =~ ^release/[0-9]{4}\\.[0-9]+\\.[0-9]+$ ]]; then',
);
expect(run).toContain(
'release_branch_sha="$(git rev-parse "refs/remotes/origin/${INPUT_REF}")"',
);
expect(run).toContain(
'if [[ "$selected_revision" == "$release_branch_sha" ]]; then\n trusted_reason="release-branch-head"\n fi\nelse\n pr_head_count=',
);
expect(run).toContain(
'echo "selected_revision=$selected_revision" >> "$GITHUB_OUTPUT"\necho "trusted_reason=$trusted_reason" >> "$GITHUB_OUTPUT"',
);
}
}
});
it("keeps shared Mantis reaction ownership stable", () => {
const resolveWorkflowPath = ".github/workflows/mantis-resolve-request.yml";
const cleanupWorkflowPath = ".github/workflows/mantis-clear-reaction.yml";
const resolveSource = readFileSync(resolveWorkflowPath, "utf8");
const cleanupSource = readFileSync(cleanupWorkflowPath, "utf8");
const resolveWorkflow = parse(resolveSource);
const cleanupWorkflow = parse(cleanupSource);
const expectedWorkflowCallSecrets = {
MANTIS_GITHUB_APP_ID: { required: true },
MANTIS_GITHUB_APP_PRIVATE_KEY: { required: true },
};
const resolveJob = resolveWorkflow.jobs.resolve;
const cleanupJob = cleanupWorkflow.jobs.clear;
const resolveSteps = resolveJob.steps as WorkflowStep[];
const cleanupSteps = cleanupJob.steps as WorkflowStep[];
const findStep = (steps: WorkflowStep[], id: string, workflowPath: string) =>
expectDefined(
steps.find((step) => step.id === id),
`${workflowPath} ${id}`,
);
const createTokenStep = findStep(resolveSteps, "mantis_reaction_token", resolveWorkflowPath);
const createStep = findStep(resolveSteps, "add_reaction", resolveWorkflowPath);
const cleanupTokenStep = findStep(cleanupSteps, "mantis_reaction_token", cleanupWorkflowPath);
const deleteStep = expectDefined(
cleanupSteps.find((step) => step.env?.REACTION_ID),
`${cleanupWorkflowPath} reaction cleanup step`,
);
expect(resolveWorkflow.on.workflow_call.secrets, resolveWorkflowPath).toEqual(
expectedWorkflowCallSecrets,
);
expect(cleanupWorkflow.on.workflow_call.secrets, cleanupWorkflowPath).toEqual(
expectedWorkflowCallSecrets,
);
expect(resolveJob.outputs.reaction_id, resolveWorkflowPath).toBe(
"${{ steps.add_reaction.outputs.reaction_id }}",
);
for (const [label, tokenStep] of [
["creation", createTokenStep],
["cleanup", cleanupTokenStep],
] as const) {
expect(tokenStep, `${label} token`).toMatchObject({
uses: CREATE_GITHUB_APP_TOKEN_V3,
with: {
"app-id": "${{ secrets.MANTIS_GITHUB_APP_ID }}",
"private-key": "${{ secrets.MANTIS_GITHUB_APP_PRIVATE_KEY }}",
},
});
expect(
Object.entries(tokenStep.with ?? {}).filter(([key]) => key.startsWith("permission-")),
`${label} permissions`,
).toEqual([["permission-issues", "write"]]);
}
expect(createStep, resolveWorkflowPath).toMatchObject({
if: "${{ steps.resolve.outputs.request_source == 'issue_comment' && steps.mantis_reaction_token.outcome == 'success' }}",
uses: "actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3",
with: { "github-token": "${{ steps.mantis_reaction_token.outputs.token }}" },
});
expect(createStep.with?.script, resolveWorkflowPath).toContain("createForIssueComment");
expect(createStep.with?.script, resolveWorkflowPath).toContain(
'core.setOutput("reaction_id", String(reaction.id))',
);
expect(resolveSource.match(/createForIssueComment/gu), resolveWorkflowPath).toHaveLength(1);
expect(cleanupJob.permissions, cleanupWorkflowPath).toEqual({});
expect(deleteStep, cleanupWorkflowPath).toMatchObject({
env: {
COMMENT_ID: "${{ inputs.comment-id }}",
REACTION_ID: "${{ inputs.reaction-id }}",
},
uses: "actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3",
with: { "github-token": "${{ steps.mantis_reaction_token.outputs.token }}" },
});
expect(deleteStep.with?.script, cleanupWorkflowPath).toContain("deleteForIssueComment");
expect(deleteStep.with?.script, cleanupWorkflowPath).toContain(
"Number(process.env.REACTION_ID)",
);
expect(deleteStep.with?.script, cleanupWorkflowPath).toContain("reaction_id: reactionId");
expect(JSON.stringify(cleanupJob), cleanupWorkflowPath).not.toMatch(
/listForIssueComment|\.filter\(|github-actions\[bot\]/u,
);
});
it.each(MANTIS_MANUAL_ONLY_WORKFLOWS)(
"keeps legacy Mantis scenarios on manual dispatch in %s",
(workflowPath) => {
const workflow = parse(readFileSync(workflowPath, "utf8"));
expect(workflow.on.workflow_dispatch, workflowPath).toBeDefined();
expect(workflow.on.issue_comment, workflowPath).toBeUndefined();
},
);
it("bounds release ref validation fetches across checkout auth modes", () => {
const resolveTargetSteps = readReleaseChecksWorkflow().jobs.resolve_target.steps;
for (const stepName of [
"Validate selected ref belongs to this repository",
"Validate Tideclaw alpha target matches workflow branch",
]) {
const step = resolveTargetSteps.find(
(candidate: WorkflowStep) => candidate.name === stepName,
);
expect(step?.run, stepName).toContain("local -a git_args=(git)");
expect(step?.run, stepName).toContain(
'git_args+=(-c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth_header}")',
);
expect(step?.run, stepName).toContain(
'timeout --signal=TERM --kill-after=10s 120s "${git_args[@]}" fetch "$@"',
);
expect(step?.run, stepName).not.toContain('git -c "http.https://github.com/.extraheader');
}
});
describe.skipIf(process.platform !== "linux")("release fallback history with real Git", () => {
it.each(["branch", "tag"] as const)("accepts a small valid %s history", (route) => {
const { result, events } = runReleaseFallbackHistoryFixture({ route });
expect(result.status, result.stderr).toBe(0);
expect(events.filter((event) => event.op.startsWith("fetch-"))).toMatchObject([
{ op: "fetch-branches", status: 0, signal: null },
{ op: "fetch-tags", status: 0, signal: null },
]);
expect(events.find((event) => event.op === `${route}-producer`)).toMatchObject({
status: 0,
signal: null,
});
});
it.each(["orphan", "non-release-tag"] as const)("rejects %s history", (route) => {
const { result, events } = runReleaseFallbackHistoryFixture({ route });
expect(result.status).toBe(1);
expect(result.stderr).toContain("but that commit is not reachable");
expect(events.filter((event) => event.op.endsWith("-producer"))).toMatchObject([
{ op: "tag-producer", status: 0, signal: null },
{ op: "branch-producer", status: 0, signal: null },
]);
});
it.each(["fetch-branches", "fetch-tags"] as const)(
"fails closed when the real %s command fails",
(failure) => {
const { result, events } = runReleaseFallbackHistoryFixture({ route: "branch", failure });
expect(result.status).not.toBe(0);
expect(events.find((event) => event.op === failure)).toMatchObject({
status: 128,
signal: null,
});
expect(events.some((event) => event.op.endsWith("-producer"))).toBe(false);
expect(result.stderr).not.toContain("but that commit is not reachable");
},
);
it.each(["branch", "tag"] as const)(
"does not accept matching %s output followed by a real Git failure",
(route) => {
const { result, events } = runReleaseFallbackHistoryFixture({
route,
failure: `${route}-producer`,
});
expect(result.status).toBe(1);
expect(result.stderr).toContain("but that commit is not reachable");
expect(events.find((event) => event.op === `${route}-producer`)).toMatchObject({
status: 0,
signal: null,
});
expect(events.find((event) => event.op === "post-output-failure")).toMatchObject({
status: 128,
signal: null,
});
},
);
it.each(["branch", "tag"] as const)(
"accepts valid %s enumeration larger than the pipe capacity",
(route) => {
const { result, events } = runReleaseFallbackHistoryFixture({ route, many: true });
expect(events.filter((event) => event.op.startsWith("fetch-"))).toMatchObject([
{ op: "fetch-branches", status: 0, signal: null },
{ op: "fetch-tags", status: 0, signal: null },
]);
const producer = events.find((event) => event.op === `${route}-producer`);
if (result.status !== 0) {
expect(result.stderr).toContain("but that commit is not reachable");
expect(producer).toMatchObject({ status: null, signal: "SIGPIPE", exitCode: 141 });
}
expect(result.status, JSON.stringify({ producer, stderr: result.stderr })).toBe(0);
expect(producer).toMatchObject({ status: 0, signal: null });
},
60_000,
);
});
it("checks the generated Git owner in the workflow guard lane", () => {
const check = spawnSync(process.execPath, ["scripts/generate-ci-git-owner.mts", "--check"], {
encoding: "utf8",
});
expect(check.status, check.stderr).toBe(0);
});
it("uses the maintained authenticated checkout for security-fast", () => {
const workflow = readCiWorkflow();
const checkoutStep = workflow.jobs["security-fast"].steps.find(
(step: WorkflowStep) => step.name === "Checkout",
);
const manualCheckoutStep = workflow.jobs["security-fast"].steps.find(
(step: WorkflowStep) => step.name === "Checkout manual target",
);
expect(checkoutStep.uses).toBe(CHECKOUT_V6);
expect(checkoutStep.if).toBe(
"github.event_name != 'workflow_dispatch' || inputs.target_ref == ''",
);
expect(checkoutStep.with["persist-credentials"]).toBe(false);
expect(checkoutStep.with["fetch-depth"]).toBe(2);
expect(manualCheckoutStep.if).toBe(
"github.event_name == 'workflow_dispatch' && inputs.target_ref != ''",
);
expect(manualCheckoutStep.run).toContain("workflow_dispatch target_ref");
});
it("uses native preflight tooling unless a dispatch selects a different revision", () => {
const workflow = readCiWorkflow();
const steps = workflow.jobs.preflight.steps as WorkflowStep[];
const setupPnpm = expectDefined(
steps.find((step) => step.name === "Setup manifest pnpm"),
"manifest pnpm setup",
);
const installDependencies = expectDefined(
steps.find((step) => step.name === "Install manifest dependencies"),
"manifest dependency install",
);
const buildManifest = expectDefined(
steps.find((step) => step.name === "Build CI manifest"),
"manifest builder",
);
const checkProtocolCoverage = expectDefined(
steps.find((step) => step.name === "Check mobile protocol event coverage"),
"protocol coverage owner",
);
const workflowSha = "a".repeat(40);
const otherSha = "b".repeat(40);
const cases = [
["same-revision dispatch", "workflow_dispatch", workflowSha, "", false, false],
[
"same-revision explicit target dispatch",
"workflow_dispatch",
workflowSha,
workflowSha,
false,
false,
],
["same-revision release gate", "workflow_dispatch", workflowSha, workflowSha, true, false],
["push", "push", otherSha, "", false, false],
["pull request", "pull_request", otherSha, "", false, false],
["different-revision dispatch", "workflow_dispatch", otherSha, otherSha, false, true],
["different-revision release gate", "workflow_dispatch", otherSha, otherSha, true, true],
] as const;
for (const [
label,
eventName,
checkoutRevision,
targetRef,
releaseGate,
usesCompatibilityTooling,
] of cases) {
const context = {
eventName,
releaseGate,
repository: "openclaw/openclaw",
runAttempt: 1,
steps: { checkout_ref: { outputs: { sha: checkoutRevision } } },
targetRef,
workflowSha,
};
const evaluateStep = (step: WorkflowStep) =>
evaluateWorkflowExpression(`\${{ ${step.if} }}`, context);
expect([evaluateStep(setupPnpm), evaluateStep(installDependencies)], label).toEqual([
usesCompatibilityTooling,
usesCompatibilityTooling,
]);
const invocationOptions = (step: WorkflowStep) => ({
checkoutRevision: String(
evaluateWorkflowExpression(step.env?.OPENCLAW_CI_CHECKOUT_REVISION, context),
),
eventName,
workflowRevision: String(
evaluateWorkflowExpression(step.env?.OPENCLAW_CI_WORKFLOW_REVISION, context),
),
});
expect(
runPreflightNodeInvocation(
expectDefined(buildManifest.run, "manifest script"),
invocationOptions(buildManifest),
),
label,
).toEqual(
usesCompatibilityTooling
? ["--import", "tsx", "--input-type=module"]
: ["--input-type=module"],
);
expect(
runPreflightNodeInvocation(
expectDefined(checkProtocolCoverage.run, "protocol coverage script"),
invocationOptions(checkProtocolCoverage),
),
label,
).toEqual([
usesCompatibilityTooling
? "scripts/check-protocol-event-coverage.mjs"
: "scripts/check-protocol-event-coverage.mts",
]);
}
});
it("keeps manual candidates separate from trusted cache authority", () => {
const workflow = readCiWorkflow();
const preflight = workflow.jobs.preflight;
const checkoutStep = expectDefined(
preflight.steps.find((step: WorkflowStep) => step.name === "Checkout"),
"preflight checkout owner",
);
expect(checkoutStep.env?.WORKFLOW_SHA).toBe("${{ github.workflow_sha }}");
const harnessSteps = preflight.steps.filter(
(step: WorkflowStep) =>
step.uses?.startsWith("actions/checkout@") && step.with?.path === ".ci-harness",
);
expect(harnessSteps).toHaveLength(1);
const harnessStep = expectDefined(harnessSteps[0], "different-revision harness checkout");
expect(harnessStep).toMatchObject({
uses: CHECKOUT_V6,
with: {
ref: "${{ github.workflow_sha }}",
path: ".ci-harness",
"sparse-checkout":
"/.github/actions/\n/scripts/lib/release-context.mjs\n/scripts/lib/release-version.mjs\n",
"sparse-checkout-cone-mode": false,
"persist-credentials": false,
},
});
const resolvedIndex = preflight.steps.findIndex(
(step: WorkflowStep) => step.id === "checkout_ref",
);
const harnessIndex = preflight.steps.indexOf(harnessStep);
const consumerIndex = preflight.steps.findIndex((step: WorkflowStep) =>
step.uses?.startsWith("./.ci-harness/"),
);
expect(preflight.steps.indexOf(checkoutStep)).toBeLessThan(resolvedIndex);
expect(resolvedIndex).toBeLessThan(harnessIndex);
expect(harnessIndex).toBeLessThan(consumerIndex);
const workflowSha = "a".repeat(40);
for (const eventName of ["push", "pull_request", "workflow_dispatch"] as const) {
for (const headRepository of ["openclaw/openclaw", "contributor/openclaw"]) {
for (const selectedSha of [workflowSha, "b".repeat(40)]) {
expect(
evaluateWorkflowExpression(harnessStep.if, {
eventName,
headRepository,
repository: "openclaw/openclaw",
runAttempt: 1,
steps: { checkout_ref: { outputs: { sha: selectedSha } } },
workflowSha,
}),
).toBe(selectedSha !== workflowSha);
}
}
}
const trustStep = expectDefined(
preflight.steps.find((step: WorkflowStep) => step.name === "Classify candidate cache trust"),
"candidate cache trust step",
);
const nativeCheckout = expectDefined(
workflow.jobs["native-i18n"].steps.find((step: WorkflowStep) => step.name === "Checkout"),
"native i18n checkout",
);
expect(preflight.outputs).toMatchObject({
candidate_trust: "${{ steps.candidate_trust.outputs.trust }}",
cache_mode: "${{ steps.candidate_trust.outputs.cache_mode }}",
cache_write_allowed: "${{ steps.candidate_trust.outputs.cache_write_allowed }}",
});
expect(trustStep.env).toMatchObject({
CHECKOUT_REVISION: "${{ steps.checkout_ref.outputs.sha }}",
DEFAULT_SHA: "${{ steps.diff_base.outputs.default_sha }}",
TARGET_REF: "${{ inputs.target_ref }}",
WORKFLOW_REVISION: "${{ github.workflow_sha }}",
});
expect(trustStep.run).toContain("trust=untrusted");
expect(trustStep.run).toContain("cache_mode=off");
expect(trustStep.run).toContain("cache_write_allowed=false");
expect(trustStep.run).toContain('elif [[ "$GITHUB_EVENT_NAME" == "workflow_dispatch" ]]');
expect(trustStep.run).toContain('"$RELEASE_GATE" == "true"');
expect(trustStep.run).toContain('"$CHECKOUT_REVISION" == "$DEFAULT_SHA"');
expect(trustStep.run).toContain('"$CHECKOUT_REVISION" == "$WORKFLOW_REVISION"');
expect(trustStep.run).toContain("cache_write_allowed=true");
const ciLocalActions = Object.values(workflow.jobs).flatMap(
(job) =>
(job as { steps?: WorkflowStep[] }).steps?.filter((step) =>
step.uses?.includes("/.github/actions/"),
) ?? [],
);
expect(ciLocalActions.length).toBeGreaterThan(0);
for (const step of ciLocalActions) {
expect(step.uses, step.name).toContain("./.ci-harness/.github/actions/");
}
expect(nativeCheckout.uses).toBeUndefined();
expect(nativeCheckout.env).toMatchObject({
CHECKOUT_SHA: "${{ needs.preflight.outputs.checkout_revision }}",
WORKFLOW_SHA: "${{ github.workflow_sha }}",
});
for (const [jobName, job] of Object.entries(workflow.jobs)) {
for (const step of (job as { steps?: WorkflowStep[] }).steps ?? []) {
if (step.uses?.startsWith("actions/cache/restore@")) {
expect(String(step.if), `${jobName}: ${step.name}`).toContain(
"preflight.outputs.cache_mode != 'off'",
);
}
if (step.uses?.startsWith("actions/cache/save@")) {
expect(String(step.if), `${jobName}: ${step.name}`).toContain(
"preflight.outputs.cache_write_allowed == 'true'",
);
}
}
}
const goSetup = expectDefined(
workflow.jobs["checks-node-core-test-nondist-shard"].steps.find(
(step: WorkflowStep) => step.name === "Setup Go for docs i18n",
),
"docs i18n Go setup",
);
expect(goSetup.with?.cache).toBe(false);
});
it("classifies cache write authority from proven candidate identity", () => {
const workflowRevision = "a".repeat(40);
const defaultRevision = "b".repeat(40);
const arbitraryRevision = "c".repeat(40);
const cases = [
{
expected: { cache_mode: "off", cache_write_allowed: "false", trust: "untrusted" },
options: {
checkoutRevision: arbitraryRevision,
eventName: "workflow_dispatch" as const,
targetRef: arbitraryRevision,
workflowRevision,
},
},
{
expected: { cache_mode: "restore", cache_write_allowed: "false", trust: "workflow" },
options: {
checkoutRevision: workflowRevision,
eventName: "workflow_dispatch" as const,
workflowRevision,
},
},
{
expected: { cache_mode: "restore", cache_write_allowed: "true", trust: "main" },
options: {
checkoutRevision: defaultRevision,
defaultRevision,
eventName: "workflow_dispatch" as const,
targetRef: defaultRevision,
workflowRevision,
},
},
{
expected: { cache_mode: "restore", cache_write_allowed: "true", trust: "release" },
options: {
checkoutRevision: arbitraryRevision,
eventName: "workflow_dispatch" as const,
targetContextTarget: true,
targetRef: arbitraryRevision,
workflowRevision,
},
},
{
expected: {
cache_mode: "restore",
cache_write_allowed: "false",
trust: "pull-request",
},
options: {
checkoutRevision: arbitraryRevision,
eventName: "workflow_dispatch" as const,
releaseGate: true,
targetRef: arbitraryRevision,
workflowRevision,
},
},
{
expected: {
cache_mode: "restore",
cache_write_allowed: "false",
trust: "pull-request",
},
options: {
checkoutRevision: arbitraryRevision,
eventName: "pull_request" as const,
workflowRevision,
},
},
{
expected: { cache_mode: "restore", cache_write_allowed: "true", trust: "main" },
options: {
checkoutRevision: defaultRevision,
eventName: "push" as const,
ref: "refs/heads/main",
workflowRevision,
},
},
];
for (const testCase of cases) {
const result = runCandidateTrustClassification(testCase.options);
expect(result.status, result.output).toBe(0);
expect(result.outputs).toMatchObject(testCase.expected);
}
});
it("uses the maintained checkout across workflow sanity jobs", () => {
const workflow = readWorkflowSanityWorkflow();
for (const jobName of ["no-tabs", "actionlint", "generated-doc-baselines"]) {
const checkoutStep = workflow.jobs[jobName].steps.find(
(step: WorkflowStep) => step.name === "Checkout",
);
expect(checkoutStep.uses, jobName).toBe(CHECKOUT_V6);
expect(checkoutStep.with, jobName).toEqual({
"fetch-depth": 1,
"persist-credentials": false,
});
}
});
it("pins workflow sanity's typed Git policy after Python setup", () => {
const steps: WorkflowStep[] = readWorkflowSanityWorkflow().jobs.actionlint.steps;
const python = expectDefined(
steps.find((step) => step.name === "Setup Python"),
"Python",
);
const owner = expectDefined(
steps.find((step) => step.name === "Prepare Git owner"),
"owner",
);
const policy = expectDefined(
steps.find((step) => step.name === "Prepare trusted workflow audit configs"),
"policy",
);
expect(python.with).toEqual({ "python-version": "3.12" });
expect(owner.uses).toBe(
"openclaw/openclaw/.github/actions/git-owner@dd4528b6393e7d00063067a080ca7241b48ce475",
);
expect(owner.with).toBeUndefined();
expect(steps.indexOf(python)).toBeLessThan(steps.indexOf(owner));
expect(steps.indexOf(owner)).toBeLessThan(steps.indexOf(policy));
expect(policy.if).toBe("github.event_name == 'pull_request'");
expect(policy.env).toEqual({
BASE_REF: "${{ github.event.pull_request.base.ref }}",
BASE_SHA: "${{ github.event.pull_request.base.sha }}",
});
expect(policy.run).toContain("exec python3 -I -S \"$CI_GIT_OWNER\" --policy - <<'PYTHON'");
expect(policy.run).not.toMatch(
/timeout --|fetch_status|fetch_base_ref|sleep 5|subprocess\.PIPE|except (?:Exception|BaseException|SystemExit|RuntimeError)/u,
);
expect(policy.run?.match(/timeout=\d+/gu)).toEqual(["timeout=30"]);
expect(policy.run).toContain("range(1, 4)");
expect(policy.run).toContain("backoff(5)");
for (const contract of [
"--no-tags",
"--depth=1",
"reclaim_locks=True",
"refs/remotes/origin/security-base",
"refs/heads/",
".pre-commit-config.yaml",
".github/zizmor.yml",
"pre-commit-base.yaml",
"zizmor-base.yml",
"PRE_COMMIT_CONFIG_PATH=",
]) {
expect(policy.run).toContain(contract);
}
const audit = expectDefined(
steps.find((step) => step.name === "Audit all workflows with zizmor"),
"audit",
);
expect(audit.run).toContain(
'pre-commit run --config "${PRE_COMMIT_CONFIG_PATH:-.pre-commit-config.yaml}" zizmor',
);
});
it("prepares Testbox checkouts with one maintained owner and scoped history", () => {
const workflowPaths = [
[
".github/workflows/ci-check-testbox.yml",
"1",
"${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || 'HEAD' }}",
"1.27.1",
],
[
".github/workflows/ci-check-arm-testbox.yml",
"0",
"${{ github.event.pull_request.base.sha || 'refs/remotes/origin/main' }}",
"1.27.1",
],
[
".github/workflows/ci-build-artifacts-testbox.yml",
"0",
"${{ github.event.pull_request.base.sha || 'refs/remotes/origin/main' }}",
undefined,
],
] as const;
for (const [workflowPath, dispatchFetchDepth, baseRef, goVersion] of workflowPaths) {
const workflow = parse(readFileSync(workflowPath, "utf8"));
const job = Object.values(workflow.jobs)[0] as { steps: WorkflowStep[] };
const checkoutStep = job.steps.find((step) => step.name === "Checkout");
const prepareStep = job.steps.find((step) => step.name === "Prepare Testbox shell");
expect(checkoutStep?.uses, workflowPath).toBe(CHECKOUT_V6);
expect(checkoutStep?.with?.["persist-credentials"], workflowPath).toBe(false);
for (const [eventName, expectedDepth] of [
["pull_request", "2"],
["workflow_dispatch", dispatchFetchDepth],
] as const) {
expect(
evaluateWorkflowExpression(checkoutStep?.with?.["fetch-depth"], {
eventName,
repository: "openclaw/openclaw",
runAttempt: 1,
}),
`${workflowPath} ${eventName}`,
).toBe(expectedDepth);
}
expect(prepareStep?.uses, workflowPath).toBe("./.github/actions/prepare-testbox-shell");
expect(prepareStep?.with?.["base-ref"], workflowPath).toBe(baseRef);
expect(prepareStep?.with?.["go-version"], workflowPath).toBe(goVersion);
const ensureBaseStep = job.steps.find(
(step: WorkflowStep) => step.name === "Ensure Testbox base commit",
);
expect(ensureBaseStep, workflowPath).toBeUndefined();
expect(JSON.stringify(job.steps), workflowPath).not.toContain(
"+refs/heads/main:refs/remotes/origin/main",
);
}
const action = parse(readFileSync(".github/actions/prepare-testbox-shell/action.yml", "utf8"));
expect(action.inputs["go-version"]).toMatchObject({ required: false });
const setupGo = expectDefined(
action.runs.steps.find((step: WorkflowStep) => step.uses === SETUP_GO_V6),
"Testbox Go setup",
);
expect(setupGo).toMatchObject({
if: "inputs.go-version != ''",
with: {
cache: false,
"go-version": "${{ inputs.go-version }}",
},
});
const exposeGo = expectDefined(
action.runs.steps.find((step: WorkflowStep) => step.name === "Expose Go tools"),
"Testbox Go exposure",
);
expect(exposeGo.if).toBe("inputs.go-version != ''");
expect(exposeGo.run).toContain('test "$(go env GOVERSION)" = "go${TESTBOX_GO_VERSION}"');
expect(exposeGo.run).toContain('go_root="$(go env GOROOT)"');
expect(exposeGo.run).toContain("for tool in go gofmt; do");
expect(exposeGo.run).toContain('"/usr/local/bin/$tool"');
const prepare = expectDefined(
action.runs.steps.find(
(step: WorkflowStep) => step.name === "Pin Testbox base and Node tools",
),
"Testbox base preparation",
);
const run = prepare.run as string;
expect(run).toContain('base_ref="${TESTBOX_BASE_REF:-HEAD}"');
expect(run).toContain('git rev-parse --verify "${base_ref}^{commit}"');
expect(run).toContain('git update-ref refs/remotes/origin/main "$base_sha"');
expect(run).not.toContain("git fetch");
});
it("bounds the workflow sanity ShellCheck download", () => {
const workflow = readWorkflowSanityWorkflow();
const shellcheckStep = expectDefined(
workflow.jobs.actionlint.steps.find(
(step: WorkflowStep) => step.name === "Install ShellCheck",
),
"ShellCheck install step",
);
expect(shellcheckStep.run).toContain("curl --connect-timeout 10 --max-time 120");
expect(shellcheckStep.run).toContain("--retry 5 --retry-delay 2 --retry-all-errors");
});
it("pins workflow and pre-commit actionlint to the large-stdin deadlock fix", () => {
const revision = "011a6d15e749bb3f2d771eed9c7aa0e7e3e10ee7";
const steps: WorkflowStep[] = readWorkflowSanityWorkflow().jobs.actionlint.steps;
const setupGo = expectDefined(
steps.find((step) => step.uses === SETUP_GO_V6),
"Go setup",
);
const install = expectDefined(
steps.find((step) => step.name === "Install actionlint"),
"actionlint install",
);
expect(setupGo.with).toEqual({ "go-version": "1.25.0", cache: false });
expect(steps.indexOf(setupGo)).toBeLessThan(steps.indexOf(install));
expect(install.run).toContain(`ACTIONLINT_REVISION="${revision}"`);
expect(install.run).toContain('export GOBIN="$RUNNER_TEMP/actionlint-bin"');
expect(install.run).toContain(
'go install "github.com/rhysd/actionlint/cmd/actionlint@${ACTIONLINT_REVISION}"',
);
expect(install.run).toContain('"$GOBIN/actionlint" -version');
expect(install.run).toContain("v1.7.13-0.20260419144658-${ACTIONLINT_REVISION:0:12}");
expect(install.run).toContain('echo "$GOBIN" >> "$GITHUB_PATH"');
const preCommit = parse(readFileSync(".pre-commit-config.yaml", "utf8"));
expect(
preCommit.repos.find(
(repo: { repo: string }) => repo.repo === "https://github.com/rhysd/actionlint",
).rev,
).toBe(revision);
});
it("runs committed generated baseline drift checks in workflow sanity", () => {
const workflow = readWorkflowSanityWorkflow();
const steps = workflow.jobs["generated-doc-baselines"].steps;
const stepNames = steps.map((step: WorkflowStep) => step.name);
expect(stepNames).toContain("Check SQLite sessions/transcripts schema baseline drift");
expect(stepNames).toContain("Check plugin SDK surface budget");
expect(
stepNames.indexOf("Check SQLite sessions/transcripts schema baseline drift"),
).toBeLessThan(stepNames.indexOf("Check plugin SDK surface budget"));
expect(
steps.find(
(step: WorkflowStep) =>
step.name === "Check SQLite sessions/transcripts schema baseline drift",
).run,
).toBe("pnpm sqlite:sessions-schema:check");
expect(
steps.find((step: WorkflowStep) => step.name === "Check plugin SDK surface budget").run,
).toBe("pnpm plugin-sdk:surface:check");
});
it("shares checkout ownership across Linux and native platforms with their existing budgets", () => {
const source = readFileSync(".github/workflows/ci.yml", "utf8");
const workflow = readCiWorkflow();
expect(source.match(/&platform_checkout_step/gu) ?? []).toHaveLength(1);
expect(source.match(/\*platform_checkout_step/gu) ?? []).toHaveLength(4);
expect(source.match(/&owned_checkout_run/gu) ?? []).toHaveLength(1);
const linuxCheckout = workflow.jobs["checks-fast-core"].steps.find(
(step: WorkflowStep) => step.name === "Checkout",
);
for (const runner of ["Linux", "macOS", "Windows"]) {
const defaults = spawnSync(
process.platform === "win32" ? "python" : "python3",
[
"-I",
"-S",
"-c",
'import json,runpy; owner=runpy.run_path(".github/actions/git-owner/owner.py"); print(json.dumps([owner["fetch_timeout_seconds"], owner["cleanup_seconds"]]))',
],
{ encoding: "utf8", env: { ...process.env, RUNNER_OS: runner } },
);
expect(defaults.status, defaults.stderr).toBe(0);
expect(JSON.parse(defaults.stdout)).toEqual([runner === "Linux" ? 120 : 90, 10]);
}
for (const jobName of [
"checks-windows",
"macos-node",
"macos-swift",
"ios-build",
"ios-screenshot-shard",
]) {
const checkoutStep = workflow.jobs[jobName].steps.find(
(step: WorkflowStep) => step.name === "Checkout",
);
expect(checkoutStep.run, jobName).toBe(linuxCheckout.run);
expect(checkoutStep.env, jobName).toEqual(linuxCheckout.env);
// Bootstrap cannot load Python startup code from the candidate checkout.
expect(checkoutStep.run, jobName).toContain('exec "$python_command" -I -S -');
}
const macosNodeSetup = workflow.jobs["macos-node"].steps.find(
(step: WorkflowStep) => step.name === "Setup Node environment",
);
expect(macosNodeSetup.with).toMatchObject({
"cache-mode": "${{ needs.preflight.outputs.cache_mode }}",
"install-bun": "false",
});
});
it("checks native and Node state schema versions in the macOS lane", () => {
const workflow = readCiWorkflow();
const schemaVersionStep = workflow.jobs["macos-swift"].steps.find(
(step: WorkflowStep) => step.name === "Native state schema version contract",
);
expect(schemaVersionStep.run).toContain("node scripts/check-native-state-schema-version.mjs");
expect(schemaVersionStep.run).toContain('elif [[ "$HISTORICAL_TARGET" == "true" ]]');
});
it("prepares offline Apple assets before CI opens a macOS SwiftPM graph", () => {
for (const [workflowPath, jobName] of [
[".github/workflows/ci.yml", "macos-swift"],
[".github/workflows/macos-periphery.yml", "scan"],
[".github/workflows/shared-openclawkit-periphery.yml", "scan-macos"],
] as const) {
const workflow = parse(readFileSync(workflowPath, "utf8"));
const steps = workflow.jobs[jobName].steps as WorkflowStep[];
const setupIndex = steps.findIndex((step) => step.uses?.endsWith("/setup-node-env"));
const installIndex = steps.findIndex(
(step) => step.name === "Install Mermaid renderer dependencies",
);
const prepareIndex = steps.findIndex((step) =>
step.run?.includes("node scripts/prepare-apple-mermaid.mjs"),
);
const graphIndex = steps.findIndex((step) =>
/swift (?:build|package)|periphery scan/u.test(step.run ?? ""),
);
const setupStep = expectDefined(steps[setupIndex], `${workflowPath}: dependency setup`);
const installStep = expectDefined(steps[installIndex], `${workflowPath}: dependency install`);
expect(setupStep.with?.["install-deps"]).toBe("false");
expect(installStep.env).toEqual({ CI: "true" });
expect(installStep.run?.trim().split(/\s+/u)).toEqual([
"pnpm",
"install",
"--frozen-lockfile",
"--prefer-offline",
"--optional",
"--filter",
"'@openclaw/mermaid-renderer...'",
"--config.ignore-scripts=false",
"--config.engine-strict=false",
"--config.enable-pre-post-scripts=true",
"--config.side-effects-cache=true",
]);
expect(installIndex, `${workflowPath}: filtered dependency install`).toBeGreaterThan(
setupIndex,
);
expect(prepareIndex, `${workflowPath}: resource preparation`).toBeGreaterThan(installIndex);
expect(graphIndex, `${workflowPath}: SwiftPM graph`).toBeGreaterThan(prepareIndex);
}
});
it.each([
{ historical: false, helperPresent: true, expectedStatus: 0 },
{ historical: true, helperPresent: false, expectedStatus: 0 },
{ historical: false, helperPresent: false, expectedStatus: 1 },
])(
"preserves the Apple asset contract for $historical historical / $helperPresent helper",
(testCase) => {
const steps = readCiWorkflow().jobs["macos-swift"].steps as WorkflowStep[];
const step = expectDefined(
steps.find((candidate) =>
candidate.run?.includes("node scripts/prepare-apple-mermaid.mjs"),
),
"Apple asset preparation step",
);
const root = tempDirs.make("openclaw-apple-assets-workflow-");
const marker = path.join(root, "prepared");
if (testCase.helperPresent) {
mkdirSync(path.join(root, "scripts"));
writeFileSync(
path.join(root, "scripts/prepare-apple-mermaid.mjs"),
'import { writeFileSync } from "node:fs"; writeFileSync("prepared", "ready");',
);
}
const result = runWorkflowShellScript(expectDefined(step.run, "asset preparation script"), {
cwd: root,
env: { ...process.env, HISTORICAL_TARGET: String(testCase.historical) },
});
expect(result.status, result.stderr).toBe(testCase.expectedStatus);
expect(existsSync(marker)).toBe(testCase.helperPresent);
},
);
it.each([
{ historical: false, hasWatchRtc: true, expected: true },
{ historical: false, hasWatchRtc: false, expected: true },
{ historical: true, hasWatchRtc: true, expected: true },
{ historical: true, hasWatchRtc: false, expected: false },
])("prepares Watch RTC by source capability: %j", ({ historical, hasWatchRtc, expected }) => {
const workflow = readCiWorkflow();
for (const jobName of ["ios-build", "ios-screenshot-shard"]) {
const install = workflow.jobs[jobName].steps.find(
(step: WorkflowStep) => step.name === "Install Watch Rust toolchain",
);
const engine = workflow.jobs["ios-build"].steps.find(
(step: WorkflowStep) => step.name === "Test Watch RTC engine",
);
for (const phase of ["smoke", "tests", "release"]) {
const context: Parameters<typeof evaluateWorkflowExpression>[1] = {
eventName: "workflow_dispatch",
repository: "openclaw/openclaw",
runAttempt: 1,
env: { HISTORICAL_TARGET: String(historical) },
matrix: { phase },
fileHashes: hasWatchRtc ? { "apps/shared/OpenClawWatchRTC/Cargo.toml": "present" } : {},
};
expect(evaluateWorkflowExpression(`\${{ ${install.if} }}`, context)).toBe(expected);
expect(evaluateWorkflowExpression(`\${{ ${engine.if} }}`, context)).toBe(
expected && phase === "tests",
);
}
}
});
it("retries macOS release builds only when Sparkle metadata is incomplete", () => {
const workflow = readCiWorkflow();
const macosInstallStep = workflow.jobs["macos-swift"].steps.find(
(step: WorkflowStep) => step.name === "Install XcodeGen / SwiftLint / SwiftFormat",
);
const iosInstallStep = workflow.jobs["ios-build"].steps.find(
(step: WorkflowStep) => step.name === "Install iOS Swift tooling",
);
const macosLintStep = workflow.jobs["macos-swift"].steps.find(
(step: WorkflowStep) => step.name === "Swift lint",
);
const iosLintStep = workflow.jobs["ios-build"].steps.find(
(step: WorkflowStep) => step.name === "Swift lint",
);
const buildStep = workflow.jobs["macos-swift"].steps.find(
(step: WorkflowStep) => step.name === "Swift build (release)",
);
const validateCacheStep = workflow.jobs["macos-swift"].steps.find(
(step: WorkflowStep) => step.name === "Validate Swift build cache",
);
for (const installStep of [macosInstallStep, iosInstallStep]) {
const currentTargetBranch = installStep.run.split('elif [[ "$HISTORICAL_TARGET"')[0];
expect(currentTargetBranch).toContain(
"if [[ -x ./scripts/install-xcodegen.sh && -x ./scripts/install-swift-tools.sh ]]; then",
);
expect(currentTargetBranch).toContain('./scripts/install-xcodegen.sh "$swift_tools_dir"');
expect(currentTargetBranch).toContain('"$swift_tools_dir/xcodegen" --version');
expect(currentTargetBranch).not.toContain("brew ");
expect(installStep.run).toContain("brew install xcodegen swiftlint");
expect(installStep.run).not.toContain("brew install xcodegen swiftlint swiftformat");
expect(installStep.run).toContain(
"https://github.com/nicklockwood/SwiftFormat/releases/download/$swiftformat_version/swiftformat.zip",
);
expect(installStep.run).toContain("--connect-timeout 10 --max-time 120");
expect(installStep.run).toContain("--retry 3 --retry-max-time 120");
expect(installStep.run).toContain(
'swiftformat_checksum="b990400779aceb7d7020796eb9ba814d4480543f671d38fc0ff48cb72f04c584"',
);
expect(installStep.run).toContain(
'swiftformat_checksum="7cb1cb1fae04932047c7015441c543848e8e60e1572d808d080e0a1f1661114a"',
);
expect(installStep.run).toContain(
'[[ "$("$swift_tools_dir/swiftformat" --version)" == "$swiftformat_version" ]]',
);
}
for (const jobName of ["macos-swift", "ios-build"]) {
expect(workflow.jobs[jobName].env.HISTORICAL_TARGET).toBe(
"${{ needs.preflight.outputs.compatibility_target }}",
);
}
expect(iosInstallStep.run).toContain('swiftformat_link="$(brew --prefix)/bin/swiftformat"');
expect(iosInstallStep.run).toContain(
'ln -sfn "$swift_tools_dir/swiftformat" "$swiftformat_link"',
);
expect(iosInstallStep.run).toContain(
'[[ "$("$swiftformat_link" --version)" == "$swiftformat_version" ]]',
);
for (const lintStep of [macosLintStep, iosLintStep]) {
expect(lintStep.run).toContain(
"if [[ -x ./scripts/lint-swift.sh && -x ./scripts/format-swift.sh ]]; then",
);
}
expect(macosLintStep.run).toContain("swiftlint lint --config config/swiftlint.yml");
expect(macosLintStep.run).toContain("swiftformat --lint apps/macos/Sources");
expect(iosLintStep.run).toContain("skipping iOS lint for this frozen target");
const runCacheFixture = (artifactState: "no-build" | "absent" | "incomplete" | "complete") => {
const root = tempDirs.make(`openclaw-swift-cache-${artifactState}-`);
const binDir = path.join(root, "bin");
const buildDir = path.join(root, "apps/macos/.build");
const frameworkDir = path.join(
root,
"apps/macos/.build/artifacts/sparkle/Sparkle/Sparkle.xcframework",
);
const callsPath = path.join(root, "swift-calls");
const outputPath = path.join(root, "github-output");
mkdirSync(binDir, { recursive: true });
if (artifactState === "absent") {
mkdirSync(buildDir, { recursive: true });
} else if (artifactState === "incomplete" || artifactState === "complete") {
mkdirSync(frameworkDir, { recursive: true });
}
if (artifactState === "complete") {
writeFileSync(path.join(frameworkDir, "Info.plist"), "complete\n", "utf8");
}
writeFileSync(
path.join(binDir, "swift"),
`#!/usr/bin/env bash
set -euo pipefail
printf '%s\\n' "$*" >> "$SWIFT_CALLS"
`,
"utf8",
);
chmodSync(path.join(binDir, "swift"), 0o755);
const result = runWorkflowShellScript(validateCacheStep.run, {
cwd: root,
env: {
...process.env,
GITHUB_OUTPUT: outputPath,
PATH: `${binDir}:${process.env.PATH ?? ""}`,
SWIFT_CALLS: callsPath,
},
});
const calls = existsSync(callsPath) ? readFileSync(callsPath, "utf8").trim().split("\n") : [];
return {
calls,
output: readFileSync(outputPath, "utf8").trim(),
status: result.status,
};
};
for (const artifactState of ["no-build", "complete"] as const) {
const result = runCacheFixture(artifactState);
expect(result.status).toBe(0);
expect(result.calls).toEqual([]);
expect(result.output).toBe("cache-valid=true");
}
for (const artifactState of ["absent", "incomplete"] as const) {
const result = runCacheFixture(artifactState);
expect(result.status).toBe(0);
expect(result.calls).toEqual(["package --package-path apps/macos reset"]);
expect(result.output).toBe("cache-valid=false");
}
const runBuildFixture = (
artifactState: "absent" | "incomplete" | "complete",
buildOutcome: "recover" | "fail",
) => {
const root = tempDirs.make(`openclaw-swift-build-${artifactState}-${buildOutcome}-`);
const binDir = path.join(root, "bin");
const frameworkDir = path.join(
root,
"apps/macos/.build/artifacts/sparkle/Sparkle/Sparkle.xcframework",
);
const callsPath = path.join(root, "swift-calls");
mkdirSync(binDir, { recursive: true });
if (artifactState === "incomplete" || artifactState === "complete") {
mkdirSync(frameworkDir, { recursive: true });
}
if (artifactState === "complete") {
writeFileSync(path.join(frameworkDir, "Info.plist"), "complete\n", "utf8");
}
writeFileSync(
path.join(binDir, "swift"),
`#!/usr/bin/env bash
set -euo pipefail
printf '%s\\n' "$*" >> "$SWIFT_CALLS"
if [[ "\${1:-}" == "package" ]]; then
exit 0
fi
build_count="$(grep -c '^build ' "$SWIFT_CALLS")"
if [[ "$BUILD_OUTCOME" == "recover" && "$build_count" -eq 2 ]]; then
exit 0
fi
exit 1
`,
"utf8",
);
chmodSync(path.join(binDir, "swift"), 0o755);
const result = runWorkflowShellScript(buildStep.run, {
cwd: root,
env: {
...process.env,
BUILD_OUTCOME: buildOutcome,
PATH: `${binDir}:${process.env.PATH ?? ""}`,
SWIFT_CALLS: callsPath,
},
});
return {
calls: readFileSync(callsPath, "utf8").trim().split("\n"),
output: `${result.stdout}${result.stderr}`,
status: result.status,
};
};
const releaseBuildCommand =
"build --package-path apps/macos --product OpenClaw --configuration release";
const packageResetCommand = "package --package-path apps/macos reset";
const absentFramework = runBuildFixture("absent", "fail");
expect(absentFramework.status).toBe(1);
expect(absentFramework.calls).toEqual([releaseBuildCommand]);
const recovered = runBuildFixture("incomplete", "recover");
expect(recovered.status).toBe(0);
expect(recovered.calls).toEqual([
releaseBuildCommand,
packageResetCommand,
releaseBuildCommand,
]);
expect(recovered.output).toContain("did not produce complete Sparkle metadata");
const completeFramework = runBuildFixture("complete", "fail");
expect(completeFramework.status).toBe(1);
expect(completeFramework.calls).toEqual([releaseBuildCommand]);
const secondFailure = runBuildFixture("incomplete", "fail");
expect(secondFailure.status).toBe(1);
expect(secondFailure.calls).toEqual([
releaseBuildCommand,
packageResetCommand,
releaseBuildCommand,
]);
});
it("uses native macOS Swift tests and preserves the first failure", () => {
const workflow = readCiWorkflow();
const macosSwift = workflow.jobs["macos-swift"];
const testStep = macosSwift.steps.find((step: WorkflowStep) => step.name === "Swift test");
const buildCache = macosSwift.steps.find(
(step: WorkflowStep) => step.id === "swift-build-cache",
);
const nativeCachePrefix =
"${{ runner.os }}-swift-build-v6-${{ matrix.phase }}-${{ hashFiles('scripts/swift-build-cache-metadata.py') }}-graph-${{ steps.swift-toolchain.outputs.key }}-" +
"${{ hashFiles('apps/macos/Package*.swift', 'apps/macos/Package.resolved', 'apps/shared/**/Package*.swift', 'apps/shared/**/Package.resolved', 'apps/swabble/Package*.swift', 'apps/swabble/Package.resolved') }}-";
expect(buildCache.with).toMatchObject({
key: expect.stringContaining(nativeCachePrefix),
"restore-keys": `${nativeCachePrefix}\n`,
});
const restoreMetadata = macosSwift.steps.find(
(step: WorkflowStep) => step.name === "Restore Swift build input timestamps",
);
const recordMetadata = macosSwift.steps.find(
(step: WorkflowStep) => step.name === "Record Swift build input timestamps",
);
const saveBuildCache = macosSwift.steps.find(
(step: WorkflowStep) => step.name === "Save Swift build directory cache",
);
expect(restoreMetadata.if).toBe(
"steps.validate-swift-build-cache.outputs.cache-valid == 'true' && env.HISTORICAL_TARGET != 'true'",
);
expect(restoreMetadata.run).toBe("python3 -I -S scripts/swift-build-cache-metadata.py restore");
expect(recordMetadata.run).toBe("python3 -I -S scripts/swift-build-cache-metadata.py record");
const saveEligibility = saveBuildCache.if.split(" && (env.HISTORICAL_TARGET")[0];
expect(recordMetadata.if).toBe(`${saveEligibility} && env.HISTORICAL_TARGET != 'true'`);
expect(saveBuildCache.if).toBe(
`${saveEligibility} && (env.HISTORICAL_TARGET == 'true' || steps.record-swift-build-cache-metadata.outcome == 'success')`,
);
const stepIndex = (step: WorkflowStep) => macosSwift.steps.indexOf(step);
expect(stepIndex(restoreMetadata)).toBeLessThan(stepIndex(testStep));
expect(stepIndex(recordMetadata)).toBeGreaterThan(stepIndex(testStep));
expect(stepIndex(recordMetadata) + 1).toBe(stepIndex(saveBuildCache));
expect(macosSwift.env).not.toHaveProperty("SWIFT_TEST_EXECUTION");
expect(testStep.id).toBe("swift-test");
const currentTargetBranch = testStep.run.split('elif [[ "$HISTORICAL_TARGET" == "true" ]]')[0];
expect(currentTargetBranch).toContain('logical_cpu="$(sysctl -n hw.logicalcpu)"');
expect(currentTargetBranch).toContain('[[ ! "$logical_cpu" =~ ^[1-9][0-9]*$ ]]');
expect(currentTargetBranch).toContain(
"swift_test_width=$(( logical_cpu < 12 ? logical_cpu : 12 ))",
);
expect(currentTargetBranch).toContain(
'swift_test_args+=(--experimental-maximum-parallelization-width "$swift_test_width")',
);
expect(currentTargetBranch).not.toContain("swift_test_args+=(--parallel)");
expect(currentTargetBranch).not.toContain("--no-parallel");
expect(testStep.run).toContain("swift_test_args+=(--no-parallel)");
for (const buildExitCode of [0, 23]) {
const root = tempDirs.make(`openclaw-swift-test-${buildExitCode}-`);
const binDir = path.join(root, "bin");
const callsPath = path.join(root, "swift-calls");
const outputPath = path.join(root, "github-output");
mkdirSync(binDir, { recursive: true });
symlinkSync(path.resolve("scripts"), path.join(root, "scripts"), "dir");
writeFileSync(
path.join(binDir, "swift"),
`#!/usr/bin/env bash
set -euo pipefail
SWIFT_CALLS=${JSON.stringify(callsPath)}
GITHUB_OUTPUT=${JSON.stringify(outputPath)}
BUILD_EXIT_CODE=${buildExitCode}
printf '%s\\n' "$*" >> "$SWIFT_CALLS"
if [[ "\${1:-}" == "build" ]]; then
[[ ! -s "$GITHUB_OUTPUT" ]] || exit 24
exit "$BUILD_EXIT_CODE"
fi
test_count="$(grep -c '^test ' "$SWIFT_CALLS")"
[[ "$test_count" -gt 1 ]]
`,
"utf8",
);
chmodSync(path.join(binDir, "swift"), 0o755);
writeFileSync(path.join(binDir, "sysctl"), "#!/usr/bin/env bash\nprintf '4\\n'\n", {
mode: 0o755,
});
// This fixture executes the real launcher: never fall through to host Security.
writeFileSync(
path.join(binDir, "security"),
`#!${process.execPath}
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const args = process.argv.slice(2);
assert.notEqual(process.env.HOME, ${JSON.stringify(root)});
assert.equal(path.dirname(args.at(-1)), path.join(process.env.HOME, 'Library/Keychains'));
if (args[0] === 'create-keychain') fs.writeFileSync(args.at(-1), 'inert keychain');
if (args[0] === 'delete-keychain') fs.unlinkSync(args.at(-1));
`,
{ mode: 0o755 },
);
const result = runWorkflowShellScript(testStep.run, {
cwd: root,
env: {
...process.env,
CI: "true",
GITHUB_ACTIONS: "true",
RUNNER_OS: "macOS",
RUNNER_TEMP: root,
HOME: root,
GITHUB_OUTPUT: outputPath,
PATH: `${binDir}:${process.env.PATH ?? ""}`,
SWIFT_TEST_EXECUTION: "serial",
},
});
const calls = readFileSync(callsPath, "utf8").trim().split("\n");
expect(result.status).toBe(buildExitCode || 1);
expect(calls).toEqual([
"build --package-path apps/macos --build-system native --enable-code-coverage --build-tests",
...(buildExitCode === 0
? [
expect.stringMatching(
/^test --package-path apps\/macos --build-system native --enable-code-coverage --skip-build --experimental-maximum-parallelization-width 4 --skip AppStateIsolationTests\|ProfileChatPreferencesTests --event-stream-output-path \S+\/swift-testing-events\.jsonl --event-stream-version 6\.3$/,
),
]
: []),
]);
const output = existsSync(outputPath) ? readFileSync(outputPath, "utf8").trim() : "";
const outputLines = output.split("\n");
if (buildExitCode === 0) {
expect(outputLines).toHaveLength(2);
expect(outputLines[0]).toBe("debug-tests-built=true");
expect(
outputLines[1]?.startsWith(`menu-default-artifact-path=${root}/openclaw-menu-default-`),
).toBe(true);
} else {
expect(output).toBe("");
}
}
});
it("bounds the Windows Crabbox hydrate main fetch", () => {
const workflow = readFileSync(".github/workflows/crabbox-hydrate.yml", "utf8");
expect(workflow).toContain("$fetchInfo = New-Object System.Diagnostics.ProcessStartInfo");
expect(workflow).toContain('$fetchInfo.FileName = "git"');
expect(workflow).toContain("$fetchInfo.WorkingDirectory = $repo");
expect(workflow).toContain("$fetchInfo.UseShellExecute = $false");
expect(workflow).not.toContain("$fetchInfo.RedirectStandardOutput = $true");
expect(workflow).not.toContain("$fetchInfo.RedirectStandardError = $true");
expect(workflow).toContain(
"--no-tags --no-progress --prune --no-recurse-submodules --depth=50",
);
expect(workflow).toContain("$fetch = New-Object System.Diagnostics.Process");
expect(workflow).toContain("$fetch.StartInfo = $fetchInfo");
expect(workflow).toContain("$fetch.WaitForExit(30000)");
expect(workflow).toContain("$fetch.Kill()");
expect(workflow).not.toContain("StandardOutput.ReadToEnd()");
expect(workflow).not.toContain("StandardError.ReadToEnd()");
expect(workflow).toContain('throw "git fetch failed with exit code $($fetch.ExitCode)"');
expect(workflow).toContain('throw "git fetch timed out after 30 seconds"');
expect(workflow).not.toContain(
'git fetch --no-tags --depth=50 origin "+refs/heads/main:refs/remotes/origin/main"',
);
});
it("bounds Mantis Slack runner IP discovery", () => {
const workflow = parse(
readFileSync(".github/workflows/mantis-slack-desktop-smoke.yml", "utf8"),
) as { jobs: { run_slack_desktop: { steps: WorkflowStep[] } } };
const runStep = workflow.jobs.run_slack_desktop.steps.find(
(step) => step.name === "Run Slack desktop scenario",
);
expect(runStep?.run).toContain("for attempt in 1 2 3");
expect(runStep?.run).toContain(
"curl -fsS --connect-timeout 5 --max-time 15 https://checkip.amazonaws.com",
);
expect(runStep?.run).not.toContain("--retry");
expect(runStep?.run).toContain('runner_ip=""');
expect(runStep?.run).toContain('[[ ! "$runner_ip" =~ ^(0|[1-9][0-9]{0,2})\\.');
expect(runStep?.run).toContain("((10#$octet > 255))");
const discoveryBlock = runStep?.run?.match(
/runner_ip=""[\s\S]*?echo "Using AWS SSH CIDR \$\{CRABBOX_AWS_SSH_CIDRS\}"/u,
)?.[0];
expect(discoveryBlock).toBeTruthy();
const root = mkdtempSync(path.join(tmpdir(), "openclaw-mantis-runner-ip-"));
try {
const fakeBin = path.join(root, "bin");
const callCount = path.join(root, "curl-calls");
mkdirSync(fakeBin);
writeFileSync(callCount, "0\n");
writeFileSync(
path.join(fakeBin, "curl"),
`#!/bin/bash
count="$(<"$CURL_CALL_COUNT")"
count=$((count + 1))
printf '%s\n' "$count" >"$CURL_CALL_COUNT"
if [[ "$count" == "1" ]]; then
printf '198.51.'
exit 28
fi
printf '%s\n' "\${CURL_SUCCESS_IP:-203.0.113.7}"
`,
{ mode: 0o755 },
);
writeFileSync(path.join(fakeBin, "sleep"), "#!/bin/sh\nexit 0\n", { mode: 0o755 });
const result = spawnSync(
"bash",
[
"-c",
`set -euo pipefail\n${discoveryBlock}\nprintf 'result=%s\\n' "$CRABBOX_AWS_SSH_CIDRS"`,
],
{
encoding: "utf8",
env: {
CURL_CALL_COUNT: callCount,
PATH: `${fakeBin}:${process.env.PATH}`,
},
},
);
expect(result.status, result.stderr).toBe(0);
expect(result.stdout).toContain("result=203.0.113.7/32");
expect(result.stdout).not.toContain("198.51.");
expect(readFileSync(callCount, "utf8")).toBe("2\n");
for (const invalidIp of ["999.0.0.1", "203.0.113.7."]) {
writeFileSync(callCount, "0\n");
const invalidResult = spawnSync("bash", ["-c", `set -euo pipefail\n${discoveryBlock}`], {
encoding: "utf8",
env: {
CURL_CALL_COUNT: callCount,
CURL_SUCCESS_IP: invalidIp,
PATH: `${fakeBin}:${process.env.PATH}`,
},
});
expect(invalidResult.status).toBe(1);
expect(invalidResult.stderr).toContain(
"Could not resolve GitHub runner public IPv4 for AWS SSH ingress.",
);
}
} finally {
rmSync(root, { recursive: true, force: true });
}
});
it("fails Windows Testbox setup when Blacksmith phone-home is not accepted", () => {
const workflow = readFileSync(".github/workflows/windows-blacksmith-testbox.yml", "utf8");
const job = parse(workflow).jobs.windows;
const prepare = job.steps.find((step: WorkflowStep) => step.name === "Prepare Windows SSH");
const finalize = job.steps.find((step: WorkflowStep) => step.name === "Run Testbox").run;
// Windows administrators use the effective ProgramData file and native ACLs.
// The native handshake is the behavioral proof; this guards workflow wiring.
expect(prepare?.env).toEqual({
TESTBOX_PUBLIC_KEY_PATH: "${{ steps.begin_testbox.outputs.public_key_path }}",
});
const nativeSetup = prepare?.run ?? "";
expect(nativeSetup).toContain("WindowsPrincipal");
expect(nativeSetup).toContain("System32\\OpenSSH\\sshd.exe");
expect(nativeSetup).toContain('-T -C "user=$nativeUser"');
expect(nativeSetup).toContain(
"authorizedkeysfile __PROGRAMDATA__/ssh/administrators_authorized_keys",
);
expect(nativeSetup).toContain("System32\\OpenSSH\\ssh-keygen.exe");
expect(nativeSetup).toContain("-E sha256 -lf $env:TESTBOX_PUBLIC_KEY_PATH");
expect(nativeSetup).toContain("[IO.File]::AppendAllText($authorizedKeys,");
expect(nativeSetup).toContain("S-1-5-18");
expect(nativeSetup).toContain("S-1-5-32-544");
expect(nativeSetup).toContain("SetAccessRuleProtection($true, $false)");
expect(nativeSetup).toContain('"FullControl", "Allow"');
expect(nativeSetup).toContain("Set-Acl -LiteralPath $authorizedKeys");
expect(workflow).not.toContain(">> ~/.ssh/authorized_keys");
expect(finalize).toMatch(
/if \[ "\$JOB_STATUS" != "success" \]; then\s+phone_home_status="hydration_failed"/u,
);
expect(finalize).toContain('--arg status "$phone_home_status"');
expect(finalize.match(/\/api\/testbox\/phone-home/gu)).toHaveLength(1);
expect(finalize.slice(0, finalize.indexOf('echo "Testbox ready!"'))).toMatch(
/if \[ "\$phone_home_status" != "ready" \]; then[^]*?exit 1\s+fi/u,
);
expect(workflow.match(/--connect-timeout 10 --max-time 30/gu)).toHaveLength(2);
expect(workflow).toContain('echo "phone_home_hydrating_curl=${hydrating_curl_status}"');
expect(workflow).toContain('echo "phone_home_hydrating_http=${hydrating_http_code}"');
expect(workflow).toContain('echo "phone_home_${phone_home_status}_curl=${final_curl_status}"');
expect(workflow).toContain('echo "phone_home_${phone_home_status}_http=${http_code}"');
expect(workflow).toContain('jq -e \'type == "number"\' <<<"$installation_model_id"');
expect(workflow).toContain('--arg testbox_id "$TESTBOX_ID"');
expect(workflow).toContain('--arg testbox_id "$testbox_id"');
expect(workflow).toContain('--argjson installation_model_id "$installation_model_id"');
expect(workflow).toContain('--data-binary @"$hydrating_body"');
expect(workflow).toContain('--data-binary @"$final_body"');
const hydratingFailureBlock = workflow.slice(
workflow.indexOf(
'if (( hydrating_curl_status != 0 )) || [[ ! "$hydrating_http_code" =~ ^2 ]]; then',
),
workflow.indexOf('response="$(cat "$hydrating_response")"'),
);
const missingSshKeyFailureBlock = workflow.slice(
workflow.indexOf('if [ -z "$ssh_public_key" ]; then'),
workflow.indexOf('public_key_path="$(cygpath'),
);
const finalFailureBlock = workflow.slice(
workflow.indexOf('if (( final_curl_status != 0 )) || [[ ! "$http_code" =~ ^2 ]]; then'),
workflow.indexOf('echo "============================================"'),
);
expect(workflow).toContain(')" || hydrating_curl_status=$?');
expect(workflow).toContain(')" || final_curl_status=$?');
expect(hydratingFailureBlock).toContain("exit 1");
expect(missingSshKeyFailureBlock).toContain("exit 1");
expect(finalFailureBlock).toContain("exit 1");
expect(workflow).toContain(
"Blacksmith phone-home did not return an SSH public key; testbox cannot accept native SSH connections.",
);
expect(workflow).not.toContain(
'phone_home_${phone_home_status}_http=${http_code}"\n\n echo "============================================"',
);
expect(workflow).not.toContain('\\"testbox_id\\": \\"${TESTBOX_ID}\\"');
expect(workflow).not.toContain('cat > "$final_body" <<JSON');
expect(workflow).not.toContain('"testbox_id": "${testbox_id}"');
});
it("runs dependency policy guards in PR CI preflight", () => {
const parsedWorkflow = readCiWorkflow();
const workflow = readFileSync(".github/workflows/ci.yml", "utf8");
const preflightGuards = workflow.slice(
workflow.indexOf("guards)"),
workflow.indexOf("npm-lock)"),
);
const npmLockGuards = workflow.slice(
workflow.indexOf("npm-lock)"),
workflow.indexOf("prod-types)"),
);
expect(workflow).toContain("check-guards");
expect(workflow).toContain("check-npm-lock");
expect(preflightGuards).toContain('has_package_script "check:doctor-deprecation-registry"');
expect(preflightGuards).toContain("pnpm check:doctor-deprecation-registry");
expect(preflightGuards).toContain(
"[skip] frozen target predates the wall-clock doctor deprecation registry guard",
);
expect(preflightGuards).toContain(
"Current CI targets must provide the check:doctor-deprecation-registry package script.",
);
expect(preflightGuards.indexOf('elif [[ "$FROZEN_TARGET" == "true" ]]')).toBeGreaterThan(
preflightGuards.indexOf("pnpm check:doctor-deprecation-registry"),
);
const checkShard = parsedWorkflow.jobs["check-shard"].steps.find(
(step: WorkflowStep) => step.name === "Run check shard",
);
expect(checkShard.env.FROZEN_TARGET).toBe("${{ needs.preflight.outputs.frozen_target }}");
expect(parsedWorkflow.jobs.preflight.outputs.frozen_target).toBe(
"${{ steps.manifest.outputs.frozen_target }}",
);
expect(preflightGuards).toContain(
'if [[ "$FROZEN_TARGET" == "true" ]]; then\n' +
" pnpm dup:check:coverage\n" +
" else\n" +
" pnpm dup:check\n" +
" fi",
);
expect(npmLockGuards).toContain("pnpm deps:npm-lock:check");
expect(preflightGuards).toContain("pnpm deps:patches:check");
expect(preflightGuards).toContain('has_package_script "check:coercion-helpers"');
expect(preflightGuards).toContain("pnpm check:coercion-helpers");
expect(preflightGuards).toContain(
"[skip] historical target predates the coercion-helper declaration guard",
);
expect(preflightGuards).toContain(
"Current CI targets must provide the check:coercion-helpers package script.",
);
expect(parsedWorkflow.jobs.preflight.outputs.diff_base_revision).toBe(
"${{ steps.diff_base.outputs.sha }}",
);
const diffBaseStep = parsedWorkflow.jobs.preflight.steps.find(
(step: WorkflowStep) => step.name === "Resolve exact diff base",
);
expect(diffBaseStep.run).toContain("--prefer-first-parent");
expect(diffBaseStep.env.DEFAULT_BRANCH).toBe("${{ github.event.repository.default_branch }}");
expect(diffBaseStep.env.GH_TOKEN).toBe(
"${{ github.event_name == 'workflow_dispatch' && !inputs.release_gate && github.token || '' }}",
);
expect(diffBaseStep.run).toContain(
'"repos/${GITHUB_REPOSITORY}/compare/${default_sha}...${head_sha}"',
);
expect(diffBaseStep.run).toContain("Could not resolve an exact diff base");
expect(diffBaseStep.run).toContain(AMBIGUOUS_MAIN_PUSH_GUARD);
const securityDiffBase = parsedWorkflow.jobs["security-fast"].steps.find(
(step: WorkflowStep) => step.name === "Resolve security diff base",
).run;
expect(securityDiffBase).toContain("git rev-list --parents -n 1 HEAD");
expect(securityDiffBase).not.toContain("node scripts/lib/merge-head-diff-base.mjs");
expect(securityDiffBase).toContain(AMBIGUOUS_MAIN_PUSH_GUARD);
const checkShardStep = parsedWorkflow.jobs["check-shard"].steps.find(
(step: WorkflowStep) => step.name === "Run check shard",
);
expect(checkShardStep.run).not.toContain("--checkout-git");
expect(checkShardStep.run).toContain(
'test "$(git rev-parse refs/remotes/origin/ci-ratchet-base^{commit})" = "$CHECKOUT_BASE_SHA"',
);
});
it.each([
{ job: "check-shard", task: "prod-types", events: [] },
{
job: "checks-fast-core",
task: "baseline-ratchets",
events: ["pull_request", "push", "workflow_dispatch"],
},
{
job: "checks-fast-core",
task: "release-lint-core-1",
events: ["pull_request", "push", "workflow_dispatch"],
},
{ job: "checks-fast-core", task: "ci-routing", events: [] },
])("prepares the frozen diff base for $job/$task at checkout", ({ job, task, events }) => {
const expression = readCiWorkflow().jobs[job].env?.CHECKOUT_BASE_SHA;
const base = "c".repeat(40);
expect(typeof expression).toBe("string");
for (const eventName of ["pull_request", "push", "workflow_dispatch"] as const) {
expect(
evaluateWorkflowExpression(expression, {
eventName,
repository: "openclaw/openclaw",
runAttempt: 1,
matrix: { task },
preflightOutputs: { diff_base_revision: base },
}),
eventName,
).toBe(events.includes(eventName) ? base : "");
}
});
it.each([
{ label: "manual run", checkoutBase: "", changedScript: true },
{ label: "target without changed checks", checkoutBase: "c".repeat(40), changedScript: false },
])("keeps the full npm-lock sweep for $label", ({ checkoutBase, changedScript }) => {
const result = runCheckShardFixture({
task: "npm-lock",
frozenTarget: false,
checkoutBase,
scripts: ["deps:npm-lock:check", ...(changedScript ? ["deps:npm-lock:check:changed"] : [])],
});
expect(result.status, result.output).toBe(0);
expect(result.calls).toEqual(["deps:npm-lock:check"]);
});
it.each([false, true])(
"preserves absent npm-lock capability handling (historical=%s)",
(historical) => {
const result = runCheckShardFixture({
task: "npm-lock",
frozenTarget: historical,
scripts: [],
});
expect(result.status, result.output).toBe(historical ? 0 : 1);
expect(result.calls).toEqual([]);
expect(result.output).toContain(
historical
? "[skip] historical target predates the transient npm lock contract"
: "Current CI targets must provide the deps:npm-lock:check package script.",
);
},
);
it("runs temp path guardrails in the hosted guard shard", () => {
const requiredScripts = [
"check:doctor-deprecation-registry",
"check:browser-inspect-script:swift",
"check:coercion-helpers",
];
const current = runCheckShardFixture({
frozenTarget: false,
scripts: [...requiredScripts, "check:temp-path-guardrails"],
});
expect(current.status, current.output).toBe(0);
expect(current.calls).toContain("check:temp-path-guardrails");
expect(current.calls.indexOf("check:temp-path-guardrails")).toBeLessThan(
current.calls.indexOf("dup:check"),
);
const frozenMissing = runCheckShardFixture({
frozenTarget: true,
scripts: requiredScripts,
});
expect(frozenMissing.status, frozenMissing.output).toBe(0);
expect(frozenMissing.calls).not.toContain("check:temp-path-guardrails");
expect(frozenMissing.calls).toContain("dup:check:coverage");
expect(frozenMissing.output).toContain(
"[skip] frozen target predates the temp path guardrails",
);
const currentMissing = runCheckShardFixture({
frozenTarget: false,
scripts: requiredScripts,
});
expect(currentMissing.status).toBe(1);
expect(currentMissing.calls).not.toContain("check:temp-path-guardrails");
expect(currentMissing.calls).not.toContain("dup:check");
expect(currentMissing.output).toContain(
"Current CI targets must provide the check:temp-path-guardrails package script.",
);
const workflow = readFileSync(".github/workflows/ci.yml", "utf8");
const preflightGuards = workflow.slice(
workflow.indexOf("guards)"),
workflow.indexOf("npm-lock)"),
);
expect(preflightGuards.indexOf("pnpm check:temp-path-guardrails")).toBeLessThan(
preflightGuards.indexOf("pnpm dup:check"),
);
});
it.each([
{
scripts: ["tsgo:scripts", "tsgo:test:root"],
frozenTarget: false,
status: 0,
calls: ["tsgo:extensions:test", "tsgo:scripts", "tsgo:test:root"],
},
{
scripts: ["tsgo:scripts"],
frozenTarget: false,
status: 1,
calls: ["tsgo:extensions:test", "tsgo:scripts"],
},
{
scripts: [],
frozenTarget: false,
status: 1,
calls: ["tsgo:extensions:test"],
},
{
scripts: [],
frozenTarget: true,
status: 0,
calls: ["tsgo:extensions:test"],
},
{
scripts: ["tsgo:test:root"],
frozenTarget: true,
status: 0,
calls: ["tsgo:extensions:test", "tsgo:test:root"],
},
])(
"runs declared typechecks for scripts=$scripts frozen=$frozenTarget",
({ scripts, frozenTarget, status, calls }) => {
const result = runCheckShardFixture({ scripts, frozenTarget, task: "test-types" });
expect(result.status, result.output).toBe(status);
expect(result.calls).toEqual(calls);
},
);
it("routes eligible core test leaves to one type owner before runner allocation", () => {
const changedPaths = [
"src/commands/doctor-config-preflight.plugin-persistence.test.ts",
"docs/ci.md",
];
const manifest = runCiManifestFixture({
bundledPlanner: true,
changedCoreTestSupport: true,
changedPlannerDependencies: changedPaths.slice(0, 1),
eventName: "pull_request",
runnerProfile: "hybrid",
changedPaths,
});
expect(manifest.status, manifest.output).toBe(0);
expect(manifest.outputs.changed_core_test_paths_json).toBe(
JSON.stringify(changedPaths.slice(0, 1)),
);
const result = runCheckShardFixture({
frozenTarget: false,
task: "test-types",
scripts: ["tsgo:scripts", "tsgo:test:root"],
types: {
compose: true,
changedPathsJson: manifest.outputs.changed_core_test_paths_json,
boundary: true,
},
});
expect(result.status, result.output).toBe(0);
expect(result.rows).toEqual([
{ name: "central", status: 0 },
{ name: "boundary", status: 0 },
]);
expect(result.typeCalls.filter((call) => call.row === "central")).toEqual([
{
row: "central",
localCheck: null,
command: `node --changed-paths-json ${JSON.stringify(changedPaths.slice(0, 1))} --concurrency 2`,
},
...["tsgo:extensions:test", "tsgo:scripts", "tsgo:test:root"].map((command) => ({
row: "central",
localCheck: "0",
command: `pnpm ${command}`,
})),
]);
expect(
result.typeCalls
.filter((call) => call.row === "boundary")
.map((call) => call.command)
.toSorted(),
).toEqual(
BOUNDARY_CHECKS.filter((check) => check.label !== "lint:tmp:tsgo-core-boundary")
.map((check) => [check.command, ...check.args].join(" "))
.toSorted(),
);
const workflow = readCiWorkflow();
expect(
evaluateWorkflowExpression(workflow.jobs["check-test-types-hosted-core-shard"].if, {
eventName: "pull_request",
repository: "openclaw/openclaw",
runAttempt: 1,
runnerProfile: "hybrid",
preflightOutputs: manifest.outputs,
}),
).toBe(false);
});
it.each([
{ changedPaths: null, invalid: true },
{ changedPaths: [] },
{ changedPaths: ["docs/ci.md"] },
{
changedPaths: [
"src/commands/doctor-config-preflight.plugin-persistence.test.ts",
"src/commands/deleted-core-leaf.test.ts",
"docs/ci.md",
],
changedPlannerDependencies: [
"src/commands/doctor-config-preflight.plugin-persistence.test.ts",
],
},
{ changedPaths: ["src/commands/doctor.test.ts", "package.json"] },
{ changedPaths: ["src/commands/doctor.test.ts", "src/shared.test-support.ts"] },
{ changedPaths: ["packages/mermaid-renderer/src/render.test.ts"] },
{ changedPaths: ["src/gateway/gateway-acp-bind.live.test.ts"] },
{ changedPaths: ["src/commands/doctor.test.ts"], changedCoreTestSupport: false },
{ changedPaths: ["src/commands/doctor.test.ts"], eventName: "push" as const },
{ changedPaths: ["src/commands/doctor.test.ts"], eventName: "workflow_dispatch" as const },
{
changedPaths: ["src/commands/doctor.test.ts"],
scopeEnv: { OPENCLAW_CI_CHANGED_PATHS_JSON: "invalid" },
invalid: true,
},
])("retains full type owners for ineligible manifest inputs %j", (options) => {
const manifest = runCiManifestFixture({
bundledPlanner: true,
changedCoreTestSupport: true,
eventName: "pull_request",
runnerProfile: "hybrid",
...options,
});
if ("invalid" in options) {
expect(manifest.status).not.toBe(0);
expect(manifest.output).toContain("Current PR CI requires complete changed paths");
expect(manifest.outputs.changed_core_test_paths_json).toBeUndefined();
} else {
expect(manifest.status, manifest.output).toBe(0);
expect(manifest.outputs.changed_core_test_paths_json).toBe("");
expect(
evaluateWorkflowExpression(readCiWorkflow().jobs["check-test-types-hosted-core-shard"].if, {
eventName: options.eventName ?? "pull_request",
repository: "openclaw/openclaw",
runAttempt: 1,
runnerProfile: "hybrid",
preflightOutputs: manifest.outputs,
}),
).toBe(true);
}
});
it.each([
["hybrid", "pull_request", false, true, true, true],
["github", "workflow_dispatch", false, true, true, true],
["blacksmith", "push", false, true, true, false],
["hybrid", "workflow_dispatch", true, false, true, false],
["hybrid", "workflow_dispatch", true, true, false, false],
["hybrid", "workflow_dispatch", true, true, true, true],
] as const)(
"preserves type workload for %s %s frozen=%s hosted-contract=%s stripe-support=%s",
(profile, eventName, frozenTarget, hostedContract, stripeSupport, striped) => {
const result = runCheckShardFixture({
task: "test-types",
scripts: ["tsgo:scripts", "tsgo:test:root"],
frozenTarget,
types: { compose: true, profile, eventName, hostedContract, stripeSupport },
});
expect(result.status, result.output).toBe(0);
const stripes = result.typeCalls.filter((call) => call.command.startsWith("node "));
const packages = result.typeCalls.filter((call) => call.command.startsWith("pnpm "));
expect(packages.map((call) => call.localCheck)).toEqual(packages.map(() => "0"));
if (striped) {
expect(result.rows).toHaveLength(3);
expect(
result.rows.map((row) =>
stripes
.filter((call) => call.row === row.name)
.map((call) => call.command.split(" ")[2]),
),
).toEqual([["1/5", "2/5"], ["3/5", "4/5"], ["5/5"]]);
for (const call of stripes) {
const args = call.command.split(" ").slice(1);
expect(args).toEqual(["--stripe", expect.any(String), "--concurrency", "2"]);
expect(call.localCheck).toBeNull();
}
expect(result.calls).toEqual(["tsgo:extensions:test", "tsgo:scripts", "tsgo:test:root"]);
} else {
expect(stripes).toEqual([]);
expect(result.calls).toEqual(["check:test-types", "tsgo:scripts"]);
}
},
);
it.each(["1/5", "5/5"])("halts only the type row whose first stripe %s fails", (failStripe) => {
const result = runCheckShardFixture({
task: "test-types",
scripts: ["tsgo:scripts", "tsgo:test:root"],
frozenTarget: false,
types: { compose: true, failStripe },
});
expect(result.status, result.output).toBe(17);
expect(readCiWorkflow().jobs["check-test-types-hosted-core-shard"].strategy["fail-fast"]).toBe(
false,
);
const failed = result.rows.filter((row) => row.status !== 0);
expect(failed).toHaveLength(1);
expect(result.rows.filter((row) => row.status === 0)).toHaveLength(2);
expect(
result.typeCalls.filter((call) => call.row === failed[0]!.name).map((call) => call.command),
).toEqual([`node --stripe ${failStripe} --concurrency 2`]);
});
it.each(["main", "trunk/release"])(
"resolves manual diff and cache bases from authenticated %s when anonymous Git is unavailable",
(defaultBranch) => {
const result = runDiffBaseFixture({
commitCount: 2,
eventBaseSha: "",
defaultBranch,
manual: true,
});
expect(result.status, result.output).toBe(0);
expect(result.outputs).toEqual({
default_sha: result.parentSha,
sha: result.parentSha,
head_sha: result.headSha,
});
expect(result.emittedBaseIsCommit).toBe(true);
},
);
it.each(["ref", "comparison"] as const)(
"rejects unavailable authenticated manual diff-base %s evidence",
(apiError) => {
const result = runDiffBaseFixture({
commitCount: 2,
eventBaseSha: "",
manual: true,
apiError,
});
expect(result.status).not.toBe(0);
expect(result.output).toContain("HTTP 503");
expect(result.outputs).not.toHaveProperty("sha");
},
);
it("rejects ambiguous zero-before main pushes and preserves concrete bases", () => {
const zeroSha = "0".repeat(40);
const threeCommit = runDiffBaseFixture({ commitCount: 3, eventBaseSha: zeroSha });
expect(threeCommit.status, threeCommit.output).toBe(1);
expect(threeCommit.output).toContain(AMBIGUOUS_MAIN_PUSH_DIAGNOSTIC);
expect(threeCommit.outputs).not.toHaveProperty("sha");
expect(threeCommit.emittedBaseIsCommit).toBe(false);
const rootCommit = runDiffBaseFixture({ commitCount: 1, eventBaseSha: zeroSha });
expect(rootCommit.status, rootCommit.output).toBe(1);
expect(rootCommit.output).toContain(AMBIGUOUS_MAIN_PUSH_DIAGNOSTIC);
expect(rootCommit.outputs).not.toHaveProperty("sha");
expect(rootCommit.emittedBaseIsCommit).toBe(false);
const concreteBase = runDiffBaseFixture({
commitCount: 3,
eventBaseSha: "parent",
});
expect(concreteBase.status, concreteBase.output).toBe(0);
expect(concreteBase.outputs.sha).toBe(concreteBase.eventBaseSha);
expect(concreteBase.emittedBaseIsCommit).toBe(true);
});
it("uses stable deadcode checks for current and frozen checkouts", () => {
const modern = runDependencyCheckFixture({
historicalTarget: false,
scripts: ["deadcode:dependencies", "deadcode:unused-files", "deadcode:exports"],
});
expect(modern.status, modern.output).toBe(0);
// The scripts launch concurrently; completion order is nondeterministic.
expect(modern.calls.toSorted()).toEqual([
"deadcode:dependencies",
"deadcode:exports",
"deadcode:unused-files",
]);
const frozenWithExports = runDependencyCheckFixture({
historicalTarget: true,
releaseToolingEntry: true,
scripts: ["deadcode:dependencies", "deadcode:unused-files", "deadcode:exports"],
});
expect(frozenWithExports.status, frozenWithExports.output).toBe(0);
expect(frozenWithExports.calls.toSorted()).toEqual([
"deadcode:dependencies",
"deadcode:exports",
"deadcode:unused-files",
]);
const frozen = runDependencyCheckFixture({
historicalTarget: true,
scripts: [
"deadcode:ci",
"deadcode:dependencies",
"deadcode:report:ci:ts-unused",
"deadcode:unused-files",
],
});
expect(frozen.status, frozen.output).toBe(0);
expect(frozen.calls.toSorted()).toEqual(["deadcode:dependencies", "deadcode:unused-files"]);
const currentWithoutExports = runDependencyCheckFixture({
historicalTarget: false,
scripts: ["deadcode:dependencies", "deadcode:unused-files"],
});
expect(currentWithoutExports.status).toBe(1);
// The missing-script contract violation now fails fast before launching
// the concurrent scans instead of wasting two Knip runs first.
expect(currentWithoutExports.calls).toEqual([]);
expect(currentWithoutExports.output).toContain(
"Current CI targets must provide the deadcode:exports package script.",
);
const legacy = runDependencyCheckFixture({
historicalTarget: true,
scripts: ["deadcode:ci"],
});
expect(legacy.status, legacy.output).toBe(0);
expect(legacy.calls).toEqual(["deadcode:ci"]);
const incompleteCurrent = runDependencyCheckFixture({
historicalTarget: false,
scripts: ["deadcode:dependencies"],
});
expect(incompleteCurrent.status).toBe(1);
expect(incompleteCurrent.calls).toEqual([]);
expect(incompleteCurrent.output).toContain(
"Target does not provide a supported deadcode check.",
);
});
it("keeps the preflight manifest import closure dependency-free", () => {
const manifestStep = readCiWorkflow().jobs.preflight.steps.find(
(step: WorkflowStep) => step.name === "Build CI manifest",
);
const manifestRun = expectDefined(manifestStep?.run, "Build CI manifest script");
const manifestSource = expectDefined(
manifestRun.match(/--input-type=module <<'([A-Z][A-Z0-9_]*)'\n([\s\S]*?)\n\1(?=\n|$)/u)?.[2],
"Build CI manifest Node source",
);
const { result, manifest } = runDependencyFreePreflight(
manifestSource,
tempDirs.make("ci-preflight-dependencies-"),
testNodeExecPath,
);
expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(0);
expect(manifest).toContain("run_node=true\n");
expect(manifest).toContain("run_windows=true\n");
});
it("runs mobile protocol coverage for Node and native-only changes", () => {
const workflow = readCiWorkflow();
const coverageStep = workflow.jobs.preflight.steps.find(
(step: WorkflowStep) => step.name === "Check mobile protocol event coverage",
);
const checkShardRun = workflow.jobs["check-shard"].steps.find(
(step: WorkflowStep) => step.name === "Run check shard",
).run;
// Current-source preflight runs the .mts natively; dispatches selecting
// another revision retain that target's tsx shim.
expect(coverageStep.run).toContain("node scripts/check-protocol-event-coverage.mts");
expect(coverageStep.run).toContain("node scripts/check-protocol-event-coverage.mjs");
expect(coverageStep.if).toBe("steps.manifest.outputs.run_protocol_event_coverage == 'true'");
expect(checkShardRun).not.toContain("check:protocol-coverage");
});
it("keeps type-aware oxlint within hosted fork-runner resources", () => {
const workflow = readCiWorkflow();
const manifestStep = workflow.jobs.preflight.steps.find(
(step: WorkflowStep) => step.name === "Build CI manifest",
);
const checkShardStep = workflow.jobs["check-shard"].steps.find(
(step: WorkflowStep) => step.name === "Run check shard",
);
const checkShardRun = checkShardStep.run;
const hostedCoreLint = workflow.jobs["check-lint-hosted-core-shard"];
const hostedCoreTypes = workflow.jobs["check-test-types-hosted-core-shard"];
expect(manifestStep.env.OPENCLAW_CI_RUNNER_PROFILE).toBe(
"${{ steps.runner_profile.outputs.runner_profile }}",
);
expect(manifestStep.run).toContain("runnerBackend: runnerProfile");
expect(checkShardStep.env.RUNNER_PROFILE).toBe("${{ needs.preflight.outputs.runner_profile }}");
expect(checkShardStep.env.HOSTED_RUNNER_STRIPES).toContain(
"needs.preflight.outputs.hosted_runner_profile_contract == 'true'",
);
expect(checkShardRun).toContain('if [ "$HOSTED_RUNNER_STRIPES" = "true" ]; then');
expect(checkShardStep.env.RELEASE_GATE).toBe("${{ inputs.release_gate && 'true' || 'false' }}");
expect(checkShardRun).toContain("lint_args=(--only=extensions --only=scripts --threads=1)");
expect(checkShardRun).toContain('if [ "$RELEASE_GATE" = "true" ]; then');
expect(checkShardRun).toContain("lint_args=(--only=scripts --threads=1)");
expect(checkShardRun).toContain('elif [ "$(nproc)" -lt 8 ]; then');
expect(checkShardRun).toContain("lint_args=(--threads=1)");
expect(checkShardRun).not.toContain("lint_args=(--split-core --threads=1)");
expect(checkShardRun).toContain('pnpm lint "${lint_args[@]}"');
expect(checkShardRun).toContain(
'node --import tsx scripts/run-oxlint-shards.mts "${lint_args[@]}"',
);
for (const job of [hostedCoreLint, hostedCoreTypes]) {
expect(job.if).toContain("needs.preflight.outputs.runner_profile == 'github'");
expect(job.if).toContain("needs.preflight.outputs.runner_profile == 'hybrid'");
expect(job.if).toContain("needs.preflight.outputs.hosted_runner_profile_contract == 'true'");
expect(
evaluateWorkflowExpression(job.if, {
eventName: "workflow_dispatch",
frozenTarget: true,
hostedRunnerProfileContract: false,
repository: "openclaw/openclaw",
runnerProfile: "blacksmith",
runAttempt: 1,
}),
).toBe(false);
expect(
evaluateWorkflowExpression(job.if, {
eventName: "workflow_dispatch",
frozenTarget: true,
hostedRunnerProfileContract: true,
repository: "openclaw/openclaw",
runnerProfile: "github",
runAttempt: 1,
}),
).toBe(true);
for (const [runnerProfile, expected] of [
["blacksmith", false],
["github", true],
["hybrid", true],
] as const) {
expect(
evaluateWorkflowExpression(job.if, {
eventName: "pull_request",
frozenTarget: false,
hostedRunnerProfileContract: true,
repository: "openclaw/openclaw",
runnerProfile,
runAttempt: 1,
}),
).toBe(expected);
}
}
expect(hostedCoreLint["runs-on"]).toBe("ubuntu-24.04");
expect(hostedCoreLint.strategy["fail-fast"]).toBe(false);
expect(hostedCoreLint.strategy["max-parallel"]).toBe(5);
const coreLintStep = hostedCoreLint.steps.find(
(step: WorkflowStep) => step.name === "Run hosted core lint stripe",
);
expect(coreLintStep.env.CORE_STRIPE).toBe("${{ matrix.stripe }}");
type GoEnv = Partial<Pick<NodeJS.ProcessEnv, "GOMAXPROCS" | "GOGC" | "GOMEMLIMIT">>;
const goEnvKeys = ["GOMAXPROCS", "GOGC", "GOMEMLIMIT"] as const;
const runLintOwner = ({
capability,
cpuCount = 32,
eventName = "workflow_dispatch",
failStripe,
frozenTarget = !capability,
goEnv = {},
expectedGoEnv = goEnv,
lane,
profile,
releaseGate = false,
stripe = 1,
}: {
capability: boolean;
cpuCount?: number;
eventName?: "pull_request" | "push" | "workflow_dispatch";
failStripe?: number;
frozenTarget?: boolean;
goEnv?: GoEnv;
expectedGoEnv?: GoEnv;
lane: "check" | "core";
profile: "blacksmith" | "github" | "hybrid";
releaseGate?: boolean;
stripe?: number;
}) => {
const root = tempDirs.make("openclaw-hosted-lint-owner-");
const binDir = path.join(root, "bin");
const callsPath = path.join(root, "calls.txt");
const goEnvPath = path.join(root, "go-env.txt");
mkdirSync(path.join(root, "scripts"), { recursive: true });
mkdirSync(binDir);
writeFileSync(
path.join(root, "scripts/run-oxlint-shards.mts"),
capability ? "// --extension-stripe\n" : "// legacy runner\n",
);
for (const command of ["node", "pnpm"]) {
writeExecutable(path.join(binDir, command), [
"#!/usr/bin/env bash",
"set -euo pipefail",
`printf '${command} %s\\n' "$*" >> "$LINT_CALLS"`,
'printf \'%s\\t%s\\t%s\\n\' "${GOMAXPROCS-}" "${GOGC-}" "${GOMEMLIMIT-}" >> "$LINT_GO_ENV"',
...(failStripe === undefined
? []
: [`if [[ " $* " == *" --core-stripe=${failStripe}/5 "* ]]; then exit 23; fi`]),
]);
}
writeExecutable(path.join(binDir, "nproc"), [
"#!/usr/bin/env bash",
`printf '${cpuCount}\\n'`,
]);
const coreRun = coreLintStep.run.replace(/\$\{\{[\s\S]*?\}\}/gu, (expression: string) =>
String(
evaluateWorkflowExpression(expression, {
eventName,
frozenTarget,
matrix: { stripe },
releaseGate,
repository: "openclaw/openclaw",
runnerProfile: profile,
runAttempt: 1,
}),
),
);
const stepEnv = lane === "check" ? checkShardStep.env : coreLintStep.env;
const result = spawnSync("bash", ["-c", lane === "check" ? checkShardRun : coreRun], {
cwd: root,
encoding: "utf8",
env: {
...process.env,
GOMAXPROCS: undefined,
GOGC: undefined,
GOMEMLIMIT: undefined,
...goEnv,
...Object.fromEntries(
goEnvKeys.flatMap((key) => (stepEnv[key] === undefined ? [] : [[key, stepEnv[key]]])),
),
FORMAT_CHECK: "false",
CORE_STRIPE: String(stripe),
FROZEN_TARGET: frozenTarget ? "true" : "false",
HISTORICAL_TARGET: capability ? "false" : "true",
HOSTED_RUNNER_STRIPES: profile === "blacksmith" ? "false" : "true",
LINT_CALLS: callsPath,
LINT_GO_ENV: goEnvPath,
OPENCLAW_LOCAL_CHECK: "0",
PATH: `${binDir}:${process.env.PATH ?? ""}`,
RELEASE_GATE: releaseGate ? "true" : "false",
RUN_CONTROL_UI_I18N: "false",
RUNNER_PROFILE: profile,
RUN_UI_TESTS: "false",
TASK: "lint",
},
});
expect(result.status, `${result.stdout}${result.stderr}`).toBe(
failStripe === undefined ? 0 : 23,
);
const calls = existsSync(callsPath)
? readFileSync(callsPath, "utf8").trim().split("\n").filter(Boolean)
: [];
expect(calls.length).toBeGreaterThan(0);
expect(readFileSync(goEnvPath, "utf8").split("\n").filter(Boolean)).toEqual(
calls.map(() => goEnvKeys.map((key) => expectedGoEnv[key] ?? "").join("\t")),
);
return calls;
};
const coreLintRows = (
context: Partial<Parameters<typeof evaluateWorkflowExpression>[1]>,
): number[] => {
const stripes = hostedCoreLint.strategy.matrix.stripe;
return Array.isArray(stripes)
? stripes
: evaluateWorkflowExpression(stripes, {
eventName: "pull_request",
repository: "openclaw/openclaw",
runnerProfile: "hybrid",
runAttempt: 1,
...context,
});
};
for (const eventName of ["pull_request", "push"] as const) {
const rows = coreLintRows({ eventName });
expect(rows).toEqual([1, 2]);
expect(
rows.map((stripe) =>
runLintOwner({ capability: true, eventName, lane: "core", profile: "hybrid", stripe }),
),
).toEqual(
[
[1, 2],
[3, 4, 5],
].map((stripes) =>
stripes.map(
(stripe) =>
`node --import tsx scripts/run-oxlint-shards.mts --only=core --split-core --core-stripe=${stripe}/5 --threads=1`,
),
),
);
}
for (const context of [
{ runnerProfile: "github" as const },
{ eventName: "workflow_dispatch" as const },
{ frozenTarget: true },
{ releaseGate: true },
]) {
expect(coreLintRows(context)).toEqual([1, 2, 3, 4, 5]);
}
expect(
runLintOwner({
capability: true,
eventName: "pull_request",
failStripe: 1,
lane: "core",
profile: "hybrid",
}),
).toEqual([
"node --import tsx scripts/run-oxlint-shards.mts --only=core --split-core --core-stripe=1/5 --threads=1",
]);
expect(
runLintOwner({
capability: true,
eventName: "pull_request",
failStripe: 4,
lane: "core",
profile: "hybrid",
stripe: 2,
}),
).toEqual([
"node --import tsx scripts/run-oxlint-shards.mts --only=core --split-core --core-stripe=3/5 --threads=1",
"node --import tsx scripts/run-oxlint-shards.mts --only=core --split-core --core-stripe=4/5 --threads=1",
]);
expect(runLintOwner({ capability: true, lane: "check", profile: "github" })).toEqual([
"node --import tsx scripts/run-oxlint-shards.mts --only=extensions --extension-stripe=6/6 --threads=1",
"node --import tsx scripts/run-oxlint-shards.mts --only=scripts --threads=1",
]);
expect(
runLintOwner({
capability: true,
eventName: "pull_request",
lane: "core",
profile: "github",
}),
).toEqual([
"node --import tsx scripts/run-oxlint-shards.mts --only=core --split-core --core-stripe=1/5 --threads=1",
"node --import tsx scripts/run-oxlint-shards.mts --only=extensions --extension-stripe=1/6 --threads=1",
]);
for (const scenario of [
{
capability: false,
lane: "check" as const,
profile: "github" as const,
expectedGoEnv: { GOMAXPROCS: "2", GOGC: "30", GOMEMLIMIT: "3GiB" },
},
{ capability: true, lane: "check" as const, profile: "hybrid" as const },
]) {
expect(runLintOwner(scenario)).toEqual([
"node --import tsx scripts/run-oxlint-shards.mts --only=extensions --only=scripts --threads=1",
]);
}
expect(runLintOwner({ capability: true, lane: "check", profile: "blacksmith" })).toEqual([
"node --import tsx scripts/run-oxlint-shards.mts --threads=8",
]);
expect(
runLintOwner({ capability: true, lane: "check", profile: "github", releaseGate: true }),
).toEqual(["node --import tsx scripts/run-oxlint-shards.mts --only=scripts --threads=1"]);
for (const scenario of [
{
capability: false,
lane: "core" as const,
profile: "github" as const,
expectedGoEnv: { GOMAXPROCS: "2" },
},
{ capability: true, lane: "core" as const, profile: "hybrid" as const },
{
capability: true,
lane: "core" as const,
profile: "github" as const,
releaseGate: true,
},
]) {
expect(runLintOwner(scenario)).toEqual([
"node --import tsx scripts/run-oxlint-shards.mts --only=core --split-core --core-stripe=1/5 --threads=1",
]);
}
for (const lane of ["check", "core"] as const) {
runLintOwner({ capability: true, cpuCount: 4, lane, profile: "hybrid" });
runLintOwner({
capability: true,
lane,
profile: "github",
goEnv: { GOMAXPROCS: "3", GOGC: "80", GOMEMLIMIT: "5GiB" },
});
}
runLintOwner({ capability: true, cpuCount: 4, lane: "check", profile: "blacksmith" });
for (const [profile, cpuCount] of [
["hybrid", 32],
["blacksmith", 4],
] as const) {
runLintOwner({
capability: true,
cpuCount,
frozenTarget: true,
lane: "check",
profile,
expectedGoEnv: { GOMAXPROCS: "2", GOGC: "30", GOMEMLIMIT: "3GiB" },
});
}
runLintOwner({
capability: true,
frozenTarget: true,
lane: "check",
profile: "blacksmith",
});
expect(coreLintStep.env.FROZEN_TARGET).toBe("${{ needs.preflight.outputs.frozen_target }}");
});
it.skipIf(process.platform === "win32").each(
(["bundled-protocol", "guards", "npm-lock"] as const).flatMap((task) =>
(["pull_request", "push", "workflow_dispatch"] as const).map((eventName) => ({
task,
eventName,
})),
),
)(
"uses prefetched CI base without later network access ($task, $eventName)",
async ({ task, eventName }) => {
const base = "c".repeat(40);
const baseRef = "refs/remotes/origin/ci-ratchet-base";
const jobName = task === "bundled-protocol" ? "checks-fast-core" : "check-shard";
const job = readCiWorkflow().jobs[jobName];
const needsBase =
task === "bundled-protocol" ||
(task === "guards" ? eventName === "pull_request" : eventName !== "workflow_dispatch");
const checkoutBase = evaluateWorkflowExpression(job.env?.CHECKOUT_BASE_SHA ?? "${{ '' }}", {
eventName,
repository: "fixture/checkout",
runAttempt: 1,
matrix: { task },
preflightOutputs: { diff_base_revision: base },
});
const report = await runCiGitStep({
job: jobName,
step:
task === "bundled-protocol"
? "Run ${{ matrix.task }} (${{ matrix.runtime }})"
: "Run check shard",
checkoutBeforeStep: true,
// The authenticated checkout and trusted harness fetch succeed. Network
// access is unavailable afterward, even though the base is already local.
fetchResults: [0, 0, 128],
baseAvailableAfter: 0,
revisions: { [`${baseRef}^{commit}`]: base },
env: {
TASK: task,
GITHUB_EVENT_NAME: eventName,
CHECKOUT_KIND: "linux-node",
CHECKOUT_BASE_SHA: String(checkoutBase),
CHECKOUT_TOKEN: "fixture-checkout-token",
},
});
expect(report.code, report.output).toBe(0);
expect(report.fetches).toHaveLength(2);
const sourceFetch = report.fetches.find(({ cwd }) => cwd === report.workspace);
expect(sourceFetch?.args.includes(`+${base}:refs/remotes/origin/ci-ratchet-base`)).toBe(
needsBase,
);
const consumers = report.commands.filter(({ tool }) => tool === "node" || tool === "pnpm");
if (task === "bundled-protocol") {
expect(consumers.map(({ args }) => args)).toEqual([["test:bundled"], ["protocol:check"]]);
} else if (task === "guards") {
const tempReport = consumers.find(
({ args }) => args[0] === "scripts/report-test-temp-creations.mjs",
);
expect(tempReport?.args).toEqual(
needsBase
? [
"scripts/report-test-temp-creations.mjs",
"--base",
base,
"--head",
"HEAD",
"--no-merge-base",
]
: undefined,
);
} else {
expect(consumers.filter(({ tool }) => tool === "pnpm").map(({ args }) => args)).toEqual([
needsBase
? ["deps:npm-lock:check:changed", "--base", base, "--head", "HEAD"]
: ["deps:npm-lock:check"],
]);
}
},
55_000,
);
it("runs the startup corpus once when a canonical PR admits both complete Node files", () => {
const revision = "a".repeat(40);
const shards = createNodeTestShardBundles({
compactMode: "pull-request",
includeReleaseOnlyPluginShards: false,
});
const manifest = runCiManifestFixture({
bundledPlanner: true,
eventName: "pull_request",
nodeTestShards: shards,
startupCorpusCoverage: true,
changedPaths: ["scripts/lib/ci-node-test-plan.mts"],
scopeEnv: { OPENCLAW_CI_WORKFLOW_REVISION: revision },
});
expect(manifest.status, manifest.output).toBe(0);
expect(manifest.outputs.startup_corpus_node_revision).toBe(revision);
expect(manifest.outputs.run_checks_node_core_nondist).toBe("true");
const step = readCiWorkflow().jobs["checks-fast-core"].steps.find(
(entry: WorkflowStep) => entry.name === "Check startup corpus",
);
expect(
evaluateWorkflowExpression(`\${{ ${step.if} }}`, {
eventName: "pull_request",
repository: "openclaw/openclaw",
matrix: { task: "baseline-ratchets" },
runAttempt: 1,
preflightOutputs: { ...manifest.outputs, checkout_revision: revision },
}),
).toBe(false);
for (const result of ["failure", "skipped"]) {
expect(
runCiGateFixture(
`checks-fast-core=success|true\nchecks-node-core-test-nondist-shard=${result}|true`,
).status,
).toBe(1);
}
});
it.each<{ label: string } & Partial<Parameters<typeof runCiManifestFixture>[0]>>([
{ label: "missing planner capability", startupCorpusCoverage: false },
{ label: "different source tree", scopeEnv: { OPENCLAW_CI_WORKFLOW_REVISION: "b".repeat(40) } },
{
label: "unknown source",
scopeEnv: { OPENCLAW_CI_CHECKOUT_REVISION: "", OPENCLAW_CI_WORKFLOW_REVISION: "" },
},
{ label: "fast-only PR", nodeFastOnly: true },
{ label: "Node not admitted", runNode: false },
{ label: "different repository", repository: "fixture/openclaw" },
{ label: "release merge", eventName: "workflow_dispatch", releaseGate: true },
{
label: "frozen target",
eventName: "workflow_dispatch",
scopeEnv: { OPENCLAW_CI_WORKFLOW_REVISION: "b".repeat(40) },
},
])(
"retains the startup corpus without a coverage receipt: $label",
({ label: _label, ...options }) => {
const manifest = runCiManifestFixture({
bundledPlanner: true,
startupCorpusCoverage: true,
eventName: "pull_request",
changedPaths: ["scripts/lib/ci-node-test-plan.mts"],
scopeEnv: { OPENCLAW_CI_WORKFLOW_REVISION: "a".repeat(40) },
nodeTestShards: [
{
checkName: "complete-corpus",
shardName: "complete-corpus",
requiresDist: false,
configs: [],
runner: "ubuntu-24.04",
groups: [
{
shard_name: "core-runtime-config",
requiresDist: false,
runner: "ubuntu-24.04",
configs: ["test/vitest/vitest.runtime-config.config.ts"],
includePatterns: [
"src/config/config-startup-corpus.test.ts",
"src/config/state-startup-corpus.test.ts",
],
},
],
},
],
...options,
});
expect(manifest.status, manifest.output).toBe(0);
expect(manifest.outputs.startup_corpus_node_revision).toBe("");
},
);
it.each(["", "b".repeat(40)])(
"retains the startup corpus for an unbound receipt %j",
(revision) => {
const step = readCiWorkflow().jobs["checks-fast-core"].steps.find(
(entry: WorkflowStep) => entry.name === "Check startup corpus",
);
expect(
evaluateWorkflowExpression(`\${{ ${step.if} }}`, {
eventName: "pull_request",
repository: "openclaw/openclaw",
matrix: { task: "baseline-ratchets" },
runAttempt: 1,
preflightOutputs: {
startup_corpus_node_revision: revision,
checkout_revision: "a".repeat(40),
},
}),
).toBe(true);
},
);
it("runs the startup corpus once on full canonical main pushes", () => {
const files = [
"src/config/config-startup-corpus.test.ts",
"src/config/state-startup-corpus.test.ts",
];
const groups = createNodeTestShardBundles({
compactMode: "push",
includeReleaseOnlyPluginShards: false,
}).flatMap((shard) => shard.groups);
const steps: WorkflowStep[] = readCiWorkflow().jobs["checks-fast-core"].steps;
for (const file of files) {
expect(
buildVitestRunPlans([file]).map((plan) => plan.config),
file,
).toEqual(["test/vitest/vitest.runtime-config.config.ts"]);
const nodeOwners = groups.filter(
(group) =>
group.configs.includes("test/vitest/vitest.runtime-config.config.ts") &&
(!group.includePatterns ||
group.includePatterns.some((pattern) => minimatch(file, pattern))),
);
expect(nodeOwners, file).toHaveLength(1);
const extraOwners = steps.filter(
(step) =>
step.run?.includes(file) &&
(!step.if ||
evaluateWorkflowExpression(`\${{ ${step.if} }}`, {
eventName: "push",
repository: "openclaw/openclaw",
ref: "refs/heads/main",
matrix: { task: "baseline-ratchets" },
runCheck: true,
runAttempt: 1,
})),
);
expect(nodeOwners.length + extraOwners.length, file).toBe(1);
}
});
it.each([
{ eventName: "pull_request", runCheck: true, frozenTarget: false },
{ eventName: "pull_request", runCheck: false },
{ eventName: "push", runCheck: false },
{ eventName: "push", ref: "refs/heads/release" },
{ eventName: "push", repository: "fixture/openclaw" },
{ eventName: "workflow_dispatch", releaseGate: false },
{ eventName: "workflow_dispatch", releaseGate: true, frozenTarget: true },
] as const)("retains the startup corpus outside full canonical main: %j", (scenario) => {
const steps: WorkflowStep[] = readCiWorkflow().jobs["checks-fast-core"].steps;
const selected = steps.filter(
(step) =>
step.run?.includes("src/config/state-startup-corpus.test.ts") &&
(!step.if ||
evaluateWorkflowExpression(`\${{ ${step.if} }}`, {
repository: "openclaw/openclaw",
matrix: { task: "baseline-ratchets" },
runAttempt: 1,
...scenario,
})),
);
expect(selected).toHaveLength(1);
expect(selected[0]?.run).toContain("src/config/config-startup-corpus.test.ts");
if ("frozenTarget" in scenario) {
const directory = tempDirs.make("startup-corpus-command-");
const bin = path.join(directory, "bin");
const argsPath = path.join(directory, "args");
mkdirSync(bin);
writeExecutable(path.join(bin, "pnpm"), [
"#!/bin/sh",
'[ "$*" = "build qaRuntime" ] || exit 1',
"mkdir dist || exit 1",
"touch dist/.buildstamp",
]);
writeExecutable(path.join(bin, "node"), [
"#!/bin/sh",
'test -f dist/.buildstamp || { echo "runtime not prepared" >&2; exit 1; }',
'label="${OPENCLAW_TEST_STARTUP_CORPUS_SHARD:-config}"',
'case "$label" in */*) label="${label%/*}-${label#*/}" ;; esac',
'printf "%s\\n" "$@" > "$STARTUP_CORPUS_ARGS.$label"',
]);
const script = expectDefined(selected[0]?.run, "startup corpus command").replace(
/\$\{\{[\s\S]*?\}\}/gu,
(expression) =>
String(
evaluateWorkflowExpression(expression, {
repository: "openclaw/openclaw",
runAttempt: 1,
...scenario,
}),
),
);
const result = runWorkflowShellScript(script, {
cwd: directory,
env: {
...process.env,
PATH: `${bin}${path.delimiter}${process.env.PATH ?? ""}`,
STARTUP_CORPUS_ARGS: argsPath,
},
});
expect(result.status, result.stdout + result.stderr).toBe(0);
const readArgs = (label: string) =>
readFileSync(`${argsPath}.${label.replace("/", "-")}`, "utf8")
.trim()
.split("\n");
const commonArgs = [
"scripts/run-vitest.mjs",
"run",
"--config",
"test/vitest/vitest.runtime-config.config.ts",
...(scenario.frozenTarget
? []
: [
"--reporter",
"verbose",
"--reporter",
"github-actions",
"--reporter",
"./scripts/lib/vitest-resource-reporter.mts",
]),
];
expect(readArgs("config")).toEqual([
...commonArgs,
"src/config/config-startup-corpus.test.ts",
]);
for (const shard of ["1/4", "2/4", "3/4", "4/4"]) {
expect(readArgs(shard), shard).toEqual([
...commonArgs,
"src/config/state-startup-corpus.test.ts",
]);
}
}
});
it("runs all baseline ratchets against the exact tested tree", () => {
const workflow = readCiWorkflow();
const maxLinesRatchet = readFileSync("scripts/check-max-lines-ratchet.mts", "utf8");
const checksFastJob = workflow.jobs["checks-fast-core"];
const checksFastSteps = checksFastJob.steps;
const checkout = checksFastSteps.find((step: WorkflowStep) => step.name === "Checkout");
const checksFastRun = checksFastSteps.find(
(step: WorkflowStep) => step.name === "Run ${{ matrix.task }} (${{ matrix.runtime }})",
);
const releaseGateMerge = checksFastSteps.find(
(step: WorkflowStep) => step.name === "Prepare release-gate ratchet merge tree",
);
expect(
checksFastSteps.some((step: WorkflowStep) => step.name === "Resolve manual protocol base"),
).toBe(false);
expect(workflow.jobs["checks-fast-core"].permissions).toEqual({
contents: "read",
"pull-requests": "read",
});
expect(checkout.env.CHECKOUT_SHA).toBe("${{ needs.preflight.outputs.checkout_revision }}");
expect(releaseGateMerge.if).toBe(
"(matrix.task == 'baseline-ratchets' || startsWith(matrix.task, 'release-lint-')) && github.event_name == 'workflow_dispatch' && inputs.release_gate",
);
expect(checksFastRun.run).toContain("baseline-ratchets)");
expect(checksFastRun.run).toContain("coercion-helpers)");
expect(checksFastRun.run).toContain("pnpm check:coercion-helpers");
expect(checksFastRun.run).toContain("bun-launcher)");
expect(checksFastRun.run).toContain(
"OPENCLAW_E2E_SKIP_BUILD=1 OPENCLAW_TEST_BUN_LAUNCHER=1 pnpm test test/openclaw-launcher.e2e.test.ts",
);
expect(checksFastRun.run).toContain(
"if [[ -f src/plugins/plugin-module-generation.test.ts ]]; then",
);
expect(checksFastRun.run).toContain(
'elif [[ "${{ needs.preflight.outputs.frozen_target }}" != "true" ]]; then',
);
expect(checksFastRun.run).toContain(
"for required_script in check:max-lines-ratchet check:assertion-safety config:docs:check plugins:inventory:check; do",
);
expect(checksFastRun.run).toContain('has_package_script "$required_script"');
expect(checksFastRun.env.RATCHET_PR_HEAD_SHA).toBe(
"${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || '' }}",
);
expect(checksFastRun.env).not.toHaveProperty("RATCHET_EVENT_BASE_SHA");
expect(checksFastRun.env).not.toHaveProperty("RATCHET_MANUAL_TARGET_SHA");
expect(checksFastRun.env).not.toHaveProperty("GH_TOKEN");
expect(checksFastRun.env).not.toHaveProperty("PROTOCOL_MANUAL_BASE_SHA");
expect(checksFastRun.env.PROTOCOL_SINCE_BASE_SHA).toBe(
"${{ needs.preflight.outputs.diff_base_revision }}",
);
expect(releaseGateMerge.run).toContain(
'gh api --method GET "repos/${GITHUB_REPOSITORY}/pulls/${PULL_REQUEST_NUMBER}"',
);
expect(releaseGateMerge.run).toContain(
"release-gate pull request must be open and match the target head",
);
expect(releaseGateMerge.run).toContain("for attempt in {1..6}");
expect(releaseGateMerge.run).toContain(
'"+refs/pull/${PULL_REQUEST_NUMBER}/merge:refs/remotes/origin/ci-ratchet-merge"',
);
expect(releaseGateMerge.run).toContain('"$merge_head" == "$TARGET_SHA"');
expect(releaseGateMerge.run).toContain('git show -s --format=%P "$merge_sha"');
expect(releaseGateMerge.run).toContain(
"Freeze GitHub's canonical merge snapshot once it contains the exact head",
);
expect(releaseGateMerge.run).toContain(
"Base freshness belongs to the landing gate; chasing moving main here can never converge",
);
expect(releaseGateMerge.run).toContain(
"release-gate merge tree did not refresh to the target head",
);
expect(releaseGateMerge.run).not.toContain(".base.sha");
expect(releaseGateMerge.run).toContain('--git 0 checkout --detach "$merge_sha"');
expect(releaseGateMerge.run).toContain(
'echo "RATCHET_BASE_REF=${frozen_base_sha}" >> "$GITHUB_ENV"',
);
expect(checksFastRun.run).not.toContain("PROTOCOL_MANUAL_BASE_SHA");
expect(checksFastRun.run).not.toContain("protocol-since-base");
expect(checksFastRun.run).toContain(
'test "$(git rev-parse refs/remotes/origin/ci-ratchet-base^{commit})" = "$PROTOCOL_SINCE_BASE_SHA"',
);
expect(checksFastRun.run).toContain(
'base_ref="${RATCHET_BASE_REF:-refs/remotes/origin/ci-ratchet-base}"',
);
expect(checksFastRun.run).toContain('git cat-file -e "${base_ref}^{commit}"');
expect(checksFastRun.run).toContain(
"mapfile -t merge_parents < <(git cat-file -p HEAD | sed -n 's/^parent //p')",
);
expect(checksFastRun.run).toContain('"${#merge_parents[@]}" != "2"');
expect(checksFastRun.run).toContain('"${merge_parents[1]:-}" != "$RATCHET_PR_HEAD_SHA"');
expect(checksFastRun.run).toContain('prepared_base="$(git rev-parse "$base_ref")"');
expect(checksFastRun.run).toContain('"${merge_parents[0]}" != "$prepared_base"');
expect(checksFastRun.run).not.toContain("ci-ratchet-target^");
expect(checksFastRun.run).not.toContain("resolve_manual_merge_base");
expect(checksFastRun.run).not.toContain("+${merge_base}:refs/remotes/origin/ci-ratchet-base");
expect(checksFastRun.run).toContain('pnpm check:max-lines-ratchet --base "$base_ref"');
expect(checksFastRun.run).toMatch(
/if \[\[ -n "\$\{RATCHET_PR_HEAD_SHA:-\}" \]\]; then\s+pnpm check:line-cap-ratchet --base "\$base_ref"\s+fi/u,
);
expect(checksFastRun.run).toContain('pnpm check:assertion-safety --base "$base_ref"');
expect(checksFastRun.run).toContain("pnpm config:docs:check");
expect(checksFastRun.run).toContain("pnpm plugins:inventory:check");
expect(maxLinesRatchet).toContain('} from "./check-env-var-count.mts";');
expect(maxLinesRatchet).toContain(
"checkEnvVarCount(envVarCountArgs(argv), root, envVarNames);",
);
expect(checksFastRun.run).toContain(
'--only=core --split-core --core-stripe="${stripe}/5" --threads=1',
);
expect(checksFastRun.run).toContain(
"node --import tsx scripts/run-oxlint-shards.mts --only=extensions --threads=1",
);
expect(checksFastRun.run).not.toContain(
"node scripts/run-oxlint.mjs src ui/src packages extensions",
);
const fastOnly = runCiManifestFixture({
bundledPlanner: true,
eventName: "pull_request",
historicalCompatibility: false,
nodeFastOnly: true,
nodeFastPluginContracts: true,
});
expect(fastOnly.status, fastOnly.output).toBe(0);
expect(fastOnly.outputs.run_check).toBe("false");
expect(fastOnly.outputs.run_checks_fast_core).toBe("true");
expect(
JSON.parse(expectDefined(fastOnly.outputs.checks_fast_core_matrix, "fast-only checks matrix"))
.include,
).toEqual([
{
check_name: "checks-fast-baseline-ratchets",
runtime: "node",
task: "baseline-ratchets",
},
{
check_name: "checks-fast-coercion-helpers",
runtime: "node",
task: "coercion-helpers",
},
]);
const releaseGate = runCiManifestFixture({
bundledPlanner: true,
eventName: "workflow_dispatch",
historicalCompatibility: false,
releaseGate: true,
runnerProfile: "github",
});
expect(releaseGate.status, releaseGate.output).toBe(0);
expect(
JSON.parse(
expectDefined(releaseGate.outputs.checks_fast_core_matrix, "release-gate checks matrix"),
).include.filter((entry: { task: string }) => entry.task.startsWith("release-lint-")),
).toEqual([
...Array.from({ length: 5 }, (_, index) => {
const stripe = index + 1;
return {
check_name: `checks-fast-release-lint-core-${stripe}`,
runtime: "node",
stripe,
task: `release-lint-core-${stripe}`,
};
}),
{
check_name: "checks-fast-release-lint-extensions",
runtime: "node",
task: "release-lint-extensions",
},
]);
});
it.each([
{
label: "test-only routing",
changedPath: "test/scripts/changed-path-facts.test.ts",
taskOverride: null,
},
{
label: "source-only routing",
changedPath: "scripts/lib/changed-path-facts.mjs",
taskOverride: null,
},
{
label: "legacy combined contract and routing task",
changedPath: "test/scripts/changed-path-facts.test.ts",
taskOverride: "contracts-plugins-ci-routing",
},
])(
"executes standalone changed-path-facts coverage for $label",
({ changedPath, taskOverride }) => {
const root = tempDirs.make("openclaw-fast-ci-routing-");
const changedPaths = [changedPath];
const scopeEnv = Object.fromEntries(
Object.entries(runCiChangedScopeFixture(changedPaths)).map(([key, value]) => [
`OPENCLAW_CI_${key.toUpperCase()}`,
value,
]),
);
const manifest = runCiManifestFixture({
bundledPlanner: true,
eventName: "pull_request",
historicalCompatibility: false,
changedPaths,
scopeEnv: { ...scopeEnv, OPENCLAW_CI_DOCS_CHANGED: "false" },
});
expect(manifest.status, manifest.output).toBe(0);
expect(
Object.entries(manifest.outputs)
.filter(([key, value]) => key.startsWith("run_") && value === "true")
.map(([key]) => key)
.toSorted(),
).toEqual([
"run_checks_fast_core",
"run_format_check",
"run_node",
"run_protocol_event_coverage",
]);
for (const matrix of [
"checks_node_core_nondist_matrix",
"plugin_contracts_matrix",
"channel_contracts_matrix",
"checks_windows_matrix",
"macos_node_matrix",
"android_matrix",
]) {
expect(JSON.parse(expectDefined(manifest.outputs[matrix], matrix)).include, matrix).toEqual(
[],
);
}
const fastTasks = JSON.parse(
expectDefined(manifest.outputs.checks_fast_core_matrix, "fast checks matrix"),
).include as Array<{ task: string }>;
expect(fastTasks.map(({ task }) => task)).toEqual([
"baseline-ratchets",
"coercion-helpers",
"ci-routing",
]);
const routingTask = expectDefined(
fastTasks.find(({ task }) => task === "ci-routing"),
"CI routing task",
);
const runStep = readCiWorkflow().jobs["checks-fast-core"].steps.find(
(step: WorkflowStep) => step.name === "Run ${{ matrix.task }} (${{ matrix.runtime }})",
);
const fakeBin = path.join(root, "bin");
const callsPath = path.join(root, "pnpm-calls.jsonl");
mkdirSync(fakeBin);
writeExecutable(path.join(fakeBin, "pnpm"), [
"#!/usr/bin/env node",
'require("node:fs").appendFileSync(process.env.PNPM_CALLS, JSON.stringify(process.argv.slice(2)) + "\\n");',
]);
// The current manifest selects ci-routing; exercise the retained combined Bash case directly.
const run = spawnSync("bash", ["-c", runStep.run], {
encoding: "utf8",
env: {
...process.env,
PATH: `${fakeBin}:${process.env.PATH ?? ""}`,
PNPM_CALLS: callsPath,
TASK: taskOverride ?? routingTask.task,
},
});
expect(run.status, `${run.stdout}${run.stderr}`).toBe(0);
const calls = readFileSync(callsPath, "utf8")
.trim()
.split("\n")
.map((line) => JSON.parse(line) as string[]);
expect(calls.map(([command]) => command)).toEqual(
taskOverride ? ["test:contracts:plugins", "test"] : ["test"],
);
expect(
calls.find(([command]) => command === "test"),
"executed routing test argv",
).toContain("test/scripts/changed-path-facts.test.ts");
},
);
it.each<{
label: string;
changedPath: string;
eventName?: "pull_request" | "push" | "workflow_dispatch";
releaseGate?: boolean;
legacyOutput?: boolean;
selectedJobs: string[];
}>([
{
label: "Git-owner action",
changedPath: ".github/actions/git-owner/owner.py",
selectedJobs: ["macos-node", "checks-windows"],
},
{
label: "Android toolchain action",
changedPath: ".github/actions/setup-android-toolchain/action.yml",
selectedJobs: ["android"],
},
{
label: "Docs Agent",
changedPath: ".github/workflows/docs-agent.yml",
selectedJobs: ["macos-node", "checks-windows"],
},
...[
".github/workflows/openclaw-performance.yml",
"test/scripts/openclaw-performance-workflow.test-support.ts",
"test/scripts/openclaw-performance-git-lifecycle.test.ts",
"test/scripts/openclaw-performance-workflow.test.ts",
].map((changedPath) => ({
label: `Performance owner ${changedPath}`,
changedPath,
selectedJobs: ["macos-node", "checks-windows"],
})),
{
label: "Git-owner fixture",
changedPath: "test/scripts/fixtures/ci-platform-checkout.mjs",
selectedJobs: ["macos-node", "checks-windows"],
},
{
label: "Windows process census fixture",
changedPath: "test/scripts/fixtures/ci-windows-process-census.py",
selectedJobs: ["macos-node", "checks-windows"],
},
{
label: "Mac app",
changedPath: "apps/macos/Sources/Foo.swift",
selectedJobs: ["macos-node", "macos-swift"],
},
{
label: "Mac artifact proof",
changedPath: "test/scripts/mac-elevation-artifact.test.ts",
selectedJobs: ["macos-node", "macos-swift"],
},
{
label: "iOS app pull request",
changedPath: "apps/ios/Sources/Foo.swift",
selectedJobs: ["ios-build"],
},
{
label: "iOS app main push",
changedPath: "apps/ios/Sources/Foo.swift",
eventName: "push",
selectedJobs: ["ios-build"],
},
{
label: "iOS app exact-head release gate",
changedPath: "apps/ios/Sources/Foo.swift",
eventName: "workflow_dispatch",
releaseGate: true,
selectedJobs: ["checks-windows", "ios-build", "android"],
},
{
label: "shared native",
changedPath: "apps/shared/OpenClawKit/Sources/Foo.swift",
selectedJobs: ["macos-node", "macos-swift", "ios-build", "android"],
},
{ label: "docs", changedPath: "docs/ci.md", selectedJobs: [] },
{
label: "unrelated CI workflow",
changedPath: ".github/workflows/ci.yml",
selectedJobs: ["checks-windows"],
},
{
label: "ordinary manual",
changedPath: ".github/actions/git-owner/owner.py",
eventName: "workflow_dispatch",
selectedJobs: ["macos-node", "macos-swift", "checks-windows", "ios-build"],
},
{
label: "historical Mac scope without native Node output",
changedPath: "apps/macos/Sources/Foo.swift",
eventName: "workflow_dispatch",
releaseGate: true,
legacyOutput: true,
selectedJobs: ["macos-node", "macos-swift", "checks-windows", "android"],
},
{
label: "historical non-Mac scope without native Node output",
changedPath: "src/config/defaults.ts",
eventName: "workflow_dispatch",
releaseGate: true,
legacyOutput: true,
selectedJobs: ["checks-windows", "android"],
},
])(
"routes native CI jobs through scope output and manifest ($label)",
({
changedPath,
eventName = "pull_request",
releaseGate = false,
legacyOutput,
selectedJobs,
}) => {
const workflow = readCiWorkflow();
const manifestStep = workflow.jobs.preflight.steps.find(
(step: WorkflowStep) => step.name === "Build CI manifest",
);
const changedPaths = [changedPath];
const scopeOutputs = runCiChangedScopeFixture(changedPaths);
if (legacyOutput) {
delete scopeOutputs.run_macos_node;
}
const context = {
eventName,
releaseGate,
repository: "openclaw/openclaw",
runAttempt: 1,
steps: { changed_scope: { outputs: scopeOutputs } },
};
const scopeEnv = Object.fromEntries(
Object.entries(manifestStep.env)
.filter(([key]) => key.startsWith("OPENCLAW_CI_RUN_"))
.map(([key, expression]) => [
key,
String(evaluateWorkflowExpression(expression, context)),
]),
);
const manifest = runCiManifestFixture({
bundledPlanner: !legacyOutput,
changedPaths,
eventName,
releaseGate,
scopeEnv,
});
expect(manifest.status, manifest.output).toBe(0);
const preflightOutputs = Object.fromEntries(
Object.entries(workflow.jobs.preflight.outputs)
.filter(([, expression]) => String(expression).includes("steps.manifest.outputs."))
.map(([key, expression]) => [
key,
String(
evaluateWorkflowExpression(expression, {
...context,
steps: { manifest: { outputs: manifest.outputs } },
}),
),
]),
);
for (const jobName of [
"macos-node",
"macos-swift",
"checks-windows",
"ios-build",
"android",
]) {
const job = workflow.jobs[jobName];
const expression = job.if.startsWith("${{") ? job.if : `\${{ ${job.if} }}`;
expect(
evaluateWorkflowExpression(expression, { ...context, preflightOutputs }),
jobName,
).toBe(selectedJobs.includes(jobName));
}
expect(
JSON.parse(expectDefined(manifest.outputs.macos_node_matrix, "Mac Node matrix")).include,
).toEqual(
selectedJobs.includes("macos-node")
? legacyOutput
? [{ check_name: "macos-node", runtime: "node", task: "test" }]
: [1, 2, 3].map((part) => ({
check_name: `macos-node-${part}`,
runtime: "node",
task: `test-${part}`,
}))
: [],
);
expect(
JSON.parse(expectDefined(manifest.outputs.checks_windows_matrix, "Windows matrix")).include,
).toHaveLength(selectedJobs.includes("checks-windows") ? 2 : 0);
if (eventName === "pull_request" && selectedJobs.includes("android")) {
expect(
JSON.parse(expectDefined(preflightOutputs.android_matrix, "Android matrix")).include,
).toEqual([
{ check_name: "android-test-play", task: "test-play", lint: true },
{ check_name: "android-test-third-party", task: "test-third-party", lint: true },
{ check_name: "android-test-wear", task: "test-wear", lint: true },
{ check_name: "android-ktlint", task: "ktlint" },
]);
}
},
);
it.each(
[
{ scope: "npm-beta", packageVersion: "2026.9.1-beta.1", branch: "release/2026.9.1" },
{ scope: "npm-stable", packageVersion: "2026.9.1", branch: "release/2026.9.1" },
{ scope: "npm-stable", packageVersion: "2026.9.1-1", branch: "release/2026.9.1-1" },
].flatMap(({ scope, packageVersion, branch }) =>
["release branch", "release tag"].map((context) => ({
scope,
packageVersion,
branch,
context,
})),
),
)(
"qualifies $scope $packageVersion without native app jobs through a validated $context",
({ scope, packageVersion, branch, context }) => {
const options = {
bundledPlanner: true,
packageVersion,
scopeEnv: {
OPENCLAW_CI_TARGET_REF: "a".repeat(40),
OPENCLAW_CI_TARGET_CONTEXT_REF: context === "release branch" ? branch : "",
OPENCLAW_CI_TARGET_CONTEXT_TARGET: String(context === "release branch"),
OPENCLAW_CI_HISTORICAL_TARGET_TAG: context === "release tag" ? `v${packageVersion}` : "",
OPENCLAW_CI_HISTORICAL_TARGET: String(context === "release tag"),
OPENCLAW_CI_RUN_UI_TESTS: "true",
},
};
const full = runCiManifestFixture(options);
const qualification = runCiManifestFixture({
...options,
scopeEnv: { ...options.scopeEnv, OPENCLAW_CI_RELEASE_SCOPE: scope },
});
expect(full.status, full.output).toBe(0);
expect(qualification.status, qualification.output).toBe(0);
expect(qualification.output).toContain(`CI release scope: ${scope}`);
expect(qualification.summary).toContain(`Scope: \`${scope}\``);
expect(qualification.summary).toContain("Native app qualification: deferred");
expect(qualification.outputs).toEqual({
...full.outputs,
release_scope: scope,
run_macos_swift: "false",
run_openclawkit_tests: "false",
run_ios_build: "false",
run_android: "false",
run_android_job: "false",
run_native_i18n: "false",
android_matrix: JSON.stringify({ include: [] }),
});
for (const output of [
"run_node",
"run_macos_node",
"run_checks_windows",
"run_build_artifacts",
"run_check_additional",
"run_protocol_event_coverage",
"run_ui_tests",
]) {
expect(qualification.outputs[output], output).toBe("true");
}
for (const jobName of ["ios-screenshot-shard", "ios-screenshot-evidence"]) {
expect(
evaluateWorkflowExpression(readCiWorkflow().jobs[jobName].if, {
eventName: "workflow_dispatch",
repository: "openclaw/openclaw",
runAttempt: 1,
preflightOutputs: {
...qualification.outputs,
compatibility_target: "false",
run_ios_screenshots: "true",
},
}),
jobName,
).toBe(false);
}
},
);
it.skipIf(process.platform === "win32").each([
{ label: "base branch correction", context: "branch", direct: "a", accepted: true },
{ label: "base tag correction", context: "tag", direct: "a", accepted: true },
{ label: "annotated base tag", context: "tag", direct: "c", peeled: "a", accepted: true },
{
label: "versioned correction without a base lookup",
context: "branch",
packageVersion: "2026.9.1-1",
accepted: true,
},
{ label: "missing base tag", context: "branch", accepted: false },
{ label: "different base source", context: "branch", direct: "c", accepted: false },
{ label: "different peeled source", context: "tag", direct: "a", peeled: "c", accepted: false },
])(
"binds npm stable $label to the exact source",
({ context, direct, peeled, packageVersion, accepted }) => {
const result = runCiManifestFixture({
bundledPlanner: true,
packageVersion: packageVersion ?? "2026.9.1",
remoteTagRefs: {
...(direct ? { "refs/tags/v2026.9.1": direct.repeat(40) } : {}),
...(peeled ? { "refs/tags/v2026.9.1^{}": peeled.repeat(40) } : {}),
},
scopeEnv: {
OPENCLAW_CI_RELEASE_SCOPE: "npm-stable",
OPENCLAW_CI_TARGET_REF: "a".repeat(40),
OPENCLAW_CI_TARGET_CONTEXT_REF: context === "branch" ? "release/2026.9.1-1" : "",
OPENCLAW_CI_TARGET_CONTEXT_TARGET: String(context === "branch"),
OPENCLAW_CI_HISTORICAL_TARGET_TAG: context === "tag" ? "v2026.9.1-1" : "",
OPENCLAW_CI_HISTORICAL_TARGET: String(context === "tag"),
},
});
expect(result.status === 0, result.output).toBe(accepted);
if (accepted) {
expect(result.outputs.run_ios_build).toBe("false");
expect(result.outputs.run_node).toBe("true");
} else {
expect(result.output).toContain(
direct ? "correction base v2026.9.1 does not resolve" : "HTTP 404",
);
expect(result.outputs).not.toHaveProperty("run_node");
}
},
);
it.each<{ label: string } & Omit<Parameters<typeof runCiManifestFixture>[0], "bundledPlanner">>([
{ label: "stable target", packageVersion: "2026.9.1" },
{ label: "alpha target", packageVersion: "2026.9.1-alpha.1" },
{ label: "PR event", eventName: "pull_request" as const },
{ label: "fork repository", repository: "example/openclaw" },
{ label: "PR release gate", releaseGate: true },
{ label: "PR number", scopeEnv: { OPENCLAW_CI_PULL_REQUEST_NUMBER: "123" } },
{ label: "mutable target", scopeEnv: { OPENCLAW_CI_TARGET_REF: "release/2026.9.1" } },
{ label: "wrong target", scopeEnv: { OPENCLAW_CI_TARGET_REF: "c".repeat(40) } },
{ label: "unvalidated branch", scopeEnv: { OPENCLAW_CI_TARGET_CONTEXT_TARGET: "false" } },
{
label: "wrong release train",
scopeEnv: { OPENCLAW_CI_TARGET_CONTEXT_REF: "release/2026.9.2" },
},
{
label: "wrong release tag",
scopeEnv: {
OPENCLAW_CI_TARGET_CONTEXT_TARGET: "false",
OPENCLAW_CI_HISTORICAL_TARGET: "true",
OPENCLAW_CI_HISTORICAL_TARGET_TAG: "v2026.9.2-beta.1",
},
},
{ label: "unknown scope", scopeEnv: { OPENCLAW_CI_RELEASE_SCOPE: "package" } },
...["2026.9.1-beta.1", "2026.9.1-alpha.1", "2026.9.33", "2026.9.33-1"].map(
(packageVersion) => ({
label: `npm-stable with ${packageVersion}`,
packageVersion,
scopeEnv: { OPENCLAW_CI_RELEASE_SCOPE: "npm-stable" },
}),
),
{
label: "correction package in a different release context",
packageVersion: "2026.9.1-1",
scopeEnv: { OPENCLAW_CI_RELEASE_SCOPE: "npm-stable" },
},
])(
"rejects scoped npm CI qualification for $label",
({ label: _label, scopeEnv, ...options }) => {
const result = runCiManifestFixture({
bundledPlanner: true,
historicalCompatibility: false,
packageVersion: "2026.9.1-beta.1",
...options,
scopeEnv: {
OPENCLAW_CI_RELEASE_SCOPE: "npm-beta",
OPENCLAW_CI_TARGET_REF: "a".repeat(40),
OPENCLAW_CI_TARGET_CONTEXT_REF: "release/2026.9.1",
OPENCLAW_CI_TARGET_CONTEXT_TARGET: "true",
...scopeEnv,
},
});
expect(result.status, result.output).not.toBe(0);
expect(result.output).toContain("release_scope");
expect(result.outputs).not.toHaveProperty("run_node");
},
);
it.each([
["pull_request", "openclaw/openclaw", true],
["pull_request", "example/openclaw", false],
["push", "openclaw/openclaw", false],
["workflow_dispatch", "openclaw/openclaw", false],
] as const)(
"forwards changed paths only to canonical PR fallback (%s, %s)",
(eventName, repository, forwardsChangedPaths) => {
const changedPaths = [
"src/plugins/manifest-tool-availability.ts",
"src/plugins/tools.optional.test.ts",
];
const manifest = runCiManifestFixture({
bundledPlanner: true,
changedPaths,
eventName,
repository,
});
expect(manifest.status, manifest.output).toBe(0);
const rows = JSON.parse(
expectDefined(manifest.outputs.checks_node_core_nondist_matrix, "fallback matrix"),
).include;
expect(rows).toHaveLength(1);
expect(rows[0].check_name).toBe("bundled-node-plan");
expect(rows[0].includePatterns).toEqual(forwardsChangedPaths ? changedPaths : undefined);
},
);
it.each([false, true])(
"projects immutable reader history only when the selected target has the reader (present=%s)",
(historicalReader) => {
const manifest = runCiManifestFixture({
bundledPlanner: true,
changedPaths: ["src/audit/message-delivery-progress-store.test.ts"],
eventName: "pull_request",
historicalReader,
});
expect(manifest.status, manifest.output).toBe(0);
const rows = JSON.parse(
expectDefined(manifest.outputs.checks_node_core_nondist_matrix, "reader matrix"),
).include;
expect(rows).toHaveLength(1);
expect(rows[0].git_commits).toEqual(
historicalReader ? ["5dc4cf602bc5e263e83cd16a12bb1e100544f4c3"] : [],
);
},
);
it.each([
["pull_request", "compact", "blacksmith", 120],
["pull_request", "precise", "github", 120],
["push", "compact", "hybrid", 64],
["workflow_dispatch", "compact", "blacksmith", null],
] as const)(
"bounds the final Node matrix for %s %s plans",
(eventName, selection, runnerProfile, limit) => {
for (const count of [limit ?? 120, (limit ?? 120) + 1]) {
const hasFallback = eventName === "pull_request" && selection === "compact";
const nodeTestShards = Array.from({ length: count - Number(hasFallback) }, (_, index) => ({
checkName: `node-admission-${index}`,
shardName: `node-admission-${index}`,
configs: ["test/vitest/vitest.infra.config.ts"],
runner: "blacksmith-8vcpu-ubuntu-2404",
requiresDist: false,
}));
const result = runCiManifestFixture({
bundledPlanner: true,
changedPaths: ["extensions/matrix/src/channel.ts"],
changedPlannerSource:
selection === "precise"
? `export { createNodeTestShards as createChangedNodeTestShards } from "./ci-node-test-plan.mts";
export const createChangedExtensionFallbackShards = () => [];`
: undefined,
eventName,
nodeTestShards: [
...nodeTestShards,
{
checkName: "node-admission-dist",
shardName: "node-admission-dist",
configs: ["test/vitest/vitest.infra.config.ts"],
runner: "blacksmith-8vcpu-ubuntu-2404",
requiresDist: true,
},
],
runnerProfile,
});
if (limit !== null && count > limit) {
expect(result.status, result.output).toBe(1);
expect(result.output).toContain(
`Canonical ${eventName} Node matrix has ${count} jobs, exceeding limit ${limit}`,
);
expect(result.outputs.checks_node_core_nondist_matrix).toBeUndefined();
expect(result.outputs.run_checks_node_core_nondist).toBeUndefined();
} else {
expect(result.status, result.output).toBe(0);
const rows = JSON.parse(
expectDefined(result.outputs.checks_node_core_nondist_matrix, "bounded Node matrix"),
).include;
expect(rows.map((row: { check_name: string }) => row.check_name)).toEqual([
...(hasFallback ? ["changed-extension-fallback-plan"] : []),
...nodeTestShards.map((shard) => shard.checkName),
]);
expect(result.outputs.run_checks_node_core_dist).toBe("true");
}
}
},
);
it("admits slow compact and plugin fallback rows before shorter work", () => {
const result = runCiManifestFixture({
bundledPlanner: true,
changedPaths: ["extensions/matrix/src/channel.ts"],
eventName: "pull_request",
nodeTestShards: [30, 240, 240].map((predictedSeconds, index) => ({
checkName: `compact-${index}`,
shardName: `compact-${index}`,
configs: ["test/vitest/vitest.infra.config.ts"],
runner: "ubuntu-24.04",
requiresDist: false,
predictedSeconds,
})),
});
expect(result.status, result.output).toBe(0);
const rows = JSON.parse(
expectDefined(result.outputs.checks_node_core_nondist_matrix, "Node matrix"),
).include;
expect(rows.map((row: { check_name: string }) => row.check_name)).toEqual([
"compact-1",
"compact-2",
"changed-extension-fallback-plan",
"compact-0",
]);
expect(rows.map((row: { predicted_seconds: number }) => row.predicted_seconds)).toEqual([
240, 240, 120, 30,
]);
});
it("uses target-owned CI plans and capabilities for older release checkouts", () => {
const androidRun = readCiWorkflow().jobs.android.steps.find(
(step: WorkflowStep) => step.name === "Run Android ${{ matrix.task }}",
).run;
expect(androidRun).toContain("build-play-compat)");
expect(androidRun).toContain("test-play-compat)");
expect(androidRun).toContain(":app:assemblePlayDebug");
const legacy = runCiManifestFixture({ bundledPlanner: false });
expect(legacy.status, legacy.output).toBe(0);
expect(legacy.outputs.historical_target).toBe("true");
expect(legacy.outputs.use_compatible_android_ci).toBe("true");
expect(legacy.outputs.run_ios_build).toBe("false");
expect(legacy.outputs.run_native_i18n).toBe("false");
expect(legacy.outputs.run_openclawkit_tests).toBe("false");
expect(legacy.outputs.run_qa_smoke_ci).toBe("false");
expect(legacy.outputs.run_docker_seed_e2e).toBe("false");
expect(legacy.outputs.docker_seed_lanes).toBe("");
expect(legacy.outputs.run_channel_contracts_shards).toBe("false");
expect(legacy.outputs.run_protocol_event_coverage).toBe("false");
expect(
JSON.parse(expectDefined(legacy.outputs.android_matrix, "legacy Android matrix output"))
.include,
).toEqual([
{ check_name: "android-test-play", task: "test-play-compat" },
{ check_name: "android-test-third-party", task: "test-third-party" },
{ check_name: "android-build-play", task: "build-play-compat" },
]);
expect(
JSON.parse(
expectDefined(
legacy.outputs.checks_node_core_nondist_matrix,
"legacy node core nondist matrix output",
),
).include,
).toContainEqual(
expect.objectContaining({
check_name: "legacy-node-plan",
shard_name: "legacy-node-plan",
}),
);
const current = runCiManifestFixture({ bundledPlanner: true });
expect(current.status, current.output).toBe(0);
expect(current.outputs.use_compatible_android_ci).toBe("false");
expect(current.outputs.run_ios_build).toBe("true");
expect(current.outputs.run_native_i18n).toBe("true");
expect(current.outputs.run_openclawkit_tests).toBe("true");
expect(current.outputs.run_qa_smoke_ci).toBe("true");
expect(current.outputs.run_docker_seed_e2e).toBe("true");
expect(current.outputs.docker_seed_lanes).toBe("published-upgrade-survivor");
expect(current.outputs.run_sqlite_session_lifecycle).toBe("true");
expect(current.outputs.run_channel_contracts_shards).toBe("true");
expect(current.outputs.run_protocol_event_coverage).toBe("true");
expect(current.outputs.run_format_check).toBe("true");
expect(
JSON.parse(expectDefined(current.outputs.android_matrix, "current Android matrix output"))
.include,
).toEqual([
{ check_name: "android-test-play", task: "test-play" },
{ check_name: "android-test-third-party", task: "test-third-party" },
{ check_name: "android-test-wear", task: "test-wear" },
{ check_name: "android-build-play", task: "build-play" },
{ check_name: "android-build-wear", task: "build-wear" },
{ check_name: "android-ktlint", task: "ktlint" },
]);
const currentMissingAndroidCapabilities = runCiManifestFixture({
androidCiCapabilities: false,
bundledPlanner: true,
changedPaths: ["package.json"],
eventName: "pull_request",
});
expect(currentMissingAndroidCapabilities.status, currentMissingAndroidCapabilities.output).toBe(
0,
);
expect(
JSON.parse(
expectDefined(
currentMissingAndroidCapabilities.outputs.android_matrix,
"current fallback-resistant Android matrix output",
),
).include,
).toEqual([
{ check_name: "android-test-play", task: "test-play", lint: true },
{ check_name: "android-test-third-party", task: "test-third-party", lint: true },
{ check_name: "android-test-wear", task: "test-wear", lint: true },
{ check_name: "android-ktlint", task: "ktlint" },
]);
expect(
JSON.parse(
expectDefined(
current.outputs.checks_node_core_nondist_matrix,
"current node core nondist matrix output",
),
).include,
).toContainEqual(
expect.objectContaining({
check_name: "bundled-node-plan",
env: {
OPENCLAW_CI_TEST_COMPACT_MODE: "full",
OPENCLAW_CI_TEST_RUNNER_BACKEND: "blacksmith",
},
shard_name: "bundled-node-plan",
}),
);
for (const runnerBackend of [undefined, "github", "hybrid"] as const) {
const push = runCiManifestFixture({
bundledPlanner: true,
eventName: "push",
runnerBackend,
});
expect(push.status, push.output).toBe(0);
expect(
JSON.parse(
expectDefined(
push.outputs.checks_node_core_nondist_matrix,
`${runnerBackend ?? "default"} push node core nondist matrix output`,
),
).include,
).toContainEqual(
expect.objectContaining({
check_name: "bundled-node-plan",
env: {
OPENCLAW_CI_TEST_COMPACT_MODE: "push",
OPENCLAW_CI_TEST_RUNNER_BACKEND: runnerBackend ?? "blacksmith",
},
}),
);
}
const dockerSeedPath = "scripts/e2e/docker-openai-seed.ts";
const changedPullRequest = runCiManifestFixture({
bundledPlanner: true,
changedPaths: ["src/focused.ts", "extensions/codex/src/focused.ts", dockerSeedPath],
eventName: "pull_request",
});
expect(changedPullRequest.status, changedPullRequest.output).toBe(0);
expect(
JSON.parse(
expectDefined(
changedPullRequest.outputs.checks_node_core_nondist_matrix,
"changed PR node matrix output",
),
).include,
).toEqual([
expect.objectContaining({
check_name: "changed-node-plan",
shard_name: "changed-node-plan",
targets: ["src/focused.test.ts"],
}),
]);
expect(
JSON.parse(
expectDefined(
changedPullRequest.outputs.checks_node_core_nondist_matrix,
"changed PR node matrix output",
),
).include,
).not.toContainEqual(
expect.objectContaining({ check_name: "changed-extension-fallback-plan" }),
);
expect(changedPullRequest.outputs.run_checks_node_core_dist).toBe("true");
expect(changedPullRequest.outputs.run_sqlite_session_lifecycle).toBe("false");
expect(changedPullRequest.outputs.run_docker_seed_e2e).toBe("true");
expect(changedPullRequest.outputs.docker_seed_lanes).toBe("mcp-channels cron-mcp-cleanup");
const mixedFallbackPullRequest = runCiManifestFixture({
bundledPlanner: true,
changedPaths: [
"packages/gateway-protocol/src/frame-guards.ts",
"extensions/codex/src/focused.ts",
],
eventName: "pull_request",
});
expect(mixedFallbackPullRequest.status, mixedFallbackPullRequest.output).toBe(0);
expect(
JSON.parse(
expectDefined(
mixedFallbackPullRequest.outputs.checks_node_core_nondist_matrix,
"mixed fallback PR node matrix output",
),
).include,
).toEqual(
expect.arrayContaining([
expect.objectContaining({
check_name: "bundled-node-plan",
env: {
OPENCLAW_CI_TEST_COMPACT_MODE: "pull-request",
OPENCLAW_CI_TEST_RUNNER_BACKEND: "blacksmith",
},
}),
expect.objectContaining({ check_name: "changed-extension-fallback-plan" }),
]),
);
const matrixFallbackPullRequest = runCiManifestFixture({
bundledPlanner: true,
changedPaths: [
"packages/gateway-protocol/src/frame-guards.ts",
"extensions/matrix/src/channel.ts",
],
eventName: "pull_request",
});
expect(matrixFallbackPullRequest.status, matrixFallbackPullRequest.output).toBe(0);
expect(
JSON.parse(
expectDefined(
matrixFallbackPullRequest.outputs.checks_node_core_nondist_matrix,
"Matrix fallback PR node matrix output",
),
).include,
).toEqual(
expect.arrayContaining([
expect.objectContaining({
check_name: "changed-extension-fallback-plan",
configs: ["test/vitest/vitest.extension-matrix.config.ts"],
includePatterns: [
"extensions/matrix/src/client.test.ts",
"extensions/matrix/src/monitor.test.ts",
],
}),
]),
);
const sqliteLifecycleTestPullRequest = runCiManifestFixture({
bundledPlanner: true,
changedPaths: ["test/scripts/sqlite-sessions-transcripts-flip-proof.built-cli.e2e.test.ts"],
eventName: "pull_request",
});
expect(sqliteLifecycleTestPullRequest.status, sqliteLifecycleTestPullRequest.output).toBe(0);
expect(sqliteLifecycleTestPullRequest.outputs.run_sqlite_session_lifecycle).toBe("true");
expect(sqliteLifecycleTestPullRequest.outputs.run_build_artifacts).toBe("true");
const emptyPullRequest = runCiManifestFixture({
bundledPlanner: true,
changedPaths: [],
eventName: "pull_request",
});
expect(emptyPullRequest.status, emptyPullRequest.output).toBe(0);
expect(
JSON.parse(
expectDefined(emptyPullRequest.outputs.checks_node_core_nondist_matrix, "empty PR matrix"),
).include,
).toEqual([expect.objectContaining({ check_name: "bundled-node-plan", includePatterns: [] })]);
for (const [changedPlannerSource, error] of [
[null, "Current CI target does not provide ./scripts/lib/ci-changed-node-test-plan.mjs"],
['throw new Error("planner import failure");', "planner import failure"],
[
"export const createChangedExtensionFallbackShards = () => [];",
"Current PR CI target does not export createChangedNodeTestShards",
],
[
"export const createChangedNodeTestShards = () => [];",
"Current PR CI target does not export createChangedExtensionFallbackShards",
],
[
`export const createChangedNodeTestShards = () => { throw new Error("precise planning failure"); };
export const createChangedExtensionFallbackShards = () => [];`,
"precise planning failure",
],
[
`export const createChangedNodeTestShards = () => null;
export const createChangedExtensionFallbackShards = () => { throw new Error("fallback planning failure"); };`,
"fallback planning failure",
],
] as const) {
const failure = runCiManifestFixture({
bundledPlanner: true,
changedPaths: ["package.json", "extensions/codex/src/focused.ts"],
changedPlannerSource,
eventName: "pull_request",
});
expect(failure.status, failure.output).toBe(1);
expect(failure.output).toContain(error);
expect(failure.outputs.checks_node_core_nondist_matrix).toBeUndefined();
}
for (const changedPaths of [undefined, null]) {
const failure = runCiManifestFixture({
bundledPlanner: true,
changedPaths,
eventName: "pull_request",
});
expect(failure.status, failure.output).toBe(1);
expect(failure.output).toContain(
"Current PR CI requires complete changed paths for Node test planning",
);
expect(failure.outputs.checks_node_core_nondist_matrix).toBeUndefined();
}
const currentMissingIos = runCiManifestFixture({
bundledPlanner: true,
changedPaths: ["package.json"],
eventName: "pull_request",
iosCapabilities: false,
});
expect(currentMissingIos.status, currentMissingIos.output).toBe(0);
expect(currentMissingIos.outputs.historical_target).toBe("false");
expect(currentMissingIos.outputs.run_ios_build).toBe("true");
expect(currentMissingIos.outputs.run_macos_swift).toBe("true");
const currentMissingQaPlan = runCiManifestFixture({
bundledPlanner: true,
changedPaths: ["package.json"],
eventName: "pull_request",
qaSmokePlan: false,
});
expect(currentMissingQaPlan.status, currentMissingQaPlan.output).toBe(0);
expect(currentMissingQaPlan.outputs.run_qa_smoke_ci).toBe("true");
const frozenMissingCurrentCapabilities = runCiManifestFixture({
bundledPlanner: true,
historicalCompatibility: false,
iosCapabilities: false,
iosBuildCapability: true,
nativeI18nCapabilities: false,
protocolCoverage: false,
qaSmokePlan: false,
formatCheck: false,
});
expect(frozenMissingCurrentCapabilities.status, frozenMissingCurrentCapabilities.output).toBe(
0,
);
expect(frozenMissingCurrentCapabilities.outputs.historical_target).toBe("false");
expect(frozenMissingCurrentCapabilities.outputs.frozen_target).toBe("true");
expect(frozenMissingCurrentCapabilities.outputs.run_ios_build).toBe("false");
expect(frozenMissingCurrentCapabilities.outputs.run_macos_swift).toBe("false");
expect(frozenMissingCurrentCapabilities.outputs.run_native_i18n).toBe("false");
expect(frozenMissingCurrentCapabilities.outputs.run_qa_smoke_ci).toBe("false");
expect(frozenMissingCurrentCapabilities.outputs.run_protocol_event_coverage).toBe("false");
expect(frozenMissingCurrentCapabilities.outputs.run_format_check).toBe("false");
const releaseCandidateMissingSwiftWrappers = runCiManifestFixture({
bundledPlanner: true,
historicalCompatibility: false,
iosCapabilities: false,
iosBuildCapability: true,
releaseCandidateCompatibility: true,
});
expect(releaseCandidateMissingSwiftWrappers.status).toBe(0);
expect(releaseCandidateMissingSwiftWrappers.outputs.compatibility_target).toBe("true");
expect(releaseCandidateMissingSwiftWrappers.outputs.use_compatible_android_ci).toBe("false");
expect(releaseCandidateMissingSwiftWrappers.outputs.run_ios_build).toBe("true");
expect(releaseCandidateMissingSwiftWrappers.outputs.run_macos_swift).toBe("true");
const releaseCandidateMissingIosBuild = runCiManifestFixture({
bundledPlanner: true,
historicalCompatibility: false,
iosCapabilities: false,
iosBuildCapability: false,
releaseCandidateCompatibility: true,
});
expect(releaseCandidateMissingIosBuild.status).toBe(0);
expect(releaseCandidateMissingIosBuild.outputs.run_ios_build).toBe("false");
const frozenTargetContext = runCiManifestFixture({
bundledPlanner: false,
historicalCompatibility: false,
targetContextCompatibility: true,
});
expect(frozenTargetContext.status, frozenTargetContext.output).toBe(0);
expect(frozenTargetContext.outputs.compatibility_target).toBe("true");
expect(
JSON.parse(
expectDefined(
frozenTargetContext.outputs.checks_node_core_nondist_matrix,
"frozen target context node core nondist matrix output",
),
).include,
).toContainEqual(expect.objectContaining({ check_name: "legacy-node-plan" }));
const pullRequestMissingProtocolCoverage = runCiManifestFixture({
bundledPlanner: true,
changedPaths: ["package.json"],
eventName: "pull_request",
protocolCoverage: false,
});
expect(
pullRequestMissingProtocolCoverage.status,
pullRequestMissingProtocolCoverage.output,
).toBe(0);
expect(pullRequestMissingProtocolCoverage.outputs.historical_target).toBe("false");
expect(pullRequestMissingProtocolCoverage.outputs.run_protocol_event_coverage).toBe("true");
const currentMissingPlanner = runCiManifestFixture({
bundledPlanner: false,
eventName: "pull_request",
});
expect(currentMissingPlanner.status).not.toBe(0);
expect(currentMissingPlanner.output).toContain(
"CI target does not export a supported Node test shard planner",
);
const workflow = readCiWorkflow();
const historicalTargetStep = workflow.jobs.preflight.steps.find(
(step: { name?: string }) => step.name === "Validate historical release target",
);
expect(historicalTargetStep.if).toBe("inputs.historical_target_tag != ''");
expect(historicalTargetStep.run).toContain('[[ "$tag_sha" != "$EXPECTED_SHA" ]]');
const releaseCandidateStep = workflow.jobs.preflight.steps.find(
(step: { name?: string }) => step.name === "Validate release candidate target",
);
expect(releaseCandidateStep.if).toBe("inputs.release_candidate_ref != ''");
expect(releaseCandidateStep.run).toContain('[[ "$branch_sha" != "$EXPECTED_SHA" ]]');
expect(workflow.jobs["qa-smoke-ci-profile"].if).toBe(
"needs.preflight.outputs.run_qa_smoke_ci == 'true'",
);
expect(workflow.jobs["checks-fast-channel-contracts-shard"].if).toBe(
"needs.preflight.outputs.run_channel_contracts_shards == 'true'",
);
const swiftInstall = workflow.jobs["macos-swift"].steps.find(
(step: { name?: string }) => step.name === "Install XcodeGen / SwiftLint / SwiftFormat",
);
const swiftLint = workflow.jobs["macos-swift"].steps.find(
(step: { name?: string }) => step.name === "Swift lint",
);
const openClawKitTests = workflow.jobs["macos-swift"].steps.find(
(step: { name?: string }) => step.name === "OpenClawKit tests",
);
expect(swiftInstall.run).toContain("brew install xcodegen swiftlint");
expect(swiftInstall.run).not.toContain("brew install xcodegen swiftlint swiftformat");
expect(swiftInstall.run).toContain(
"https://github.com/nicklockwood/SwiftFormat/releases/download/$swiftformat_version/swiftformat.zip",
);
expect(swiftInstall.run).toContain(
'swiftformat_checksum="b990400779aceb7d7020796eb9ba814d4480543f671d38fc0ff48cb72f04c584"',
);
expect(swiftInstall.run).toContain(
'swiftformat_checksum="7cb1cb1fae04932047c7015441c543848e8e60e1572d808d080e0a1f1661114a"',
);
expect(swiftInstall.run).toContain(
'swiftformat_min_version="$(awk \'$1 == "--min-version" { print $2; exit }\' config/swiftformat)"',
);
expect(swiftInstall.run).toContain(
'echo "Unsupported frozen-target SwiftFormat minimum: $swiftformat_min_version" >&2',
);
expect(swiftInstall.run).toContain('echo "$swift_tools_dir" >> "$GITHUB_PATH"');
expect(swiftInstall.run).toContain(
'[[ "$("$swift_tools_dir/swiftformat" --version)" == "$swiftformat_version" ]]',
);
expect(workflow.jobs["macos-swift"].env.HISTORICAL_TARGET).toBe(
"${{ needs.preflight.outputs.compatibility_target }}",
);
expect(swiftInstall.run).toContain('elif [[ "$HISTORICAL_TARGET" == "true" ]]');
expect(swiftLint.run).toContain("swiftlint lint --config config/swiftlint.yml");
expect(swiftLint.run).toContain('elif [[ "$HISTORICAL_TARGET" == "true" ]]');
expect(openClawKitTests.if).toBe(
"matrix.phase == 'tests' && needs.preflight.outputs.run_openclawkit_tests == 'true'",
);
const checkShard = workflow.jobs["check-shard"].steps.find(
(step: { name?: string }) => step.name === "Run check shard",
);
expect(checkShard.env.HISTORICAL_TARGET).toBe(
"${{ needs.preflight.outputs.compatibility_target }}",
);
expect(checkShard.run).toContain("pnpm tsgo:scripts");
expect(checkShard.run).toContain('elif [[ "$HISTORICAL_TARGET" != "true" ]]');
expect(checkShard.run).toContain('has_package_script "deps:npm-lock:check"');
expect(checkShard.run).toContain(
"Current CI targets must provide the deps:npm-lock:check package script.",
);
expect(checkShard.run).toContain(
"[skip] historical target predates the transient npm lock contract",
);
expect(checkShard.run).toContain('has_package_script "deadcode:dependencies"');
expect(checkShard.run).toContain('has_package_script "deadcode:unused-files"');
expect(checkShard.run).toContain('has_package_script "deadcode:exports"');
// The concurrent launcher invokes scripts through the dc_scripts array.
expect(checkShard.run).toContain("dc_scripts+=(deadcode:exports)");
expect(checkShard.run).toContain(
"Current CI targets must provide the deadcode:exports package script.",
);
expect(checkShard.run).toContain(
'elif [[ "$HISTORICAL_TARGET" == "true" ]] && has_package_script "deadcode:ci"',
);
expect(checkShard.run).toContain("Target does not provide a supported deadcode check.");
const uiInstall = workflow.jobs["checks-ui"].steps.find(
(step: { name?: string }) => step.name === "Install Playwright Chromium",
);
const uiBrowserCache = workflow.jobs["checks-ui"].steps.find(
(step: { name?: string }) => step.name === "Cache Playwright Chromium",
);
const uiTest = workflow.jobs["checks-ui"].steps.find(
(step: { name?: string }) => step.name === "Test Control UI",
);
expect(workflow.jobs["checks-ui"].env.COMPATIBILITY_TARGET).toBe(
"${{ needs.preflight.outputs.compatibility_target }}",
);
expect(uiInstall.env.FROZEN_TARGET).toBe("${{ needs.preflight.outputs.frozen_target }}");
expect(uiInstall.run).toContain('if [[ "${COMPATIBILITY_TARGET:-false}" == "true" ]]');
expect(uiInstall.run).toContain("pnpm --dir ui exec playwright install chromium");
expect(uiInstall.run).toContain("node --import tsx scripts/ensure-playwright-chromium.mts");
expect(uiInstall.run).toContain(
'elif [[ "$FROZEN_TARGET" == "true" && -f scripts/ensure-playwright-chromium.mjs ]]',
);
expect(uiInstall.run).toContain("node scripts/ensure-playwright-chromium.mjs");
expect(uiInstall.run).toContain(
"Target does not provide a supported Playwright Chromium installer.",
);
expect(uiInstall.run).not.toContain("OPENCLAW_UI_E2E_ALLOW_MISSING_CHROMIUM");
const playwrightVersion = JSON.parse(readFileSync("package.json", "utf8")).devDependencies
.playwright;
expect(playwrightVersion).toBe(
JSON.parse(readFileSync("ui/package.json", "utf8")).devDependencies.playwright,
);
expect(uiBrowserCache).toMatchObject({
if: "needs.preflight.outputs.cache_mode != 'off' && needs.preflight.outputs.compatibility_target != 'true'",
uses: CACHE_V5,
with: {
key: "${{ runner.os }}-playwright-chromium-" + playwrightVersion,
path: "~/.cache/ms-playwright",
},
});
expect(uiTest.run).toContain('if [[ "$COMPATIBILITY_TARGET" == "true" ]]');
expect(uiTest.run).toContain("pnpm --dir ui test --testTimeout=30000 --isolate");
expect(uiTest.run).not.toContain("--retry");
expect(uiTest.run).toContain("pnpm --dir ui test");
});
it.each([
{ label: "current", frozenTarget: false, compatibilityTarget: false, shards: [1, 2, 3] },
{ label: "frozen current", frozenTarget: true, compatibilityTarget: false, shards: [1] },
{ label: "frozen legacy", frozenTarget: true, compatibilityTarget: true, shards: [1] },
])("executes the $label standalone UI envelope", async (scenario) => {
const workflow = readCiWorkflow();
const ui = workflow.jobs["checks-ui"];
const lint = ui.steps.find(
(step: WorkflowStep) => step.name === "Lint Control UI window.open usage",
);
const test = ui.steps.find((step: WorkflowStep) => step.name === "Test Control UI");
const diagnostics = expectDefined(
ui.steps.find((step: WorkflowStep) => step.name === "Upload Control UI timeout diagnostics"),
"Control UI timeout diagnostic upload",
);
expect(ui.steps.indexOf(diagnostics)).toBeGreaterThan(ui.steps.indexOf(test));
expect(diagnostics).toMatchObject({
if: "failure()",
uses: UPLOAD_ARTIFACT_V7,
with: {
name: "control-ui-test-timeout-${{ matrix.shard }}-${{ github.run_attempt }}",
path: `${test.env.OPENCLAW_UI_E2E_DIAGNOSTIC_DIR}/failure-*/failure.public.json`,
"if-no-files-found": "ignore",
"retention-days": 7,
},
});
const context = {
eventName: scenario.frozenTarget ? "workflow_dispatch" : "pull_request",
frozenTarget: scenario.frozenTarget,
preflightOutputs: { compatibility_target: String(scenario.compatibilityTarget) },
repository: "openclaw/openclaw",
runAttempt: 1,
runnerBackend: "hybrid",
} as const;
// A workflow job without a matrix executes once.
const shards = ui.strategy
? evaluateWorkflowExpression(ui.strategy.matrix.shard, context)
: [1];
expect(shards).toEqual(scenario.shards);
if (!scenario.frozenTarget) {
expect(ui.strategy).toMatchObject({ "fail-fast": false, "max-parallel": 3 });
}
expect(ui.needs).toEqual(["preflight"]);
expect(ui.if).toBe("needs.preflight.outputs.run_ui_tests == 'true'");
expect(ui.permissions).toEqual({ contents: "read" });
expect(ui["timeout-minutes"]).toBe(20);
expect(workflow.jobs["ci-gate"].needs).toContain("checks-ui");
const root = tempDirs.make("openclaw-ui-workflow-");
const bin = path.join(root, "bin");
const callsPath = path.join(root, "calls.txt");
const argsPath = path.join(root, "vitest-args.json");
mkdirSync(bin);
for (const command of ["node", "pnpm"]) {
writeExecutable(path.join(bin, command), [
"#!/usr/bin/env bash",
"set -euo pipefail",
`printf '%s\\n' '${command} '"$*" >> "$UI_COMMAND_CALLS"`,
...(command === "node"
? ['printf "%s\\n" "$OPENCLAW_NODE_TEST_VITEST_ARGS_JSON" > "$UI_VITEST_ARGS"']
: []),
]);
}
for (const shard of scenario.shards) {
const rowContext = { ...context, matrix: { shard }, workspace: root };
const resolveValue = (value: unknown): string =>
String(value).replace(/\$\{\{[\s\S]*?\}\}/gu, (expression) =>
String(evaluateWorkflowExpression(expression, rowContext)),
);
expect(resolveValue(ui.name)).toBe(
scenario.frozenTarget ? "checks-ui" : `checks-ui (${shard}/3)`,
);
expect(evaluateWorkflowExpression(ui["runs-on"], rowContext)).toBe(
scenario.frozenTarget ? "ubuntu-24.04" : "blacksmith-8vcpu-ubuntu-2404",
);
const env = Object.fromEntries(
Object.entries({ ...ui.env, ...test.env }).map(([key, value]) => [
key,
resolveValue(value),
]),
);
expect(env.OPENCLAW_NODE_TEST_PLAN_CONCURRENCY).toBe("1");
expect(env.OPENCLAW_UI_E2E_DIAGNOSTIC_DIR).toBe(
`${root}/.artifacts/control-ui-e2e-timeouts/ui-shard-${shard}-attempt-1`,
);
const flags = [
"--maxWorkers",
"3",
"--reporter=verbose",
"--reporter=github-actions",
"--reporter=./scripts/lib/vitest-resource-reporter.mts",
...(scenario.frozenTarget ? [] : [`--shard=${shard}/3`]),
];
const steps = [
...(!lint.if || evaluateWorkflowExpression(lint.if, rowContext) ? [lint] : []),
test,
];
for (const step of steps) {
const result = runWorkflowShellScript(step.run, {
cwd: root,
env: {
...process.env,
...env,
PATH: `${bin}:${process.env.PATH ?? ""}`,
UI_COMMAND_CALLS: callsPath,
UI_VITEST_ARGS: argsPath,
},
});
expect(result.status, result.stdout + result.stderr).toBe(0);
}
if (!scenario.compatibilityTarget) {
env.OPENCLAW_NODE_TEST_VITEST_ARGS_JSON = readFileSync(argsPath, "utf8");
expect(JSON.parse(env.OPENCLAW_NODE_TEST_VITEST_ARGS_JSON)).toEqual(flags);
const forwarded: string[][] = [];
expect(
await runShardPlans(resolveShardPlans(env), {
concurrency: Number(env.OPENCLAW_NODE_TEST_PLAN_CONCURRENCY),
env,
scratchDir: root,
runChild: async (args, childEnv) => {
forwarded.push(args);
expect(childEnv.OPENCLAW_TEST_PROJECTS_PARALLEL).toBe("1");
expect(childEnv.OPENCLAW_UI_E2E_DIAGNOSTIC_DIR).toBe(
resolveValue(test.env.OPENCLAW_UI_E2E_DIAGNOSTIC_DIR),
);
return 0;
},
}),
).toBe(0);
expect(forwarded).toEqual([["ui/vitest.config.ts", "--", ...flags]]);
}
}
const calls = readFileSync(callsPath, "utf8").trim().split("\n");
expect(calls.filter((call) => call === "pnpm lint:ui:no-raw-window-open")).toHaveLength(1);
expect(calls.filter((call) => call !== "pnpm lint:ui:no-raw-window-open")).toEqual(
scenario.compatibilityTarget
? ["pnpm --dir ui test --testTimeout=30000 --isolate"]
: scenario.shards.map(() => "node --import tsx scripts/ci-run-node-test-shard.mts"),
);
});
it("keeps private Control UI servers and resource-sensitive files under one serial owner", () => {
assertControlUiE2eOwnership((prefix) => tempDirs.make(prefix));
});
it("retains shared worker limits and local throttling in the bundled UI project", () => {
const original = sharedVitestConfig.test;
try {
for (const [maxWorkers, fileParallelism, expectedWorkers] of [
[1, false, 1],
[8, true, 2],
] as const) {
sharedVitestConfig.test = { ...original, maxWorkers, fileParallelism };
const config = createUiE2eVitestConfig({}, []);
const projects = config.test?.projects as Array<{
test: { maxWorkers?: number; fileParallelism: boolean };
}>;
expect(config.test?.maxWorkers).toBe(expectedWorkers);
expect(projects.map((project) => project.test.maxWorkers)).toEqual([
undefined,
undefined,
1,
1,
]);
expect(projects.map((project) => project.test.fileParallelism)).toEqual([
fileParallelism,
fileParallelism,
false,
false,
]);
}
} finally {
sharedVitestConfig.test = original;
}
});
it("uses the target-owned UI project capability for frozen manual matrices and commands", () => {
for (const [runnerBackend, legacyJobCount] of [
["blacksmith", 4],
["github", 14],
["hybrid", 14],
] as const) {
for (const uiE2eProjectsCapability of [false, true]) {
const jobCount = uiE2eProjectsCapability ? 13 : legacyJobCount;
const manifest = runCiManifestFixture({
bundledPlanner: true,
eventName: "workflow_dispatch",
historicalCompatibility: false,
runnerBackend,
uiE2eProjectsCapability,
});
expect(manifest.status, manifest.output).toBe(0);
expect(manifest.outputs.frozen_target).toBe("true");
expect(manifest.outputs.compatibility_target).toBe("false");
expect(
JSON.parse(
expectDefined(manifest.outputs.ui_e2e_matrix, `${runnerBackend} UI E2E matrix`),
),
).toEqual({
include: Array.from({ length: jobCount }, (_, index) => {
const shard = index + 1;
return {
shard,
shard_count: jobCount,
task: shard === jobCount ? "browser-extension" : "control-ui",
vitest_shard_count: jobCount - 1,
};
}),
});
}
}
const uiE2E = readCiWorkflow().jobs["checks-ui-e2e"];
const scenario = expectDefined(
uiE2E.steps.find((step: WorkflowStep) => step.name === "Test Control UI end-to-end"),
"Control UI E2E suite",
);
const commandRoot = tempDirs.make("openclaw-ui-e2e-project-command-");
const commandBin = path.join(commandRoot, "bin");
const commandArgs = path.join(commandRoot, "args");
mkdirSync(commandBin);
writeFileSync(
path.join(commandBin, "node"),
'#!/bin/sh\nprintf "%s\\n" "$@" > "$UI_E2E_COMMAND_ARGS"\n',
{ mode: 0o755 },
);
const runCommand = (env: Record<string, string>) => {
const result = runWorkflowShellScript(expectDefined(scenario.run, "UI E2E command"), {
cwd: commandRoot,
env: {
...process.env,
...env,
PATH: `${commandBin}:${process.env.PATH ?? ""}`,
UI_E2E_COMMAND_ARGS: commandArgs,
},
});
expect(result.status, result.stdout + result.stderr).toBe(0);
return readFileSync(commandArgs, "utf8").trim().split("\n");
};
expect(runCommand({ VITEST_SHARD_COUNT: "3", VITEST_SHARD_INDEX: "1" })).toEqual([
"scripts/run-vitest.mjs",
"run",
"--config",
"test/vitest/vitest.ui-e2e.config.ts",
"--configLoader",
"runner",
"--shard",
"1/3",
]);
expect(
evaluateWorkflowExpression(`\${{ ${uiE2E.if} }}`, {
eventName: "workflow_dispatch",
preflightOutputs: { compatibility_target: "true", run_ui_tests: "true" },
repository: "openclaw/openclaw",
runAttempt: 1,
}),
).toBe(false);
});
it("gates current Control UI changes on ordinary and real-Gateway Chromium E2E", () => {
const workflow = readCiWorkflow();
const ui = workflow.jobs["checks-ui"];
const uiE2e = workflow.jobs["checks-ui-e2e"];
const uiE2eRealGateway = workflow.jobs["checks-ui-e2e-real-gateway"];
expect(readFileSync("test/vitest/vitest.ui-e2e.config.ts", "utf8")).toContain(
"ui-e2e-projects-contract-v1",
);
expect(uiE2e.permissions).toEqual({ contents: "read" });
expect(uiE2e.needs).toEqual(["preflight"]);
expect(uiE2e.if).toBe(
"needs.preflight.outputs.run_ui_e2e == 'true' && needs.preflight.outputs.compatibility_target != 'true'",
);
expect(uiE2e["runs-on"]).not.toBe(ui["runs-on"]);
expect(uiE2e["timeout-minutes"]).toBe(25);
expect(uiE2e.env).toEqual({ OPENCLAW_UI_E2E_SKIP_REAL_GATEWAY: "1" });
expect(uiE2e.strategy["fail-fast"]).toBe(false);
expect(uiE2e.strategy["max-parallel"]).toBe(14);
expect(uiE2e.strategy.matrix).toBe("${{ fromJson(needs.preflight.outputs.ui_e2e_matrix) }}");
const expectedUiE2eMatrices = [6, 12].map((vitestShardCount) => ({
include: Array.from({ length: vitestShardCount + 1 }, (_, index) => {
const shard = index + 1;
return {
shard,
shard_count: vitestShardCount + 1,
task: shard === vitestShardCount + 1 ? "browser-extension" : "control-ui",
vitest_shard_count: vitestShardCount,
};
}),
}));
for (const runnerBackend of ["blacksmith", "github", "hybrid"] as const) {
for (const eventName of ["push", "pull_request"] as const) {
for (const runAttempt of ["1", "2", ""]) {
const manifest = runCiManifestFixture({
bundledPlanner: true,
changedPaths: [],
eventName,
historicalCompatibility: false,
runnerBackend,
uiE2eProjectsCapability: true,
scopeEnv: { GITHUB_RUN_ATTEMPT: runAttempt },
});
const assertionName = `${runnerBackend} ${eventName} attempt ${runAttempt || "missing"}`;
expect(manifest.status, manifest.output).toBe(0);
expect(
JSON.parse(expectDefined(manifest.outputs.ui_e2e_matrix, assertionName)),
assertionName,
).toEqual(expectedUiE2eMatrices[1]);
}
}
}
expect(workflow.jobs["ci-gate"].needs).toContain("checks-ui-e2e");
expect(workflow.jobs["ci-gate"].needs).toContain("checks-ui-e2e-real-gateway");
expect(uiE2eRealGateway.permissions).toEqual(uiE2e.permissions);
expect(uiE2eRealGateway.needs).toEqual(uiE2e.needs);
expect(uiE2eRealGateway.if).toBe(uiE2e.if);
expect(uiE2eRealGateway.env).toBeUndefined();
const uiE2eSetup = expectDefined(
uiE2e.steps.find((step: WorkflowStep) => step.name === "Setup Node environment"),
"Control UI E2E Node setup",
);
expect(uiE2eSetup.uses).toBe("./.ci-harness/.github/actions/setup-node-env");
const expectedSharedUiE2eSetup = {
"cache-mode": "${{ needs.preflight.outputs.cache_mode }}",
"node-version": "24.x",
"install-bun": "false",
"dependency-cache": expect.any(String),
} as const;
const expectedUiE2eSetup = {
...expectedSharedUiE2eSetup,
"restore-test-caches":
"${{ (needs.preflight.outputs.runner_profile == 'github' || needs.preflight.outputs.runner_profile == 'hybrid') && 'true' || 'false' }}",
} as const;
expect(uiE2eSetup.with).toEqual(expectedUiE2eSetup);
const realGatewaySetup = expectDefined(
uiE2eRealGateway.steps.find((step: WorkflowStep) => step.name === "Setup Node environment"),
"real-Gateway Control UI E2E Node setup",
);
expect(realGatewaySetup).toMatchObject({
uses: uiE2eSetup.uses,
with: expectedSharedUiE2eSetup,
});
expect(realGatewaySetup.with).toEqual(expectedSharedUiE2eSetup);
// Failed-job retries can retain an earlier six-shard plan while live routing
// selects hosted runners. Both widths retain the cache and contributor boundaries.
const routedUiE2eJobs = [
...expectedUiE2eMatrices
.flatMap(({ include }) => include)
.map((matrix) => ({
job: uiE2e,
name: `checks-ui-e2e (${matrix.shard}/${matrix.shard_count})`,
setup: uiE2eSetup,
matrix,
blacksmithRunner:
matrix.task === "control-ui"
? "blacksmith-16vcpu-ubuntu-2404"
: "blacksmith-8vcpu-ubuntu-2404",
})),
{
job: uiE2eRealGateway,
name: "checks-ui-e2e-real-gateway",
setup: realGatewaySetup,
matrix: {},
blacksmithRunner: "blacksmith-32vcpu-ubuntu-2404",
},
] as const;
const routingScenarios = [
{
name: "same-repo pull request first attempt",
context: {
eventName: "pull_request",
headRepository: "openclaw/openclaw",
repository: "openclaw/openclaw",
runAttempt: 1,
},
expected: { blacksmith: true, dependencyCache: "true" },
},
{
name: "same-repo pull request with GitHub backend",
context: {
eventName: "pull_request",
headRepository: "openclaw/openclaw",
repository: "openclaw/openclaw",
runnerBackend: "github",
runAttempt: 1,
},
expected: { blacksmith: false, dependencyCache: "false" },
},
{
name: "same-repo pull request with hybrid backend",
context: {
eventName: "pull_request",
headRepository: "openclaw/openclaw",
repository: "openclaw/openclaw",
runnerBackend: "hybrid",
runAttempt: 1,
},
expected: { blacksmith: true, dependencyCache: "true" },
},
{
name: "same-repo pull request retry",
context: {
eventName: "pull_request",
headRepository: "openclaw/openclaw",
repository: "openclaw/openclaw",
runAttempt: 2,
},
expected: { blacksmith: false, dependencyCache: "false" },
},
{
name: "same-repo pull request with hybrid backend retry",
context: {
eventName: "pull_request",
headRepository: "openclaw/openclaw",
repository: "openclaw/openclaw",
runnerBackend: "hybrid",
runAttempt: 2,
},
expected: { blacksmith: false, dependencyCache: "false" },
},
{
name: "canonical hybrid push retry",
context: {
eventName: "push",
repository: "openclaw/openclaw",
runnerBackend: "hybrid",
runAttempt: 2,
},
expected: { blacksmith: false, dependencyCache: "false" },
},
{
// Runner routing follows contributor trust; the exact dependency cache
// stays fork-gated either way, so a fork never writes what main reads.
name: "fork pull request from returning contributor",
context: {
authorAssociation: "CONTRIBUTOR",
eventName: "pull_request",
headRepository: "contributor/openclaw",
repository: "openclaw/openclaw",
runAttempt: 1,
},
expected: { blacksmith: true, dependencyCache: "false" },
},
{
name: "fork pull request from unknown author",
context: {
authorAssociation: "NONE",
eventName: "pull_request",
headRepository: "contributor/openclaw",
repository: "openclaw/openclaw",
runAttempt: 1,
},
expected: { blacksmith: false, dependencyCache: "false" },
},
{
name: "workflow dispatch",
context: {
eventName: "workflow_dispatch",
repository: "openclaw/openclaw",
runAttempt: 1,
},
expected: { blacksmith: false, dependencyCache: "false" },
},
{
name: "canonical push retry",
context: {
eventName: "push",
repository: "openclaw/openclaw",
runAttempt: 2,
},
expected: { blacksmith: true, dependencyCache: "true" },
},
] as const;
for (const { blacksmithRunner, job, matrix, name: jobName, setup } of routedUiE2eJobs) {
for (const { context, expected, name: scenarioName } of routingScenarios) {
const assertionName = `${jobName}: ${scenarioName}`;
const expectedRunner = expected.blacksmith ? blacksmithRunner : "ubuntu-24.04";
expect(
evaluateWorkflowExpression(job["runs-on"], { ...context, matrix }),
assertionName,
).toBe(expectedRunner);
expect(
evaluateWorkflowExpression(setup.with?.["dependency-cache"], {
...context,
matrix,
runnerEnvironment: expected.blacksmith ? "self-hosted" : "github-hosted",
}),
assertionName,
).toBe(expected.dependencyCache);
expect(setup.with?.["cache-mode"], assertionName).toBe(
"${{ needs.preflight.outputs.cache_mode }}",
);
}
}
const chromiumInstall = expectDefined(
uiE2e.steps.find((step: WorkflowStep) => step.name === "Install Playwright Chromium"),
"Control UI E2E Chromium installation",
);
expect(chromiumInstall.env.FROZEN_TARGET).toBe("${{ needs.preflight.outputs.frozen_target }}");
expect(chromiumInstall.run).toContain(
"node --import tsx scripts/ensure-playwright-chromium.mts",
);
expect(chromiumInstall.run).toContain("node scripts/ensure-playwright-chromium.mjs");
const chromiumCache = expectDefined(
uiE2e.steps.find((step: WorkflowStep) => step.name === "Cache Playwright Chromium"),
"Control UI E2E Chromium cache",
);
const realGatewayChromiumInstall = expectDefined(
uiE2eRealGateway.steps.find(
(step: WorkflowStep) => step.name === "Install Playwright Chromium",
),
"real-Gateway Control UI E2E Chromium installation",
);
expect(realGatewayChromiumInstall).toEqual(chromiumInstall);
const realGatewayChromiumCache = expectDefined(
uiE2eRealGateway.steps.find(
(step: WorkflowStep) => step.name === "Cache Playwright Chromium",
),
"real-Gateway Control UI E2E Chromium cache",
);
expect(realGatewayChromiumCache).toEqual(chromiumCache);
const scenario = expectDefined(
uiE2e.steps.find((step: WorkflowStep) => step.name === "Test Control UI end-to-end"),
"Control UI E2E suite",
);
expect(scenario.if).toBe("matrix.task == 'control-ui'");
expect(scenario.env).toEqual({
OPENCLAW_UI_E2E_DIAGNOSTIC_DIR:
".artifacts/control-ui-e2e-timeouts/shard-${{ matrix.shard }}-attempt-${{ github.run_attempt }}",
VITEST_SHARD_INDEX: "${{ matrix.shard }}",
VITEST_SHARD_COUNT: "${{ matrix.vitest_shard_count }}",
});
expect(scenario.run).not.toContain("--project");
const timeoutDiagnostics = expectDefined(
uiE2e.steps.find(
(step: WorkflowStep) => step.name === "Upload Control UI E2E timeout diagnostics",
),
"Control UI E2E timeout diagnostic upload",
);
expect(timeoutDiagnostics).toEqual({
name: "Upload Control UI E2E timeout diagnostics",
if: "failure() && matrix.task == 'control-ui'",
uses: UPLOAD_ARTIFACT_V7,
with: {
name: "control-ui-e2e-timeout-${{ matrix.shard }}-${{ github.run_attempt }}",
path: ".artifacts/control-ui-e2e-timeouts/shard-${{ matrix.shard }}-attempt-${{ github.run_attempt }}/failure-*/failure.public.json",
"if-no-files-found": "ignore",
"retention-days": 7,
},
});
const browserExtension = expectDefined(
uiE2e.steps.find(
(step: WorkflowStep) => step.name === "Test browser extension bootstrap end-to-end",
),
"browser extension bootstrap E2E suite",
);
expect(browserExtension.if).toBe("matrix.task == 'browser-extension'");
expect(browserExtension.run).toBe("pnpm test:e2e:browser-extension");
for (const { job } of routedUiE2eJobs) {
const jobContract = JSON.stringify(job);
expect(jobContract).not.toContain("OPENCLAW_UI_E2E_ALLOW_MISSING_CHROMIUM");
expect(jobContract).not.toContain("OPENCLAW_VITEST_NO_OUTPUT_RETRY");
}
const realGatewaySteps = uiE2eRealGateway.steps.filter((step: WorkflowStep) =>
step.name?.includes("with a real Gateway"),
);
expect(realGatewaySteps).toHaveLength(1);
const realGatewayStep = expectDefined(
realGatewaySteps[0],
"combined real-Gateway Control UI E2E suite",
);
expect(realGatewayStep.run).not.toContain("--retry");
expect(realGatewayStep.run).not.toContain("--hookTimeout");
expect(realGatewayStep.run).not.toContain("--testTimeout");
const proofUploadIndex = uiE2eRealGateway.steps.findIndex(
(step: WorkflowStep) => step.name === "Upload sanitized Control UI real-Gateway proof",
);
const proofUpload = uiE2eRealGateway.steps[proofUploadIndex];
const realGatewayIndex = uiE2eRealGateway.steps.indexOf(realGatewayStep);
const realGatewayFailureDiagnostics = expectDefined(
uiE2eRealGateway.steps.find(
(step: WorkflowStep) => step.name === "Upload Control UI real-Gateway failure diagnostics",
),
"real-Gateway Control UI failure diagnostic upload",
);
expect(realGatewayFailureDiagnostics).toEqual({
name: "Upload Control UI real-Gateway failure diagnostics",
if: "failure()",
uses: UPLOAD_ARTIFACT_V7,
with: {
name: "control-ui-real-gateway-timeout-${{ github.run_attempt }}",
path: [
".artifacts/control-ui-e2e-timeouts/real-gateway-attempt-${{ github.run_attempt }}/failure-*/failure.public.json",
"",
].join("\n"),
"if-no-files-found": "ignore",
"retention-days": 7,
},
});
expect(uiE2eRealGateway.steps.indexOf(realGatewayFailureDiagnostics)).toBeGreaterThan(
realGatewayIndex,
);
const quotaDiagnostics = expectDefined(
uiE2eRealGateway.steps.find(
(step: WorkflowStep) => step.name === "Upload quota auth and transport diagnostics",
),
"quota diagnostics retained on success and failure",
);
expect(quotaDiagnostics).toEqual({
name: "Upload quota auth and transport diagnostics",
if: "always()",
uses: UPLOAD_ARTIFACT_V7,
with: {
name: "control-ui-quota-diagnostics-${{ github.run_attempt }}",
path: ".artifacts/control-ui-e2e/real-gateway/quota-refresh-*/quota.public.json",
"if-no-files-found": "ignore",
"retention-days": 7,
},
});
expect(uiE2eRealGateway.steps.indexOf(quotaDiagnostics)).toBeGreaterThan(realGatewayIndex);
// Same-origin admission compares exact build IDs, including the build timestamp.
// Include private QA so media bootstrap cannot rebuild runtime behind the UI.
const realGatewayBuild = expectDefined(
uiE2eRealGateway.steps.find((step: WorkflowStep) => step.run === "pnpm build:ci-artifacts"),
"paired runtime and Control UI build",
);
expect(realGatewayBuild.if).toBeUndefined();
expect(realGatewayBuild["continue-on-error"]).toBeUndefined();
expect(realGatewayBuild.env).toEqual({ OPENCLAW_BUILD_PRIVATE_QA: "1" });
const realGatewayBuildIndex = uiE2eRealGateway.steps.indexOf(realGatewayBuild);
expect(realGatewayBuildIndex).toBeGreaterThan(uiE2eRealGateway.steps.indexOf(realGatewaySetup));
expect(realGatewayBuildIndex).toBeLessThan(realGatewayIndex);
const desktopProof = expectDefined(
uiE2eRealGateway.steps.find(
(step: WorkflowStep) => step.name === "Prove desktop resize over node and SSH",
),
"real desktop fixture proof",
);
expect(desktopProof).toEqual({
name: "Prove desktop resize over node and SSH",
env: {
FROZEN_TARGET: "${{ needs.preflight.outputs.frozen_target }}",
DESKTOP_PROOF_CHECKOUT_SHA: "${{ needs.preflight.outputs.checkout_revision }}",
DESKTOP_PROOF_PR_HEAD_SHA: "${{ github.event.pull_request.head.sha }}",
DESKTOP_PROOF_PR_BASE_SHA: "${{ github.event.pull_request.base.sha }}",
DESKTOP_PROOF_WORKFLOW_SHA: "${{ github.workflow_sha }}",
},
run: expect.stringContaining('node --import tsx "$bootstrap"'),
});
expect(uiE2eRealGateway.steps.indexOf(desktopProof)).toBeGreaterThan(realGatewayBuildIndex);
expect(uiE2eRealGateway.steps.indexOf(desktopProof)).toBeLessThan(realGatewayIndex);
expect(realGatewayStep.run).not.toContain("desktop-resize.real-gateway.e2e.test.ts");
const desktopUpload = expectDefined(
uiE2eRealGateway.steps.find(
(step: WorkflowStep) => step.name === "Upload sanitized desktop resize proof",
),
"sanitized desktop upload",
);
expect(desktopUpload).toEqual({
name: "Upload sanitized desktop resize proof",
if: "always()",
uses: UPLOAD_ARTIFACT_V7,
with: {
name: "desktop-resize-proof-${{ github.run_id }}-${{ github.run_attempt }}",
path: ".artifacts/control-ui-e2e/real-gateway/desktop-resize",
"if-no-files-found": "warn",
"retention-days": 14,
},
});
expect(uiE2eRealGateway.steps.indexOf(desktopUpload)).toBeGreaterThan(realGatewayIndex);
expect(realGatewayStep.env).toEqual({
FROZEN_TARGET: "${{ needs.preflight.outputs.frozen_target }}",
OPENCLAW_CAPTURE_UI_PROOF:
"${{ github.event_name == 'workflow_dispatch' && inputs.capture_ui_proof && '1' || '0' }}",
OPENCLAW_UI_E2E_ARTIFACT_DIR: proofUpload.with.path,
OPENCLAW_UI_E2E_DIAGNOSTIC_DIR:
".artifacts/control-ui-e2e-timeouts/real-gateway-attempt-${{ github.run_attempt }}",
});
expect(proofUploadIndex).toBeGreaterThan(realGatewayIndex);
});
it.each([
{ failed: false, captured: false },
{ failed: false, captured: true },
{ failed: true, captured: false },
{ failed: true, captured: true },
])("uploads only captured synthetic widget failures: %j", ({ failed, captured }) => {
const artifactRoot = ".artifacts/control-ui-e2e/control-ui-authenticated-widget-sandbox-*";
const timeline = `${artifactRoot}/widget-prompt-failure.json`;
for (const job of [
readCiWorkflow().jobs["checks-ui-e2e"],
readWorkflow(".github/workflows/openclaw-repo-e2e-reusable.yml").jobs.test,
]) {
const upload = expectDefined(
job.steps.find(
(step: WorkflowStep) => step.name === "Upload synthetic widget prompt failure evidence",
),
"synthetic widget upload",
);
expect(
evaluateWorkflowExpression(`\${{ ${upload.if} }}`, {
eventName: "workflow_dispatch",
repository: "openclaw/openclaw",
runAttempt: 1,
failed,
fileHashes: captured ? { [timeline]: "present" } : {},
}),
).toBe(failed && captured);
expect(upload.uses).toBe(UPLOAD_ARTIFACT_V7);
expect(upload.with.path.trim().split("\n")).toEqual([
timeline,
`${artifactRoot}/*.png`,
`${artifactRoot}/*.webm`,
]);
expect(upload.with["retention-days"]).toBe(7);
expect(upload.with["if-no-files-found"]).toBe("error");
}
});
it.each([
{ frozen: false, prebuilt: true, childExit: 0 },
{ frozen: true, prebuilt: true, childExit: 0 },
{ frozen: true, prebuilt: false, childExit: 0 },
{ frozen: false, prebuilt: false, childExit: 0 },
{ frozen: true, prebuilt: true, childExit: 42 },
])(
"selects the complete real-Gateway command without retrying failures (frozen: $frozen, prebuilt: $prebuilt, exit: $childExit)",
({ frozen, prebuilt, childExit }) => {
const step = expectDefined(
readCiWorkflow().jobs["checks-ui-e2e-real-gateway"].steps.find(
(candidate: WorkflowStep) =>
candidate.name === "Test Control UI suites with a real Gateway",
),
"real-Gateway command",
);
const directory = tempDirs.make("openclaw-real-gateway-command-");
const bin = path.join(directory, "bin");
const argsPath = path.join(directory, "args");
const callsPath = path.join(directory, "calls");
const prebuiltConfig = "test/vitest/vitest.ui-e2e-prebuilt.config.ts";
const serialConfig = "test/vitest/vitest.ui-e2e.config.ts";
mkdirSync(bin);
mkdirSync(path.join(directory, "test/vitest"), { recursive: true });
writeFileSync(path.join(directory, serialConfig), "export default {};\n");
if (prebuilt) {
writeFileSync(path.join(directory, prebuiltConfig), "export default {};\n");
}
writeFileSync(
path.join(bin, "node"),
'#!/bin/sh\nprintf "%s\\n" "$@" > "$REAL_GATEWAY_COMMAND_ARGS"\nprintf "called\\n" >> "$REAL_GATEWAY_COMMAND_CALLS"\nexit "$REAL_GATEWAY_COMMAND_EXIT"\n',
{ mode: 0o755 },
);
const result = runWorkflowShellScript(expectDefined(step.run, "real-Gateway script"), {
linuxWorkflow: true,
cwd: directory,
env: {
...process.env,
FROZEN_TARGET: String(frozen),
REAL_GATEWAY_COMMAND_ARGS: argsPath,
REAL_GATEWAY_COMMAND_CALLS: callsPath,
REAL_GATEWAY_COMMAND_EXIT: String(childExit),
PATH: `${bin}${path.delimiter}${process.env.PATH ?? ""}`,
},
});
const missingCurrentConfig = !prebuilt && !frozen;
expect(result.status, result.stdout + result.stderr).toBe(
missingCurrentConfig ? 1 : childExit,
);
if (missingCurrentConfig) {
expect(result.stderr).toContain(`Current target is missing ${prebuiltConfig}`);
expect(existsSync(callsPath)).toBe(false);
return;
}
expect(readFileSync(callsPath, "utf8").trim().split("\n")).toEqual(["called"]);
const args = readFileSync(argsPath, "utf8").trim().split("\n");
expect(args.slice(0, 6)).toEqual([
"scripts/run-vitest.mjs",
"run",
"--config",
prebuilt ? prebuiltConfig : serialConfig,
"--configLoader",
"runner",
]);
const reporterArgs = frozen
? []
: [
"--reporter",
"verbose",
"--reporter",
"github-actions",
"--reporter",
"default",
"--reporter",
"./scripts/lib/vitest-resource-reporter.mts",
];
expect(args.slice(6, 6 + reporterArgs.length)).toEqual(reporterArgs);
expect(args.slice(6 + reporterArgs.length).toSorted()).toEqual(
uiE2eRealGatewayTestFiles
.filter((file) => file !== "ui/src/e2e/desktop-resize.real-gateway.e2e.test.ts")
.toSorted(),
);
expect(
resolveRunVitestSpawnEnv(
{ CI: "true", OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS: "120000" },
args.slice(1),
).OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS,
).toBe("300000");
},
);
it.each([
{ frozen: false, available: true, exit: 0 },
{ frozen: true, available: true, exit: 0 },
{ frozen: false, available: true, exit: 42 },
{ frozen: true, available: false, exit: 0 },
{ frozen: false, available: false, exit: 0 },
])(
"runs available desktop proof and preserves frozen omissions: %j",
({ frozen, available, exit }) => {
const step = expectDefined(
readCiWorkflow().jobs["checks-ui-e2e-real-gateway"].steps.find(
(candidate: WorkflowStep) => candidate.name === "Prove desktop resize over node and SSH",
),
"desktop proof command",
);
const directory = tempDirs.make("desktop-proof-command-");
const bin = path.join(directory, "bin");
const calls = path.join(directory, "calls");
mkdirSync(bin);
mkdirSync(path.join(directory, "scripts"));
if (available) {
writeFileSync(path.join(directory, "scripts/test-desktop-resize-real.mts"), "");
}
writeFileSync(
path.join(bin, "node"),
'#!/bin/sh\nprintf "%s\\n" "$@" > "$DESKTOP_CALLS"\nexit "$DESKTOP_EXIT"\n',
{ mode: 0o755 },
);
const result = runWorkflowShellScript(expectDefined(step.run, "desktop proof script"), {
cwd: directory,
env: {
...process.env,
FROZEN_TARGET: String(frozen),
DESKTOP_CALLS: calls,
DESKTOP_EXIT: String(exit),
PATH: `${bin}${path.delimiter}${process.env.PATH ?? ""}`,
},
});
expect(result.status, result.stdout + result.stderr).toBe(available ? exit : frozen ? 0 : 1);
if (available) {
expect(readFileSync(calls, "utf8").trim().split("\n")).toEqual([
"--import",
"tsx",
"scripts/test-desktop-resize-real.mts",
]);
} else {
expect(existsSync(calls)).toBe(false);
expect(frozen ? result.stdout : result.stderr).toContain(
frozen ? "no desktop resize proof produced" : "Current target is missing",
);
}
},
);
it("builds artifacts once and smoke-tests the built CLI with Node and Bun", () => {
const workflow = readCiWorkflow();
const buildArtifactSteps = workflow.jobs["build-artifacts"].steps;
const setupStep = buildArtifactSteps.find(
(step: WorkflowStep) => step.name === "Setup Node environment",
);
const buildDistStep = buildArtifactSteps.find(
(step: WorkflowStep) => step.name === "Build dist",
);
const nodeHelpSmoke = buildArtifactSteps.find(
(step: WorkflowStep) => step.name === "Smoke test CLI launcher help",
);
const nodeStatusSmoke = buildArtifactSteps.find(
(step: WorkflowStep) => step.name === "Smoke test CLI launcher status json",
);
const bunSmoke = buildArtifactSteps.find(
(step: WorkflowStep) => step.name === "Smoke test built CLI with Bun",
);
expect(
buildArtifactSteps.some(
(step: WorkflowStep) =>
typeof step.uses === "string" && step.uses.endsWith("/ensure-base-commit"),
),
).toBe(false);
expect(setupStep.with["install-bun"]).toBe("true");
expect(buildDistStep.run).toBe("pnpm build:ci-artifacts");
expect(buildArtifactSteps.map((step: WorkflowStep) => step.name)).not.toContain(
"Build Control UI",
);
expect(buildArtifactSteps.some((step: WorkflowStep) => step.run === "pnpm ui:build")).toBe(
false,
);
expect(nodeHelpSmoke.run).toBe("node openclaw.mjs --help");
expect(nodeStatusSmoke.run).toBe("node openclaw.mjs status --json --timeout 1");
expect(bunSmoke.run).toContain("bun openclaw.mjs --help");
expect(bunSmoke.run).toContain("bun openclaw.mjs status --json --timeout 1");
});
it("keeps automatic source-only Control UI locale drift advisory and manual CI strict", () => {
const workflow = readCiWorkflow();
const workflowSource = readFileSync(".github/workflows/ci.yml", "utf8");
const buildArtifactSteps = workflow.jobs["build-artifacts"].steps;
const localeJob = workflow.jobs["control-ui-i18n"];
const sourceStep = localeJob.steps.find(
(step: WorkflowStep) => step.name === "Verify Control UI i18n source",
);
const localeStep = localeJob.steps.find(
(step: WorkflowStep) => step.name === "Check Control UI locale parity",
);
expect(buildArtifactSteps).not.toContainEqual(
expect.objectContaining({ run: "pnpm ui:i18n:check" }),
);
expect(JSON.parse(readFileSync("package.json", "utf8")).scripts["test:ui"]).not.toContain(
"ui:i18n:check",
);
expect(workflowSource.match(/pnpm ui:i18n:verify/gu)).toHaveLength(1);
expect(workflowSource.match(/pnpm ui:i18n:check/gu)).toHaveLength(1);
expect(readFileSync("ui/src/i18n/test/translate.test.ts", "utf8")).not.toContain(
"keeps shipped locales structurally aligned with English",
);
expect(localeJob.needs).toEqual(["preflight"]);
expect(localeJob.if).toBe("needs.preflight.outputs.run_control_ui_i18n == 'true'");
expect(localeJob["continue-on-error"]).toBeUndefined();
expect(localeJob.env.COMPATIBILITY_TARGET).toBe(
"${{ needs.preflight.outputs.compatibility_target }}",
);
expect(workflow.jobs.preflight.outputs.strict_control_ui_i18n).toBe(
"${{ github.event_name == 'workflow_dispatch' && !inputs.release_gate && 'true' || steps.changed_scope.outputs.strict_control_ui_i18n }}",
);
expect(
evaluateWorkflowExpression(
"${{ github.event_name == 'workflow_dispatch' && !inputs.release_gate && 'true' || 'false' }}",
{
eventName: "workflow_dispatch",
releaseGate: false,
repository: "openclaw/openclaw",
runAttempt: 1,
},
),
).toBe("true");
expect(
evaluateWorkflowExpression(
"${{ github.event_name == 'workflow_dispatch' && !inputs.release_gate && 'true' || 'false' }}",
{
eventName: "workflow_dispatch",
releaseGate: true,
repository: "openclaw/openclaw",
runAttempt: 1,
},
),
).toBe("false");
expect(sourceStep["continue-on-error"]).toBeUndefined();
const compatibilityWithoutVerify = runControlUiI18nSourceFixture({
compatibilityTarget: true,
hasVerifyScript: false,
});
expect(compatibilityWithoutVerify.status, compatibilityWithoutVerify.output).toBe(0);
expect(compatibilityWithoutVerify.calls).toEqual([]);
expect(compatibilityWithoutVerify.summary).toContain(
"Skipping ui:i18n:verify: unavailable on the selected compatibility target.",
);
const currentWithoutVerify = runControlUiI18nSourceFixture({
compatibilityTarget: false,
hasVerifyScript: false,
});
expect(currentWithoutVerify.status).toBe(1);
expect(currentWithoutVerify.calls).toEqual([]);
expect(currentWithoutVerify.output).toContain(
"ui:i18n:verify is required for non-compatibility targets.",
);
const currentWithVerify = runControlUiI18nSourceFixture({
compatibilityTarget: false,
hasVerifyScript: true,
});
expect(currentWithVerify.status, currentWithVerify.output).toBe(0);
expect(currentWithVerify.calls).toEqual(["ui:i18n:verify"]);
expect(localeStep["continue-on-error"]).toBe(
"${{ needs.preflight.outputs.strict_control_ui_i18n != 'true' }}",
);
expect(localeStep.run).toBe("pnpm ui:i18n:check");
expect(readFileSync(".github/workflows/full-release-validation.yml", "utf8")).toContain(
'dispatch_child ci.yml "$dispatch_run_name"',
);
});
it("splits native source verification from generated locale parity", () => {
const workflow = readCiWorkflow();
const manifestStep = workflow.jobs.preflight.steps.find(
(step: WorkflowStep) => step.name === "Build CI manifest",
);
const localeJob = workflow.jobs["native-i18n"];
const sourceStep = localeJob.steps.find(
(step: WorkflowStep) => step.name === "Verify native app i18n source",
);
const parityStep = localeJob.steps.find(
(step: WorkflowStep) => step.name === "Check native app generated locale parity",
);
const packageScripts = JSON.parse(readFileSync("package.json", "utf8")).scripts;
const fullReleaseSource = readFileSync(".github/workflows/full-release-validation.yml", "utf8");
const fullReleaseCiCase = expectDefined(
fullReleaseSource.match(/case "\$CHILD_WORKFLOW_KIND" in\n\s+ci\)([\s\S]*?)\n\s+;;/u)?.[1],
"Full Release CI dispatch case",
);
expect(packageScripts["native:i18n:baseline"]).toContain("baseline --write");
expect(packageScripts["native:i18n:verify"]).toContain(" verify");
expect(workflow.jobs.preflight.outputs.strict_native_i18n).toBe(
"${{ github.event_name == 'workflow_dispatch' && !inputs.release_gate && 'true' || steps.changed_scope.outputs.strict_native_i18n }}",
);
expect(manifestStep.env.OPENCLAW_CI_RUN_NATIVE_I18N).toBe(
"${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_native_i18n || 'false' }}",
);
expect(sourceStep.run).toContain("pnpm native:i18n:verify");
expect(sourceStep.run).toContain("Historical release targets");
expect(parityStep.if).toBe("${{ needs.preflight.outputs.strict_native_i18n == 'true' }}");
expect(parityStep.run).toContain("pnpm native:i18n:check");
expect(parityStep.run).not.toContain("pnpm android:i18n:check");
expect(parityStep.run).not.toContain("pnpm apple:i18n:check");
expect(fullReleaseCiCase).toContain(
'args=(-f target_ref="$TARGET_SHA" -f release_scope="$ci_release_scope" -f include_android="$include_android" -f dispatch_id="$dispatch_id")',
);
expect(fullReleaseCiCase).toContain('dispatch_child ci.yml "$dispatch_run_name"');
expect(fullReleaseCiCase).not.toContain("release_gate");
});
it("measures startup memory before the built artifact-check wave", () => {
const workflow = readCiWorkflow();
const steps = workflow.jobs["build-artifacts"].steps;
const verifierStep = steps.find(
(step: WorkflowStep) => step.name === "Run built artifact checks",
);
// The verifiers always run, so the shared step cannot be gated on the
// selected checks; each check keeps its own RUN_* gate inside the body.
expect(verifierStep.if).toBeUndefined();
expect(steps.some((step: WorkflowStep) => step.name === "Verify built runtime artifacts")).toBe(
false,
);
// RSS measures an unloaded command on every runner, including Blacksmith.
const startupMemory = verifierStep.run.indexOf('run_verifier "startup-memory"');
const memoryBarrier = verifierStep.run.indexOf("\nwait_checks\n", startupMemory);
expect(memoryBarrier).toBeGreaterThan(startupMemory);
expect(memoryBarrier).toBeLessThan(
verifierStep.run.indexOf('run_verifier "doctor-plugin-index"'),
);
expect(verifierStep.env.OPENCLAW_STARTUP_MEMORY_PLUGINS_LIST_MB).toBe(
"${{ runner.environment == 'github-hosted' && '425' || '400' }}",
);
expect(verifierStep.env.PARALLEL_BUILT_VERIFIERS).toBe(
"${{ runner.environment != 'github-hosted' && 'true' || 'false' }}",
);
expect(verifierStep.run).toContain(
'OPENCLAW_VITEST_FS_MODULE_CACHE_PATH="${RUNNER_TEMP}/vitest-module-cache/${name}"',
);
expect(verifierStep.run).toContain(
"test/scripts/doctor-config-preflight-plugin-index.built-cli.e2e.test.ts",
);
expect(verifierStep.run).toContain(
"env OPENCLAW_E2E_USE_PREBUILT_DIST=1 OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS=660000 node scripts/run-vitest.mjs run",
);
expect(verifierStep.run).toContain("--config test/vitest/vitest.e2e.config.ts");
expect(verifierStep.run).toContain("Selected target predates");
expect(verifierStep.run).toContain("pnpm test:build:singleton");
// The startup asset rebuild must complete before any verifier forks so
// concurrent readers never observe dist mid-write.
expect(verifierStep.run).toContain("scripts/ensure-cli-startup-build.mts");
expect(verifierStep.run).toContain("scripts/check-cli-startup-memory.mjs");
expect(verifierStep.run).toContain(".artifacts/startup-memory/summary.md");
expect(verifierStep.env.RUN_CHANNELS).toBe("${{ needs.preflight.outputs.run_checks }}");
expect(verifierStep.env.FROZEN_TARGET).toBe("${{ needs.preflight.outputs.frozen_target }}");
const pluginSingleton = verifierStep.run.indexOf(
'run_verifier "plugin-singleton" pnpm test:build:singleton',
);
const pluginWriterBarrier = verifierStep.run.indexOf("\nwait_checks\n", pluginSingleton);
const parallelGatewayWatch = verifierStep.run.indexOf(
'if [ "$RUN_GATEWAY_WATCH" = "true" ] && [ "$PARALLEL_GATEWAY_WATCH" = "true" ]; then',
);
const gatewayWriterBarrier = verifierStep.run.indexOf(
"\n wait_checks\n",
parallelGatewayWatch,
);
const firstReader = verifierStep.run.indexOf(
'run_verifier "doctor-plugin-index" run_doctor_plugin_index',
);
const parallelDiscord = verifierStep.run.indexOf(
'if [ "$RUN_CHANNELS" = "true" ] && [ "$PARALLEL_BUILT_VERIFIERS" = "true" ]; then',
);
const readerWaveBarrier = verifierStep.run.indexOf("\nwait_checks\n", parallelDiscord);
const hostedDiscord = verifierStep.run.indexOf(
'if [ "$RUN_CHANNELS" = "true" ] && [ "$PARALLEL_BUILT_VERIFIERS" != "true" ]; then',
);
expect(pluginWriterBarrier).toBeGreaterThan(pluginSingleton);
expect(parallelGatewayWatch).toBeGreaterThan(pluginWriterBarrier);
expect(gatewayWriterBarrier).toBeGreaterThan(parallelGatewayWatch);
expect(firstReader).toBeGreaterThan(gatewayWriterBarrier);
expect(parallelDiscord).toBeGreaterThan(firstReader);
expect(readerWaveBarrier).toBeGreaterThan(parallelDiscord);
expect(hostedDiscord).toBeGreaterThan(readerWaveBarrier);
expect(verifierStep.run.slice(parallelDiscord, readerWaveBarrier)).toContain(
'start_check "discord-component-attachments" run_discord_component_attachments',
);
expect(verifierStep.run.slice(hostedDiscord)).toContain(
'start_check "discord-component-attachments" run_discord_component_attachments',
);
expect(verifierStep.run).toContain('["discord-component-attachments"]="skipped"');
expect(verifierStep.run).toContain("OPENCLAW_E2E_USE_PREBUILT_DIST=1 OPENCLAW_E2E_WORKERS=1");
expect(verifierStep.run).toContain("OPENCLAW_E2E_VERBOSE=1 OPENCLAW_VITEST_MAX_WORKERS=1");
const upload = steps.find(
(entry: WorkflowStep) => entry.name === "Upload Discord component attachment proof",
);
expect(upload.if).toBe("always() && needs.preflight.outputs.run_checks == 'true'");
expect(upload.with.path).toContain("${{ runner.temp }}/discord-component-attachments.json");
expect(upload.with.path).toContain("${{ runner.temp }}/discord-component-attachments.log");
// Every verifier reports through the shared results map so a failure can
// never be swallowed by the wave.
for (const name of [
"doctor-plugin-index",
"plugin-singleton",
"sqlite-session-lifecycle",
"startup-memory",
]) {
expect(verifierStep.run).toContain(`run_verifier "${name}"`);
expect(verifierStep.run).toContain(`["${name}"]="skipped"`);
}
expect(verifierStep.run).toContain(
"for name in channels core-support-boundary discord-component-attachments doctor-plugin-index gateway-watch plugin-singleton sqlite-session-lifecycle startup-memory tui-pty; do",
);
});
it.each([
{ label: "one passing named case", state: "passed", frozen: false, expected: 0 },
{ label: "a passing frozen case", state: "passed", frozen: true, expected: 0 },
{ label: "a failed named case", state: "failed", frozen: false, expected: 1 },
{ label: "a skipped current case", state: "skipped", frozen: false, expected: 1 },
{ label: "a skipped frozen case", state: "skipped", frozen: true, expected: 1 },
{ label: "a missing current case", state: "absent", frozen: false, expected: 1 },
{ label: "an unavailable historical case", state: "absent", frozen: true, expected: 0 },
{ label: "a failed suite", state: "suite-failed", frozen: false, expected: 1 },
{ label: "malformed JSON", state: "malformed", frozen: true, expected: 1 },
])("validates Discord built proof with $label", ({ state, frozen, expected }) => {
const steps = readCiWorkflow().jobs["build-artifacts"].steps;
const step = steps.find((entry: WorkflowStep) => entry.name === "Run built artifact checks");
const validator = expectDefined(
step.run.match(
/node --input-type=module <<'DISCORD_PROOF_REPORT'\n([\s\S]*?)\nDISCORD_PROOF_REPORT/u,
)?.[1],
"Discord proof report validator",
);
const scratch = tempDirs.make("openclaw-discord-proof-report-");
const fullName =
"Discord show_widget contextual presenter process proof preserves component attachment filenames through the public Gateway message action";
const report = {
success: true,
numFailedTestSuites: state === "suite-failed" ? 1 : 0,
numFailedTests: state === "failed" ? 1 : 0,
numPassedTests: state === "passed" ? 1 : 0,
testResults: [
{
name: path.resolve(
"test/e2e/qa-lab/plugins/discord-show-widget-contextual-presenter.e2e.test.ts",
),
status: "passed",
assertionResults: state === "absent" ? [] : [{ fullName, status: state }],
},
],
};
writeFileSync(
path.join(scratch, "discord-component-attachments.json"),
state === "malformed" ? "{" : JSON.stringify(report),
);
const result = spawnSync(process.execPath, ["--input-type=module", "-e", validator], {
encoding: "utf8",
env: { ...process.env, RUNNER_TEMP: scratch, FROZEN_TARGET: String(frozen) },
});
expect(result.status, result.stderr).toBe(expected);
if (state === "absent" && frozen) {
expect(result.stdout).toContain("[skip] Frozen target predates the named Discord");
}
});
it.each([
{ frozen: false, present: true, expected: true },
{ frozen: false, present: false, expected: true },
{ frozen: true, present: true, expected: true },
{ frozen: true, present: false, expected: false },
])(
"gates browser native-host proof (frozen=$frozen, present=$present)",
({ frozen, present, expected }) => {
const step = readCiWorkflow().jobs["build-artifacts"].steps.find(
(entry: WorkflowStep) => entry.name === "Verify built browser native host",
);
const file = "extensions/browser/src/browser/extension-install.native-host.e2e.test.ts";
expect(
step.if === undefined ||
evaluateWorkflowExpression(step.if, {
eventName: "workflow_dispatch",
repository: "openclaw/openclaw",
runAttempt: 1,
frozenTarget: frozen,
fileHashes: present ? { [file]: "fixture-hash" } : {},
}),
).toBe(expected);
},
);
it.each([
"passed",
"skipped",
"pending",
"todo",
"absent",
"wrong-name",
"wrong-file",
"failed",
"suite-failed",
"duplicate",
"malformed",
"missing-report",
])("validates browser native-host proof report: %s", (state) => {
const steps = readCiWorkflow().jobs["build-artifacts"].steps;
const step = steps.find(
(entry: WorkflowStep) => entry.name === "Verify built browser native host",
);
expect(steps.indexOf(step)).toBeGreaterThan(
steps.findIndex((entry: WorkflowStep) => entry.name === "Build dist"),
);
expect(step["continue-on-error"]).not.toBe(true);
const root = tempDirs.make("openclaw-browser-proof-report-");
const file = "extensions/browser/src/browser/extension-install.native-host.e2e.test.ts";
const fullName =
"native host registration launches with the exact custom installation context when Chrome has no selectors";
const assertion = {
fullName: state === "wrong-name" ? "another test" : fullName,
status: ["skipped", "pending", "todo", "failed"].includes(state) ? state : "passed",
};
const assertions =
state === "absent" ? [] : state === "duplicate" ? [assertion, assertion] : [assertion];
const report = {
success: state !== "failed" && state !== "suite-failed",
numFailedTestSuites: state === "suite-failed" ? 1 : 0,
numPendingTestSuites: 0,
numTotalTests: assertions.length,
numPassedTests: assertions.filter((entry) => entry.status === "passed").length,
numFailedTests: state === "failed" ? 1 : 0,
numPendingTests: ["skipped", "pending"].includes(state) ? 1 : 0,
numTodoTests: state === "todo" ? 1 : 0,
testResults: [
{
name: path.join(root, state === "wrong-file" ? "other.test.ts" : file),
status: state === "suite-failed" ? "failed" : "passed",
assertionResults: assertions,
},
],
};
mkdirSync(path.join(root, "scripts"));
// A previous successful report must not satisfy a run that emits no report.
writeFileSync(path.join(root, "browser-native-host.json"), JSON.stringify(report));
// Execute the workflow's shell and validator; replace only the expensive
// Vitest process with a controlled reporter at its external boundary.
writeFileSync(
path.join(root, "scripts/run-vitest.mjs"),
`
import fs from 'node:fs';
const args = process.argv.slice(2);
fs.writeFileSync('invocation.json', JSON.stringify({ args, prebuilt: process.env.OPENCLAW_E2E_USE_PREBUILT_DIST }));
const outputIndex = args.indexOf('--outputFile.json');
if (outputIndex >= 0 && ${JSON.stringify(state)} !== 'missing-report') {
fs.writeFileSync(args[outputIndex + 1], ${JSON.stringify(state === "malformed" ? "{" : JSON.stringify(report))});
}
`,
);
const result = runWorkflowShellScript(step.run, {
cwd: root,
env: { ...process.env, ...step.env, RUNNER_TEMP: root },
});
expect(result.status, result.stderr).toBe(state === "passed" ? 0 : 1);
if (state === "passed") {
expect(JSON.parse(readFileSync(path.join(root, "invocation.json"), "utf8"))).toEqual({
prebuilt: "1",
args: [
"run",
"--config",
"test/vitest/vitest.e2e.config.ts",
file,
"--reporter=default",
"--reporter=json",
"--outputFile.json",
path.join(root, "browser-native-host.json"),
],
});
}
});
it.each([
{ selected: false, exitCode: 0 },
{ selected: true, exitCode: 0 },
{ selected: true, exitCode: 1 },
{ selected: true, exitCode: 143 },
])(
"runs the built SQLite verifier (selected=$selected, exit=$exitCode)",
({ selected, exitCode }) => {
const workflow = readCiWorkflow();
const additionalJob = workflow.jobs["check-additional-shard"];
const additionalRunStep = additionalJob.steps.find(
(step: WorkflowStep) => step.name === "Run additional check shard",
);
const verifier = workflow.jobs["build-artifacts"].steps.find(
(step: WorkflowStep) => step.name === "Run built artifact checks",
);
const selection = expectDefined(
verifier.run.match(
/if \[ "\$RUN_SQLITE_SESSION_LIFECYCLE" = "true" \]; then\n[\s\S]*?\nfi/u,
)?.[0],
"scoped SQLite verifier invocation",
);
expect(readFrozenAdditionalCheckRows()).not.toContainEqual(
expect.objectContaining({ group: "sqlite-session-flip-proof" }),
);
expect(additionalRunStep.run).not.toContain("sqlite-session-flip-proof)");
expect(workflow.jobs["sqlite-session-lifecycle"]).toBeUndefined();
expect(verifier.env.RUN_SQLITE_SESSION_LIFECYCLE).toBe(
"${{ needs.preflight.outputs.run_sqlite_session_lifecycle }}",
);
expect(workflow.jobs["ci-gate"].needs).toContain("build-artifacts");
expect(workflow.jobs["ci-gate"].needs).not.toContain("sqlite-session-lifecycle");
const memoryBarrier = verifier.run.indexOf(
"\nwait_checks\n",
verifier.run.indexOf('run_verifier "startup-memory"'),
);
expect(verifier.run.indexOf(selection)).toBeGreaterThan(memoryBarrier);
expect(verifier.run.indexOf(selection)).toBeLessThan(
verifier.run.indexOf('start_check "channels"'),
);
const root = tempDirs.make("openclaw-sqlite-verifier-");
mkdirSync(path.join(root, "scripts"));
writeFileSync(
path.join(root, "scripts/run-vitest.mjs"),
`
import { writeFileSync } from "node:fs";
writeFileSync("invocation.json", JSON.stringify({
args: process.argv.slice(2),
prebuilt: process.env.OPENCLAW_E2E_USE_PREBUILT_DIST,
watchdog: process.env.OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS,
}));
process.exit(${exitCode});
`,
);
// Exercise the selected command at the existing verifier boundary. The full
// wave's associative-array scheduler needs native CI's Bash for execution.
const result = runWorkflowShellScript(
`
run_verifier() {
printf '%s\\n' "$1" > verifier-name
shift
"$@"
}
${selection}
`,
{ cwd: root, env: { ...process.env, RUN_SQLITE_SESSION_LIFECYCLE: String(selected) } },
);
expect(result.status, result.stderr).toBe(selected ? exitCode : 0);
expect(existsSync(path.join(root, "invocation.json"))).toBe(selected);
if (selected) {
expect(readFileSync(path.join(root, "verifier-name"), "utf8").trim()).toBe(
"sqlite-session-lifecycle",
);
expect(JSON.parse(readFileSync(path.join(root, "invocation.json"), "utf8"))).toEqual({
args: [
"run",
"--config",
"test/vitest/vitest.e2e.config.ts",
"test/scripts/sqlite-sessions-transcripts-flip-proof.built-cli.e2e.test.ts",
],
prebuilt: "1",
watchdog: "660000",
});
}
},
);
it("restores dist in PR CI and saves it only from the trusted warmer", () => {
const workflow = readCiWorkflow();
const buildArtifactSteps = workflow.jobs["build-artifacts"].steps;
const stepNames = buildArtifactSteps.map((step: WorkflowStep) => step.name);
const restoreStep = buildArtifactSteps.find(
(step: WorkflowStep) => step.name === "Restore dist build cache",
);
const buildDistStep = buildArtifactSteps.find(
(step: WorkflowStep) => step.name === "Build dist",
);
const warmer = parse(readFileSync(".github/workflows/vitest-cache-warm.yml", "utf8"));
const warmerSteps = warmer.jobs.warm.steps as WorkflowStep[];
const saveStep = expectDefined(
warmerSteps.find((step) => step.name === "Save dist build cache"),
"trusted dist cache save",
);
expect(stepNames.indexOf("Restore dist build cache")).toBeLessThan(
stepNames.indexOf("Build dist"),
);
expect(stepNames.indexOf("Build dist")).toBeLessThan(
stepNames.indexOf("Pack built runtime artifacts"),
);
expect(stepNames).not.toContain("Save dist build cache");
expect(restoreStep.uses).toBe(CACHE_V5);
expect(buildDistStep.if).toBe("steps.dist_build_cache.outputs.cache-hit != 'true'");
expect(saveStep.uses).toBe("actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9");
expect(saveStep.if).toContain("steps.setup-node-env.outputs.cache-mode == 'read-write'");
expect(saveStep.with?.key).toBe("${{ runner.os }}-dist-build-v3-${{ github.sha }}");
expect(restoreStep.with.path).toContain("dist/");
expect(restoreStep.with.path).toContain("dist-runtime/");
expect(restoreStep.with.path).toContain("packages/*/dist/");
expect(saveStep.with?.path).toContain("packages/*/dist/");
expect(restoreStep.with.key).toContain("dist-build-v3-");
expect(
buildArtifactSteps.find((step: WorkflowStep) => step.name === "Pack built runtime artifacts")
.run,
).toContain("packages/*/dist");
expect(restoreStep.with.path).toContain("extensions/*/src/host/**/.bundle.hash");
expect(restoreStep.with.path).toContain("extensions/*/src/host/**/*.bundle.js");
expect(warmerSteps.indexOf(saveStep)).toBeGreaterThan(
warmerSteps.findIndex((step) => step.name === "Warm build cache"),
);
expect(buildArtifactSteps.map((step: WorkflowStep) => step.name)).not.toContain(
"Cache dist build",
);
});
it("keeps the AI runtime in Testbox build artifact caches", () => {
const workflow = readBuildArtifactsTestboxWorkflow();
const steps = workflow.jobs["build-artifacts"].steps;
const resolveSeedsStep = steps.find(
(step: WorkflowStep) => step.name === "Resolve release dist cache seeds",
);
const setupStep = expectDefined(
steps.find((step: WorkflowStep) => step.name === "Setup Node environment"),
"Testbox Node setup",
);
const restoreStep = steps.find(
(step: WorkflowStep) => step.name === "Restore dist build cache",
);
const verifyStep = steps.find((step: WorkflowStep) => step.name === "Verify build artifacts");
const saveStep = steps.find((step: WorkflowStep) => step.name === "Save dist build cache");
expect(resolveSeedsStep.run).toContain('cache_prefix="${RUNNER_OS}-dist-build-v2-"');
expect(restoreStep.with.path).toContain("packages/*/dist/");
expect(restoreStep.with.key).toContain("dist-build-v2-");
expect(verifyStep.run).toContain("test -f packages/ai/dist/internal/runtime.mjs");
expect(saveStep.with.path).toContain("packages/*/dist/");
expect(saveStep.with.key).toContain("dist-build-v2-");
expect(setupStep.with["cache-mode"]).toContain("'read-write'");
expect(saveStep.if).toContain("steps.setup-node-env.outputs.cache-mode == 'read-write'");
});
it("keeps the full built TUI PTY suite out of the artifact canary gate", () => {
const workflow = readCiWorkflow();
const buildArtifactSteps = workflow.jobs["build-artifacts"].steps;
const builtArtifactChecks = buildArtifactSteps.find(
(step: WorkflowStep) => step.name === "Run built artifact checks",
);
const run = builtArtifactChecks.run;
expect(builtArtifactChecks.env.PARALLEL_GATEWAY_WATCH).toBe(
"${{ runner.environment != 'github-hosted' && 'true' || 'false' }}",
);
expect(run).toContain('start_check "channels"');
expect(run).toContain('start_check "core-support-boundary"');
expect(run).toContain('start_check "gateway-watch"');
expect(run).toContain(
'if [ "$RUN_GATEWAY_WATCH" = "true" ] && [ "$PARALLEL_GATEWAY_WATCH" = "true" ]; then',
);
expect(run).toContain(
'if [ "$RUN_GATEWAY_WATCH" = "true" ] && [ "$PARALLEL_GATEWAY_WATCH" != "true" ]; then',
);
const firstWait = run.indexOf(
"\nwait_checks\n",
run.indexOf('start_check "core-support-boundary"'),
);
const hostedGatewayWatch = run.indexOf(
'if [ "$RUN_GATEWAY_WATCH" = "true" ] && [ "$PARALLEL_GATEWAY_WATCH" != "true" ]; then',
);
const tuiPty = run.indexOf('if [ "$RUN_TUI_PTY" = "true" ]; then');
const hostedGatewayWait = run.indexOf("\n wait_checks\n", hostedGatewayWatch);
const parallelDiscord = run.indexOf(
'if [ "$RUN_CHANNELS" = "true" ] && [ "$PARALLEL_BUILT_VERIFIERS" = "true" ]; then',
);
const hostedDiscord = run.indexOf(
'if [ "$RUN_CHANNELS" = "true" ] && [ "$PARALLEL_BUILT_VERIFIERS" != "true" ]; then',
);
const hostedDiscordWait = run.indexOf("\n wait_checks\n", hostedDiscord);
const tuiPtyWait = run.indexOf("\n wait_checks\n", tuiPty);
expect(firstWait).toBeGreaterThan(run.indexOf('start_check "core-support-boundary"'));
expect(hostedGatewayWatch).toBeGreaterThan(firstWait);
expect(hostedGatewayWait).toBeGreaterThan(hostedGatewayWatch);
expect(parallelDiscord).toBeLessThan(firstWait);
expect(hostedDiscord).toBeGreaterThan(hostedGatewayWait);
expect(hostedDiscordWait).toBeGreaterThan(hostedDiscord);
expect(tuiPty).toBeGreaterThan(hostedDiscordWait);
expect(tuiPtyWait).toBeGreaterThan(tuiPty);
expect(run.slice(tuiPty, tuiPtyWait)).toContain("src/tui/tui-pty-local.e2e.test.ts");
expect(run.slice(tuiPty, tuiPtyWait)).toContain("--testNamePattern");
expect(run.slice(tuiPty, tuiPtyWait)).toContain(
"launches openclaw (chat as local mode|tui against a real Gateway) through a real PTY",
);
expect(run).toContain("wait_checks()");
// Startup memory, artifact writers, and TUI retain explicit barriers;
// hosted runners also serialize the remaining verifiers inside run_verifier.
expect(run.match(/wait_checks$/gmu)).toHaveLength(8);
});
it("keeps docs i18n CI on the workflow-owned Go toolchain", () => {
const workflow = readCiWorkflow();
const nodeTestJob = workflow.jobs["checks-node-core-test-nondist-shard"];
const setupGoStep = nodeTestJob.steps.find(
(step: WorkflowStep) => step.name === "Setup Go for docs i18n",
);
const verifyGoStep = nodeTestJob.steps.find(
(step: WorkflowStep) => step.name === "Verify docs i18n Go toolchain",
);
const resolveGoCacheStep = nodeTestJob.steps.find(
(step: WorkflowStep) => step.name === "Resolve docs i18n Go cache",
);
const restoreGoCacheStep = nodeTestJob.steps.find(
(step: WorkflowStep) => step.name === "Restore docs i18n Go cache",
);
const saveGoCacheStep = nodeTestJob.steps.find(
(step: WorkflowStep) => step.name === "Save docs i18n Go cache",
);
expect(setupGoStep).toMatchObject({
if: "matrix.requires_go == true",
uses: SETUP_GO_V6,
with: {
cache: false,
"go-version": "1.27.1",
},
});
expect(setupGoStep.with).not.toHaveProperty("go-version-file");
expect(resolveGoCacheStep).toMatchObject({
if: "matrix.requires_go == true && needs.preflight.outputs.cache_mode != 'off'",
env: {
DEPENDENCY_HASH: "${{ hashFiles('scripts/docs-i18n/go.sum') }}",
},
});
expect(resolveGoCacheStep.run).toContain(
"key=setup-go-${RUNNER_OS}-${arch}-${image_prefix}go-${version#go}-${DEPENDENCY_HASH}",
);
expect(restoreGoCacheStep).toMatchObject({
if: "matrix.requires_go == true && needs.preflight.outputs.cache_mode != 'off'",
uses: CACHE_V5,
});
expect(saveGoCacheStep).toMatchObject({
if: expect.stringContaining("needs.preflight.outputs.cache_write_allowed == 'true'"),
uses: CACHE_SAVE_V5,
});
expect(verifyGoStep).toMatchObject({
if: "matrix.requires_go == true",
run: 'test "$(go env GOVERSION)" = "go1.27.1"',
});
const goMod = readTrackedText("scripts/docs-i18n/go.mod");
expect(goMod).toMatch(/^go 1\.26\.0$/mu);
expect(goMod).toMatch(/^toolchain go1\.27\.1$/mu);
const tooling = {
configs: ["test/vitest/vitest.tooling.config.ts"],
shard_name: "core-tooling-1",
};
const goTest = "test/scripts/docs-i18n.test.ts";
const otherTest = "test/scripts/ci-git-owner.test.ts";
const selections = [
{ includePatterns: [goTest] },
{ includePatterns: [otherTest] },
{ includePatterns: ["test/scripts/docs-*.test.ts"] },
{ targets: [goTest] },
{ targets: [otherTest] },
{},
{ groups: [{ ...tooling, includePatterns: [otherTest] }] },
{
groups: [
{ ...tooling, includePatterns: [otherTest] },
{ ...tooling, includePatterns: [goTest] },
],
},
{ groups: [{ ...tooling, configs: ["test/vitest/legacy-tooling.config.ts"] }] },
{ groups: [{ ...tooling, configs: ["test/vitest/vitest.tooling-isolated.config.ts"] }] },
{ groups: [{ ...tooling, configs: ["test/vitest/vitest.tooling-docker.config.ts"] }] },
{
groups: [
{
...tooling,
configs: [
"test/vitest/vitest.tooling-docker.config.ts",
"test/vitest/vitest.tooling-isolated.config.ts",
],
},
],
},
{
groups: [
{
...tooling,
configs: [...tooling.configs, "test/vitest/vitest.tooling-isolated.config.ts"],
},
],
},
{
groups: [
{
...tooling,
configs: [
"test/vitest/vitest.tooling-isolated.config.ts",
"test/vitest/legacy-tooling.config.ts",
],
},
],
},
{ groups: [{ ...tooling, configs: undefined }] },
{
groups: [
{
...tooling,
configs: ["test/vitest/vitest.tooling-isolated.config.ts"],
includePatterns: [goTest],
},
],
},
];
const result = runCiManifestFixture({
bundledPlanner: true,
nodeTestShards: selections.map((selection, index) =>
Object.assign(
{
checkName: `tooling-${index}`,
configs: tooling.configs,
requiresDist: false,
runner: "ubuntu-24.04",
shardName: "groups" in selection ? "compact-small-1" : "core-tooling-1",
},
selection,
),
),
});
expect(result.status, result.output).toBe(0);
const matrix = JSON.parse(
expectDefined(result.outputs.checks_node_core_nondist_matrix, "non-dist Node matrix"),
) as {
include: { requires_go: boolean }[];
};
expect(matrix.include.map((row) => row.requires_go)).toEqual([
true,
false,
true,
true,
false,
true,
false,
true,
true,
false,
false,
false,
true,
true,
true,
true,
]);
});
it("packs grouped Node matrix rows and unpacks them in the shard runner", () => {
const groups = [
{
configs: ["test/vitest/vitest.unit-fast.config.ts"],
env: undefined,
includePatterns: ["src/a.test.ts", "src/b.test.ts"],
requiresDist: false,
runner: "ubuntu-24.04",
shard_name: "core-unit-fast-1",
timing_key: "core-unit-fast-1#include-2-abcd",
},
{
configs: ["test/vitest/vitest.infra.config.ts"],
env: { OPENCLAW_VITEST_MAX_WORKERS: "2" },
requiresDist: false,
runner: "ubuntu-24.04",
shard_name: "core-runtime-infra-misc",
},
];
const projectedGroups = groups.map(
({ configs, env, includePatterns, shard_name, timing_key }) => ({
configs,
env,
includePatterns,
shard_name,
timing_key,
}),
);
const manifest = runCiManifestFixture({
bundledPlanner: true,
nodeTestShards: [
{
checkName: "checks-node-compact-small-1",
groups,
requiresDist: false,
runner: "ubuntu-24.04",
shardName: "compact-small-1",
},
],
});
expect(manifest.status, manifest.output).toBe(0);
const [row] = JSON.parse(
expectDefined(manifest.outputs.checks_node_core_nondist_matrix, "packed Node matrix"),
).include;
expect(row).toMatchObject({
check_name: "checks-node-compact-small-1",
groups_gzip_base64: expect.any(String),
requires_go: false,
});
expect(row).not.toHaveProperty("groups");
const runStep = readCiWorkflow().jobs["checks-node-core-test-nondist-shard"].steps.find(
(step: WorkflowStep) => step.name === "Run Node test shard",
);
const context = {
eventName: "pull_request" as const,
matrix: row,
repository: "openclaw/openclaw",
runAttempt: 1,
};
const packedEnv = evaluateWorkflowExpression(
runStep.env.OPENCLAW_NODE_TEST_GROUPS_GZIP_BASE64,
context,
);
const legacyEnv = evaluateWorkflowExpression(
runStep.env.OPENCLAW_NODE_TEST_GROUPS_JSON,
context,
);
expect(legacyEnv).toBe("");
expect(
resolveShardPlans({ OPENCLAW_NODE_TEST_GROUPS_GZIP_BASE64: String(packedEnv) }).map((plan) =>
plan.kind === "group" ? plan.plan : plan,
),
).toEqual(projectedGroups);
});
it.each(["github", "hybrid", "blacksmith"] as const)(
"keeps the complete %s manifest output below the safety budget",
(runnerProfile) => {
const manifest = runCiManifestFixture({
bundledPlanner: true,
changedPaths: ["src/auto-reply/full-plan.ts"],
eventName: "pull_request",
nodeTestShards: createNodeTestShardBundles({
compactMode: "pull-request",
includeReleaseOnlyPluginShards: false,
runnerBackend: runnerProfile,
}),
runnerProfile,
});
expect(manifest.status, manifest.output).toBe(0);
expect(manifest.outputChars, runnerProfile).toBeLessThan(262_144);
},
);
it("uses projected legacy groups for historical targets without the codec", () => {
const groups = [
{
configs: ["test/vitest/vitest.infra.config.ts"],
env: { OPENCLAW_CI_TEST_GROUP: "legacy" },
includePatterns: ["src/legacy.test.ts"],
requiresDist: false,
runner: "ubuntu-24.04",
shard_name: "core-legacy",
timing_key: "core-legacy#include-1-abcd",
},
];
const ungrouped = runCiManifestFixture({
bundledPlanner: true,
historicalCompatibility: false,
nodeTestGroupsCodec: false,
});
expect(ungrouped.status, ungrouped.output).toBe(0);
expect(ungrouped.outputs.frozen_target).toBe("true");
expect(ungrouped.outputs.compatibility_target).toBe("false");
const rows = JSON.parse(
expectDefined(ungrouped.outputs.checks_node_core_nondist_matrix, "manual target matrix"),
).include;
expect(rows).toEqual([expect.objectContaining({ check_name: "bundled-node-plan" })]);
expect(rows[0]).not.toHaveProperty("groups_gzip_base64");
const grouped = runCiManifestFixture({
bundledPlanner: true,
historicalCompatibility: true,
nodeTestGroupsCodec: false,
nodeTestShards: [
{
checkName: "checks-node-compact-small-1",
groups,
requiresDist: false,
runner: "ubuntu-24.04",
shardName: "compact-small-1",
},
],
});
expect(grouped.status, grouped.output).toBe(0);
const [row] = JSON.parse(
expectDefined(grouped.outputs.checks_node_core_nondist_matrix, "legacy Node matrix"),
).include;
expect(row).not.toHaveProperty("groups_gzip_base64");
expect(Object.keys(row.groups[0]).toSorted()).toEqual([
"configs",
"env",
"includePatterns",
"shard_name",
"timing_key",
]);
const runStep = readCiWorkflow().jobs["checks-node-core-test-nondist-shard"].steps.find(
(step: WorkflowStep) => step.name === "Run Node test shard",
);
const context = {
eventName: "workflow_dispatch" as const,
matrix: row,
repository: "openclaw/openclaw",
runAttempt: 1,
};
const packedEnv = evaluateWorkflowExpression(
runStep.env.OPENCLAW_NODE_TEST_GROUPS_GZIP_BASE64,
context,
);
const legacyEnv = evaluateWorkflowExpression(
runStep.env.OPENCLAW_NODE_TEST_GROUPS_JSON,
context,
);
expect(packedEnv).toBe("");
expect(
resolveShardPlans({ OPENCLAW_NODE_TEST_GROUPS_JSON: String(legacyEnv) }).map((plan) =>
plan.kind === "group" ? plan.plan : plan,
),
).toEqual(row.groups);
});
it("provisions ripgrep for real filesystem contract selections", () => {
const contract = "src/agents/filesystem-tools-output-contract.test.ts";
const nativeTools = "src/agents/sessions/tools/index.test.ts";
const bytePaths = "src/agents/sessions/tools/grep.byte-path.test.ts";
const unrelated = "src/agents/run-wait.test.ts";
const selections = [
{ targets: [contract] },
{ includePatterns: [contract] },
{ includePatterns: ["src/agents/filesystem-*.test.ts"] },
{ targets: [nativeTools] },
{ targets: [bytePaths] },
{ includePatterns: [bytePaths] },
{ groups: [{ shard_name: "agentic-agents-support", targets: [bytePaths] }] },
{ groups: [{ shard_name: "agentic-agents-support", includePatterns: [bytePaths] }] },
{ includePatterns: [unrelated] },
{ shardName: "agentic-agents-core-runtime" },
{ shardName: "agentic-agents-support" },
{ shardName: "agentic-agents-core-runtime", includePatterns: [unrelated] },
{ groups: [{ shard_name: "agentic-agents-core-runtime", includePatterns: [contract] }] },
{ groups: [{ shard_name: "agentic-agents-support", includePatterns: [nativeTools] }] },
{ groups: [{ shard_name: "agentic-agents-core-runtime", includePatterns: [unrelated] }] },
{ groups: [{ shard_name: "agentic-agents-core-runtime" }] },
];
const result = runCiManifestFixture({
bundledPlanner: true,
nodeTestShards: selections.map((selection, index) =>
Object.assign(
{
checkName: `grep-${index}`,
configs: ["test/vitest/vitest.agents-core.config.ts"],
requiresDist: false,
runner: "ubuntu-24.04",
shardName: "compact-small-1",
},
selection,
),
),
});
expect(result.status, result.output).toBe(0);
const matrix = JSON.parse(
expectDefined(result.outputs.checks_node_core_nondist_matrix, "non-dist Node matrix"),
) as { include: { requires_ripgrep?: boolean }[] };
expect(matrix.include.map((row) => Boolean(row.requires_ripgrep))).toEqual([
true,
true,
true,
true,
true,
true,
true,
true,
false,
true,
true,
false,
true,
true,
false,
true,
]);
});
it("fails and retries quiet Node test shard stalls quickly", () => {
const workflow = readCiWorkflow();
const preflightJob = workflow.jobs.preflight;
const manifestStep = preflightJob.steps.find(
(step: WorkflowStep) => step.name === "Build CI manifest",
);
const nodeTestJob = workflow.jobs["checks-node-core-test-nondist-shard"];
const runStep = nodeTestJob.steps.find(
(step: WorkflowStep) => step.name === "Run Node test shard",
);
const buildRuntimeStep = nodeTestJob.steps.find(
(step: WorkflowStep) => step.name === "Build Node test runtime",
);
const installRipgrepStep = nodeTestJob.steps.find(
(step: WorkflowStep) => step.name === "Install ripgrep for native grep tests",
);
expect(JSON.stringify(preflightJob.steps)).toContain("timeout_minutes: shard.timeoutMinutes");
expect(manifestStep.run).toContain("pretest_build_mode: shard.pretestBuildMode");
expect(manifestStep.run).toContain("requires_ripgrep:");
expect(manifestStep.run).toContain("src/agents/sessions/tools/index.test.ts");
expect(nodeTestJob["timeout-minutes"]).toBe("${{ matrix.timeout_minutes || 60 }}");
expect(runStep.env.OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS).toBe(
"${{ needs.preflight.outputs.compatibility_target == 'true' && '660000' || '300000' }}",
);
expect(runStep.env.OPENCLAW_VITEST_NO_OUTPUT_RETRY).toBe("1");
expect(runStep.env.OPENCLAW_NODE_TEST_ENV_JSON).toBe("${{ toJson(matrix.env) }}");
expect(runStep.env.OPENCLAW_NODE_TEST_TARGETS_JSON).toBe("${{ toJson(matrix.targets) }}");
expect(runStep.env.OPENCLAW_NODE_TEST_GROUPS_GZIP_BASE64).toBe(
"${{ matrix.groups_gzip_base64 || '' }}",
);
expect(runStep.env.OPENCLAW_NODE_TEST_GROUPS_JSON).toBe(
"${{ matrix.groups && toJson(matrix.groups) || '' }}",
);
expect(runStep.env.OPENCLAW_NODE_TEST_VITEST_ARGS_JSON).toBe(
"${{ needs.preflight.outputs.compatibility_target == 'true' && '[\"--hookTimeout=600000\"]' || '[]' }}",
);
expect(buildRuntimeStep).toMatchObject({
if: "matrix.pretest_build_mode != null",
env: {
OPENCLAW_BUILD_PRIVATE_QA: "${{ matrix.pretest_build_mode == 'private-qa' && '1' || '0' }}",
VITEST: "1",
},
run: "pnpm build qaRuntime",
});
expect(installRipgrepStep).toMatchObject({
if: "matrix.requires_ripgrep == true && runner.os == 'Linux'",
run: expect.stringContaining("apt-get install -y --no-install-recommends ripgrep"),
});
expect(nodeTestJob.steps.indexOf(buildRuntimeStep)).toBeLessThan(
nodeTestJob.steps.indexOf(runStep),
);
expect(nodeTestJob.steps.indexOf(installRipgrepStep)).toBeLessThan(
nodeTestJob.steps.indexOf(runStep),
);
const trustedRunnerStep = nodeTestJob.steps.find(
(step: WorkflowStep) => step.name === "Checkout trusted Node shard runner",
);
expect(trustedRunnerStep).toMatchObject({
if: "${{ hashFiles('scripts/ci-run-node-test-shard.mts') == '' }}",
uses: CHECKOUT_V6,
with: {
ref: "${{ github.workflow_sha }}",
path: ".ci-workflow",
"sparse-checkout": expect.stringContaining("scripts/ci-run-node-test-shard.mts"),
"sparse-checkout-cone-mode": false,
"persist-credentials": false,
},
});
// Non-cone sparse-checkout ignores missing paths silently, so a renamed
// script would surface only as a runtime module-not-found on the frozen
// lane. Require every listed path to exist at this revision.
const sparseCheckoutPaths = String(trustedRunnerStep?.with?.["sparse-checkout"] ?? "")
.split("\n")
.map((line) => line.trim())
.filter((line) => line.length > 0);
expect(sparseCheckoutPaths).toContain("scripts/ci-run-node-test-shard.mts");
for (const sparsePath of sparseCheckoutPaths) {
expect({ sparsePath, exists: existsSync(sparsePath) }).toEqual({ sparsePath, exists: true });
}
});
it("clamps Node test workers to the detected core count", () => {
const workflow = readCiWorkflow();
const nodeTestJob = workflow.jobs["checks-node-core-test-nondist-shard"];
const resourceStep = nodeTestJob.steps.find(
(step: WorkflowStep) => step.name === "Configure Node test resources",
);
expect(resourceStep.run).toContain('if [ "$workers" -gt "$cores" ]; then');
expect(resourceStep.run).toContain('workers="$cores"');
expect(resourceStep.run.indexOf('workers="$cores"')).toBeLessThan(
resourceStep.run.indexOf("OPENCLAW_VITEST_MAX_WORKERS"),
);
});
it("uses candidate-owned script interfaces for frozen target CI", () => {
const workflow = readCiWorkflow();
const buildChecks = workflow.jobs["build-artifacts"].steps.find(
(step: WorkflowStep) => step.name === "Run built artifact checks",
);
const qaBuild = workflow.jobs["qa-smoke-ci-profile"].steps.find(
(step: WorkflowStep) => step.name === "Build QA smoke runtime",
);
const additionalChecks = workflow.jobs["check-additional-shard"].steps.find(
(step: WorkflowStep) => step.name === "Run additional check shard",
);
expect(buildChecks.run).toContain("pnpm test:gateway:watch-regression -- --skip-build");
expect(buildChecks.run).not.toContain("scripts/check-gateway-watch-regression.mts");
expect(buildChecks.run).toContain(
"startup_builder=(node --import tsx scripts/ensure-cli-startup-build.mts)",
);
expect(buildChecks.run).toContain(
"startup_builder=(node scripts/ensure-cli-startup-build.mjs)",
);
expect(qaBuild.run.match(/pnpm build qaRuntime/gu)).toHaveLength(1);
expect(qaBuild.run).not.toContain("package-openclaw-for-docker");
expect(additionalChecks.run).toContain(
"boundary_runner=(node --import tsx scripts/run-additional-boundary-checks.mts)",
);
expect(additionalChecks.run).toContain(
"boundary_runner=(node scripts/run-additional-boundary-checks.mjs)",
);
expect(additionalChecks.run).not.toContain(
"if [ ! -f scripts/check-session-accessor-boundary.mts ]",
);
expect(additionalChecks.run).not.toContain(
"if [ ! -f scripts/check-session-transcript-reader-boundary.mts ]",
);
const checkLint = workflow.jobs["check-shard"].steps.find(
(step: WorkflowStep) => step.name === "Run check shard",
);
const hostedCoreLint = workflow.jobs["check-lint-hosted-core-shard"].steps.find(
(step: WorkflowStep) => step.name === "Run hosted core lint stripe",
);
const lintBoundaryFingerprint = workflow.jobs["check-shard"].steps.find(
(step: WorkflowStep) => step.name === "Compute extension boundary input fingerprint",
);
const additionalBoundaryFingerprint = workflow.jobs["check-additional-shard"].steps.find(
(step: WorkflowStep) => step.name === "Compute extension boundary input fingerprint",
);
// The frozen candidate owns the older full lint and boundary builders;
// current-only stripe and cache mechanics must not replace that coverage.
expect(checkLint.run).toContain("if [[ ! -f scripts/run-oxlint-shards.mts ]]; then");
expect(checkLint.run).toContain("pnpm lint");
expect(hostedCoreLint.run).toContain("target does not support core lint stripes");
expect(lintBoundaryFingerprint.run).toContain("enabled=false");
expect(additionalBoundaryFingerprint.run).toContain("enabled=false");
});
it.skipIf(process.platform === "win32")(
"keeps missing performance coverage fatal outside historical targets",
() => {
const job = readCiWorkflow().jobs["control-ui-performance"];
expect(job.needs).toEqual(["preflight"]);
expect(job.env.CHECKOUT_BASE_SHA).toBe("${{ needs.preflight.outputs.diff_base_revision }}");
const step = job.steps.find(
(candidate: WorkflowStep) => candidate.name === "Check Control UI performance against base",
);
const root = tempDirs.make("openclaw-performance-workflow-");
const summary = path.join(root, "summary.md");
writeFileSync(path.join(root, "package.json"), "{}");
for (const compatibility of ["true", "false"]) {
const result = runWorkflowShellScript(step.run, {
cwd: root,
env: {
...process.env,
COMPATIBILITY_TARGET: compatibility,
GITHUB_STEP_SUMMARY: summary,
},
});
expect(result.status, `${result.stdout}${result.stderr}`).toBe(
compatibility === "true" ? 0 : 1,
);
}
expect(readFileSync(summary, "utf8")).toContain(
"unavailable on the selected compatibility target",
);
},
);
it("emits one final CI gate after every selected lane", () => {
const workflow = readCiWorkflow();
const gate = workflow.jobs["ci-gate"];
const requiredJobs = ["preflight", "security-fast"];
const selectedJobs = [
"build-artifacts",
"control-ui-performance",
"native-i18n",
"checks-ui",
"checks-ui-e2e",
"checks-ui-e2e-real-gateway",
"control-ui-i18n",
"checks-fast-core",
"qa-smoke-ci-profile",
"checks-fast-plugin-contracts-shard",
"checks-fast-channel-contracts-shard",
"checks-node-compat",
"checks-node-core-test-nondist-shard",
"check-shard",
"check-lint-hosted-core-shard",
"check-test-types-hosted-core-shard",
"check-additional-shard",
"check-docs",
"skills-python",
"checks-windows",
"macos-node",
"macos-swift",
"ios-build",
"ios-screenshot-shard",
"ios-screenshot-evidence",
"android",
"docker-seed-e2e",
];
expect(workflow.on.pull_request).not.toHaveProperty("paths-ignore");
expect(gate.name).toBe("openclaw/ci-gate");
expect(gate.needs).toEqual([...requiredJobs, ...selectedJobs]);
// Every job in the file is gated; a new lane cannot slip in ungated.
expect(gate.needs.toSorted()).toEqual(
Object.keys(workflow.jobs)
.filter((job) => job !== "ci-gate")
.toSorted(),
);
expect(gate.if).toBe(
"${{ !cancelled() && (github.event_name != 'pull_request' || !github.event.pull_request.draft) }}",
);
expect(gate.permissions).toEqual({ contents: "read" });
const verifyStep = gate.steps.find(
(step: WorkflowStep) => step.name === "Verify selected CI lanes",
);
expect(Object.keys(verifyStep.env)).toEqual(["JOB_RESULTS"]);
const resultRows: string[] = verifyStep.env.JOB_RESULTS.trim().split("\n");
expect(resultRows.slice(0, requiredJobs.length)).toEqual(
requiredJobs.map((job) => `${job}=\${{ needs.${job}.result }}|true`),
);
for (const job of selectedJobs) {
expect(verifyStep.env.JOB_RESULTS).toContain(`${job}=\${{ needs.${job}.result }}|`);
}
expect(resultRows).toHaveLength(gate.needs.length);
});
it("does not admit the final gate for cancelled workflows or draft pull requests", () => {
const gate = readCiWorkflow().jobs["ci-gate"];
for (const eventName of ["pull_request", "push", "workflow_dispatch"] as const) {
for (const cancelled of [true, false]) {
for (const draft of [true, false]) {
expect(
evaluateWorkflowExpression(gate.if, {
cancelled,
draft,
eventName,
repository: "openclaw/openclaw",
runAttempt: 1,
}),
JSON.stringify({ cancelled, draft, eventName }),
).toBe(!cancelled && (eventName !== "pull_request" || !draft));
}
}
}
});
it("ci-gate selection projections match their owning job predicates", () => {
const workflow = readCiWorkflow();
const step = workflow.jobs["ci-gate"].steps.find(
(candidate: WorkflowStep) => candidate.name === "Verify selected CI lanes",
);
const rows: string[] = step.env.JOB_RESULTS.trim().split("\n").slice(2);
for (const row of rows) {
const match = expectDefined(
row.match(/^([\w-]+)=\$\{\{ needs\.([\w-]+)\.result \}\}\|\$\{\{ (.+) \}\}$/u),
row,
);
const job = expectDefined(match[1], row);
const selection = expectDefined(match[3], row);
expect(match[2], row).toBe(job);
// Gate inputs duplicate eligibility, never dependency status or cancellation.
// Bind that projection to its owner so a routing change cannot leave stale selection.
const eligible = workflow.jobs[job].if
.replace(/^\$\{\{\s*|\s*\}\}$/gu, "")
.replace(/!cancelled\(\)\s*&&\s*always\(\)\s*&&\s*/gu, "")
.replace(/\s+/gu, " ")
.trim();
const projected = /^needs\.preflight\.outputs\.\w+$/u.test(selection)
? `${selection} == 'true'`
: selection;
expect(projected, job).toBe(eligible);
}
});
it.skipIf(process.platform === "win32").each<{
label: string;
context: Partial<Parameters<typeof evaluateWorkflowExpression>[1]>;
expected: Record<string, boolean>;
}>([
{
label: "same-repo Blacksmith PR",
context: { eventName: "pull_request" },
expected: {
"checks-node-compat": false,
"ios-build": true,
"ios-screenshot-shard": false,
},
},
{
label: "canonical Blacksmith push",
context: { eventName: "push" },
expected: {
"checks-node-compat": false,
"ios-build": true,
"ios-screenshot-shard": false,
},
},
{
label: "GitHub push",
context: { eventName: "push", runnerProfile: "github" },
expected: {
"check-lint-hosted-core-shard": true,
"check-test-types-hosted-core-shard": true,
},
},
{
label: "hybrid PR",
context: { eventName: "pull_request", runnerProfile: "hybrid" },
expected: { "check-lint-hosted-core-shard": true },
},
{
label: "targeted core test PR",
context: {
eventName: "pull_request",
runnerProfile: "hybrid",
preflightOutputs: { changed_core_test_paths_json: '["src/commands/doctor.test.ts"]' },
},
expected: {
"check-shard": true,
"check-additional-shard": true,
"check-lint-hosted-core-shard": true,
"check-test-types-hosted-core-shard": false,
},
},
{
label: "Blacksmith has no hosted stripes",
context: { frozenTarget: true },
expected: {
"check-lint-hosted-core-shard": false,
"check-test-types-hosted-core-shard": false,
},
},
{
label: "frozen target without hosted capability",
context: { frozenTarget: true, hostedRunnerProfileContract: false, runnerProfile: "github" },
expected: {
"check-lint-hosted-core-shard": false,
"check-test-types-hosted-core-shard": false,
},
},
{
label: "frozen target with hosted capability",
context: { frozenTarget: true, runnerProfile: "hybrid" },
expected: {
"check-lint-hosted-core-shard": true,
"check-test-types-hosted-core-shard": true,
},
},
{
label: "current target needs no capability fallback",
context: { hostedRunnerProfileContract: false, runnerProfile: "github" },
expected: { "check-lint-hosted-core-shard": true },
},
{
label: "hosted checks out of scope",
context: { runnerProfile: "github", preflightOutputs: { run_check: "false" } },
expected: {
"check-shard": false,
"check-lint-hosted-core-shard": false,
"check-test-types-hosted-core-shard": false,
},
},
{
label: "compatibility target",
context: { preflightOutputs: { compatibility_target: "true" } },
expected: {
"checks-ui": true,
"checks-ui-e2e": false,
"checks-ui-e2e-real-gateway": false,
"ios-screenshot-shard": false,
},
},
{
label: "current target",
context: {},
expected: {
"checks-ui-e2e": true,
"checks-ui-e2e-real-gateway": true,
"ios-screenshot-shard": true,
"checks-node-compat": true,
},
},
{
label: "manual Node 22 without artifacts",
context: { preflightOutputs: { run_build_artifacts: "false" } },
expected: { "checks-node-compat": false },
},
{
label: "UI performance without runtime artifact changes",
context: {
preflightOutputs: { run_control_ui_performance: "true", run_build_artifacts: "false" },
},
expected: { "control-ui-performance": true },
},
{
label: "unrelated runtime build skips UI performance",
context: {
preflightOutputs: { run_control_ui_performance: "false", run_build_artifacts: "true" },
},
expected: { "control-ui-performance": false },
},
{
label: "UI performance outside build and UI scope",
context: {
preflightOutputs: {
run_control_ui_performance: "false",
run_build_artifacts: "false",
run_ui_tests: "false",
},
},
expected: { "control-ui-performance": false },
},
{
label: "ordinary manual screenshot override",
context: { preflightOutputs: { run_ios_screenshots: "false" } },
expected: { "ios-screenshot-shard": true },
},
{
label: "release gate without screenshot scope",
context: { releaseGate: true, preflightOutputs: { run_ios_screenshots: "false" } },
expected: { "ios-screenshot-shard": false },
},
{
label: "release gate keeps smoke without screenshots",
context: { releaseGate: true },
expected: { "ios-build": true, "ios-screenshot-shard": false },
},
{
label: "npm-beta excludes manual screenshots",
context: { preflightOutputs: { release_scope: "npm-beta" } },
expected: { "ios-screenshot-shard": false },
},
{
label: "npm-beta excludes PR screenshots",
context: { eventName: "pull_request", preflightOutputs: { release_scope: "npm-beta" } },
expected: { "ios-screenshot-shard": false },
},
{
label: "npm-stable excludes manual screenshots",
context: { preflightOutputs: { release_scope: "npm-stable" } },
expected: { "ios-screenshot-shard": false },
},
{
label: "npm-stable excludes PR screenshots",
context: { eventName: "pull_request", preflightOutputs: { release_scope: "npm-stable" } },
expected: { "ios-screenshot-shard": false },
},
{
label: "PR screenshot scope off",
context: { eventName: "pull_request", preflightOutputs: { run_ios_screenshots: "false" } },
expected: { "ios-screenshot-shard": false },
},
])("ci-gate preserves eligibility: $label", ({ context, expected }) => {
const jobResults = renderCiGateEnvironment(context);
const selections = Object.fromEntries(
jobResults
.trim()
.split("\n")
.map((row) => {
const [job, , selected] = row.split(/[=|]/u);
return [expectDefined(job, row), expectDefined(selected, row)];
}),
);
for (const [job, selected] of Object.entries(expected)) {
expect(selections[job], job).toBe(String(selected));
}
expect(selections["ios-screenshot-evidence"]).toBe(selections["ios-screenshot-shard"]);
const results = Object.fromEntries(
Object.entries(selections).map(([job, selected]) => [
job,
selected === "true" ? "success" : "skipped",
]),
);
const outcome = runCiGateFixture(renderCiGateEnvironment(context, results));
expect(outcome.status, `${outcome.stdout}\n${outcome.stderr}`).toBe(0);
if (expected["ios-build"]) {
for (const terminal of ["failure", "cancelled", "skipped"]) {
const missingSmoke = runCiGateFixture(
renderCiGateEnvironment(context, { ...results, "ios-build": terminal }),
);
expect(missingSmoke.status, missingSmoke.stdout).toBe(1);
}
}
if (context.preflightOutputs?.changed_core_test_paths_json) {
for (const terminal of ["failure", "skipped"]) {
const missingOwner = runCiGateFixture(
renderCiGateEnvironment(context, { ...results, "check-shard": terminal }),
);
expect(missingOwner.status).not.toBe(0);
}
}
});
it("runs Node 24 minimum compatibility only from manual CI dispatches", () => {
const workflow = readCiWorkflow();
const compatibilityJob = workflow.jobs["checks-node-compat"];
const fullReleaseWorkflow = readWorkflow(".github/workflows/full-release-validation.yml");
const fullReleaseDispatch = fullReleaseWorkflow.jobs.normal_ci.steps.find(
(step: WorkflowStep) => step.name === "Dispatch CI",
);
expect(compatibilityJob.name).toBe("checks-node-compat-node24");
expect(compatibilityJob.if).toBe(
"needs.preflight.outputs.run_build_artifacts == 'true' && github.event_name == 'workflow_dispatch'",
);
expect(fullReleaseDispatch.env.CHILD_WORKFLOW_KIND).toBe("ci");
expect(fullReleaseDispatch.run).toContain('dispatch_child ci.yml "$dispatch_run_name"');
expect(fullReleaseDispatch.run).toContain('-f target_ref="$TARGET_SHA"');
expect(compatibilityJob.steps.at(-1)?.run).toContain(
"src/config/sessions/session-accessor.test.ts",
);
expect(compatibilityJob.steps.at(-1)?.run).toContain(
"src/config/sessions/store-writer.test.ts",
);
expect(compatibilityJob.steps.at(-1)?.run).toContain("src/config/sessions/sessions.test.ts");
});
it.skipIf(process.platform === "win32")("ci-gate rejects an unexpected selected skip", () => {
const result = runCiGateFixture(renderCiGateEnvironment({}, { "checks-ui": "skipped" }));
expect(result.stdout).toContain("checks-ui: skipped");
expect(result.status, result.stdout).toBe(1);
});
it.skipIf(process.platform === "win32").each([
[true, "success", 0],
[true, "skipped", 1],
[true, "failure", 1],
[true, "cancelled", 1],
[true, "", 1],
[true, "unknown", 1],
[false, "success", 0],
[false, "skipped", 0],
[false, "failure", 1],
[false, "cancelled", 1],
[false, "", 1],
[false, "unknown", 1],
] as const)(
"ci-gate checks all downstream lanes (selected=%s, result=%s)",
(selected, result, exit) => {
const workflow = readCiWorkflow();
const jobs: string[] = workflow.jobs["ci-gate"].needs.slice(2);
const jobResults = renderCiGateEnvironment(
{
eventName: selected ? "workflow_dispatch" : "pull_request",
runnerProfile: "github",
preflightOutputs: Object.fromEntries(
Object.keys(workflow.jobs.preflight.outputs)
.filter((key) => key.startsWith("run_"))
.map((key) => [key, String(selected)]),
),
},
Object.fromEntries(jobs.map((job) => [job, result])),
);
const outcome = runCiGateFixture(jobResults);
expect(outcome.status, `${outcome.stdout}\n${outcome.stderr}`).toBe(exit);
for (const job of jobs) {
expect(jobResults).toContain(`${job}=${result}|${selected}\n`);
expect(outcome.stdout).toContain(`${job}: ${result} (selected=${selected})`);
if (exit !== 0) {
expect(outcome.stdout).toContain(`${job} finished with ${result} (selected=${selected})`);
}
}
},
);
it
.skipIf(process.platform === "win32")
.each(["failure", "cancelled", "skipped", "", "unknown", "success="])(
"ci-gate rejects required result %s independently of downstream success",
(result) => {
const outcome = runCiGateFixture(
renderCiGateEnvironment({}, { preflight: result, "security-fast": result }),
);
expect(outcome.status, outcome.stdout).toBe(1);
for (const job of ["preflight", "security-fast"]) {
expect(outcome.stdout).toContain(`${job} finished with ${result} (selected=true)`);
}
},
);
it
.skipIf(process.platform === "win32")
.each(["", "unknown", "TRUE", "true|false", "true|", "false="])(
"ci-gate rejects missing or malformed selection %s even after success",
(selection) => {
const outcome = runCiGateFixture(
renderCiGateEnvironment({ preflightOutputs: { run_ui_tests: selection } }),
);
expect(outcome.status, outcome.stdout).toBe(1);
expect(outcome.stdout).toContain(
`checks-ui finished with success (selected=${selection || "missing"})`,
);
},
);
it.skipIf(process.platform === "win32")(
"ci-gate reports failed upstream and selected dependent skips",
() => {
const jobResults = renderCiGateEnvironment(
{},
{
"ios-screenshot-shard": "failure",
"ios-screenshot-evidence": "skipped",
},
);
const outcome = runCiGateFixture(jobResults);
expect(outcome.status, outcome.stdout).toBe(1);
expect(outcome.stdout).toContain(
"ios-screenshot-shard finished with failure (selected=true)",
);
expect(outcome.stdout).toContain(
"ios-screenshot-evidence finished with skipped (selected=true)",
);
},
);
it.skipIf(process.platform === "win32")(
"resolves topology-aware protocol bases and drives the real guard",
() => {
const topology = createQaProtocolTopology();
const cases = [
["main", topology.mainHead, "main-ancestor", topology.mainBase],
[topology.releaseBranch, topology.releaseHead, "release-branch-head", topology.mainBase],
[topology.releaseTag, topology.releaseTagHead, "release-tag", topology.mainBase],
[topology.releaseTagHead, topology.releaseTagHead, "release-tag", topology.mainBase],
[topology.mainReleaseTag, topology.mainHead, "release-tag", topology.mainHead],
] as const;
for (const [inputRef, revision, trustedReason, protocolBase] of cases) {
const result = runQaSelectedRefValidation(topology, inputRef, revision);
expect(result.status, `${result.stdout}${result.stderr}`).toBe(0);
expect(result.outputs).toEqual({
protocol_base_revision: protocolBase,
selected_revision: revision,
trusted_reason: trustedReason,
});
}
runGit(topology.checkout, ["checkout", "-q", "--detach", topology.mainHead]);
const mainCheck = runProtocolSinceFixture(topology.checkout, topology.mainBase);
expect(mainCheck.status, `${mainCheck.stdout}${mainCheck.stderr}`).toBe(0);
expect(mainCheck.stdout).toContain("1 new core method");
runGit(topology.checkout, ["checkout", "-q", "--detach", topology.compatibilityHead]);
const compatibilityCheck = runProtocolSinceFixture(topology.checkout, topology.mainBase);
expect(
compatibilityCheck.status,
`${compatibilityCheck.stdout}${compatibilityCheck.stderr}`,
).toBe(0);
expect(compatibilityCheck.stdout).toContain("1 restored compatibility method");
runGit(topology.checkout, ["checkout", "-q", "--detach", topology.invalidCompatibilityHead]);
const invalidCompatibilityCheck = runProtocolSinceFixture(
topology.checkout,
topology.mainBase,
);
expect(invalidCompatibilityCheck.status).not.toBe(0);
expect(invalidCompatibilityCheck.stderr).toContain(
"restored compatibility methods must retain <= vintage metadata",
);
runGit(topology.checkout, ["checkout", "-q", "--detach", topology.releaseHead]);
const releaseCheck = runProtocolSinceFixture(topology.checkout, topology.mainBase);
expect(releaseCheck.status).not.toBe(0);
expect(releaseCheck.stderr).toContain("sessions.releaseOnly is missing since metadata");
for (const [expectedSha, inputRef, revision] of [
["not-a-sha", "main", topology.mainHead],
[topology.featureHead, topology.featureHead, topology.featureHead],
[topology.mainHead, topology.releaseTag, topology.releaseTagHead],
] as const) {
const result = runQaSelectedRefValidation(topology, inputRef, revision, expectedSha);
expect(result.status, `${result.stdout}${result.stderr}`).not.toBe(0);
expect(result.outputs).toEqual({});
}
},
);
it.skipIf(process.platform === "win32")(
"wires and fetches one explicit protocol base before QA execution",
() => {
const qaWorkflow = readQaProfileEvidenceWorkflow();
const maturityWorkflow = readMaturityScorecardWorkflow();
const validateJob = qaWorkflow.jobs.validate_selected_ref;
const runJob = qaWorkflow.jobs.run_qa_profile_shard;
const aggregateJob = qaWorkflow.jobs.aggregate_qa_profile;
const stepNames = runJob.steps.map((step: WorkflowStep) => step.name);
const buildStep = expectDefined(
runJob.steps.find((step: WorkflowStep) => step.name === "Build private QA runtime"),
"private QA runtime build",
);
const fetchStep = expectDefined(
runJob.steps.find((step: WorkflowStep) => step.name === "Fetch protocol comparison base"),
"protocol comparison base fetch",
);
const runStep = expectDefined(
runJob.steps.find((step: WorkflowStep) => step.name === "Run QA profile shard"),
"QA profile shard run",
);
const evidenceStep = expectDefined(
aggregateJob.steps.find(
(step: WorkflowStep) => step.name === "Finalize QA profile evidence",
),
"QA profile evidence finalization",
);
const protocolOutput = "${{ needs.validate_selected_ref.outputs.protocol_base_revision }}";
const trustedInput = "${{ inputs.trusted_ref || inputs.ref }}";
expect(qaWorkflow.env.OPENCLAW_TSDOWN_MAX_OLD_SPACE_MB).toBe("8192");
expect(qaWorkflow.on.workflow_call.inputs.trusted_ref).toEqual({
description: "Optional trusted branch, tag, or SHA identity for an immutable ref",
required: false,
default: "",
type: "string",
});
expect(validateJob.outputs.protocol_base_revision).toBe(
"${{ steps.validate.outputs.protocol_base_revision }}",
);
const validateStep = expectDefined(
validateJob.steps.find((step: WorkflowStep) => step.name === "Validate selected ref"),
"QA selected-ref validation",
);
expect(validateStep.env.INPUT_REF).toBe(trustedInput);
const ordered = [
"Checkout trusted QA harness",
"Restore trusted QA harness revision",
"Setup Node environment",
"Checkout selected ref",
"Install selected dependencies",
"Fetch protocol comparison base",
"Build private QA runtime",
"Run QA profile shard",
].map((name) => stepNames.indexOf(name));
expect(ordered.every((index, position) => index > (ordered[position - 1] ?? -1))).toBe(true);
expect(fetchStep.env?.PROTOCOL_SINCE_BASE_SHA).toBe(protocolOutput);
expect(buildStep.run).toBe("pnpm build qaRuntime");
expect(runStep.env?.PROTOCOL_SINCE_BASE_SHA).toBe(protocolOutput);
expect(runStep.env?.REQUESTED_REF).toBe(trustedInput);
expect(runStep.run).toContain("protocolBaseSha: process.env.PROTOCOL_SINCE_BASE_SHA");
expect(evidenceStep.env?.PROTOCOL_BASE_SHA).toBe(protocolOutput);
expect(evidenceStep.env?.REQUESTED_REF).toBe(trustedInput);
expect(evidenceStep.run).toContain("protocolBaseSha: process.env.PROTOCOL_BASE_SHA");
expect(maturityWorkflow.jobs.generate_qa_evidence.with.trusted_ref).toBe("${{ inputs.ref }}");
const topology = createQaProtocolTopology();
const checkout = tempDirs.make("openclaw-qa-protocol-fetch-");
runGit(checkout, ["init", "-q", "-b", "main"]);
runGit(checkout, ["remote", "add", "origin", topology.origin]);
runGit(checkout, [
"fetch",
"-q",
"--depth=1",
"origin",
`+${topology.mainHead}:refs/remotes/origin/selected`,
]);
runGit(checkout, ["checkout", "-q", "--detach", "refs/remotes/origin/selected"]);
const sentinel = path.join(checkout, "qa-sentinel");
const runFetch = (baseSha: string) =>
runWorkflowShellScript(
`${expectDefined(fetchStep.run, "protocol fetch script")}\nprintf 'ran\\n' > "$QA_SENTINEL"\n`,
{
cwd: checkout,
env: {
...process.env,
CI_GIT_OWNER: topology.gitOwner,
PROTOCOL_SINCE_BASE_SHA: baseSha,
QA_SENTINEL: sentinel,
},
},
);
const success = runFetch(topology.mainBase);
expect(success.status, `${success.stdout}${success.stderr}`).toBe(0);
expect(runGit(checkout, ["rev-parse", "refs/remotes/origin/qa-protocol-base"])).toBe(
topology.mainBase,
);
expect(existsSync(sentinel)).toBe(true);
rmSync(sentinel);
const failure = runFetch("f".repeat(40));
expect(failure.status, `${failure.stdout}${failure.stderr}`).not.toBe(0);
expect(existsSync(sentinel)).toBe(false);
},
);
it("pins the QA Git owner before checkouts and preserves all ten terminal fetch contracts", () => {
const workflow = readQaProfileEvidenceWorkflow();
const gitJobs = [
"validate_selected_ref",
"plan_qa_profile",
"run_qa_profile_shard",
"aggregate_qa_profile",
];
const calls: string[] = [];
for (const job of gitJobs) {
const steps = workflow.jobs[job].steps as WorkflowStep[];
const ownerIndex = steps.findIndex((step) => step.name === "Prepare Git owner");
expect(steps.filter((step) => step.name === "Prepare Git owner")).toHaveLength(1);
expect(steps[ownerIndex]).toEqual({
name: "Prepare Git owner",
uses: "openclaw/openclaw/.github/actions/git-owner@dd4528b6393e7d00063067a080ca7241b48ce475",
});
expect(steps[ownerIndex - 1]?.name).toBe(
job === "validate_selected_ref"
? "Resolve job workflow identity"
: "Require authorized workflow actor",
);
expect(steps[ownerIndex + 1]?.name).toBe(
job === "validate_selected_ref" ? "Checkout selected ref" : "Checkout trusted QA harness",
);
expect(steps.some((step) => step.uses?.startsWith("actions/setup-python@"))).toBe(false);
for (const step of steps) {
const run = (step.run ?? "").replace(/[ \t]*\\\n[ \t]*/gu, " ");
const fetches = run
.split("\n")
.map((line) => line.trim())
.filter((line) => /\bfetch\b/u.test(line));
if (fetches.length === 0) {
continue;
}
expect(run.startsWith("set -euo pipefail\n")).toBe(true);
for (const fetch of fetches) {
expect(fetch).toMatch(/^python3 -I -S "\$CI_GIT_OWNER" --checkout-git (?:0|120) fetch /u);
expect(fetch).not.toMatch(/\|\||&&|;|\$\?/u);
}
expect(run).not.toMatch(/^\s*(?:timeout|for|while|until)\b|\$\?/mu);
calls.push(...fetches);
}
}
expect(calls).toHaveLength(10);
expect(calls.filter((call) => call.includes("--checkout-git 120 fetch"))).toHaveLength(4);
expect(calls.filter((call) => call.includes("--checkout-git 0 fetch"))).toHaveLength(6);
const validateSelectedRef = expectDefined(
workflow.jobs.validate_selected_ref.steps.find(
(step: WorkflowStep) => step.name === "Validate selected ref",
),
"QA profile selected-ref validation step",
);
expect(validateSelectedRef["working-directory"]).toBeUndefined();
expect(calls.slice(0, 3)).toEqual([
'python3 -I -S "$CI_GIT_OWNER" --checkout-git 120 fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main',
'python3 -I -S "$CI_GIT_OWNER" --checkout-git 120 fetch --no-tags origin "+refs/tags/${tag_candidate}:refs/tags/${tag_candidate}"',
'python3 -I -S "$CI_GIT_OWNER" --checkout-git 120 fetch --no-tags origin "+refs/heads/${branch_candidate}:refs/remotes/origin/${branch_candidate}"',
]);
expect(validateSelectedRef.run).toContain(
'release_tag_sha="$(git rev-parse "refs/tags/${tag_candidate}^{commit}")"',
);
expect(validateSelectedRef.run).toContain(
'release_branch_sha="$(git rev-parse "refs/remotes/origin/${branch_candidate}")"',
);
for (const name of ["Restore trusted QA harness revision", "Checkout selected ref"]) {
const bodies = gitJobs
.slice(1)
.map(
(job) => workflow.jobs[job].steps.find((step: WorkflowStep) => step.name === name)?.run,
);
expect(bodies[0]).toBeTypeOf("string");
expect(new Set(bodies).size).toBe(1);
}
const protocolFetch = workflow.jobs.run_qa_profile_shard.steps.find(
(step: WorkflowStep) => step.name === "Fetch protocol comparison base",
);
expect(protocolFetch["working-directory"]).toBe("selected");
expect(calls[7]).toBe(
'python3 -I -S "$CI_GIT_OWNER" --checkout-git 120 fetch --no-tags --no-recurse-submodules --depth=1 origin "+${PROTOCOL_SINCE_BASE_SHA}:refs/remotes/origin/qa-protocol-base"',
);
expect(protocolFetch.run).toContain(
'test "$(git rev-parse refs/remotes/origin/qa-protocol-base^{commit})" = "$PROTOCOL_SINCE_BASE_SHA"',
);
expect(readFileSync(".github/workflows/qa-profile-evidence.yml", "utf8")).not.toMatch(
/\bgit(?: -C selected)? fetch\b/u,
);
});
it.skipIf(process.platform !== "linux")(
"classifies QA timeouts only from isolated supervisor diagnostics",
() => {
const scenarios = [
{
exitCode: 124,
mode: "natural-124",
supervisorSignals: [],
timedOut: false,
timeoutOutcome: "none",
},
{
exitCode: 137,
mode: "self-kill",
supervisorSignals: [],
timedOut: false,
timeoutOutcome: "none",
},
{
exitCode: 124,
mode: "term",
supervisorSignals: ["TERM"],
timedOut: true,
timeoutOutcome: "term",
},
{
exitCode: 137,
mode: "kill",
supervisorSignals: ["TERM", "KILL"],
timedOut: true,
timeoutOutcome: "kill",
},
] as const;
for (const scenario of scenarios) {
const result = runQaProfileTimeoutFixture(scenario.mode);
expect(result.commandStatus, `${result.stdout}\n${result.stderr}`).toBe(0);
expect(result.status).toMatchObject({
exitCode: scenario.exitCode,
target: { protocolBaseSha: "b".repeat(40) },
timedOut: scenario.timedOut,
timeoutOutcome: scenario.timeoutOutcome,
});
expect(result.githubOutput).toContain(`qa_exit_code=${scenario.exitCode}`);
expect(result.stderr).toContain(`child-stderr-sentinel:${scenario.mode}`);
expect(result.stderr).toContain("child-locale:POSIX");
expect(result.timeoutVersion).not.toBe("");
const supervisorSignals: readonly ("TERM" | "KILL")[] = scenario.supervisorSignals;
for (const signal of ["TERM", "KILL"] as const) {
const diagnostic = `timeout: sending signal ${signal} to command 'env'`;
if (supervisorSignals.includes(signal)) {
expect(result.timeoutSupervisorLog).toContain(diagnostic);
} else {
expect(result.timeoutSupervisorLog).not.toContain(diagnostic);
}
}
if (scenario.mode === "natural-124") {
expect(result.stderr).toContain(
"timeout: sending signal KILL to command 'spoofed-child'",
);
expect(result.timeoutSupervisorLog).not.toContain("spoofed-child");
}
if (scenario.timeoutOutcome === "term") {
expect(result.stdout).toContain(
"::warning::QA profile 'all' timed out after 0.4 seconds and was terminated",
);
} else if (scenario.timeoutOutcome === "kill") {
expect(result.stdout).toContain(
"::warning::QA profile 'all' timed out after 0.4 seconds and required SIGKILL after the 0.05-second grace period",
);
} else {
expect(result.stdout).not.toContain("::warning::QA profile");
}
}
},
);
it("keeps maturity scorecard generated QA evidence handoff strict", () => {
const maturityWorkflow = readMaturityScorecardWorkflow();
const qaEvidenceWorkflow = readQaProfileEvidenceWorkflow();
const generateJob = maturityWorkflow.jobs.generate_qa_evidence;
const publisherPreflight = maturityWorkflow.jobs.publisher_preflight;
const publishJob = maturityWorkflow.jobs.publish;
const publishPrJob = maturityWorkflow.jobs.publish_generated_pr;
const qaAuthorizeJob = qaEvidenceWorkflow.jobs.authorize_actor;
const qaPlanJob = qaEvidenceWorkflow.jobs.plan_qa_profile;
const qaShardJob = qaEvidenceWorkflow.jobs.run_qa_profile_shard;
const qaAggregateJob = qaEvidenceWorkflow.jobs.aggregate_qa_profile;
const qaValidateJob = qaEvidenceWorkflow.jobs.validate_selected_ref;
expect(maturityWorkflow.on.workflow_call.inputs).toMatchObject({
qa_evidence_run_id: {
description: "Optional workflow run id containing qa-evidence.json",
required: false,
default: "",
type: "string",
},
ref: {
description: "OpenClaw branch, tag, or SHA containing the maturity score source",
required: true,
type: "string",
},
expected_sha: {
description: "Optional full SHA that ref must resolve to",
required: false,
default: "",
type: "string",
},
allow_failures: {
description: "Allow rendering from valid incomplete QA evidence",
required: false,
default: false,
type: "boolean",
},
});
expect(maturityWorkflow.on.workflow_dispatch.inputs.allow_failures).toEqual({
description: "Allow rendering from valid incomplete QA evidence",
required: false,
default: true,
type: "boolean",
});
expect(maturityWorkflow.on.workflow_dispatch.inputs.publish_pull_request).toEqual({
description: "Open or update a pull request for generated maturity files",
required: false,
default: true,
type: "boolean",
});
expect(maturityWorkflow.on.workflow_call.inputs).not.toHaveProperty("publish_pull_request");
expect(maturityWorkflow.on.workflow_call.secrets.OPENAI_API_KEY.required).toBe(true);
expect(
maturityWorkflow.on.workflow_call.secrets.OPENCLAW_MATURITY_SCORECARD_AGENT_OPENAI_API_KEY
.required,
).toBe(false);
expect(Object.keys(maturityWorkflow.on.workflow_call.secrets).toSorted()).toEqual([
"CLAWSWEEPER_APP_PRIVATE_KEY",
"MANTIS_GITHUB_APP_PRIVATE_KEY",
"OPENAI_API_KEY",
"OPENCLAW_MATURITY_SCORECARD_AGENT_OPENAI_API_KEY",
"OPENCLAW_QA_CONVEX_SECRET_CI",
"OPENCLAW_QA_CONVEX_SITE_URL",
]);
for (const secret of [
"CLAWSWEEPER_APP_PRIVATE_KEY",
"MANTIS_GITHUB_APP_PRIVATE_KEY",
"OPENCLAW_QA_CONVEX_SECRET_CI",
"OPENCLAW_QA_CONVEX_SITE_URL",
]) {
expect(maturityWorkflow.on.workflow_call.secrets[secret].required).toBe(false);
}
expect(qaEvidenceWorkflow.on.workflow_dispatch.inputs).not.toHaveProperty("fail_on_qa_failure");
expect(qaEvidenceWorkflow.on.workflow_call.inputs).not.toHaveProperty("fail_on_qa_failure");
for (const trigger of ["workflow_dispatch", "workflow_call"] as const) {
expect(qaEvidenceWorkflow.on[trigger].inputs.allow_failures).toEqual({
description: "Continue after validated QA result failures",
required: false,
default: false,
type: "boolean",
});
}
expect(qaEvidenceWorkflow.on.workflow_dispatch.inputs.qa_profile).not.toHaveProperty("options");
expect(qaEvidenceWorkflow.on.workflow_dispatch.inputs.qa_profile.default).toBe("all");
expect(qaEvidenceWorkflow.on.workflow_call.inputs.qa_profile.type).toBe("string");
for (const outputName of [
"artifact_name",
"qa_profile",
"qa_exit_code",
"qa_passed",
"target_sha",
"trusted_reason",
"qa_evidence_path",
]) {
expect(qaEvidenceWorkflow.on.workflow_call.outputs[outputName].value).toContain(
`jobs.aggregate_qa_profile.outputs.${outputName}`,
);
}
expect(qaPlanJob.needs).toBe("validate_selected_ref");
expect(qaPlanJob.outputs).toEqual({
channel_driver: "${{ steps.plan.outputs.channel_driver }}",
matrix: "${{ steps.plan.outputs.matrix }}",
profile: "${{ steps.plan.outputs.profile }}",
shard_count: "${{ steps.plan.outputs.shard_count }}",
});
const qaAuthorizeStep = expectDefined(
qaAuthorizeJob.steps.find(
(step: WorkflowStep) => step.name === "Require maintainer-level repository access",
),
"QA workflow actor authorization",
);
expect(qaAuthorizeStep.env).toEqual({
CALLER_WORKFLOW_REF: "${{ github.workflow_ref }}",
JOB_CONTEXT: "${{ toJSON(job) }}",
});
expect(qaAuthorizeStep.with?.script).toContain("callerWorkflowRef !== calledWorkflowRef");
expect(qaAuthorizeStep.with?.script).toContain(
'job.workflow_repository === "openclaw/openclaw"',
);
expect(qaAuthorizeStep.with?.script).toContain("job.workflow_ref === calledWorkflowRef");
expect(qaAuthorizeStep.with?.script).toContain(
'core.setOutput("authorized", trustedMainCaller ? "true" : "false")',
);
expect(qaValidateJob.outputs.workflow_sha).toBe("${{ steps.workflow.outputs.workflow_sha }}");
expect(qaValidateJob.outputs).not.toHaveProperty("workflow_repository");
const workflowIdentityStep = qaValidateJob.steps[0];
expect(workflowIdentityStep).toMatchObject({
name: "Resolve job workflow identity",
id: "workflow",
env: { JOB_CONTEXT: "${{ toJSON(job) }}" },
});
expect(workflowIdentityStep.run).toContain("job.workflow_repository");
expect(workflowIdentityStep.run).toContain("job.workflow_sha");
expect(workflowIdentityStep.run).toContain("^[A-Za-z0-9_.-]+\\/[A-Za-z0-9_.-]+$");
expect(workflowIdentityStep.run).toContain("^[0-9a-f]{40}$");
const selectedCodeSteps = new Map([
[qaPlanJob, ["Build private QA runtime", "Resolve taxonomy profile shards"]],
[
qaShardJob,
[
"Fetch protocol comparison base",
"Build private QA runtime",
"Ensure Playwright Chromium",
"Run QA profile shard",
"Validate QA profile shard evidence",
],
],
[
qaAggregateJob,
[
"Build private QA runtime",
"Aggregate validated shard evidence",
"Finalize QA profile evidence",
],
],
]);
for (const [job, codeStepNames] of selectedCodeSteps) {
expect(job.environment).toBe("qa-live-shared");
const stepIndex = (name: string) =>
job.steps.findIndex((step: WorkflowStep) => step.name === name);
const permissionStep = expectDefined(
job.steps.find((step: WorkflowStep) => step.name === "Require authorized workflow actor"),
"selected QA actor permission check",
);
const trustedCheckout = expectDefined(
job.steps.find((step: WorkflowStep) => step.name === "Checkout trusted QA harness"),
"trusted QA harness checkout",
);
const restoreTrusted = expectDefined(
job.steps.find((step: WorkflowStep) => step.name === "Restore trusted QA harness revision"),
"trusted QA harness revision restore",
);
const setupStep = expectDefined(
job.steps.find((step: WorkflowStep) => step.name === "Setup Node environment"),
"trusted QA harness Node setup",
);
const selectedCheckout = expectDefined(
job.steps.find((step: WorkflowStep) => step.name === "Checkout selected ref"),
"selected QA checkout",
);
const installSelected = expectDefined(
job.steps.find((step: WorkflowStep) => step.name === "Install selected dependencies"),
"selected QA dependency install",
);
expect(permissionStep).toMatchObject({
uses: "actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3",
env: {
CALLER_WORKFLOW_REF: "${{ github.workflow_ref }}",
JOB_CONTEXT: "${{ toJSON(job) }}",
},
});
expect(permissionStep.with?.script).toContain("getCollaboratorPermissionLevel");
expect(permissionStep.with?.script).toContain('new Set(["admin", "maintain", "write"])');
expect(permissionStep.with?.script).toContain("callerWorkflowRef !== calledWorkflowRef");
expect(permissionStep.with?.script).toContain(
'job.workflow_repository === "openclaw/openclaw"',
);
expect(permissionStep.with?.script).toContain("job.workflow_ref === calledWorkflowRef");
expect(permissionStep.with?.script).toContain("if (!trustedMainCaller)");
expect(trustedCheckout).toMatchObject({
name: "Checkout trusted QA harness",
uses: CHECKOUT_V6,
with: {
repository: "openclaw/openclaw",
ref: "main",
"fetch-depth": 1,
"persist-credentials": false,
},
});
const checkoutSteps = job.steps.filter((step: WorkflowStep) =>
step.uses?.startsWith("actions/checkout@"),
);
expect(checkoutSteps).toHaveLength(1);
expect(checkoutSteps[0]?.with).toMatchObject({
repository: "openclaw/openclaw",
ref: "main",
});
expect(restoreTrusted).toMatchObject({
env: {
EXPECTED_WORKFLOW_SHA: "${{ needs.validate_selected_ref.outputs.workflow_sha }}",
},
shell: "bash",
});
expect(restoreTrusted["working-directory"]).toBeUndefined();
expect(restoreTrusted.run).toContain("^[0-9a-f]{40}$");
expect(restoreTrusted.run).toContain(
'python3 -I -S "$CI_GIT_OWNER" --checkout-git 0 fetch --no-tags --no-recurse-submodules --depth=1 origin "$EXPECTED_WORKFLOW_SHA"',
);
expect(restoreTrusted.run).toContain('git checkout --detach "$EXPECTED_WORKFLOW_SHA"');
expect(restoreTrusted.run).toContain(
'test "$(git rev-parse HEAD)" = "$EXPECTED_WORKFLOW_SHA"',
);
expect(job.steps.some((step: WorkflowStep) => step.uses?.startsWith("actions/cache/"))).toBe(
false,
);
expect(setupStep.with?.["install-deps"]).toBe("false");
expect(setupStep.with?.["cache-mode"]).toBe("off");
expect(selectedCheckout).toMatchObject({
env: {
EXPECTED_SHA: "${{ needs.validate_selected_ref.outputs.selected_revision }}",
},
shell: "bash",
});
expect(selectedCheckout).not.toHaveProperty("uses");
expect(selectedCheckout["working-directory"]).toBeUndefined();
expect(selectedCheckout.run).toContain("^[0-9a-f]{40}$");
expect(selectedCheckout.run).toContain("[[ ! -e selected ]]");
expect(selectedCheckout.run).toContain("git init selected");
expect(selectedCheckout.run).toContain(
'git -C selected remote add origin "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY"',
);
expect(selectedCheckout.run).toContain(
'cd selected\npython3 -I -S "$CI_GIT_OWNER" --checkout-git 0 fetch --no-tags --no-recurse-submodules --depth=1 origin "$EXPECTED_SHA"',
);
expect(selectedCheckout.run).toContain("git checkout --detach FETCH_HEAD");
expect(selectedCheckout.run).toContain('test "$(git rev-parse HEAD)" = "$EXPECTED_SHA"');
expect(
job.steps.some((step: WorkflowStep) => step.name === "Verify selected checkout SHA"),
).toBe(false);
expect(installSelected["working-directory"]).toBe("selected");
expect(installSelected.run).toContain(
'--store-dir "$RUNNER_TEMP/openclaw-qa-selected-pnpm-store"',
);
for (const installFlag of [
"--frozen-lockfile",
"--config.ignore-scripts=false",
"--config.engine-strict=false",
"--config.enable-pre-post-scripts=true",
"--config.side-effects-cache=true",
]) {
expect(installSelected.run).toContain(installFlag);
}
const securitySequence = [
"Require authorized workflow actor",
"Prepare Git owner",
"Checkout trusted QA harness",
"Restore trusted QA harness revision",
"Setup Node environment",
"Checkout selected ref",
"Install selected dependencies",
];
expect(
job.steps.slice(0, securitySequence.length).map((step: WorkflowStep) => step.name),
).toEqual(securitySequence);
const ordered = securitySequence.map(stepIndex);
expect(ordered.every((index, position) => index > (ordered[position - 1] ?? -1))).toBe(true);
for (const codeStepName of codeStepNames) {
const codeStep = expectDefined(
job.steps.find((step: WorkflowStep) => step.name === codeStepName),
`selected QA step ${codeStepName}`,
);
expect(codeStep["working-directory"], codeStepName).toBe("selected");
}
}
const validateProfileStep = qaPlanJob.steps.find(
(step: WorkflowStep) => step.name === "Resolve taxonomy profile shards",
);
expect(validateProfileStep.run).toContain("createQaProfileEvidenceShardPlan(requested)");
expect(validateProfileStep.run).toContain("matrix=${JSON.stringify({ include: plan.shards })}");
expect(validateProfileStep.run).toContain("shard_count=${plan.shards.length}");
expect(qaShardJob["timeout-minutes"]).toBe(150);
expect(qaShardJob.needs).toEqual(["validate_selected_ref", "plan_qa_profile"]);
expect(qaShardJob.strategy).toMatchObject({
"fail-fast": false,
"max-parallel": 8,
matrix: "${{ fromJSON(needs.plan_qa_profile.outputs.matrix) }}",
});
const ensurePlaywrightStep = qaShardJob.steps.find(
(step: WorkflowStep) => step.name === "Ensure Playwright Chromium",
);
expect(ensurePlaywrightStep.run).toContain("scripts/ensure-playwright-chromium.mts");
expect(ensurePlaywrightStep.run).toContain("scripts/ensure-playwright-chromium.mjs");
const prepareSandboxStep = expectDefined(
qaShardJob.steps.find(
(step: WorkflowStep) => step.name === "Prepare Docker sandbox image when selected",
),
"QA sandbox image preparation",
);
expect(prepareSandboxStep["working-directory"]).toBe("selected");
expect(prepareSandboxStep.env?.SCENARIO_IDS_JSON).toBe("${{ toJSON(matrix.scenarioIds) }}");
expect(prepareSandboxStep.run).toBe(`set -euo pipefail
if jq -e '
index("openclaw-sandbox-workspace-isolation") != null or
index("agent-sandboxed-exec-behavior") != null
' <<<"$SCENARIO_IDS_JSON" >/dev/null; then
scripts/sandbox-setup.sh
fi
`);
expect(qaShardJob.steps.indexOf(prepareSandboxStep)).toBeLessThan(
qaShardJob.steps.findIndex((step: WorkflowStep) => step.name === "Run QA profile shard"),
);
const runProfileStep = qaShardJob.steps.find(
(step: WorkflowStep) => step.name === "Run QA profile shard",
);
expect(runProfileStep.env?.OPENCLAW_QA_ALLOW_UPDATE_RUN_SELF).toBe("1");
expect(runProfileStep.env?.OPENCLAW_QA_CREDENTIAL_ACQUIRE_TIMEOUT_MS).toBe("120000");
expect(runProfileStep.env?.PROTOCOL_SINCE_BASE_SHA).toBe(
"${{ needs.validate_selected_ref.outputs.protocol_base_revision }}",
);
expect(runProfileStep.env?.REQUESTED_REF).toBe("${{ inputs.trusted_ref || inputs.ref }}");
expect(runProfileStep.env?.TARGET_SHA).toBe(
"${{ needs.validate_selected_ref.outputs.selected_revision }}",
);
expect(runProfileStep.run).toContain("--concurrency 3");
expect(runProfileStep.run).toContain("--fast");
expect(runProfileStep.run).toContain('qa_output_dir=".artifacts/qa-e2e/');
expect(runProfileStep.run).toContain(
'published_output_dir="${GITHUB_WORKSPACE}/selected/${qa_output_dir}"',
);
expect(runProfileStep.run).toContain('mkdir -p "$qa_output_dir"');
expect(runProfileStep.run).toContain('echo "output_dir=${published_output_dir}"');
expect(runProfileStep.run).toContain('--output-dir "$qa_output_dir"');
expect(runProfileStep.run).toContain('OUTPUT_DIR="$published_output_dir"');
expect(runProfileStep.run.indexOf('mkdir -p "$qa_output_dir"')).toBeLessThan(
runProfileStep.run.indexOf('echo "output_dir=${published_output_dir}"'),
);
expect(runProfileStep.run).toContain(
"LC_ALL=C timeout --verbose --signal=TERM --kill-after=30s 110m",
);
expect(runProfileStep.run).toContain("qa_exit_code=$?");
expect(runProfileStep.run).toContain('timeout_child_env+=("LC_ALL=$LC_ALL")');
expect(runProfileStep.run).toContain('timeout_child_env+=("-u" "LC_ALL")');
expect(runProfileStep.run).toContain(`bash -c 'exec "$@" 2>&3' bash`);
expect(runProfileStep.run).toContain('3>&2 2>"$timeout_supervisor_fifo"');
expect(runProfileStep.run).toContain('mkfifo "$timeout_supervisor_fifo"');
expect(runProfileStep.run).toContain(
'tee "$timeout_supervisor_log" <"$timeout_supervisor_fifo" >&2 &',
);
expect(runProfileStep.run).toContain("supervisor_tee_pid=$!");
expect(runProfileStep.run).toContain("trap cleanup_timeout_supervisor EXIT");
expect(runProfileStep.run).toContain(
'rm -f "$timeout_supervisor_fifo" "$timeout_supervisor_log"',
);
expect(runProfileStep.run).not.toContain(">(tee");
const teeWait = runProfileStep.run.indexOf('wait "$supervisor_tee_pid"');
const timeoutClassification = runProfileStep.run.indexOf(
'grep -Eq "^timeout: sending signal KILL',
);
expect(teeWait).toBeGreaterThan(-1);
expect(teeWait).toBeLessThan(timeoutClassification);
expect(runProfileStep.run).toContain(
`[[ "$qa_exit_code" -eq 137 ]] && grep -Eq "^timeout: sending signal KILL to command '[A-Za-z0-9_./+-]+'$"`,
);
expect(runProfileStep.run).toContain(
`[[ "$qa_exit_code" -eq 124 ]] && grep -Eq "^timeout: sending signal TERM to command '[A-Za-z0-9_./+-]+'$"`,
);
expect(runProfileStep.run).not.toContain('case "$qa_exit_code"');
expect(runProfileStep.run).toContain('TIMEOUT_OUTCOME="$timeout_outcome"');
expect(runProfileStep.run).toContain("qa-profile-run-status.json");
expect(runProfileStep.run).toContain("protocolBaseSha: process.env.PROTOCOL_SINCE_BASE_SHA");
expect(runProfileStep.run).toContain("exitCode: Number(process.env.QA_EXIT_CODE)");
expect(runProfileStep.run).toContain('timedOut: process.env.TIMEOUT_OUTCOME !== "none"');
expect(runProfileStep.run).toContain("timeoutOutcome: process.env.TIMEOUT_OUTCOME");
expect(runProfileStep.run).toContain("completedAt: new Date().toISOString()");
expect(runProfileStep.run).toContain("id: process.env.QA_SHARD_ID");
expect(runProfileStep.run).toContain("scenarioIds: JSON.parse(process.env.SCENARIO_IDS_JSON)");
expect(runProfileStep.run).not.toContain("--allow-failures");
const shardEvidenceStep = qaShardJob.steps.find(
(step: WorkflowStep) => step.name === "Validate QA profile shard evidence",
);
expect(shardEvidenceStep.if).toBe("always()");
expect(shardEvidenceStep.run).toContain("qaProfileEvidencePlan.attest");
const shardUploadStep = qaShardJob.steps.find(
(step: WorkflowStep) => step.name === "Upload QA profile shard evidence",
);
expect(shardUploadStep.if).toBe("always()");
expect(shardUploadStep.with).toMatchObject({
name: "qa-profile-evidence-shard-${{ matrix.id }}-${{ needs.validate_selected_ref.outputs.selected_revision }}",
path: "${{ steps.run_profile.outputs.output_dir }}",
"if-no-files-found": "error",
});
expect(qaAggregateJob.needs).toEqual([
"validate_selected_ref",
"plan_qa_profile",
"run_qa_profile_shard",
]);
expect(qaAggregateJob.if.replace(/\s+/gu, " ")).toBe(
"${{ always() && needs.validate_selected_ref.result == 'success' && needs.plan_qa_profile.result == 'success' }}",
);
const aggregateDownloadStep = qaAggregateJob.steps.find(
(step: WorkflowStep) => step.name === "Download QA profile shard evidence",
);
expect(aggregateDownloadStep.with).toMatchObject({
pattern:
"qa-profile-evidence-shard-*-${{ needs.validate_selected_ref.outputs.selected_revision }}",
path: "selected/.artifacts/qa-profile-shards",
"merge-multiple": false,
});
const aggregateStep = qaAggregateJob.steps.find(
(step: WorkflowStep) => step.name === "Aggregate validated shard evidence",
);
expect(aggregateStep.run).toContain(
"Expected ${SHARD_COUNT} completed status and evidence files",
);
expect(aggregateStep.run).toContain("Timed-out QA shard cannot contribute partial evidence");
expect(aggregateStep.run).toContain("-mindepth 2 -maxdepth 2");
expect(aggregateStep.run).toContain("aggregateQaProfileEvidenceShards");
expect(aggregateStep.run).toContain(
`jq -s --argjson exitCode "$qa_exit_code" 'map(.shard + {})' "\${status_paths[@]}" >/dev/null`,
);
expect(aggregateStep.run).toContain("if jq -e '.timedOut == true'");
expect(aggregateStep.env?.OUTPUT_DIR).toContain(
"${{ github.workspace }}/selected/.artifacts/qa-e2e/",
);
const aggregateUploadStep = qaAggregateJob.steps.find(
(step: WorkflowStep) => step.name === "Upload QA profile evidence",
);
expect(aggregateUploadStep.with?.path).toBe("${{ steps.aggregate.outputs.output_dir }}");
const diagnosticStep = qaAggregateJob.steps.find(
(step: WorkflowStep) => step.name === "Collect QA profile diagnostics",
);
expect(diagnosticStep.if).toBe("always()");
expect(diagnosticStep["continue-on-error"]).toBe(true);
expect(diagnosticStep).not.toHaveProperty("working-directory");
expect(diagnosticStep.run).toContain('test "$(git rev-parse HEAD)" = "$EXPECTED_WORKFLOW_SHA"');
expect(diagnosticStep.run).toContain("node scripts/qa/qa-profile-run-status.mjs");
expect(diagnosticStep.env.EXPECTED_WORKFLOW_SHA).toBe(
"${{ needs.validate_selected_ref.outputs.workflow_sha }}",
);
expect(diagnosticStep.env.PLAN_MATRIX_JSON).toBe("${{ needs.plan_qa_profile.outputs.matrix }}");
expect(diagnosticStep.env.QA_EXIT_CODE).toBe("${{ steps.aggregate.outputs.qa_exit_code }}");
expect(diagnosticStep.env.FINALIZE_OUTCOME).toBe("${{ steps.evidence.outcome }}");
const diagnosticUpload = qaAggregateJob.steps.find(
(step: WorkflowStep) => step.name === "Upload QA profile diagnostics",
);
expect(diagnosticUpload.if).toBe("always()");
expect(diagnosticUpload["continue-on-error"]).toBe(true);
expect(diagnosticUpload.with.name).toBe(
"qa-profile-diagnostics-${{ needs.plan_qa_profile.outputs.profile }}-${{ needs.validate_selected_ref.outputs.selected_revision }}-${{ github.run_id }}-${{ github.run_attempt }}",
);
expect(diagnosticUpload.with.name).not.toMatch(/^qa-profile-evidence-/u);
expect(diagnosticUpload.with.path).toBe(
`${diagnosticStep.env.OUTPUT_DIR}/qa-profile-run-status.json`,
);
expect(diagnosticUpload.with["if-no-files-found"]).toBe("warn");
const finalizerIndex = qaAggregateJob.steps.findIndex(
(step: WorkflowStep) => step.name === "Finalize QA profile evidence",
);
expect(qaAggregateJob.steps.indexOf(diagnosticStep)).toBeGreaterThan(finalizerIndex);
expect(qaAggregateJob.steps.indexOf(diagnosticUpload)).toBeLessThan(
qaAggregateJob.steps.indexOf(aggregateUploadStep),
);
expect(JSON.stringify(qaAggregateJob.outputs)).not.toContain("diagnostics");
const diagnosticWarning = qaAggregateJob.steps.find(
(step: WorkflowStep) => step.name === "Warn if QA profile diagnostics were not retained",
);
expect(diagnosticWarning.if).toBe(
"always() && (steps.collect_diagnostics.outcome == 'failure' || steps.upload_diagnostics.outcome == 'failure')",
);
const failProfileStep = qaAggregateJob.steps.find(
(step: WorkflowStep) => step.name === "Fail if QA profile failed",
);
expect(failProfileStep.env?.ALLOW_FAILURES).toBe("${{ inputs.allow_failures }}");
expect(failProfileStep.run).toContain('[[ -z "${QA_EXIT_CODE:-}" ]]');
expect(failProfileStep.run).toContain(
'[[ "$QA_EXIT_CODE" != "0" && "$ALLOW_FAILURES" != "true" ]]',
);
expect(failProfileStep.run).toContain('exit "$QA_EXIT_CODE"');
expect(generateJob.needs).toEqual(["validate_selected_ref", "publisher_preflight"]);
expect(generateJob.if.replace(/\s+/gu, " ")).toBe(
"${{ always() && needs.validate_selected_ref.result == 'success' && (!inputs.publish_pull_request || needs.publisher_preflight.result == 'success') && inputs.qa_evidence_run_id == '' }}",
);
expect(generateJob.uses).toBe("./.github/workflows/qa-profile-evidence.yml");
expect(generateJob.with).toMatchObject({
ref: "${{ needs.validate_selected_ref.outputs.selected_revision }}",
trusted_ref: "${{ inputs.ref }}",
expected_sha: "${{ needs.validate_selected_ref.outputs.selected_revision }}",
qa_profile: "all",
allow_failures: "${{ inputs.allow_failures }}",
});
expect(generateJob.with).not.toHaveProperty("fail_on_qa_failure");
expect(generateJob.secrets).toMatchObject({
OPENAI_API_KEY: "${{ secrets.OPENAI_API_KEY }}",
OPENCLAW_QA_CONVEX_SECRET_CI: "${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}",
OPENCLAW_QA_CONVEX_SITE_URL: "${{ secrets.OPENCLAW_QA_CONVEX_SITE_URL }}",
});
const maturityPermissionStep = expectDefined(
maturityWorkflow.jobs.validate_selected_ref.steps.find(
(step: WorkflowStep) => step.name === "Require authorized workflow actor",
),
"maturity workflow actor authorization",
);
const workflowStep = maturityWorkflow.jobs.validate_selected_ref.steps.find(
(step: WorkflowStep) => step.name === "Resolve job workflow identity",
);
const authorizeStep = maturityWorkflow.jobs.validate_selected_ref.steps.find(
(step: WorkflowStep) => step.name === "Authorize workflow invocation",
);
const validateRefStep = maturityWorkflow.jobs.validate_selected_ref.steps.find(
(step: WorkflowStep) => step.name === "Validate selected ref",
);
expect(maturityPermissionStep).toMatchObject({
uses: "actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3",
env: {
CALLER_WORKFLOW_REF: "${{ github.workflow_ref }}",
JOB_CONTEXT: "${{ toJSON(job) }}",
},
});
expect(maturityPermissionStep.with?.script).toContain("getCollaboratorPermissionLevel");
expect(maturityPermissionStep.with?.script).toContain(
"callerWorkflowRef !== calledWorkflowRef",
);
expect(maturityPermissionStep.with?.script).toContain(`"${MATURITY_SCORECARD_WORKFLOW_REF}"`);
expect(maturityPermissionStep.with?.script).toContain(
'job.workflow_repository === "openclaw/openclaw"',
);
expect(maturityPermissionStep.with?.script).toContain("job.workflow_ref === calledWorkflowRef");
expect(workflowStep.env.JOB_CONTEXT).toBe("${{ toJSON(job) }}");
expect(workflowStep.run).toContain("job.workflow_sha must be a full lowercase commit SHA");
expect(authorizeStep.env).toEqual({
CALLER_EVENT_NAME: "${{ github.event_name }}",
CALLER_WORKFLOW_REF: "${{ github.workflow_ref }}",
JOB_WORKFLOW_FILE_PATH: "${{ steps.workflow.outputs.workflow_file_path }}",
JOB_WORKFLOW_REF: "${{ steps.workflow.outputs.workflow_ref }}",
JOB_WORKFLOW_REPOSITORY: "${{ steps.workflow.outputs.workflow_repository }}",
PUBLISH_PULL_REQUEST: "${{ inputs.publish_pull_request || false }}",
});
expect(authorizeStep.run).toContain(
`expected_workflow_ref="${MATURITY_SCORECARD_WORKFLOW_REF}"`,
);
expect(authorizeStep.run).toContain(
'[[ "$PUBLISH_PULL_REQUEST" == "true" && "$canonical_direct" != "true" ]]',
);
expect(authorizeStep.run).toContain(
"Reusable maturity workflows are artifact-only and cannot publish pull requests.",
);
expect(validateRefStep.env.EXPECTED_SHA).toBe("${{ inputs.expected_sha }}");
expect(validateRefStep.env.PUBLISH_PULL_REQUEST).toBe("${{ inputs.publish_pull_request }}");
expect(validateRefStep.env).not.toHaveProperty("TRUSTED_WORKFLOW_SHA");
expect(validateRefStep.env.EVIDENCE_RUN_ID).toBe(
"${{ inputs.qa_evidence_run_id || github.run_id }}",
);
for (const fragment of [
"expected_sha must be a full 40-character SHA",
'input_ref.removeprefix("refs/heads/")',
"floating_default_branch = False",
'not expected_sha.replace(" ", "") and branch_candidate == default_branch',
'selected_revision = revision("refs/remotes/origin/main")',
"floating_default_branch and publication_base == default_branch",
"if code != 2:",
"Unable to determine whether '{input_ref}' is a remote branch",
'probe("merge-base", "--is-ancestor", selected_revision',
'":(exclude)qa/maturity-scores.yaml"',
'":(exclude)docs/maturity/scorecard.md"',
'":(exclude)docs/maturity/taxonomy.md"',
"qa_evidence_run_id must be a numeric GitHub Actions run id",
'publication_head = f"automation/maturity-scorecard-',
]) {
expect(validateRefStep.run).toContain(fragment);
}
expect(maturityWorkflow.jobs.validate_selected_ref.outputs).toMatchObject({
publication_base: "${{ steps.validate.outputs.publication_base }}",
publication_head: "${{ steps.validate.outputs.publication_head }}",
workflow_file_path: "${{ steps.workflow.outputs.workflow_file_path }}",
workflow_ref: "${{ steps.workflow.outputs.workflow_ref }}",
workflow_repository: "${{ steps.workflow.outputs.workflow_repository }}",
workflow_sha: "${{ steps.workflow.outputs.workflow_sha }}",
});
const trustedPublisherCondition = [
"${{ inputs.publish_pull_request &&",
"github.event_name == 'workflow_dispatch' &&",
`github.workflow_ref == '${MATURITY_SCORECARD_WORKFLOW_REF}' &&`,
`needs.validate_selected_ref.outputs.workflow_file_path == '${MATURITY_SCORECARD_WORKFLOW}' &&`,
`needs.validate_selected_ref.outputs.workflow_ref == '${MATURITY_SCORECARD_WORKFLOW_REF}' &&`,
"needs.validate_selected_ref.outputs.workflow_repository == 'openclaw/openclaw' }}",
].join(" ");
expect(publisherPreflight.needs).toBe("validate_selected_ref");
expect(publisherPreflight.if).toBe("${{ inputs.publish_pull_request }}");
const preflightCheckoutStep = publisherPreflight.steps.find(
(step: WorkflowStep) => step.name === "Checkout trusted workflow source",
);
const preflightTokensStep = publisherPreflight.steps.find(
(step: WorkflowStep) => step.name === "Create generated PR tokens",
);
expect(preflightCheckoutStep).toMatchObject({
uses: CHECKOUT_V6,
with: {
repository: "${{ needs.validate_selected_ref.outputs.workflow_repository }}",
ref: "${{ needs.validate_selected_ref.outputs.workflow_sha }}",
"persist-credentials": false,
submodules: false,
},
});
expect(preflightTokensStep.if.replace(/\s+/gu, " ")).toBe(trustedPublisherCondition);
expect(preflightTokensStep).toMatchObject({
uses: "./.github/actions/create-generated-pr-tokens",
with: {
"contents-client-id": "Iv23liOECG0slfuhz093",
"contents-private-key": "${{ secrets.CLAWSWEEPER_APP_PRIVATE_KEY }}",
"pull-request-client-id": MANTIS_GITHUB_APP_CLIENT_ID,
"pull-request-private-key": "${{ secrets.MANTIS_GITHUB_APP_PRIVATE_KEY }}",
},
});
expect(publishJob.needs).toEqual([
"validate_selected_ref",
"publisher_preflight",
"generate_qa_evidence",
]);
expect(publishJob.if.replace(/\s+/gu, " ")).toBe(
"${{ always() && needs.validate_selected_ref.result == 'success' && (!inputs.publish_pull_request || needs.publisher_preflight.result == 'success') && (inputs.qa_evidence_run_id != '' || needs.generate_qa_evidence.result == 'success') }}",
);
expect(JSON.stringify(publishJob)).not.toMatch(
/CLAWSWEEPER_APP_PRIVATE_KEY|MANTIS_GITHUB_APP/u,
);
const generatedDownloadStep = publishJob.steps.find(
(step: WorkflowStep) => step.name === "Download generated QA evidence artifact",
);
expect(generatedDownloadStep.if).toBe("${{ inputs.qa_evidence_run_id == '' }}");
expect(generatedDownloadStep.env.GENERATED_ARTIFACT_NAME).toBe(
"${{ needs.generate_qa_evidence.outputs.artifact_name }}",
);
expect(generatedDownloadStep.run).toContain('gh run download "$GITHUB_RUN_ID"');
expect(generatedDownloadStep.run).toContain('--name "$GENERATED_ARTIFACT_NAME"');
expect(generatedDownloadStep.run).not.toContain("--pattern");
const requireEvidenceStep = publishJob.steps.find(
(step: WorkflowStep) => step.name === "Require one QA evidence file",
);
expect(requireEvidenceStep.run).toContain(
"Expected exactly one aggregate QA evidence manifest",
);
expect(requireEvidenceStep.run).toContain("qa-profile-evidence-manifest.json");
expect(requireEvidenceStep.run).toContain(
'evidence_path="$(dirname "${manifest_paths[0]}")/qa-evidence.json"',
);
expect(requireEvidenceStep.run).toContain('[[ ! -f "$evidence_path" || -L "$evidence_path" ]]');
const validateManifestStep = publishJob.steps.find(
(step: WorkflowStep) => step.name === "Validate QA evidence manifest",
);
expect(validateManifestStep.id).toBe("validate_evidence");
expect(validateManifestStep.run).toContain("qa-profile-evidence-manifest.json");
expect(validateManifestStep.run).toContain("qa-evidence.json profile must be all");
expect(validateManifestStep.run).toContain("QA evidence manifest profile must be all");
expect(validateManifestStep.run).toContain("manifest.targetSha !== targetSha");
expect(validateManifestStep.run).toMatch(
/qaProfileEvidencePlan\.attest\(\s*evidence\.profilePlan,\s*manifest\.qaPassed === true,\s*evidence,?\s*\)/u,
);
expect(validateManifestStep.run).toContain("profilePlanSha256");
expect(validateManifestStep.run).toContain("rerun the QA Profile Evidence workflow");
expect(validateManifestStep.run).toContain("counts.fail === 0 && counts.blocked === 0");
expect(validateManifestStep.run).toContain("scorecard_passed=");
expect(validateManifestStep.run).toContain("### Maturity scorecard result");
expect(publishJob.outputs).toEqual({
blocked_count: "${{ steps.validate_evidence.outputs.blocked_count }}",
failed_count: "${{ steps.validate_evidence.outputs.failed_count }}",
scorecard_passed: "${{ steps.validate_evidence.outputs.scorecard_passed }}",
});
expect(qaAggregateJob.outputs.artifact_name).toBe(
"${{ steps.evidence.outputs.artifact_name }}",
);
const qaEvidenceStep = qaAggregateJob.steps.find(
(step: WorkflowStep) => step.name === "Finalize QA profile evidence",
);
expect(qaEvidenceStep.env.ARTIFACT_NAME).toBe(
"qa-profile-evidence-${{ needs.plan_qa_profile.outputs.profile }}-${{ needs.validate_selected_ref.outputs.selected_revision }}",
);
expect(qaEvidenceStep.run).toContain("qa-profile-evidence-manifest.json");
expect(qaEvidenceStep.run).toContain("validateQaEvidenceSummaryJson");
expect(qaEvidenceStep.run).toMatch(
/qaProfileEvidencePlan\.attest\(\s*payload\.profilePlan,\s*process\.env\.QA_EXIT_CODE === "0",?\s*\)/u,
);
expect(qaEvidenceStep.run).toContain("profilePlanSha256");
expect(qaEvidenceStep.env.PROTOCOL_BASE_SHA).toBe(
"${{ needs.validate_selected_ref.outputs.protocol_base_revision }}",
);
expect(qaEvidenceStep.env.REQUESTED_REF).toBe("${{ inputs.trusted_ref || inputs.ref }}");
expect(qaEvidenceStep.env.ALLOW_FAILURES).toBe("${{ inputs.allow_failures }}");
expect(qaEvidenceStep.run).toContain("qaExitCode: Number(process.env.QA_EXIT_CODE)");
expect(qaEvidenceStep.run).toContain('qaPassed: process.env.QA_EXIT_CODE === "0"');
expect(qaEvidenceStep.run).toContain('allowFailures: process.env.ALLOW_FAILURES === "true"');
expect(qaEvidenceStep.run).toContain("protocolBaseSha: process.env.PROTOCOL_BASE_SHA");
const qaUploadStep = qaAggregateJob.steps.find(
(step: WorkflowStep) => step.name === "Upload QA profile evidence",
);
expect(qaUploadStep.if).toBe("always() && steps.evidence.outcome == 'success'");
expect(qaUploadStep.with).toMatchObject({
name: "qa-profile-evidence-${{ needs.plan_qa_profile.outputs.profile }}-${{ needs.validate_selected_ref.outputs.selected_revision }}",
path: "${{ steps.aggregate.outputs.output_dir }}",
"if-no-files-found": "error",
});
const renderCheckoutStep = publishJob.steps.find(
(step: WorkflowStep) => step.name === "Checkout selected ref",
);
const generatedPrUploadStep = publishJob.steps.find(
(step: WorkflowStep) => step.name === "Upload generated PR files",
);
expect(renderCheckoutStep.with["fetch-depth"]).toBe(0);
expect(generatedPrUploadStep).toMatchObject({
if: "${{ inputs.publish_pull_request }}",
uses: UPLOAD_ARTIFACT_V7,
with: {
name: "maturity-scorecard-pr-${{ github.run_id }}-${{ github.run_attempt }}",
"retention-days": 1,
"if-no-files-found": "error",
},
});
expect(generatedPrUploadStep.with.path.trim().split("\n")).toEqual(MATURITY_GENERATED_PR_PATHS);
const prepareRenderEvidenceStep = publishJob.steps.find(
(step: WorkflowStep) => step.name === "Prepare aggregate QA evidence for rendering",
);
expect(prepareRenderEvidenceStep.env.QA_EVIDENCE_PATH).toBe(
"${{ steps.evidence.outputs.qa_evidence_path }}",
);
expect(prepareRenderEvidenceStep.run).toContain(
'render_evidence_dir=".artifacts/maturity-render-evidence"',
);
expect(prepareRenderEvidenceStep.run).toContain(
'install -m 0644 "$QA_EVIDENCE_PATH" "$render_evidence_dir/qa-evidence.json"',
);
for (const stepName of ["Render artifact docs", "Render committed docs preview"]) {
const renderStep = publishJob.steps.find((step: WorkflowStep) => step.name === stepName);
expect(renderStep.env.ALLOW_FAILURES).toBe("${{ inputs.allow_failures }}");
expect(renderStep.run).toContain('[[ "$ALLOW_FAILURES" == "true" ]]');
expect(renderStep.run).toContain("allow_failures_args+=(--allow-failures)");
expect(renderStep.run).toContain("--evidence-dir .artifacts/maturity-render-evidence");
expect(renderStep.run).not.toContain("--evidence-dir .artifacts/maturity-evidence");
expect(renderStep.run).toContain('"${allow_failures_args[@]}"');
}
const renderArtifactStep = publishJob.steps.find(
(step: WorkflowStep) => step.name === "Render artifact docs",
);
expect(renderArtifactStep.run).toContain("QA failures allowed:");
expect(publishPrJob.needs).toEqual(["validate_selected_ref", "publisher_preflight", "publish"]);
expect(publishPrJob["runs-on"]).toBe("ubuntu-24.04");
expect(publishPrJob.permissions).toEqual({ actions: "read", contents: "read" });
for (const fragment of [
"needs.publisher_preflight.result == 'success'",
"needs.publish.result == 'success'",
`github.workflow_ref == '${MATURITY_SCORECARD_WORKFLOW_REF}'`,
`needs.validate_selected_ref.outputs.workflow_ref == '${MATURITY_SCORECARD_WORKFLOW_REF}'`,
]) {
expect(publishPrJob.if).toContain(fragment);
}
expect(publishPrJob.if).not.toContain("needs.publish.outputs.scorecard_passed");
const resultJob = maturityWorkflow.jobs.maturity_result;
expect(resultJob.needs).toEqual(["publish", "publish_generated_pr"]);
expect(resultJob.if.replace(/\s+/gu, " ")).toBe(
"${{ always() && needs.publish.result == 'success' && (needs.publish_generated_pr.result == 'success' || needs.publish_generated_pr.result == 'skipped') }}",
);
const resultGateStep = resultJob.steps.find(
(step: WorkflowStep) => step.name === "Fail incomplete maturity evidence",
);
expect(resultGateStep.env).toEqual({
BLOCKED_COUNT: "${{ needs.publish.outputs.blocked_count }}",
FAILED_COUNT: "${{ needs.publish.outputs.failed_count }}",
SCORECARD_PASSED: "${{ needs.publish.outputs.scorecard_passed }}",
});
expect(resultGateStep.run).toContain('[[ "$SCORECARD_PASSED" != "true" ]]');
expect(resultGateStep.run).toContain(
"Generated maturity PR was still published when requested.",
);
const trustedPublishCheckoutStep = publishPrJob.steps.find(
(step: WorkflowStep) => step.name === "Checkout trusted workflow source",
);
const selectedCheckoutStep = publishPrJob.steps.find(
(step: WorkflowStep) => step.name === "Checkout selected ref",
);
const downloadPrFilesStep = publishPrJob.steps.find(
(step: WorkflowStep) => step.name === "Download generated PR files",
);
const openDocsPrStep = publishPrJob.steps.find(
(step: WorkflowStep) => step.name === "Open or update generated docs PR",
);
expect(trustedPublishCheckoutStep).toMatchObject({
uses: CHECKOUT_V6,
with: {
repository: "${{ needs.validate_selected_ref.outputs.workflow_repository }}",
ref: "${{ needs.validate_selected_ref.outputs.workflow_sha }}",
"persist-credentials": false,
},
});
expect(selectedCheckoutStep).toMatchObject({
uses: CHECKOUT_V6,
with: {
ref: "${{ needs.validate_selected_ref.outputs.selected_revision }}",
path: "selected",
"fetch-depth": 0,
"persist-credentials": false,
},
});
expect(downloadPrFilesStep).toMatchObject({
uses: DOWNLOAD_ARTIFACT_V8,
with: {
name: "maturity-scorecard-pr-${{ github.run_id }}-${{ github.run_attempt }}",
path: "${{ steps.staging.outputs.path }}",
},
});
expect(openDocsPrStep.if.replace(/\s+/gu, " ")).toBe(trustedPublisherCondition);
expect(openDocsPrStep.uses).toBe("./.github/actions/publish-generated-pr");
expect(openDocsPrStep.with).toMatchObject({
"contents-client-id": "Iv23liOECG0slfuhz093",
"contents-private-key": "${{ secrets.CLAWSWEEPER_APP_PRIVATE_KEY }}",
"pull-request-client-id": MANTIS_GITHUB_APP_CLIENT_ID,
"pull-request-private-key": "${{ secrets.MANTIS_GITHUB_APP_PRIVATE_KEY }}",
"base-branch": "${{ needs.validate_selected_ref.outputs.publication_base }}",
"head-branch": "${{ needs.validate_selected_ref.outputs.publication_head }}",
"working-directory": "selected",
"commit-message": "docs: update maturity scorecard",
"pr-title": "docs: update maturity scorecard",
"invalidation-paths": "",
"overlap-policy": "fail",
});
expect(openDocsPrStep.with["generated-paths"].trim().split("\n")).toEqual(
MATURITY_GENERATED_PR_PATHS,
);
for (const heading of [
"## What Problem This Solves",
"## Why This Change Was Made",
"## User Impact",
"## Evidence",
]) {
expect(openDocsPrStep.with["pr-body"]).toContain(heading);
}
expect(publishPrJob.steps).not.toContainEqual(
expect.objectContaining({ name: "Create generated docs PR app token" }),
);
const maturityWorkflowSource = readFileSync(".github/workflows/maturity-scorecard.yml", "utf8");
expect(maturityWorkflowSource).not.toContain("permission-pull-requests: write");
expect(maturityWorkflowSource).not.toContain("GH_APP_PRIVATE_KEY");
expect(maturityWorkflowSource).not.toContain("gh auth setup-git");
expect(maturityWorkflowSource).not.toContain("git push --force-with-lease");
});
it.skipIf(process.platform === "win32")(
"Mantis evidence readers project a passing retry from the canonical owner",
() => {
for (const kind of ["status-reactions", "thread-attachment"] as const) {
const scenarioId =
kind === "status-reactions"
? "discord-status-reactions-tool-only"
: "discord-thread-reply-filepath-attachment";
const evidence = workflowOccurrenceEvidence([
{ scenarioId, attempts: ["fail", "pass"], selected: 1 },
]);
const result = runMantisEvidenceReader(kind, evidence);
expect(result.status, `${result.stdout}${result.stderr}`).toBe(0);
expect(result.stdout.trim()).toBe("pass");
}
},
);
it.skipIf(process.platform === "win32").each(["full", "slim"] as const)(
"Mantis evidence readers preserve v2 %s rows without new API exports",
(evidenceMode) => {
for (const kind of ["status-reactions", "thread-attachment"] as const) {
const scenarioId =
kind === "status-reactions"
? "discord-status-reactions-tool-only"
: "discord-thread-reply-filepath-attachment";
const evidence = {
kind: "openclaw.qa.evidence-summary",
schemaVersion: 2,
generatedAt: "2026-08-05T00:00:00.000Z",
evidenceMode,
entries: ["fail", "pass"].map((status) => ({
test: { kind: "scenario", id: scenarioId, title: scenarioId },
coverage: [],
result: { status },
})),
};
const result = runMantisEvidenceReader(kind, evidence, false);
expect(result.status, `${result.stdout}${result.stderr}`).toBe(0);
expect(result.stdout.trim()).toBe(kind === "status-reactions" ? "fail" : "fail\npass");
}
},
);
it.skipIf(process.platform === "win32").each([
{ name: "unresolved first", firstId: null, attempts: [], selected: undefined, first: "null" },
{
name: "foreign first owner",
firstId: "another-scenario",
attempts: ["pass"],
selected: 0,
first: "null",
},
{
name: "nonpassing retry",
firstId: null,
attempts: ["fail", "blocked"],
selected: 0,
first: "fail",
},
] as const)("Mantis evidence readers retain $name and independent instances", (testCase) => {
for (const kind of ["status-reactions", "thread-attachment"] as const) {
const scenarioId =
kind === "status-reactions"
? "discord-status-reactions-tool-only"
: "discord-thread-reply-filepath-attachment";
const evidence = workflowOccurrenceEvidence(
[
{
scenarioId: testCase.firstId ?? scenarioId,
attempts: [...testCase.attempts],
selected: testCase.selected,
},
{ scenarioId, attempts: ["pass"], selected: 0 },
],
"slim",
);
const result = runMantisEvidenceReader(kind, evidence);
expect(result.status, `${result.stdout}${result.stderr}`).toBe(0);
expect(result.stdout.trim()).toBe(
kind === "status-reactions"
? testCase.first
: testCase.firstId
? "pass"
: `${testCase.first}\npass`,
);
}
});
it
.skipIf(process.platform === "win32")
.each(["invalid JSON", "null", "invalid binding", "missing v3 accessor"])(
"Mantis evidence readers reject %s without legacy fallback",
(invalid) => {
for (const kind of ["status-reactions", "thread-attachment"] as const) {
const scenarioId =
kind === "status-reactions"
? "discord-status-reactions-tool-only"
: "discord-thread-reply-filepath-attachment";
const evidence = workflowOccurrenceEvidence([
{ scenarioId, attempts: ["pass"], selected: 0 },
]);
if (invalid === "invalid binding") {
evidence.entries[0]!.binding.occurrenceId = "foreign";
}
const input = invalid === "invalid JSON" ? "{" : invalid === "null" ? null : evidence;
const result = runMantisEvidenceReader(kind, input, invalid !== "missing v3 accessor");
expect(result.status, `${result.stdout}${result.stderr}`).not.toBe(0);
expect(result.stderr).toContain(
invalid === "invalid JSON"
? "SyntaxError"
: invalid === "missing v3 accessor"
? "scenario reader"
: "ZodError",
);
}
},
);
it.skipIf(process.platform === "win32")(
"Mantis evidence readers use legacy summaries only when evidence is absent",
() => {
for (const kind of ["status-reactions", "thread-attachment"] as const) {
const result = runMantisEvidenceReader(kind, undefined);
expect(result.status, `${result.stdout}${result.stderr}`).toBe(0);
expect(result.stdout.trim()).toBe("pass");
}
},
);
it.skipIf(process.platform === "win32")(
"round-trips profile evidence and rejects digest drift",
() => {
const qaWorkflow = readQaProfileEvidenceWorkflow();
const maturityWorkflow = readMaturityScorecardWorkflow();
const producerStep = qaWorkflow.jobs.aggregate_qa_profile.steps.find(
(step: WorkflowStep) => step.name === "Finalize QA profile evidence",
);
const consumerStep = maturityWorkflow.jobs.publish.steps.find(
(step: WorkflowStep) => step.name === "Validate QA evidence manifest",
);
const producerScript = expectDefined(producerStep?.run, "QA evidence producer script");
const consumerScript = expectDefined(consumerStep?.run, "QA evidence consumer script");
const root = tempDirs.make("openclaw-qa-profile-artifact-");
const shellTempDir = path.join(root, "shell-temp");
mkdirSync(shellTempDir);
const selectedRoot = path.join(root, "selected");
writeWorkflowEvidenceApi(selectedRoot, false);
mkdirSync(path.join(selectedRoot, "extensions/qa-lab/src"), { recursive: true });
writeFileSync(
path.join(selectedRoot, "extensions/qa-lab/src/profile-evidence-plan.ts"),
`export { qaProfileEvidencePlan } from ${JSON.stringify(pathToFileURL(path.resolve("extensions/qa-lab/src/profile-evidence-plan.ts")).href)};\n`,
);
const evidencePath = path.join(root, "qa-evidence.json");
const manifestPath = path.join(root, "qa-profile-evidence-manifest.json");
const protocolBaseSha = "b".repeat(40);
const targetSha = "a".repeat(40);
const expectedCell = {
scenarioId: "scenario-one",
executionKind: "flow",
channel: null,
};
const scorecard = {
filters: { surface: null, category: null },
run: { evidenceEntryCount: 0 },
categories: { total: 1, fulfilled: 1, partial: 0, missing: 0, fulfillmentPercent: 100 },
features: { total: 1, fulfilled: 1, partial: 0, missing: 0, fulfillmentPercent: 100 },
coverageIds: {
total: 1,
fulfilled: 1,
missing: 0,
fulfillmentPercent: 100,
},
categoryReports: [
{
id: "surface.category",
surfaceId: "surface",
name: "Category",
status: "fulfilled",
features: {
total: 1,
fulfilled: 1,
partial: 0,
missing: 0,
fulfillmentPercent: 100,
},
coverageIds: {
total: 1,
fulfilled: 1,
missing: 0,
fulfillmentPercent: 100,
secondaryOnly: 0,
},
missingCoverageIds: [],
},
],
};
const writeEvidence = (status: "pass" | "fail" = "pass") => {
writeFileSync(
evidencePath,
`${JSON.stringify({
kind: "openclaw.qa.evidence-summary",
schemaVersion: 2,
generatedAt: "2026-08-05T00:00:00.000Z",
evidenceMode: "full",
entries: [
{
test: { kind: "scenario", id: "scenario-one", title: "Scenario one" },
coverage: [],
result: { status },
},
],
profile: "all",
profilePlan: {
profile: "all",
membership: ["scenario-one"],
selected: ["scenario-one"],
excluded: [],
expectedCells: [expectedCell],
observedCells: [expectedCell],
missingCells: [],
counts: {
membership: 1,
selected: 1,
excluded: 0,
expectedCells: 1,
observedCells: 1,
missingCells: 0,
},
},
scorecard,
})}\n`,
"utf8",
);
};
const runProducer = (qaExitCode: string) =>
runWorkflowShellScript(producerScript, {
tempDir: shellTempDir,
env: {
...process.env,
ALLOW_FAILURES: "true",
ARTIFACT_NAME: `qa-profile-evidence-all-${targetSha}`,
GITHUB_OUTPUT: path.join(root, "github-output"),
GITHUB_STEP_SUMMARY: path.join(root, "github-summary"),
OUTPUT_DIR: root,
PROTOCOL_BASE_SHA: protocolBaseSha,
QA_EXIT_CODE: qaExitCode,
QA_PROFILE: "all",
REQUESTED_REF: targetSha,
TARGET_SHA: targetSha,
TRUSTED_REASON: "fixture",
},
});
const runConsumer = () =>
runWorkflowShellScript(consumerScript, {
tempDir: shellTempDir,
cwd: selectedRoot,
env: {
...process.env,
GITHUB_OUTPUT: path.join(root, "consumer-output"),
GITHUB_STEP_SUMMARY: path.join(root, "consumer-summary"),
QA_EVIDENCE_PATH: evidencePath,
TARGET_SHA: targetSha,
},
});
try {
writeEvidence();
const completeProducer = runProducer("0");
expect(
completeProducer.status,
`${completeProducer.stdout}${completeProducer.stderr}`,
).toBe(0);
const completeManifest = readFileSync(manifestPath, "utf8");
expect(JSON.parse(completeManifest)).toMatchObject({
protocolBaseSha,
targetSha,
});
const completeConsumer = runConsumer();
expect(
completeConsumer.status,
`${completeConsumer.stdout}${completeConsumer.stderr}`,
).toBe(0);
expect(readFileSync(path.join(root, "consumer-output"), "utf8")).toContain(
"scorecard_passed=true",
);
const originalEvidence = JSON.parse(readFileSync(evidencePath, "utf8"));
writeFileSync(evidencePath, JSON.stringify({ ...originalEvidence, evidenceMode: "slim" }));
const oldApiSlim = runConsumer();
expect(oldApiSlim.status, `${oldApiSlim.stdout}${oldApiSlim.stderr}`).toBe(0);
expect(readFileSync(manifestPath, "utf8")).toBe(completeManifest);
const retryEvidence = {
...originalEvidence,
...workflowOccurrenceEvidence([
{ scenarioId: "scenario-one", attempts: ["fail", "pass"], selected: 1 },
]),
profile: originalEvidence.profile,
profilePlan: originalEvidence.profilePlan,
scorecard,
};
writeFileSync(evidencePath, JSON.stringify(retryEvidence));
const missingReader = runConsumer();
expect(missingReader.status).toBe(1);
expect(`${missingReader.stdout}${missingReader.stderr}`).toContain(
"requires the selected checkout's effective-entry reader",
);
writeWorkflowEvidenceApi(selectedRoot);
for (const evidenceMode of ["full", "slim"] as const) {
writeFileSync(evidencePath, JSON.stringify({ ...retryEvidence, evidenceMode }));
const producer = runProducer("0");
expect(producer.status, `${producer.stdout}${producer.stderr}`).toBe(0);
expect(JSON.parse(readFileSync(manifestPath, "utf8")).profilePlanSha256).toBe(
JSON.parse(completeManifest).profilePlanSha256,
);
writeFileSync(path.join(root, "consumer-output"), "");
const consumer = runConsumer();
expect(consumer.status, `${consumer.stdout}${consumer.stderr}`).toBe(0);
const output = readFileSync(path.join(root, "consumer-output"), "utf8");
expect(output).toContain("scorecard_passed=true");
expect(output).toContain("passed_count=1");
expect(output).toContain("failed_count=0");
}
for (const obligation of ["required", "advisory"] as const) {
const declared = {
...retryEvidence,
profilePlan: {
...retryEvidence.profilePlan,
proofRequirements: [
{
id: "observed-protocol",
coverageId: "qa.reporting",
obligation,
owner: "fixture-owner",
acceptedRef: "qa/fixtures/acceptance",
retryAcceptance: "selected-attempt",
alternatives: [{ protocol: "gateway:3" }],
},
],
},
};
writeFileSync(evidencePath, JSON.stringify(declared));
const producer = runProducer("0");
expect(producer.status, `${producer.stdout}${producer.stderr}`).toBe(0);
const consumer = runConsumer();
expect(consumer.status, `${consumer.stdout}${consumer.stderr}`).toBe(
obligation === "required" ? 1 : 0,
);
if (obligation === "required") {
expect(`${consumer.stdout}${consumer.stderr}`).toContain(
"observed-protocol (insufficient)",
);
}
}
writeFileSync(manifestPath, completeManifest);
for (const status of ["fail", "blocked"] as const) {
const parentFailure = {
...originalEvidence,
...workflowOccurrenceEvidence([
// Parent failure is independent of the child, not a retry of its pass.
{ scenarioId: "scenario-one", attempts: ["pass", status], selected: 1, retry: false },
]),
profile: originalEvidence.profile,
profilePlan: originalEvidence.profilePlan,
scorecard,
};
writeFileSync(evidencePath, JSON.stringify(parentFailure));
writeFileSync(path.join(root, "consumer-output"), "");
const consumer = runConsumer();
expect(consumer.status, `${consumer.stdout}${consumer.stderr}`).toBe(0);
const output = readFileSync(path.join(root, "consumer-output"), "utf8");
expect(output).toContain("scorecard_passed=false");
expect(output).toContain("passed_count=1");
expect(output).toContain(`${status === "fail" ? "failed" : "blocked"}_count=1`);
}
writeFileSync(evidencePath, JSON.stringify(retryEvidence));
writeFileSync(
manifestPath,
JSON.stringify({ ...JSON.parse(completeManifest), qaPassed: false }),
);
writeFileSync(path.join(root, "consumer-output"), "");
const failedProducer = runConsumer();
expect(failedProducer.status, `${failedProducer.stdout}${failedProducer.stderr}`).toBe(0);
expect(readFileSync(path.join(root, "consumer-output"), "utf8")).toContain(
"scorecard_passed=false",
);
writeFileSync(
manifestPath,
JSON.stringify({ ...JSON.parse(completeManifest), targetSha: protocolBaseSha }),
);
const wrongSource = runConsumer();
expect(wrongSource.status).toBe(1);
expect(`${wrongSource.stdout}${wrongSource.stderr}`).toContain(
"does not match selected ref",
);
writeFileSync(manifestPath, completeManifest);
const invalidBinding = structuredClone(retryEvidence);
invalidBinding.entries[0]!.binding.occurrenceId = "foreign";
writeFileSync(evidencePath, JSON.stringify(invalidBinding));
expect(runConsumer().status).toBe(1);
writeEvidence("fail");
writeFileSync(path.join(root, "consumer-output"), "", "utf8");
const failedEvidenceConsumer = runConsumer();
expect(
failedEvidenceConsumer.status,
`${failedEvidenceConsumer.stdout}${failedEvidenceConsumer.stderr}`,
).toBe(0);
expect(readFileSync(path.join(root, "consumer-output"), "utf8")).toContain(
"scorecard_passed=false",
);
expect(readFileSync(path.join(root, "consumer-output"), "utf8")).toContain(
"failed_count=1",
);
const manifest = JSON.parse(completeManifest) as Record<string, unknown>;
manifest.profilePlanSha256 = "0".repeat(64);
writeFileSync(manifestPath, `${JSON.stringify(manifest)}\n`, "utf8");
const mismatched = runConsumer();
expect(mismatched.status).toBe(1);
expect(`${mismatched.stdout}${mismatched.stderr}`).toContain(
"QA evidence profilePlan digest does not match the manifest",
);
} finally {
rmSync(root, { force: true, recursive: true });
}
},
);
it.skipIf(process.platform === "win32")(
"fails the maturity workflow result gate when evidence is not passing",
() => {
const maturityWorkflow = readMaturityScorecardWorkflow();
const gateStep = maturityWorkflow.jobs.maturity_result.steps.find(
(step: WorkflowStep) => step.name === "Fail incomplete maturity evidence",
);
const gateScript = expectDefined(gateStep?.run, "maturity result gate");
const failed = runWorkflowShellScript(gateScript, {
env: {
...process.env,
BLOCKED_COUNT: "51",
FAILED_COUNT: "28",
SCORECARD_PASSED: "false",
},
});
expect(failed.status).toBe(1);
expect(`${failed.stdout}${failed.stderr}`).toContain(
"28 failed and 51 blocked scenarios. Generated maturity PR was still published when requested.",
);
const passed = runWorkflowShellScript(gateScript, {
env: {
...process.env,
BLOCKED_COUNT: "0",
FAILED_COUNT: "0",
SCORECARD_PASSED: "true",
},
});
expect(passed.status).toBe(0);
},
);
it.skipIf(process.platform === "win32")(
"suppresses only reported QA result failures when explicitly allowed",
() => {
expect(runQaProfileFailureGate({ allowFailures: false, qaExitCode: "7" }).status).toBe(7);
expect(runQaProfileFailureGate({ allowFailures: true, qaExitCode: "7" }).status).toBe(0);
expect(runQaProfileFailureGate({ allowFailures: true }).status).toBe(1);
expect(runQaProfileFailureGate({ allowFailures: false, qaExitCode: "0" }).status).toBe(0);
},
);
it.skipIf(process.platform === "win32")(
"authorizes maturity PR publication only for a canonical direct dispatch",
() => {
const direct = runMaturityInvocationScenario({
callerEventName: "workflow_dispatch",
callerWorkflowRef: MATURITY_SCORECARD_WORKFLOW_REF,
publishPullRequest: true,
});
expect(direct.status).toBe(0);
},
);
it.skipIf(process.platform === "win32")(
"keeps a reusable maturity call artifact-only even when its caller was dispatched",
() => {
const callerWorkflowRef =
"openclaw/openclaw/.github/workflows/openclaw-release-checks.yml@refs/heads/main";
const artifactOnly = runMaturityInvocationScenario({
callerEventName: "workflow_dispatch",
callerWorkflowRef,
publishPullRequest: false,
});
expect(artifactOnly.status).toBe(0);
for (const identity of [
{ callerWorkflowRef },
{ callerWorkflowRef: MATURITY_SCORECARD_WORKFLOW_REF, jobWorkflowRef: callerWorkflowRef },
]) {
const rejected = runMaturityInvocationScenario({
callerEventName: "workflow_dispatch",
publishPullRequest: true,
...identity,
});
expect(rejected.status).not.toBe(0);
expect(rejected.output).toContain(
"Reusable maturity workflows are artifact-only and cannot publish pull requests.",
);
}
},
);
// Replay the Ubuntu workflow shell only where its Bash 4 and GNU install contract exists.
it.skipIf(process.platform !== "linux")(
"copies only regular allowlisted maturity publication files",
() => {
const valid = runMaturityArtifactCopyScenario();
expect(valid.status).toBe(0);
expect(valid.copied).toEqual(
MATURITY_GENERATED_PR_PATHS.map((generatedPath) => `new ${generatedPath}\n`),
);
const extra = runMaturityArtifactCopyScenario({ extraFile: true });
expect(extra.status).not.toBe(0);
expect(extra.output).toContain("Generated PR artifact must contain exactly 3 files.");
const sourceSymlink = runMaturityArtifactCopyScenario({ sourceSymlink: true });
expect(sourceSymlink.status).not.toBe(0);
expect(sourceSymlink.output).toContain(
"Generated PR artifact path must be a regular file: qa/maturity-scores.yaml",
);
const destinationSymlink = runMaturityArtifactCopyScenario({ destinationSymlink: true });
expect(destinationSymlink.status).not.toBe(0);
expect(destinationSymlink.output).toContain(
"Selected worktree destination must be a regular file: qa/maturity-scores.yaml",
);
expect(destinationSymlink.escaped).toBe("outside\n");
},
);
it("keeps exact release validation identity separate from release context", () => {
const fullReleaseWorkflow = readWorkflow(".github/workflows/full-release-validation.yml");
const releaseWorkflow = readReleaseChecksWorkflow();
const telegramWorkflow = readWorkflow(".github/workflows/openclaw-release-telegram-qa.yml");
const fullReleaseDispatchStep = fullReleaseWorkflow.jobs.release_checks_candidate.steps.find(
(step: WorkflowStep) => step.name === "Dispatch release checks candidate phase",
);
const dispatchStep = releaseWorkflow.jobs.qa_live_telegram_release_checks.steps.find(
(step: WorkflowStep) => step.name === "Dispatch and await trusted Telegram QA",
);
const identityStep = telegramWorkflow.jobs.trusted_identity.steps.find(
(step: WorkflowStep) => step.name === "Verify dispatched workflow identity",
);
const provenanceSteps = [
telegramWorkflow.jobs.build_candidate.steps.find(
(step: WorkflowStep) => step.name === "Validate candidate release provenance",
),
telegramWorkflow.jobs.run_telegram.steps.find(
(step: WorkflowStep) => step.name === "Revalidate candidate release provenance",
),
];
expect(fullReleaseWorkflow.on.workflow_dispatch.inputs.target_context_ref).toMatchObject({
required: false,
default: "",
type: "string",
});
expect(fullReleaseDispatchStep.run).toContain('-f ref="$TARGET_SHA"');
expect(fullReleaseDispatchStep.run).toContain('-f target_context_ref="$TARGET_CONTEXT_REF"');
expect(fullReleaseDispatchStep.run).not.toContain(
'release_checks_target_ref="${TARGET_CONTEXT_REF:-$TARGET_REF}"',
);
expect(releaseWorkflow.on.workflow_dispatch.inputs.target_context_ref).toMatchObject({
required: false,
default: "",
type: "string",
});
expect(telegramWorkflow.on.workflow_dispatch.inputs.target_context_ref).toMatchObject({
required: false,
default: "",
type: "string",
});
expect(dispatchStep.env.TARGET_SHA).toBe("${{ needs.resolve_target.outputs.revision }}");
expect(dispatchStep.env.TARGET_CONTEXT_REF).toBe("${{ inputs.target_context_ref }}");
expect(dispatchStep.run).toContain('-f target_context_ref="$TARGET_CONTEXT_REF"');
expect(dispatchStep.run).toContain('-f target_ref="$TARGET_SHA"');
expect(dispatchStep.run).not.toContain("telegram_target_ref=");
expect(identityStep.run).toContain(
"Telegram QA target context must be a canonical release branch or tag.",
);
expect(identityStep.run).toContain(
"Telegram QA release context requires an exact-SHA target ref.",
);
for (const provenanceStep of provenanceSteps) {
expect(provenanceStep.env.TARGET_CONTEXT_REF).toBe("${{ inputs.target_context_ref }}");
expect(provenanceStep.run.trim()).toBe(
'bash "${GITHUB_WORKSPACE}/scripts/release-telegram-provenance.sh"',
);
}
});
it("checks out the complete trusted Release Decision scripts tree", () => {
const workflow = readWorkflow(".github/workflows/full-release-validation.yml");
const checkout = workflow.jobs.release_decision.steps.find(
(step: WorkflowStep) => step.name === "Checkout release decision tooling",
);
expect(checkout?.with).toMatchObject({
ref: "${{ github.sha }}",
"sparse-checkout": "scripts",
"sparse-checkout-cone-mode": false,
"persist-credentials": false,
});
});
it("keeps maturity scorecard release docs opt-in from release checks", () => {
const releaseWorkflow = readReleaseChecksWorkflow();
const job = releaseWorkflow.jobs.maturity_scorecard_release_checks;
const summaryJob = releaseWorkflow.jobs.summary;
const verifyStep = summaryJob.steps.find(
(step: WorkflowStep) => step.name === "Verify release check results",
);
const inputs = releaseWorkflow.on.workflow_dispatch.inputs;
const resolveJob = releaseWorkflow.jobs.resolve_target;
const summarizeStep = resolveJob.steps.find(
(step: WorkflowStep) => step.name === "Summarize validated ref",
);
expect(releaseWorkflow.jobs).not.toHaveProperty("qa_profile_release_evidence_release_checks");
expect(inputs.run_maturity_scorecard).toMatchObject({
required: false,
default: false,
type: "boolean",
});
expect(resolveJob.outputs.run_maturity_scorecard).toBe(
"${{ steps.inputs.outputs.run_maturity_scorecard }}",
);
expect(summarizeStep.env.RUN_MATURITY_SCORECARD).toBe(
"${{ steps.inputs.outputs.run_maturity_scorecard }}",
);
expect(summarizeStep.run).toContain("- Maturity scorecard docs:");
expect(job.name).toBe("Render maturity scorecard release docs");
expect(job.if).toBe(
"contains(fromJSON('[\"all\",\"qa\"]'), needs.resolve_target.outputs.rerun_group) && needs.resolve_target.outputs.run_maturity_scorecard == 'true'",
);
expect(job.permissions).toMatchObject({
actions: "read",
contents: "read",
});
expect(job.uses).toBe("./.github/workflows/maturity-scorecard.yml");
expect(job.with).toMatchObject({
ref: "${{ needs.resolve_target.outputs.ref }}",
expected_sha: "${{ needs.resolve_target.outputs.revision }}",
});
expect(job.with).not.toHaveProperty("qa_profile");
expect(job.with).not.toHaveProperty("publish_pull_request");
expect(job.secrets).toMatchObject({
OPENAI_API_KEY: "${{ secrets.OPENAI_API_KEY }}",
OPENCLAW_QA_CONVEX_SECRET_CI: "${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}",
OPENCLAW_QA_CONVEX_SITE_URL: "${{ secrets.OPENCLAW_QA_CONVEX_SITE_URL }}",
});
expect(summaryJob.needs).toContain("maturity_scorecard_release_checks");
expect(verifyStep.env.MATURITY_SCORECARD_RELEASE_CHECKS_RESULT).toBe(
"${{ needs.maturity_scorecard_release_checks.result }}",
);
expect(verifyStep.run).toContain(
'"maturity_scorecard_release_checks=${MATURITY_SCORECARD_RELEASE_CHECKS_RESULT}"',
);
expect(verifyStep.run).not.toContain("qa_profile_release_evidence_release_checks");
});
it("keeps workflow guards in fast CI-routing checks", () => {
const workflow = readCiWorkflow();
const preflightStep = workflow.jobs.preflight.steps.find(
(step: WorkflowStep) => step.name === "Build CI manifest",
);
const taxonomy = parse(readFileSync("taxonomy.yaml", "utf8")) as {
surfaces: Array<{ id: string; categories: Array<{ id: string }> }>;
};
const taxonomyCategoryIds = taxonomy.surfaces.flatMap((surface) =>
surface.categories.map((category) => `${surface.id}.${category.id}`),
);
const fastCoreJob = workflow.jobs["checks-fast-core"];
const runStep = fastCoreJob.steps.find(
(step: WorkflowStep) => step.name === "Run ${{ matrix.task }} (${{ matrix.runtime }})",
);
const smokeProfileJob = workflow.jobs["qa-smoke-ci-profile"];
const smokeBuildStep = smokeProfileJob.steps.find(
(step: WorkflowStep) => step.name === "Build QA smoke runtime",
);
const smokeDockerCacheStep = smokeProfileJob.steps.find(
(step: WorkflowStep) => step.name === "Set up Blacksmith Docker layer cache",
);
const smokeRunStep = smokeProfileJob.steps.find(
(step: WorkflowStep) => step.name === "Run smoke profile part",
);
const smokeUploadStep = smokeProfileJob.steps.find(
(step: WorkflowStep) => step.name === "Upload QA smoke profile evidence",
);
const ciWorkflowText = readFileSync(".github/workflows/ci.yml", "utf8");
expect(preflightStep.run).not.toContain("qa-smoke-profile");
expect(preflightStep.run).not.toContain("qa_category");
expect(taxonomyCategoryIds.length).toBeGreaterThan(0);
for (const categoryId of taxonomyCategoryIds) {
expect(ciWorkflowText).not.toContain(`"${categoryId}"`);
}
expect(runStep.run).toContain("bundled-protocol)");
expect(runStep.run).not.toContain("qa-smoke-ci)");
expect(runStep.run).toContain("contracts-plugins-ci-routing)");
expect(runStep.run).toContain("ci-routing)");
expect(fastCoreJob["runs-on"]).toContain("matrix.runner");
expect(smokeProfileJob.name).toBe("QA Smoke CI (${{ matrix.name }})");
// Leak invariant: dist must never be packed after the private overlay
// build. Today that holds vacuously — the smoke set has no docker-lane
// scenario, so the step performs exactly one private build and no pack;
// the run step fails closed if a docker-lane scenario returns.
expect(smokeBuildStep.run).toContain("OPENCLAW_BUILD_PRIVATE_QA=1 pnpm build qaRuntime");
expect(smokeBuildStep.run.match(/pnpm build qaRuntime/g)).toHaveLength(1);
expect(smokeBuildStep.run).not.toContain("package-openclaw-for-docker");
expect(smokeBuildStep.run).not.toContain("npm pack");
expect(smokeBuildStep.env).not.toHaveProperty("OPENCLAW_BUILD_PRIVATE_QA");
const smokePlanRunStep = smokeProfileJob.steps.find(
(step: WorkflowStep) => step.name === "Run smoke profile part",
);
expect(smokePlanRunStep.run).toContain("restore the public pack step in ci.yml");
expect(smokePlanRunStep.run).not.toContain("OPENCLAW_CURRENT_PACKAGE_TGZ");
expect(workflow.jobs["qa-smoke-ci-artifacts"]).toBeUndefined();
expect(workflow.jobs["qa-smoke-ci"]).toBeUndefined();
expect(smokeProfileJob.needs).toEqual(["preflight"]);
expect(smokeProfileJob.strategy["max-parallel"]).toBe(
"${{ (needs.preflight.outputs.runner_profile == 'github' || needs.preflight.outputs.runner_profile == 'hybrid') && 6 || 4 }}",
);
expect(smokeProfileJob.strategy.matrix).toBe(
"${{ fromJson(needs.preflight.outputs.qa_smoke_ci_matrix) }}",
);
for (const [label, options, partCount] of [
["Blacksmith push", { runnerBackend: "blacksmith" }, 4],
["GitHub push", { runnerBackend: "github" }, 6],
["hybrid push", { runnerBackend: "hybrid" }, 4],
["hybrid PR", { runnerBackend: "hybrid", eventName: "pull_request" }, 4],
["hybrid retry", { runnerBackend: "hybrid", scopeEnv: { GITHUB_RUN_ATTEMPT: "2" } }, 6],
["missing attempt", { runnerBackend: "hybrid", scopeEnv: { GITHUB_RUN_ATTEMPT: "" } }, 6],
["other repository", { runnerBackend: "hybrid", repository: "example/openclaw" }, 6],
[
"current hybrid dispatch",
{
runnerBackend: "hybrid",
eventName: "workflow_dispatch",
scopeEnv: { OPENCLAW_CI_CHECKOUT_REVISION: "b".repeat(40) },
},
6,
],
[
"frozen hybrid dispatch",
{ runnerBackend: "hybrid", eventName: "workflow_dispatch", historicalCompatibility: true },
6,
],
[
"frozen Blacksmith dispatch",
{
runnerBackend: "blacksmith",
eventName: "workflow_dispatch",
historicalCompatibility: true,
},
4,
],
] as const) {
const manifest = runCiManifestFixture({
bundledPlanner: true,
changedPaths: [".github/workflows/ci.yml"],
eventName: "push",
historicalCompatibility: false,
...options,
});
expect(manifest.status, `${label}: ${manifest.output}`).toBe(0);
const matrix = JSON.parse(
expectDefined(manifest.outputs.qa_smoke_ci_matrix, `${label} QA smoke matrix`),
);
expect(matrix.include, label).toEqual(
Array.from({ length: partCount }, (_, index) => ({
name: `profile ${index + 1}/${partCount}`,
lane: `profile-${index + 1}`,
slug: `profile-${index + 1}-of-${partCount}`,
part_count: partCount,
})),
);
}
for (const [runnerBackend, expected] of [
["blacksmith", 4],
["github", 6],
["hybrid", 6],
] as const) {
expect(
evaluateWorkflowExpression(smokeProfileJob.strategy["max-parallel"], {
eventName: "push",
repository: "openclaw/openclaw",
runnerBackend,
runAttempt: 1,
}),
).toBe(expected);
}
expect(smokeProfileJob["runs-on"]).toContain("blacksmith-16vcpu-ubuntu-2404");
expect(smokeDockerCacheStep).toBeUndefined();
expect(smokeRunStep.run).toContain("createQaSmokeCiPart");
expect(smokeRunStep.run).toContain("createQaSmokeCiPart(partId, partCount)");
expect(smokeRunStep.env.PROFILE_PART_COUNT).toBe("${{ matrix.part_count }}");
expect(smokeRunStep.run).toContain("createQaSmokeCiMatrix");
expect(smokeRunStep.run).toContain("readQaScenarioPack");
expect(smokeRunStep.run).toContain("isolate each scenario");
expect(smokeRunStep.run).toContain("scenario_ids: [scenarioId]");
expect(smokeRunStep.run).not.toContain("scenarioIdsByKind");
const compatibilityScenarioBlock = smokeRunStep.run.match(
/const compatibilityScenarioIds = new Set\(\[([\s\S]*?)\]\);/u,
)?.[1];
expect(compatibilityScenarioBlock?.match(/^\s+"[^"]+",$/gmu)).toHaveLength(10);
expect(compatibilityScenarioBlock).not.toContain('"dreaming-shadow-trial-report"');
expect(compatibilityScenarioBlock).not.toContain('"control-ui-chat-flow-playwright"');
expect(compatibilityScenarioBlock).toContain('"gateway-smoke"');
expect(compatibilityScenarioBlock).toContain('"matrix-restart-resume"');
expect(smokeRunStep.run).toContain(
"console.error(`[skip] ${partId} is not declared by this checkout's smoke plan`)",
);
expect(smokeRunStep.run).not.toContain(
"console.log(`[skip] ${partId} is not declared by this checkout's smoke plan`)",
);
expect(smokeRunStep.run).toContain("No QA smoke runs assigned");
expect(smokeRunStep.run).toContain("node openclaw.mjs qa run");
expect(smokeRunStep.run).not.toContain("pnpm openclaw qa run");
expect(smokeRunStep.run).toContain(
"timeout --signal=TERM --kill-after=15s 10m node openclaw.mjs qa run",
);
expect(smokeRunStep.run).toContain("--qa-profile smoke-ci");
expect(smokeRunStep.run).toContain("--concurrency 10");
expect(smokeRunStep.env.OPENCLAW_QA_SUITE_WORKER_START_STAGGER_MS).toBe(
"${{ needs.preflight.outputs.runner_profile == 'blacksmith' && '0' || '1500' }}",
);
expect(smokeRunStep.env.OPENCLAW_QA_SUITE_WORKER_START_STAGGER_MS).toContain("'0'");
expect(smokeRunStep.env.OPENCLAW_QA_SUITE_WORKER_START_STAGGER_MS).toContain("'1500'");
expect(smokeRunStep.run).toContain('scenario_args+=(--scenario "$scenario_id")');
expect(smokeRunStep.run).toContain('done <<< "$PROFILE_RUNS_TSV"');
expect(smokeRunStep.run).not.toContain('pids+=("$!")');
expect(smokeRunStep.run).not.toContain('wait "${pids[$index]}"');
expect(smokeRunStep.run).not.toContain("--category");
expect(smokeRunStep.run).not.toContain("--allow-failures");
expect(smokeRunStep.run).toContain("qa_exit_code=0");
expect(smokeRunStep.run).toContain('exit "$qa_exit_code"');
expect(smokeRunStep.run).toContain("--max-old-space-size=16384");
expect(smokeRunStep.run).not.toContain("scripts/build-all.mts qaRuntime");
expect(smokeRunStep.run).not.toContain("OPENAI_API_KEY");
expect(smokeUploadStep.if).toBe("always()");
expect(smokeUploadStep.with).toMatchObject({
path: ".artifacts/qa-e2e/smoke-ci-profile-${{ matrix.slug }}/",
"if-no-files-found": "warn",
});
expect(runStep.run.match(/src\/scripts\/ci-changed-scope\*\.test\.ts/g)).toHaveLength(2);
expect(runStep.run.match(/test\/scripts\/ci-workflow-guards\.test\.ts/g)?.length).toBe(2);
expect(runStep.run.match(/test\/scripts\/ci-changed-node-test-plan\.test\.ts/g)?.length).toBe(
2,
);
});
it("keeps push docs validation ClawHub-backed", () => {
const workflow = readFileSync(".github/workflows/docs.yml", "utf8");
expect(workflow).toContain("repository: openclaw/clawhub");
expect(workflow).toContain("path: clawhub-source");
expect(workflow).toContain(
"OPENCLAW_DOCS_SYNC_CLAWHUB_REPO: ${{ github.workspace }}/clawhub-source",
);
});
it("skips generated-asset validation only when a frozen candidate lacks the contract", () => {
const workflow = readCiWorkflow();
const buildArtifactsJob = workflow.jobs["build-artifacts"];
const assetCheckStep = buildArtifactsJob.steps.find(
(step: WorkflowStep) => step.name === "Check bundled plugin generated assets",
);
expect(assetCheckStep.run).toContain('packageJson.scripts?.["plugins:assets:check"]');
expect(assetCheckStep.run).toContain("pnpm plugins:assets:check");
expect(assetCheckStep.run).toContain("predates plugins:assets:check");
});
it("keeps network CodeQL off unrelated source-only refactors", () => {
const workflow = readCriticalQualityWorkflow();
const networkConfig = readFileSync(
".github/codeql/codeql-network-runtime-boundary-critical-quality.yml",
"utf8",
);
const rawSocketQuery = readFileSync(
".github/codeql/openclaw-boundary/queries/raw-socket-callsite-classification.ql",
"utf8",
);
const networkSelector = workflow.slice(
workflow.indexOf(".github/codeql/codeql-network-runtime-boundary-critical-quality.yml"),
workflow.indexOf("network-runtime-boundary:"),
);
const broadCodeqlSelector = workflow.slice(
workflow.indexOf(".github/codeql/*|.github/workflows/codeql-critical-quality.yml"),
workflow.indexOf("src/**/*.test.ts|src/**/*.test.tsx"),
);
expect(broadCodeqlSelector).not.toContain("network_runtime=true");
expect(networkSelector).toContain(
".github/codeql/codeql-network-runtime-boundary-critical-quality.yml",
);
expect(networkSelector).not.toContain("src/*.ts|src/**/*.ts");
expect(networkSelector).not.toContain("extensions/*.ts|extensions/**/*.ts");
expect(networkSelector).toContain("src/infra/net/*");
expect(networkSelector).toContain("src/infra/ssh-tunnel.ts");
expect(networkSelector).toContain("packages/net-policy/src/*");
expect(networkConfig).not.toContain("\n - src\n");
expect(networkConfig).not.toContain("\n - extensions\n");
expect(networkConfig).toContain("\n - src/infra/net\n");
expect(networkConfig).toContain("\n - packages/net-policy/src\n");
expect(workflow).toContain("Fast PR network boundary diff scan");
expect(workflow).toContain(
'| select(.filename | test("(^|/)[^/]+\\\\.(?:e2e\\\\.)?test\\\\.tsx?$") | not)',
);
expect(workflow).toContain("Network runtime boundary-sensitive added lines");
expect(workflow).toContain(
'codex_transport="extensions/codex/src/app-server/transport-websocket.ts"',
);
expect(workflow).toContain(
"network_codeql_contract_pattern='^\\.github/codeql/(codeql-network-runtime-boundary-critical-quality\\.yml|openclaw-boundary/queries/(raw-socket-callsite-classification|managed-proxy-runtime-mutation)\\.ql)$'",
);
expect(workflow).toContain(
'if grep -Eq "$network_codeql_contract_pattern" "$changed_files" ||',
);
expect(workflow).not.toContain('grep -Fv "$codex_transport: " "$added_lines"');
expect(workflow).toContain("packages/net-policy/src/");
expect(workflow).toContain(
"grep -En 'HTTP_PROXY|HTTPS_PROXY|NO_PROXY|GLOBAL_AGENT_|OPENCLAW_PROXY_' \"$added_lines\"",
);
expect(workflow).toContain('echo "full_codeql=true" >> "$GITHUB_OUTPUT"');
expect(workflow).toContain(
"if: ${{ github.event_name != 'pull_request' || steps.network-diff-scan.outputs.full_codeql == 'true' }}",
);
expect(rawSocketQuery).toMatch(
/allowedOwnerScope\(\s*call\s*,\s*"extensions\/codex\/src\/app-server\/transport-websocket\.ts"\s*,\s*"connectCodexAppServerUnixSocket"\s*\)/,
);
expect(rawSocketQuery).not.toContain(
'call.getFile().getRelativePath() = "extensions/codex/src/app-server/transport-websocket.ts"',
);
});
it("keeps the Crabbox gate publisher on protected main with minimal permissions", () => {
const workflow = parse(readFileSync(".github/workflows/pr-crabbox-gate-publisher.yml", "utf8"));
const publisher = readFileSync("scripts/pr-crabbox-gate-publisher.mjs", "utf8");
const job = workflow.jobs.publish;
expect(workflow.permissions).toEqual({});
expect(workflow.on).toHaveProperty("workflow_dispatch");
expect(job["runs-on"]).toBe("ubuntu-24.04");
expect(job.environment).toBe("qa-live-shared");
expect(job["timeout-minutes"]).toBe(270);
expect(job.permissions).toEqual({
checks: "write",
contents: "read",
"pull-requests": "read",
});
expect(job.steps[0]).toMatchObject({
uses: CHECKOUT_V6,
with: {
"fetch-depth": 0,
"persist-credentials": false,
ref: "${{ github.workflow_sha }}",
},
});
expect(job.steps.at(-1)).toMatchObject({
env: {
CRABBOX_ACCESS_CLIENT_ID: "${{ secrets.CRABBOX_ACCESS_CLIENT_ID }}",
CRABBOX_ACCESS_CLIENT_SECRET: "${{ secrets.CRABBOX_ACCESS_CLIENT_SECRET }}",
CRABBOX_COORDINATOR:
"${{ secrets.CRABBOX_COORDINATOR || secrets.OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR }}",
CRABBOX_COORDINATOR_TOKEN:
"${{ secrets.CRABBOX_COORDINATOR_TOKEN || secrets.OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR_TOKEN }}",
GH_APP_TOKEN:
"${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }}",
GH_TOKEN: "${{ github.token }}",
},
run: "node scripts/pr-crabbox-gate-publisher.mjs",
});
expect(job.steps[2].run).toContain("crabbox_0.46.0_linux_amd64.tar.gz");
expect(job.steps[2].run).toContain(
"6a9341e810307356361dbed4c4b84be28a036b5cc291af1566d2ccd376570d90",
);
expect(job.steps.slice(3, 5)).toMatchObject([
{
id: "app-token",
uses: CREATE_GITHUB_APP_TOKEN_V3,
with: { "app-id": "2729701", "permission-members": "read" },
},
{
id: "app-token-fallback",
uses: CREATE_GITHUB_APP_TOKEN_V3,
with: { "app-id": "2971289", "permission-members": "read" },
},
]);
expect(publisher).toContain("const CHECK_NAME = CRABBOX_GATE_CHECK_NAME");
expect(readFileSync("scripts/pr-lib/crabbox-gate-contract.mjs", "utf8")).toContain(
'CRABBOX_GATE_CHECK_NAME = "openclaw/crabbox-gate"',
);
expect(publisher).not.toContain('const CHECK_NAME = "openclaw/ci-gate"');
expect(Object.keys(workflow.on.workflow_dispatch.inputs).toSorted()).toEqual([
"base_sha",
"head_sha",
"pr_number",
]);
});
});
it("pins generated publisher and maturity owners before credentials and selected checkout", () => {
const pinned = {
name: "Prepare Git owner",
uses: "openclaw/openclaw/.github/actions/git-owner@dd4528b6393e7d00063067a080ca7241b48ce475",
};
const action = parse(readFileSync(PUBLISH_GENERATED_PR_ACTION, "utf8"));
expect(action.runs.steps.map(({ name }: WorkflowStep) => name)).toEqual([
"Prepare Git owner",
"Create generated PR tokens",
"Publish generated pull request",
]);
expect(action.runs.steps[0]).toEqual(pinned);
const steps: WorkflowStep[] = readMaturityScorecardWorkflow().jobs.validate_selected_ref.steps;
const checkout = steps.findIndex(({ name }) => name === "Checkout selected ref");
expect(steps[checkout - 1]).toEqual(pinned);
expect(steps[checkout + 1]?.name).toBe("Validate selected ref");
const policy = expectDefined(steps[checkout + 1]?.run, "validation body");
expect(policy).toContain('exec python3 -I -S "$CI_GIT_OWNER" --policy -');
expect(policy.match(/timeout=\d+/gu)).toEqual(["timeout=60"]);
expect(policy).not.toMatch(
/timeout --|(?:^|\s)git (?:fetch|ls-remote|rev-parse|diff|tag|merge-base|check-ref-format)\b|except (?:Exception|BaseException|RuntimeError|SystemExit)|backoff\(/mu,
);
for (const file of [
CONTROL_UI_LOCALE_REFRESH_WORKFLOW,
NATIVE_APP_LOCALE_REFRESH_WORKFLOW,
".github/workflows/ci-test-timings-refit.yml",
MATURITY_SCORECARD_WORKFLOW,
]) {
const workflow = parse(readFileSync(file, "utf8"));
const publishers = Object.values(workflow.jobs).flatMap((job) => {
const jobSteps = (job as { steps?: WorkflowStep[] }).steps ?? [];
return jobSteps.flatMap((step, index) =>
step.uses === "./.github/actions/publish-generated-pr"
? [{ index, length: jobSteps.length }]
: [],
);
});
expect(publishers, file).toHaveLength(1);
expect(publishers[0]?.index, file).toBe(publishers[0]!.length - 1);
}
});
describe("Linux App validation routing", () => {
const workflow = parse(readFileSync(".github/workflows/linux-app.yml", "utf8"));
const linuxSteps: WorkflowStep[] = workflow.jobs.build.steps;
const macosSteps: WorkflowStep[] = workflow.jobs["test-macos"].steps;
const packagingSteps = [
"Stage AppImage GStreamer plugins",
"Prepare pinned AppImage tools",
"Build Linux companion bundles",
"Finalize AppImage",
"Test native first-run setup and failed Gateway startup",
"Test packaged AppImage runtime",
"Upload bundles",
];
it.each(["pull_request", "workflow_dispatch"] as const)(
"keeps native tests required and selects packaging only for manual validation: %s",
(eventName) => {
const selected = (steps: WorkflowStep[]) =>
steps.filter(
(step) =>
!step.if ||
evaluateWorkflowExpression(`\${{ ${step.if} }}`, {
eventName,
repository: "openclaw/openclaw",
runAttempt: 1,
steps: {
"inline-browser": { outputs: {}, outcome: "success" },
"gateway-switch": { outputs: {}, outcome: "success" },
},
}),
);
const linux = selected(linuxSteps);
const macos = selected(macosSteps);
expect(workflow.jobs.build.if).toBeUndefined();
expect(workflow.jobs["test-macos"].if).toBeUndefined();
expect(workflow.jobs.build["continue-on-error"]).toBeUndefined();
expect(workflow.jobs["test-macos"]["continue-on-error"]).toBeUndefined();
for (const step of [...linuxSteps, ...macosSteps]) {
expect(step["continue-on-error"], step.name).toBeUndefined();
}
expect(linux.map((step) => step.run)).toContain("cargo +stable fmt --check");
for (const steps of [linux, macos]) {
expect(steps.map((step) => step.run)).toContain(
"cargo +stable test --locked --all-targets",
);
}
expect(
linux.find((step) => step.name === "Test packaged runtime ABI scanner")?.run,
).toContain("-s apps/linux/tests -p 'test_packaged_runtime_smoke.py'");
expect(linux.map((step) => step.run)).toContain("cargo +stable build --locked");
expect(linux.find((step) => step.id === "inline-browser")?.run).toContain("--inline-browser");
expect(linux.find((step) => step.id === "gateway-switch")?.run).toContain("--gateway-switch");
for (const name of packagingSteps) {
expect(
linuxSteps.some((step) => step.name === name),
name,
).toBe(true);
expect(
linux.some((step) => step.name === name),
name,
).toBe(eventName === "workflow_dispatch");
}
if (eventName === "pull_request") {
expect(
linux
.filter((step) => step.uses?.startsWith("actions/upload-artifact@"))
.map((step) => step.with?.name),
).toEqual(["linux-inline-browser", "linux-gateway-switch"]);
}
},
);
it.each(["success", "failure", "cancelled", "skipped"] as const)(
"uploads native browser proof after an attempted run: %s",
(outcome) => {
for (const [name, id] of [
["Upload native inline browser proof", "inline-browser"],
["Upload native Gateway switching proof", "gateway-switch"],
] as const) {
const upload = expectDefined(
linuxSteps.find((step) => step.name === name),
`${id} proof upload`,
);
expect(
evaluateWorkflowExpression(`\${{ ${upload.if} }}`, {
eventName: "pull_request",
repository: "openclaw/openclaw",
runAttempt: 1,
failed: outcome === "failure",
cancelled: outcome === "cancelled",
steps: { [id]: { outputs: {}, outcome } },
}),
name,
).toBe(outcome !== "skipped");
}
},
);
it("retains both first-run cases and failure evidence for manual validation", () => {
const firstRun = linuxSteps.find(
(step) => step.name === "Test native first-run setup and failed Gateway startup",
);
expect(firstRun?.run?.match(/python3 apps\/linux\/tests\/first_run\.py/gu)).toHaveLength(2);
expect(firstRun?.run).toContain("--local-start-failure");
for (const [name, id] of [
["Upload first-run failure log", "first-run"],
["Upload packaged runtime failure evidence", "packaged-runtime"],
]) {
expect(linuxSteps.find((step) => step.name === name)?.if).toBe(
`github.event_name == 'workflow_dispatch' && failure() && steps.${id}.outcome == 'failure'`,
);
}
});
});
it.each(["publish", "promote"])(
"requests independent Linux publication after stable %s activation",
(owner) => {
const workflow = parse(readFileSync(`.github/workflows/openclaw-release-${owner}.yml`, "utf8"));
const job = workflow.jobs.publish_linux;
expect(job, "stable publication must request the Linux release owner").toBeDefined();
expect(job["continue-on-error"]).toBe(true);
for (const [tag, channel, activation, expected] of [
["v2026.9.4", "latest", "success", true],
["v2026.9.4", "beta", "success", true],
["v2026.9.4-beta.1", "beta", "success", false],
["v2026.9.4-alpha.1", "alpha", "success", false],
["v2026.8.33", "extended-stable", "success", false],
["v2026.9.4", "latest", "failure", false],
["v2026.9.4", "latest", "skipped", false],
]) {
const admitted = runInNewContext(job.if.replace(/^\$\{\{|\}\}$/gu, ""), {
cancelled: () => false,
contains: (value: string, part: string) => value.includes(part),
inputs: { tag, npm_dist_tag: channel },
needs: {
publish: {
result: "success",
outputs: { release_tag: tag, npm_dist_tag: channel },
},
finalize: { result: activation },
finalize_github_release: { result: activation },
},
});
expect(admitted, `${owner}: ${tag}/${channel}/${activation}`).toBe(expected);
}
const dispatch = (job.steps as WorkflowStep[]).find(
({ name }) => name === "Dispatch detached Linux release request",
);
expect(dispatch?.run).toContain("dispatch_linux_release_assets");
const finalize = workflow.jobs[owner === "publish" ? "finalize_github_release" : "finalize"];
expect(finalize.needs).not.toContain("publish_linux");
const approvalId = owner === "publish" ? "approve_github_release" : "approve_activation";
expect(workflow.jobs[approvalId].environment).toBe("npm-release");
expect(workflow.jobs[approvalId].permissions).toEqual({});
expect(workflow.jobs[approvalId].concurrency).toBeUndefined();
expect(finalize.environment).toBeUndefined();
expect(finalize.needs).toContain(approvalId);
for (const result of ["success", "failure", "skipped", "cancelled"]) {
expect(
runInNewContext(finalize.if.replace(/^\$\{\{|\}\}$/gu, ""), {
always: () => true,
contains: (value: string, part: string) => value.includes(part),
inputs: {
tag: "v2026.9.4",
prepared_plugins: "",
publish_openclaw_npm: true,
finalize_release_before_docker: false,
},
needs: {
publish: { result: "success" },
publish_docker: { result: "success" },
finalize_github_release_before_docker: { result: "skipped" },
verify: { result: "success" },
[approvalId]: { result },
},
}),
).toBe(result === "success");
}
const activationCommand =
owner === "publish" ? "linux-app-channel.mjs finalize-core" : "gh release edit";
const activation = (finalize.steps as WorkflowStep[]).find(({ run }) =>
run?.includes(activationCommand),
)?.run;
expect(activation).toContain("node scripts/linux-updater-manifest.mjs carry");
expect(activation?.indexOf("linux-updater-manifest.mjs carry")).toBeLessThan(
activation?.indexOf(activationCommand) ?? -1,
);
},
);
it("serializes Linux manifests with stable activation and reuses completed Linux builds", () => {
const linux = parse(readFileSync(".github/workflows/linux-app-release.yml", "utf8"));
const finalizers = (
[
["openclaw-release-publish.yml", "finalize_github_release"],
["openclaw-release-promote.yml", "finalize"],
] as const
).map(([file, job]) => parse(readFileSync(`.github/workflows/${file}`, "utf8")).jobs[job]);
for (const job of [...finalizers, linux.jobs.publish, linux.jobs.mirror_legacy]) {
expect(job.concurrency).toEqual({
group: "linux-app-release-publish",
"cancel-in-progress": false,
queue: "max",
});
}
expect(linux.concurrency["cancel-in-progress"]).toBe(false);
expect(linux.concurrency.queue).toBe("max");
expect(linux.concurrency.group).not.toBe(linux.jobs.publish.concurrency.group);
for (const alreadyPublished of ["true", "false"]) {
expect(
runInNewContext(linux.jobs.build_linux.if.replace(/^\$\{\{|\}\}$/gu, ""), {
needs: { validate_release: { outputs: { already_published: alreadyPublished } } },
}),
).toBe(alreadyPublished !== "true");
}
for (const [alreadyPublished, build, signing, expected] of [
["true", "skipped", "skipped", true],
["false", "success", "success", true],
["false", "success", "failure", false],
["false", "skipped", "skipped", false],
]) {
expect(
runInNewContext(linux.jobs.publish.if.replace(/^\$\{\{|\}\}$/gu, ""), {
always: () => true,
needs: {
validate_release: {
outputs: { already_published: alreadyPublished, desktop_test_bundles: "false" },
},
build_linux: { result: build },
sign_linux: { result: signing },
},
}),
).toBe(expected);
}
const steps = linux.jobs.publish.steps as WorkflowStep[];
for (const name of [
"Download Debian bundle",
"Download signed AppImage",
"Assemble release assets and updater manifest",
]) {
const condition = expectDefined(steps.find((step) => step.name === name)?.if, name);
expect(
runInNewContext(condition.replace(/^\$\{\{|\}\}$/gu, ""), {
needs: { validate_release: { outputs: { already_published: "true" } } },
}),
).toBe(false);
}
const publisher = expectDefined(
steps.find(
({ name }) =>
name === "Publish immutable bundles, canonical Linux channel, and legacy mirror",
),
"one publisher",
);
expect(publisher.if).toBeUndefined();
expect(publisher.run).toContain("linux-app-channel.mjs publish");
expect(publisher.run).toContain("input_args=()");
expect(publisher.run).toContain('"${input_args[@]}"');
expect(publisher.run).toContain("--request-run-id");
expect(JSON.stringify(steps)).not.toContain("linux-updater-manifest.mjs publish");
expect(JSON.stringify(steps)).not.toContain("--clobber");
});
it("detaches Linux mirror-only writers from both completed core finalizers", () => {
const linux = parse(readFileSync(".github/workflows/linux-app-release.yml", "utf8"));
for (const event_name of ["push", "pull_request", "workflow_run", "workflow_dispatch"]) {
expect(
runInNewContext(linux.jobs.mirror_legacy.if.slice(3, -2), {
github: { repository: "openclaw/openclaw", event_name },
}),
).toBe(event_name === "workflow_dispatch");
}
expect(linux.on.push).toBeUndefined();
expect(linux.on.pull_request).toBeUndefined();
const mirrorSteps = linux.jobs.mirror_legacy.steps as WorkflowStep[];
expect(JSON.stringify(mirrorSteps)).not.toContain("TAURI_SIGNING_PRIVATE_KEY");
expect(JSON.stringify(mirrorSteps)).not.toContain("linux-app-channel.mjs publish");
expect(JSON.stringify(mirrorSteps)).not.toContain("cargo");
const admission = expectDefined(
mirrorSteps.find(({ name }) => name === "Verify detached mirror dispatch identity"),
"mirror admission",
);
expect(admission.run).toContain("refs/tags/release-publish/*");
expect(admission.run).toContain('"$EXPECTED_TOOLING_SHA" == "$WORKFLOW_SHA"');
expect(admission.run).toContain("--release-publish-parent-state-policy active-or-success");
for (const [file, finalizer] of [
["openclaw-release-publish.yml", "finalize_github_release"],
["openclaw-release-promote.yml", "finalize"],
] as const) {
const workflow = parse(readFileSync(`.github/workflows/${file}`, "utf8"));
const dispatch = workflow.jobs.dispatch_linux_mirror;
expect(dispatch.needs).toContain(finalizer);
expect(dispatch["continue-on-error"]).toBe(true);
expect(dispatch["timeout-minutes"]).toBe(5);
expect(dispatch.concurrency).toBeUndefined();
expect(workflow.jobs[finalizer].needs).not.toContain("dispatch_linux_mirror");
const dispatchStep = expectDefined(
(dispatch.steps as WorkflowStep[]).find(
({ name }) => name === "Dispatch detached Linux mirror",
),
"bounded mirror dispatch",
);
expect(dispatchStep.run).toContain("dispatch_linux_mirror");
expect(dispatchStep.run).not.toMatch(/\b(?:watch|sleep|until|while)\b/u);
}
const prepared = parse(readFileSync(".github/workflows/openclaw-release-promote.yml", "utf8"));
const preparedDispatch = JSON.stringify(prepared.jobs.dispatch_linux_mirror);
expect(preparedDispatch).toContain(".releaseRunId");
expect(preparedDispatch).toContain(".releaseRunAttempt");
expect(preparedDispatch).toContain(".tooling.fullRef");
expect(preparedDispatch).not.toContain("$GITHUB_RUN_ID");
});
it("reports stale Linux release requests before selected code runs", () => {
const workflow = parse(readFileSync(".github/workflows/linux-app-release.yml", "utf8"));
const job = workflow.jobs.validate_release;
const requestStep = expectDefined((job.steps as WorkflowStep[])[0], "first request validation");
const requestSha = "a".repeat(40);
const requestRun = {
repository: { full_name: "openclaw/openclaw" },
event: "workflow_dispatch",
name: "Linux App Release Request [v2026.8.2] desktop=false",
path: ".github/workflows/linux-app-release-request.yml",
head_branch: "main",
head_sha: requestSha,
conclusion: "success",
};
const github = {
repository: "openclaw/openclaw",
event_name: "workflow_run",
workflow_sha: requestSha,
event: { workflow_run: requestRun },
};
const admitted = (context: typeof github) => runInNewContext(job.if, { github: context });
expect(admitted({ ...github, repository: "untrusted/openclaw" })).toBe(false);
for (const changedRun of [
{ repository: { full_name: "untrusted/openclaw" } },
{ event: "push" },
{ path: ".github/workflows/another-workflow.yml" },
{ head_branch: "topic" },
{ conclusion: "failure" },
]) {
expect(admitted({ ...github, event: { workflow_run: { ...requestRun, ...changedRun } } })).toBe(
false,
);
}
for (const workflowSha of ["b".repeat(40), requestSha]) {
expect(admitted({ ...github, workflow_sha: workflowSha })).toBe(true);
expect(requestStep.name).toBe("Validate trusted release request");
const output = path.join(tempDirs.make("openclaw-linux-request-"), "output");
writeFileSync(output, "");
const result = spawnSync("bash", ["-c", expectDefined(requestStep.run, "request validation")], {
encoding: "utf8",
env: {
PATH: process.env.PATH,
GITHUB_OUTPUT: output,
REQUEST_TITLE: "Linux App Release Request [v2026.8.2] desktop=false",
REQUEST_HEAD_SHA: requestSha,
WORKFLOW_SHA: workflowSha,
},
});
const matching = workflowSha === requestSha;
expect(result.status, `${result.stdout}${result.stderr}`).toBe(matching ? 0 : 1);
expect(readFileSync(output, "utf8")).toBe(
matching ? "release_tag=v2026.8.2\ndesktop_test_bundles=false\n" : "",
);
if (!matching) {
expect(`${result.stdout}${result.stderr}`).toContain(
"::error::Main advanced after this Linux release request. Dispatch a new Linux App Release Request",
);
}
}
});
it("pins simple release admission owners before selected checkout and preserves Git contracts", () => {
const pinned = {
name: "Prepare Git owner",
uses: "openclaw/openclaw/.github/actions/git-owner@dd4528b6393e7d00063067a080ca7241b48ce475",
};
const workflows = [
{
file: ".github/workflows/linux-app-release.yml",
job: "validate_release",
checkout: "Checkout selected tag",
validation: "Ensure tag commit is reachable from its release branch",
},
{
file: ".github/workflows/macos-release.yml",
job: "validate_macos_release_request",
checkout: "Checkout selected tag",
validation: "Validate release tag and package metadata",
},
{
file: ".github/workflows/npm-placeholder-bootstrap.yml",
job: "plan",
checkout: "Checkout selected source",
validation: "Validate trusted workflow and target",
},
] as const;
for (const entry of workflows) {
const workflow = parse(readFileSync(entry.file, "utf8"));
const steps = workflow.jobs[entry.job].steps as WorkflowStep[];
const checkout = steps.findIndex(({ name }) => name === entry.checkout);
expect(steps[checkout - 1]).toEqual(pinned);
const validation = steps.find(({ name }) => name === entry.validation);
const body = expectDefined(validation?.run, `${entry.file} admission body`);
expect(body).not.toMatch(/timeout --|(?:^|\s)git (?:fetch|rev-parse|merge-base)\b/mu);
expect(body).not.toMatch(/backoff\(|for attempt in range/u);
}
const request = parse(readFileSync(".github/workflows/linux-app-release-request.yml", "utf8"));
const linux = parse(readFileSync(workflows[0].file, "utf8"));
expect(request["run-name"]).toBe(
"Linux App Release Request [${{ inputs.tag }}] desktop=${{ inputs['desktop-test-bundles'] }}",
);
expect(request.permissions).toEqual({});
expect(Object.keys(request.on.workflow_dispatch.inputs)).toEqual(["tag", "desktop-test-bundles"]);
expect(request.jobs.validate_request.permissions).toBeUndefined();
expect(JSON.stringify(request)).not.toContain("${{ secrets.");
expect(linux.on.workflow_run).toEqual({
workflows: ["Linux App Release Request"],
branches: ["main"],
types: ["completed"],
});
expect(Object.keys(linux.on.workflow_dispatch.inputs)).toEqual([
"release_tag",
"source_sha",
"tooling_sha",
"release_publish_run_id",
"release_publish_run_attempt",
]);
const releaseDocs = expectDefined(
readFileSync("apps/linux/README.md", "utf8").split("## Releases\n")[1],
"Linux release documentation",
);
expect(releaseDocs).toMatch(/dispatch `Linux App Release Request` from `main`/u);
expect(releaseDocs).toContain("stable release tag in `tag`");
expect(releaseDocs).toMatch(/optional\s+`desktop-test-bundles` input/u);
expect(releaseDocs).toMatch(/successful request automatically triggers `Linux App Release`/u);
expect(releaseDocs).not.toContain("release-publish/");
expect(linux.permissions).toEqual({});
expect(linux.jobs.validate_release.if).toContain(
"github.event.workflow_run.repository.full_name == 'openclaw/openclaw'",
);
expect(linux.jobs.validate_release.if).toContain(
"github.event.workflow_run.event == 'workflow_dispatch'",
);
expect(linux.jobs.validate_release.if).toContain(
"github.event.workflow_run.head_branch == 'main'",
);
expect(linux.jobs.validate_release.if).toContain(
"github.event.workflow_run.conclusion == 'success'",
);
const tauriSigningEnvNames = [
"TAURI_SIGNING_PRIVATE_KEY",
"TAURI_SIGNING_PRIVATE_KEY_PATH",
"TAURI_SIGNING_PRIVATE_KEY_PASSWORD",
"TAURI_PRIVATE_KEY",
"TAURI_PRIVATE_KEY_PATH",
"TAURI_PRIVATE_KEY_PASSWORD",
"TAURI_KEY_PASSWORD",
];
const selectedTagJobs = ["validate_release", "build_linux", "build_macos", "build_windows"];
for (const jobName of selectedTagJobs) {
const job = linux.jobs[jobName];
const checkout = expectDefined(
(job.steps as WorkflowStep[]).find(({ name }) => name === "Checkout selected tag"),
`${jobName} selected-tag checkout`,
);
expect(job.permissions, jobName).toEqual({ contents: "read" });
expect(checkout.with?.["persist-credentials"], jobName).toBe(false);
const jobJson = JSON.stringify(job);
expect(jobJson, jobName).not.toContain("${{ secrets.");
for (const envName of tauriSigningEnvNames) {
expect(jobJson, jobName).not.toContain(envName);
}
}
expect(
Object.entries(linux.jobs)
.filter(
([, job]) =>
(job as { permissions?: { contents?: string } }).permissions?.contents === "write",
)
.map(([name]) => name),
).toEqual(["mirror_legacy"]);
expect(linux.jobs.publish.permissions).toEqual({ actions: "read", contents: "read" });
expect(
Object.entries(linux.jobs)
.filter(([, job]) => JSON.stringify(job).includes("${{ secrets.TAURI_SIGNING_PRIVATE_KEY"))
.map(([name]) => name),
).toEqual(["sign_linux", "sign_desktop"]);
const linuxSteps = linux.jobs.validate_release.steps as WorkflowStep[];
expect(
linuxSteps.find(({ name }) => name === "Checkout trusted release tooling")?.with,
).toMatchObject({
ref: "${{ github.workflow_sha }}",
path: ".release-tooling",
"persist-credentials": false,
"sparse-checkout":
"apps/linux/src-tauri/tauri.conf.json\nscripts/lib/record-shared.mjs\nscripts/release-tooling-identity.mjs\nscripts/linux-updater-manifest.mjs\nscripts/lib/release-version.mjs\n",
});
const tooling = linuxSteps.find(({ name }) => name === "Verify trusted release tooling identity");
expect(tooling?.env).toMatchObject({
WORKFLOW_FULL_REF: "${{ github.ref }}",
WORKFLOW_REF: "${{ github.ref_name }}",
WORKFLOW_SHA: "${{ github.workflow_sha }}",
});
expect(tooling?.run).toContain(
"node .release-tooling/scripts/release-tooling-identity.mjs verify",
);
expect(tooling?.run).not.toContain("--allow-prevalidated-ref");
expect(linuxSteps.indexOf(tooling!)).toBeLessThan(
linuxSteps.findIndex(({ id }) => id === "ancestry"),
);
expect(linux.jobs.validate_release.outputs).toEqual({
desktop_test_bundles: "${{ steps.request.outputs.desktop_test_bundles }}",
release_tag: "${{ steps.request.outputs.release_tag }}",
tag_sha: "${{ steps.ancestry.outputs.tag_sha }}",
updater_pubkey: "${{ steps.updater_trust.outputs.updater_pubkey }}",
already_published: "${{ steps.completion.outputs.already_published }}",
});
const releaseRequest = expectDefined(
linuxSteps.find(({ id }) => id === "request"),
"trusted release request validation",
);
expect(releaseRequest.env).toEqual({
REQUEST_TITLE: "${{ github.event.workflow_run.display_title }}",
REQUEST_HEAD_SHA: "${{ github.event.workflow_run.head_sha }}",
WORKFLOW_SHA: "${{ github.workflow_sha }}",
});
expect(releaseRequest.run).toContain("Release request title does not match");
expect(releaseRequest.run).toContain('echo "release_tag=${BASH_REMATCH[1]}"');
expect(releaseRequest.run).toContain('echo "desktop_test_bundles=${BASH_REMATCH[3]}"');
const requestRoot = tempDirs.make("openclaw-linux-release-request-");
const requestOutput = path.join(requestRoot, "output");
const acceptedRequest = spawnSync("bash", ["-c", releaseRequest.run ?? ""], {
encoding: "utf8",
env: {
...process.env,
GITHUB_OUTPUT: requestOutput,
REQUEST_TITLE: "Linux App Release Request [v2026.8.2] desktop=true",
REQUEST_HEAD_SHA: "a".repeat(40),
WORKFLOW_SHA: "a".repeat(40),
},
});
expect(acceptedRequest.status, `${acceptedRequest.stdout}${acceptedRequest.stderr}`).toBe(0);
expect(readFileSync(requestOutput, "utf8")).toBe(
"release_tag=v2026.8.2\ndesktop_test_bundles=true\n",
);
const rejectedRequest = spawnSync("bash", ["-c", releaseRequest.run ?? ""], {
encoding: "utf8",
env: {
...process.env,
GITHUB_OUTPUT: requestOutput,
REQUEST_TITLE: "Linux App Release Request [v2026.8.2] desktop=true extra",
REQUEST_HEAD_SHA: "a".repeat(40),
WORKFLOW_SHA: "a".repeat(40),
},
});
expect(rejectedRequest.status).toBe(1);
const updaterTrust = expectDefined(
linuxSteps.find(({ id }) => id === "updater_trust"),
"updater trust-root validation",
);
expect(updaterTrust.run).toContain("selected_config=apps/linux/src-tauri/tauri.conf.json");
expect(updaterTrust.run).toContain(
"trusted_config=.release-tooling/apps/linux/src-tauri/tauri.conf.json",
);
expect(updaterTrust.run).toContain('-L "$selected_config"');
expect(updaterTrust.run).toContain('-L "$trusted_config"');
expect(updaterTrust.run).toContain('"$selected_pubkey" != "$trusted_pubkey"');
expect(updaterTrust.run).toContain('echo "updater_pubkey=$trusted_pubkey" >> "$GITHUB_OUTPUT"');
for (const jobName of ["build_linux", "build_macos", "build_windows"]) {
const steps = linux.jobs[jobName].steps as WorkflowStep[];
const checkoutIndex = steps.findIndex(({ name }) => name === "Checkout selected tag");
expect(steps[checkoutIndex]?.with?.ref).toBe("${{ needs.validate_release.outputs.tag_sha }}");
expect(checkoutIndex, `${jobName} selected checkout order`).toBeGreaterThan(0);
expect(
steps.slice(0, checkoutIndex).some(({ uses }) => uses?.startsWith("./")),
`${jobName} pre-checkout local action`,
).toBe(false);
expect(
steps.slice(checkoutIndex + 1).some(({ uses }) => uses?.startsWith("./")),
`${jobName} selected-tag local action`,
).toBe(false);
expect(JSON.stringify(steps), `${jobName} Actions cache usage`).not.toContain("actions/cache");
const install = expectDefined(
steps.find(({ name }) => name === "Install selected-tag dependencies"),
`${jobName} dependency install`,
);
expect(steps.indexOf(install), `${jobName} install order`).toBeGreaterThan(checkoutIndex);
expect(install.run).toContain("corepack enable");
expect(install.run).toContain("pnpm install --frozen-lockfile");
}
const linuxBuildSteps = linux.jobs.build_linux.steps as WorkflowStep[];
const selectedTagCheckout = linuxBuildSteps.findIndex(
({ name }) => name === "Checkout selected tag",
);
const trustedToolingCheckout = linuxBuildSteps.findIndex(
({ name }) => name === "Checkout trusted Linux packaging tooling",
);
const selectedTagInstall = linuxBuildSteps.findIndex(
({ name }) => name === "Install selected-tag dependencies",
);
expect(selectedTagCheckout).toBeGreaterThan(0);
expect(selectedTagInstall).toBeGreaterThan(selectedTagCheckout);
expect(trustedToolingCheckout).toBe(selectedTagInstall + 1);
const trustedToolingOptions = linuxBuildSteps[trustedToolingCheckout]?.with;
expect(trustedToolingOptions).toMatchObject({
ref: "${{ github.workflow_sha }}",
path: ".release-tooling",
"fetch-depth": 1,
"persist-credentials": false,
"sparse-checkout-cone-mode": false,
});
const trustedToolingFiles = [
"apps/linux/scripts/stage-appimage-gstreamer.sh",
"apps/linux/scripts/tauri-appimage-tools.sh",
"apps/linux/scripts/tauri-appimage-tools-x86_64.tsv",
"apps/linux/scripts/finalize-appimage.sh",
"apps/linux/tests/packaged_runtime_smoke.py",
"apps/linux/tests/first_run.py",
];
expect(String(trustedToolingOptions?.["sparse-checkout"]).trim().split("\n")).toEqual(
trustedToolingFiles,
);
const packagedRuntimeSmoke = "apps/linux/tests/packaged_runtime_smoke.py";
const firstRunDriver = path.posix.join(path.posix.dirname(packagedRuntimeSmoke), "first_run.py");
expect(readFileSync(packagedRuntimeSmoke, "utf8")).toContain(
'Path(__file__).with_name("first_run.py")',
);
expect(firstRunDriver).toBe("apps/linux/tests/first_run.py");
expect(trustedToolingFiles).toContain(firstRunDriver);
expect(
path.posix.join(path.posix.dirname(`.release-tooling/${packagedRuntimeSmoke}`), "first_run.py"),
).toBe(".release-tooling/apps/linux/tests/first_run.py");
const buildLinuxBundles = expectDefined(
linuxBuildSteps.find(({ name }) => name === "Build Linux companion bundles"),
"Linux bundle build step",
);
expect(buildLinuxBundles["working-directory"]).toBe("apps/linux/src-tauri");
expect(buildLinuxBundles.env?.LDAI_RUNTIME_FILE).toBe(
"${{ runner.temp }}/openclaw-tauri-cache/tauri/.appimage-runtime-x86_64",
);
expect(buildLinuxBundles.run).toContain('\\"createUpdaterArtifacts\\":false');
expect(buildLinuxBundles.run).toContain('\\"useLocalToolsDir\\":false');
const stageLinuxBundles = expectDefined(
linuxBuildSteps.find(({ name }) => name === "Verify and stage unsigned Linux bundles"),
"Linux unsigned bundle staging",
);
expect(stageLinuxBundles.run).toContain(
'cp "${debs[0]}" "dist/linux-app/release/OpenClaw-${version}-amd64.deb"',
);
expect(stageLinuxBundles.run).toContain(
'cp "${appimages[0]}" "dist/linux-app/unsigned/OpenClaw-${version}-amd64.AppImage"',
);
const buildLinuxJson = JSON.stringify(linux.jobs.build_linux);
expect(buildLinuxJson).not.toContain("${{ secrets.");
for (const name of tauriSigningEnvNames) {
expect(buildLinuxJson).not.toContain(name);
}
const finalizeAppImage = expectDefined(
linuxBuildSteps.find(({ name }) => name === "Finalize AppImage"),
"Linux AppImage finalizer step",
);
for (const name of tauriSigningEnvNames) {
expect(finalizeAppImage.env ?? {}).not.toHaveProperty(name);
}
expect(finalizeAppImage.run).not.toContain("signer sign");
expect(linuxBuildSteps.find(({ name }) => name === "Sign finalized AppImage")).toBeUndefined();
expect(linux.jobs.build_linux.outputs).toEqual({
deb_artifact_id: "${{ steps.upload_deb.outputs.artifact-id }}",
unsigned_appimage_artifact_id: "${{ steps.upload_appimage.outputs.artifact-id }}",
});
expect(linuxBuildSteps.find(({ id }) => id === "upload_deb")?.with).toMatchObject({
name: "linux-app-release-deb",
path: "dist/linux-app/release/*.deb",
});
expect(linuxBuildSteps.find(({ id }) => id === "upload_appimage")?.with).toMatchObject({
name: "linux-app-release-unsigned-appimage",
path: "dist/linux-app/unsigned/*.AppImage",
});
const signingJob = linux.jobs.sign_linux;
const signingSteps = signingJob.steps as WorkflowStep[];
expect(signingJob.needs).toEqual(["validate_release", "build_linux"]);
expect(signingJob.permissions).toEqual({});
expect(signingJob.outputs).toEqual({
signed_appimage_artifact_id: "${{ steps.upload_signed_appimage.outputs.artifact-id }}",
});
expect(
signingSteps.map(({ uses }) => uses).filter((uses): uses is string => uses !== undefined),
).toEqual([DOWNLOAD_ARTIFACT_V8, UPLOAD_ARTIFACT_V7]);
expect(signingSteps.some((step) => step["working-directory"] !== undefined)).toBe(false);
const signingBodies = signingSteps.map(({ run }) => run ?? "").join("\n");
expect(signingBodies).not.toMatch(/(?:^|\s)(?:git|cargo)\s|\.release-tooling|apps\/linux\//mu);
expect(signingBodies).not.toMatch(/\b(?:npm|pnpm|npx|corepack)\b/u);
expect(
signingSteps.find(({ name }) => name === "Download finalized unsigned AppImage")?.with,
).toEqual({
"artifact-ids": "${{ needs.build_linux.outputs.unsigned_appimage_artifact_id }}",
path: "dist/signing-input",
});
const signAppImage = expectDefined(
signingSteps.find(({ name }) => name === "Sign finalized AppImage"),
"Linux AppImage signing step",
);
expect(signAppImage.env).toMatchObject({
RELEASE_TAG: "${{ needs.validate_release.outputs.release_tag }}",
TAG_SHA: "${{ needs.validate_release.outputs.tag_sha }}",
TAURI_SIGNING_PRIVATE_KEY: "${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}",
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: "${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}",
UPDATER_PUBLIC_KEY: "${{ needs.validate_release.outputs.updater_pubkey }}",
});
const signingToolsInstaller = expectDefined(
signingSteps.find(({ name }) => name === "Install trusted signing tools"),
"Linux signing tools installer",
);
expect(linux.env).toMatchObject({
MINISIGN_ARCHIVE_SHA256: "9a599b48ba6eb7b1e80f12f36b94ceca7c00b7a5173c95c3efc88d9822957e73",
MINISIGN_BINARY_SHA256: "2c74dffcc1c9a5ee55957c60971998ace2b89f22585631594ec2152c588af8db",
MINISIGN_URL:
"https://github.com/jedisct1/minisign/releases/download/0.12/minisign-0.12-linux.tar.gz",
TAURI_CLI_ARCHIVE_SHA256: "6864602a34292aa6f2ad40ae019eebe5c1064d6c623fe20696a8a8974067e60b",
TAURI_CLI_BINARY_SHA256: "23a27f61c50417fe87c92fa958fb56ecc8de7c791f78df3cac046c8579b45897",
TAURI_CLI_URL:
"https://github.com/tauri-apps/tauri/releases/download/tauri-cli-v2.11.4/cargo-tauri-x86_64-unknown-linux-gnu.tgz",
});
expect(signingToolsInstaller.run?.match(/--proto '=https' --tlsv1\.2/gu)).toHaveLength(2);
expect(signingToolsInstaller.run?.match(/--connect-timeout 10 --max-time 120/gu)).toHaveLength(2);
expect(signingToolsInstaller.run).toContain('--output "$minisign_archive" "$MINISIGN_URL"');
expect(signingToolsInstaller.run).toContain(
'printf \'%s %s\\n\' "$MINISIGN_ARCHIVE_SHA256" "$minisign_archive" | sha256sum --check -',
);
expect(signingToolsInstaller.run).toContain(
'tar -xzf "$minisign_archive" -C "${RUNNER_TEMP}/bin" --strip-components=2 \\\n minisign-linux/x86_64/minisign',
);
expect(signingToolsInstaller.run).toContain(
'printf \'%s %s\\n\' "$MINISIGN_BINARY_SHA256" "${RUNNER_TEMP}/bin/minisign" |',
);
expect(signingToolsInstaller.run).toContain(
'printf \'%s %s\\n\' "$TAURI_CLI_ARCHIVE_SHA256" "$tauri_archive" | sha256sum --check -',
);
expect(signingToolsInstaller.run).toContain('--output "$tauri_archive" "$TAURI_CLI_URL"');
expect(signingToolsInstaller.run).toContain(
'tar -xzf "$tauri_archive" -C "${RUNNER_TEMP}/bin" cargo-tauri',
);
expect(signingToolsInstaller.run).toContain(
'printf \'%s %s\\n\' "$TAURI_CLI_BINARY_SHA256" "${RUNNER_TEMP}/bin/cargo-tauri" |',
);
expect(signingToolsInstaller.run).toContain(
'chmod 0555 "${RUNNER_TEMP}/bin/cargo-tauri" "${RUNNER_TEMP}/bin/minisign"',
);
expect(signingToolsInstaller.run).toContain('"${RUNNER_TEMP}/bin/cargo-tauri" --version');
expect(signingToolsInstaller.run).toContain('"${RUNNER_TEMP}/bin/minisign" -v');
expect(signingToolsInstaller.run).not.toMatch(
/apt-get|GITHUB_PATH|(?:^|\n)\s*(?:export\s+)?PATH=/u,
);
expect(signAppImage.run).toContain(
'printf \'%s %s\\n\' "$TAURI_CLI_BINARY_SHA256" "${RUNNER_TEMP}/bin/cargo-tauri"',
);
expect(signAppImage.run).toContain(
'printf \'%s %s\\n\' "$MINISIGN_BINARY_SHA256" "${RUNNER_TEMP}/bin/minisign"',
);
expect(signAppImage.run).toContain(
'appimage="dist/signing-input/OpenClaw-${version}-amd64.AppImage"',
);
expect(signAppImage.run).toContain('"${RUNNER_TEMP}/bin/cargo-tauri" signer sign "$appimage"');
expect(signAppImage.run).toContain(
"unset TAURI_SIGNING_PRIVATE_KEY TAURI_SIGNING_PRIVATE_KEY_PASSWORD",
);
const signAppImageRun = expectDefined(signAppImage.run, "AppImage signing command");
expect(signAppImageRun.indexOf("unset TAURI_SIGNING_PRIVATE_KEY")).toBeGreaterThan(
signAppImageRun.indexOf('"${RUNNER_TEMP}/bin/cargo-tauri" signer sign "$appimage"'),
);
expect(signAppImageRun.indexOf("unset TAURI_SIGNING_PRIVATE_KEY")).toBeLessThan(
signAppImageRun.indexOf('"${RUNNER_TEMP}/bin/minisign" -Vm "$appimage"'),
);
expect(signAppImage.run).toContain('before=$(sha256sum "$appimage")');
expect(signAppImage.run).toContain('after=$(sha256sum "$appimage")');
expect(signAppImage.run).toContain('base64 --decode < "${appimage}.sig"');
expect(signAppImage.run).toContain('"${RUNNER_TEMP}/bin/minisign" -Vm "$appimage"');
expect(signAppImage.run).not.toMatch(/\b(?:curl|wget|npm|pnpm|npx|corepack)\b|https?:\/\//u);
expect(signAppImage.run).not.toMatch(/(?:^|\n)\s*(?:export\s+)?PATH=/u);
expect(signAppImage.run).not.toContain("finalize-appimage.sh");
expect(signingSteps.find(({ id }) => id === "upload_signed_appimage")?.with).toMatchObject({
name: "linux-app-release-signed-appimage",
path: "dist/linux-app",
});
for (const [jobName, job] of Object.entries(linux.jobs)) {
if (jobName === "sign_linux" || jobName === "sign_desktop") {
continue;
}
expect(JSON.stringify(job), `${jobName} must not reference signer binaries`).not.toMatch(
/\$\{RUNNER_TEMP\}\/bin\/(?:cargo-tauri|minisign)/u,
);
}
const macosBuildSteps = linux.jobs.build_macos.steps as WorkflowStep[];
const buildMacos = expectDefined(
macosBuildSteps.find(({ name }) => name === "Build macOS test bundles"),
"macOS bundle build step",
);
expect(buildMacos.run).toContain('\\"createUpdaterArtifacts\\":false');
const stageMacos = expectDefined(
macosBuildSteps.find(({ name }) => name === "Verify and stage unsigned macOS bundles"),
"macOS unsigned staging step",
);
expect(stageMacos.run).toContain(
"archives=(apps/linux/src-tauri/target/release/bundle/macos/*.app.tar.gz)",
);
expect(stageMacos.run).toContain(
"signatures=(apps/linux/src-tauri/target/release/bundle/macos/*.sig)",
);
expect(stageMacos.run).toContain(
'tar -czf "$archive" -C "$(dirname "${apps[0]}")" "$(basename "${apps[0]}")"',
);
expect(linux.jobs.build_macos.outputs).toEqual({
dmg_artifact_id: "${{ steps.upload_dmg.outputs.artifact-id }}",
unsigned_updater_artifact_id: "${{ steps.upload_updater.outputs.artifact-id }}",
});
expect(macosBuildSteps.find(({ id }) => id === "upload_dmg")?.with).toMatchObject({
name: "macos-app-release-dmg",
path: "dist/macos-app/release/*.dmg",
});
expect(macosBuildSteps.find(({ id }) => id === "upload_updater")?.with).toMatchObject({
name: "macos-app-release-unsigned-updater",
path: "dist/macos-app/unsigned/*.app.tar.gz",
});
const windowsBuildSteps = linux.jobs.build_windows.steps as WorkflowStep[];
const buildWindows = expectDefined(
windowsBuildSteps.find(({ name }) => name === "Build Windows test bundle"),
"Windows bundle build step",
);
expect(buildWindows.run).toContain('\\"createUpdaterArtifacts\\":false');
const stageWindows = expectDefined(
windowsBuildSteps.find(({ name }) => name === "Verify and stage unsigned Windows bundle"),
"Windows unsigned staging step",
);
expect(stageWindows.run).toContain(
'Get-ChildItem "apps/linux/src-tauri/target/release/bundle/nsis/*.sig"',
);
expect(linux.jobs.build_windows.outputs).toEqual({
unsigned_updater_artifact_id: "${{ steps.upload_updater.outputs.artifact-id }}",
});
expect(windowsBuildSteps.find(({ id }) => id === "upload_updater")?.with).toMatchObject({
name: "windows-app-release-unsigned-updater",
path: "dist/windows-app/unsigned/*.exe",
});
const desktopSigningJob = linux.jobs.sign_desktop;
const desktopSigningSteps = desktopSigningJob.steps as WorkflowStep[];
expect(desktopSigningJob.needs).toEqual(["validate_release", "build_macos", "build_windows"]);
expect(desktopSigningJob.permissions).toEqual({});
expect(desktopSigningJob.outputs).toEqual({
signed_desktop_artifact_id: "${{ steps.upload_signed_desktop.outputs.artifact-id }}",
});
expect(
desktopSigningSteps
.map(({ uses }) => uses)
.filter((uses): uses is string => uses !== undefined),
).toEqual([DOWNLOAD_ARTIFACT_V8, DOWNLOAD_ARTIFACT_V8, UPLOAD_ARTIFACT_V7]);
expect(desktopSigningSteps.some((step) => step["working-directory"] !== undefined)).toBe(false);
const desktopSigningBodies = desktopSigningSteps.map(({ run }) => run ?? "").join("\n");
expect(desktopSigningBodies).not.toMatch(
/(?:^|\s)(?:git|cargo)\s|\.release-tooling|apps\/linux\//mu,
);
expect(desktopSigningBodies).not.toMatch(/\b(?:npm|pnpm|npx|corepack)\b/u);
expect(
desktopSigningSteps.find(({ name }) => name === "Download finalized macOS updater archive")
?.with,
).toEqual({
"artifact-ids": "${{ needs.build_macos.outputs.unsigned_updater_artifact_id }}",
path: "dist/signing-input/macos",
});
expect(
desktopSigningSteps.find(({ name }) => name === "Download finalized Windows updater installer")
?.with,
).toEqual({
"artifact-ids": "${{ needs.build_windows.outputs.unsigned_updater_artifact_id }}",
path: "dist/signing-input/windows",
});
const signDesktop = expectDefined(
desktopSigningSteps.find(({ name }) => name === "Sign finalized desktop updater bundles"),
"desktop updater signing step",
);
expect(signDesktop.env).toMatchObject({
RELEASE_TAG: "${{ needs.validate_release.outputs.release_tag }}",
TAG_SHA: "${{ needs.validate_release.outputs.tag_sha }}",
TAURI_SIGNING_PRIVATE_KEY: "${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}",
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: "${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}",
UPDATER_PUBLIC_KEY: "${{ needs.validate_release.outputs.updater_pubkey }}",
});
expect(desktopSigningSteps.find(({ name }) => name === "Install trusted signing tools")).toEqual(
signingToolsInstaller,
);
expect(signDesktop.run).toContain(
'printf \'%s %s\\n\' "$TAURI_CLI_BINARY_SHA256" "${RUNNER_TEMP}/bin/cargo-tauri"',
);
expect(signDesktop.run).toContain(
'printf \'%s %s\\n\' "$MINISIGN_BINARY_SHA256" "${RUNNER_TEMP}/bin/minisign"',
);
expect(signDesktop.run).toContain('"${RUNNER_TEMP}/bin/cargo-tauri" signer sign "$macos"');
expect(signDesktop.run).toContain('"${RUNNER_TEMP}/bin/cargo-tauri" signer sign "$windows"');
expect(signDesktop.run).toContain(
"unset TAURI_SIGNING_PRIVATE_KEY TAURI_SIGNING_PRIVATE_KEY_PASSWORD",
);
const signDesktopRun = expectDefined(signDesktop.run, "desktop updater signing command");
expect(signDesktopRun.indexOf("unset TAURI_SIGNING_PRIVATE_KEY")).toBeGreaterThan(
signDesktopRun.indexOf('"${RUNNER_TEMP}/bin/cargo-tauri" signer sign "$windows"'),
);
expect(signDesktopRun.indexOf("unset TAURI_SIGNING_PRIVATE_KEY")).toBeLessThan(
signDesktopRun.indexOf('"${RUNNER_TEMP}/bin/minisign" -Vm "$macos"'),
);
expect(signDesktop.run).toContain('macos_before=$(sha256sum "$macos")');
expect(signDesktop.run).toContain('windows_after=$(sha256sum "$windows")');
expect(signDesktop.run).toContain('"${RUNNER_TEMP}/bin/minisign" -Vm "$macos"');
expect(signDesktop.run).toContain('"${RUNNER_TEMP}/bin/minisign" -Vm "$windows"');
expect(signDesktop.run).not.toMatch(/\b(?:curl|wget|npm|pnpm|npx|corepack)\b|https?:\/\//u);
expect(signDesktop.run).not.toMatch(/(?:^|\n)\s*(?:export\s+)?PATH=/u);
expect(desktopSigningSteps.find(({ id }) => id === "upload_signed_desktop")?.with).toMatchObject({
name: "desktop-test-release-signed-updaters",
path: "dist/desktop-test",
});
expect(linux.jobs.publish.needs).toContain("sign_linux");
expect(linux.jobs.publish.needs).toContain("sign_desktop");
expect(linux.jobs.publish.if).toContain("needs.sign_linux.result == 'success'");
expect(linux.jobs.publish.if).toContain("needs.sign_desktop.result == 'success'");
expect(linux.jobs.publish.if).not.toContain("inputs.");
expect(
(linux.jobs.publish.steps as WorkflowStep[]).find(
({ name }) => name === "Download Debian bundle",
)?.with,
).toEqual({
"artifact-ids": "${{ needs.build_linux.outputs.deb_artifact_id }}",
path: "dist/input/linux/release",
});
expect(
(linux.jobs.publish.steps as WorkflowStep[]).find(
({ name }) => name === "Download signed AppImage",
)?.with,
).toEqual({
"artifact-ids": "${{ needs.sign_linux.outputs.signed_appimage_artifact_id }}",
path: "dist/input/linux",
});
expect(
(linux.jobs.publish.steps as WorkflowStep[]).find(
({ name }) => name === "Download macOS test DMG",
)?.with,
).toEqual({
"artifact-ids": "${{ needs.build_macos.outputs.dmg_artifact_id }}",
path: "dist/input/macos/release",
});
expect(
(linux.jobs.publish.steps as WorkflowStep[]).find(
({ name }) => name === "Download signed desktop updater bundles",
)?.with,
).toEqual({
"artifact-ids": "${{ needs.sign_desktop.outputs.signed_desktop_artifact_id }}",
path: "dist/input",
});
const assembleLinuxBundles = expectDefined(
(linux.jobs.publish.steps as WorkflowStep[]).find(
({ name }) => name === "Assemble release assets and updater manifest",
),
"Linux release assembly step",
);
expect(assembleLinuxBundles.run).not.toContain('"linux-x86_64"');
const publishLinuxMetadata = expectDefined(
(linux.jobs.publish.steps as WorkflowStep[]).find(
({ name }) =>
name === "Publish immutable bundles, canonical Linux channel, and legacy mirror",
),
"Linux publication owner",
);
expect(publishLinuxMetadata.run).toContain(
'--assets dist/release --signature "dist/input/linux/signatures/OpenClaw-${RELEASE_TAG#v}-amd64.AppImage.sig"',
);
const publicationToken = expectDefined(
(linux.jobs.publish.steps as WorkflowStep[]).find(({ id }) => id === "publication_token"),
"Linux release-owner token for protected control-tag creation",
);
expect(publicationToken.with).toMatchObject({
"client-id": "Iv23liOECG0slfuhz093",
"private-key": "${{ secrets.CLAWSWEEPER_APP_PRIVATE_KEY }}",
owner: "openclaw",
repositories: "openclaw",
"permission-actions": "read",
"permission-contents": "write",
"permission-workflows": "write",
});
expect(publishLinuxMetadata.env?.GH_TOKEN).toBe("${{ steps.publication_token.outputs.token }}");
const appImageToolsPath = "apps/linux/scripts/tauri-appimage-tools.sh";
const appImageTools = readFileSync(appImageToolsPath, "utf8");
const appImageToolsManifest = readFileSync(
"apps/linux/scripts/tauri-appimage-tools-x86_64.tsv",
"utf8",
);
const aarch64AppImageToolsManifest = readFileSync(
"apps/linux/scripts/tauri-appimage-tools-aarch64.tsv",
"utf8",
);
expect(appImageTools).toContain("prepare)");
expect(appImageTools).toContain('verify_directory "$tools_dir" "$2"');
expect(appImageTools).toContain("--proto '=https' --tlsv1.2");
expect(appImageTools).toContain("--connect-timeout 10 --max-time 120");
expect(appImageTools).toContain("--retry 3 --retry-all-errors");
expect(appImageTools).toContain('mv -Tn -- "$staging_dir" "$tools_dir"');
expect(appImageTools).toContain('fail "refusing existing Tauri tool cache: $tools_dir"');
expect(appImageTools).toContain('offset=$("$plugin" --appimage-offset)');
expect(appImageTools).toContain('cmp --silent --bytes="$offset" -- "$plugin" "$runtime"');
expect(appImageToolsManifest.trim().split("\n")).toEqual([
[
"AppRun-x86_64",
"https://github.com/tauri-apps/binary-releases/releases/download/apprun-old/AppRun-x86_64",
"f30140a43a0a59e46db21bdefdf749b9e9f2c6946e92afabbacf98b8ae73fb4f",
"f30140a43a0a59e46db21bdefdf749b9e9f2c6946e92afabbacf98b8ae73fb4f",
"0555",
].join("\t"),
[
"linuxdeploy-x86_64.AppImage",
"https://github.com/tauri-apps/binary-releases/releases/download/linuxdeploy/linuxdeploy-x86_64.AppImage",
"e762bea85c8eb0d4b3508d46e5c1f037f717d0f9303ae3b4aafc8b04991fa1ef",
"20eebde3c18ae2e44279bd624fc72482503aece216d5d77f10932235342f71c1",
"0755",
].join("\t"),
[
"linuxdeploy-plugin-gtk.sh",
"https://raw.githubusercontent.com/tauri-apps/linuxdeploy-plugin-gtk/b5eb8d05b4c0ed40107fe2158c5d8527f94568ef/linuxdeploy-plugin-gtk.sh",
"cb379f9b0733e9ad9f8bd78f8c2fa038aef2478523bb7d4c8e64ff6a1ea3501a",
"cb379f9b0733e9ad9f8bd78f8c2fa038aef2478523bb7d4c8e64ff6a1ea3501a",
"0555",
].join("\t"),
[
"linuxdeploy-plugin-gstreamer.sh",
"https://raw.githubusercontent.com/tauri-apps/linuxdeploy-plugin-gstreamer/2a2e67491c32995a3f279ad0ecbe77abd512b42a/linuxdeploy-plugin-gstreamer.sh",
"c107b49d84edbffc6ab226ed1007e0626a4f7aa2c3a36b7782bef62351d49e94",
"c107b49d84edbffc6ab226ed1007e0626a4f7aa2c3a36b7782bef62351d49e94",
"0555",
].join("\t"),
[
"linuxdeploy-plugin-appimage.AppImage",
"https://github.com/linuxdeploy/linuxdeploy-plugin-appimage/releases/download/continuous/linuxdeploy-plugin-appimage-x86_64.AppImage",
"0441769ab38009504d2678c38cd7e526955388dd30a215b4a20afaa5471652f2",
"0441769ab38009504d2678c38cd7e526955388dd30a215b4a20afaa5471652f2",
"0555",
].join("\t"),
]);
expect(aarch64AppImageToolsManifest.trim().split("\n")).toEqual([
[
"AppRun-aarch64",
"https://github.com/tauri-apps/binary-releases/releases/download/apprun-old/AppRun-aarch64",
"072f17c0895a85c490282fe5395c5007e5fc75da727e553b3b8fb680feb11578",
"072f17c0895a85c490282fe5395c5007e5fc75da727e553b3b8fb680feb11578",
"0555",
].join("\t"),
[
"linuxdeploy-aarch64.AppImage",
"https://github.com/tauri-apps/binary-releases/releases/download/linuxdeploy/linuxdeploy-aarch64.AppImage",
"b12b5cc57bd0921e1f98d73f58aa364503bc1a27f54b7a69fd2870bce7fa2f55",
"a4335edd7c91b99fa9fbb2339d8e5611efbc4fd243ad07b9980ddc961b77d632",
"0755",
].join("\t"),
[
"linuxdeploy-plugin-gtk.sh",
"https://raw.githubusercontent.com/tauri-apps/linuxdeploy-plugin-gtk/b5eb8d05b4c0ed40107fe2158c5d8527f94568ef/linuxdeploy-plugin-gtk.sh",
"cb379f9b0733e9ad9f8bd78f8c2fa038aef2478523bb7d4c8e64ff6a1ea3501a",
"cb379f9b0733e9ad9f8bd78f8c2fa038aef2478523bb7d4c8e64ff6a1ea3501a",
"0555",
].join("\t"),
[
"linuxdeploy-plugin-gstreamer.sh",
"https://raw.githubusercontent.com/tauri-apps/linuxdeploy-plugin-gstreamer/2a2e67491c32995a3f279ad0ecbe77abd512b42a/linuxdeploy-plugin-gstreamer.sh",
"c107b49d84edbffc6ab226ed1007e0626a4f7aa2c3a36b7782bef62351d49e94",
"c107b49d84edbffc6ab226ed1007e0626a4f7aa2c3a36b7782bef62351d49e94",
"0555",
].join("\t"),
[
"linuxdeploy-plugin-appimage.AppImage",
"https://github.com/linuxdeploy/linuxdeploy-plugin-appimage/releases/download/continuous/linuxdeploy-plugin-appimage-aarch64.AppImage",
"ce574719bcf9cc1fb12728d60b17e48cc87d9b6c40f6f48b04cff7d273b5eb24",
"ce574719bcf9cc1fb12728d60b17e48cc87d9b6c40f6f48b04cff7d273b5eb24",
"0555",
].join("\t"),
]);
expect(appImageTools).toMatch(/continuous[\s\S]*digest-pinned/u);
const prLinux = parse(readFileSync(".github/workflows/linux-app.yml", "utf8"));
expect(prLinux.jobs.build["runs-on"]).toBe("ubuntu-22.04");
expect(prLinux.jobs.build.strategy).toBeUndefined();
expect(prLinux.on.workflow_dispatch?.inputs).toBeUndefined();
const abiScannerTest = expectDefined(
(prLinux.jobs.build.steps as WorkflowStep[]).find(
({ name }) => name === "Test packaged runtime ABI scanner",
),
"pull request AppImage ABI scanner test",
);
expect(abiScannerTest.run).toContain("python3 -m unittest discover");
expect(abiScannerTest.run).toContain("-s apps/linux/tests -p 'test_packaged_runtime_smoke.py'");
const workflowContracts = [
{
job: prLinux.jobs.build,
helper: appImageToolsPath,
label: "pull request",
},
{
job: linux.jobs.build_linux,
helper: `.release-tooling/${appImageToolsPath}`,
label: "release",
},
];
for (const contract of workflowContracts) {
const steps = contract.job.steps as WorkflowStep[];
const prepareIndex = steps.findIndex(({ name }) => name === "Prepare pinned AppImage tools");
const buildIndex = steps.findIndex(({ name }) => name === "Build Linux companion bundles");
const finalizeIndex = steps.findIndex(({ name }) => name === "Finalize AppImage");
expect(prepareIndex, `${contract.label} prepare`).toBeGreaterThan(0);
expect(buildIndex, `${contract.label} build`).toBe(prepareIndex + 1);
expect(finalizeIndex, `${contract.label} finalize`).toBe(buildIndex + 1);
for (const index of [prepareIndex, buildIndex, finalizeIndex]) {
expect(steps[index]?.env?.XDG_CACHE_HOME, `${contract.label} cache path`).toBe(
"${{ runner.temp }}/openclaw-tauri-cache",
);
}
expect(steps[prepareIndex]?.run, contract.label).toContain(`${contract.helper} prepare`);
expect(steps[prepareIndex]?.run, contract.label).toContain(
`${contract.helper} verify pre-build`,
);
expect(steps[buildIndex]?.run, contract.label).toContain("@tauri-apps/cli@2.11.4");
expect(steps[buildIndex]?.run, contract.label).toMatch(/\\?"useLocalToolsDir\\?":false/u);
expect(steps[finalizeIndex]?.run, contract.label).toMatch(
/finalize-appimage\.sh "?\\?\$?bundle_dir"?|finalize-appimage\.sh apps\/linux\/src-tauri\/target\/release\/bundle\/appimage/u,
);
expect(JSON.stringify(contract.job), contract.label).not.toContain("${{ secrets.");
}
const prPrepare = expectDefined(
(prLinux.jobs.build.steps as WorkflowStep[]).find(
({ name }) => name === "Prepare pinned AppImage tools",
),
"pull request AppImage tool preparation",
);
expect(prPrepare.run).toContain(
"runtime_file=$(apps/linux/scripts/tauri-appimage-tools.sh runtime-path)",
);
expect(prPrepare.run).toContain(
'printf \'LDAI_RUNTIME_FILE=%s\\n\' "$runtime_file" >> "$GITHUB_ENV"',
);
expect(
(prLinux.jobs.build.steps as WorkflowStep[]).find(
({ name }) => name === "Build Linux companion bundles",
)?.env,
).not.toHaveProperty("LDAI_RUNTIME_FILE");
expect(linux.jobs.build_linux["runs-on"]).toBe("ubuntu-22.04");
expect(linux.jobs.build_linux.strategy).toBeUndefined();
const finalizerSource = readFileSync("apps/linux/scripts/finalize-appimage.sh", "utf8");
const postBuildVerifications =
finalizerSource.match(/"\$tools_helper" verify post-build/gu) ?? [];
expect(postBuildVerifications).toHaveLength(2);
expect(finalizerSource.indexOf(postBuildVerifications[0]!)).toBeLessThan(
finalizerSource.indexOf("mapfile -d '' forbidden_libraries"),
);
expect(finalizerSource.lastIndexOf(postBuildVerifications[1]!)).toBeLessThan(
finalizerSource.indexOf('"$plugin" --appdir "$appdir"'),
);
expect(finalizerSource).toContain('LDAI_RUNTIME_FILE="$runtime"');
const architectureRoot = tempDirs.make("openclaw-appimage-architecture-");
const architectureBin = path.join(architectureRoot, "bin");
const architectureCache = path.join(architectureRoot, "cache");
mkdirSync(architectureBin);
writeExecutable(path.join(architectureBin, "uname"), [
"#!/usr/bin/env bash",
"set -euo pipefail",
'case "$1" in',
' -s) printf "%s\\n" "${SYNTHETIC_UNAME_SYSTEM:?}" ;;',
' -m) printf "%s\\n" "${SYNTHETIC_UNAME_MACHINE:?}" ;;',
" *) exit 64 ;;",
"esac",
]);
const architectureEnv = (system: string, machine: string, cache = architectureCache) => ({
...process.env,
PATH: `${architectureBin}${path.delimiter}${process.env.PATH ?? ""}`,
SYNTHETIC_UNAME_MACHINE: machine,
SYNTHETIC_UNAME_SYSTEM: system,
XDG_CACHE_HOME: cache,
});
for (const [machine, expected] of [
["x86_64", "x86_64"],
["amd64", "x86_64"],
["aarch64", "aarch64"],
["arm64", "aarch64"],
] as const) {
const result = spawnSync(path.resolve(appImageToolsPath), ["architecture"], {
encoding: "utf8",
env: architectureEnv("Linux", machine),
});
expect(result.status, `${result.stdout}${result.stderr}`).toBe(0);
expect(result.stdout).toBe(`${expected}\n`);
expect(existsSync(architectureCache)).toBe(false);
}
const runtimePath = spawnSync(path.resolve(appImageToolsPath), ["runtime-path"], {
encoding: "utf8",
env: architectureEnv("Linux", "arm64"),
});
expect(runtimePath.status, `${runtimePath.stdout}${runtimePath.stderr}`).toBe(0);
expect(runtimePath.stdout).toBe(`${architectureCache}/tauri/.appimage-runtime-aarch64\n`);
expect(existsSync(architectureCache)).toBe(false);
const relativeRuntimePath = spawnSync(path.resolve(appImageToolsPath), ["runtime-path"], {
encoding: "utf8",
env: architectureEnv("Linux", "x86_64", "relative-cache"),
});
expect(relativeRuntimePath.status).not.toBe(0);
for (const [system, machine] of [
["Darwin", "arm64"],
["Linux", "riscv64"],
] as const) {
const rejectedCache = path.join(architectureRoot, `${system}-${machine}`);
const result = spawnSync(path.resolve(appImageToolsPath), ["prepare"], {
encoding: "utf8",
env: architectureEnv(system, machine, rejectedCache),
});
expect(result.status).not.toBe(0);
expect(existsSync(rejectedCache)).toBe(false);
}
if (process.platform === "linux") {
const selectedTagRoot = tempDirs.make("openclaw-linux-release-v2026.8.2-");
const trustedTools = path.join(
selectedTagRoot,
".release-tooling/apps/linux/scripts/tauri-appimage-tools.sh",
);
const trustedToolsManifest = path.join(
selectedTagRoot,
".release-tooling/apps/linux/scripts/tauri-appimage-tools-x86_64.tsv",
);
const trustedArmToolsManifest = path.join(
selectedTagRoot,
".release-tooling/apps/linux/scripts/tauri-appimage-tools-aarch64.tsv",
);
const trustedFinalizer = path.join(
selectedTagRoot,
".release-tooling/apps/linux/scripts/finalize-appimage.sh",
);
const trustedSmoke = path.join(
selectedTagRoot,
".release-tooling/apps/linux/tests/packaged_runtime_smoke.py",
);
const trustedFirstRun = path.join(
selectedTagRoot,
".release-tooling/apps/linux/tests/first_run.py",
);
const bundleDir = path.join(
selectedTagRoot,
"apps/linux/src-tauri/target/release/bundle/appimage",
);
const appDir = path.join(bundleDir, "OpenClaw.AppDir");
const appImage = path.join(bundleDir, "OpenClaw_2026.8.2_amd64.AppImage");
const cacheRoot = path.join(selectedTagRoot, ".cache");
const toolSourceDir = path.join(selectedTagRoot, "tool-sources");
const fakeBin = path.join(selectedTagRoot, "fake-bin");
const pluginSentinel = path.join(selectedTagRoot, "plugin-executed");
const toolNames = [
"AppRun-x86_64",
"linuxdeploy-x86_64.AppImage",
"linuxdeploy-plugin-gtk.sh",
"linuxdeploy-plugin-gstreamer.sh",
"linuxdeploy-plugin-appimage.AppImage",
] as const;
mkdirSync(path.dirname(trustedFinalizer), { recursive: true });
mkdirSync(path.dirname(trustedSmoke), { recursive: true });
mkdirSync(toolSourceDir, { recursive: true });
mkdirSync(fakeBin, { recursive: true });
copyFileSync("apps/linux/scripts/tauri-appimage-tools.sh", trustedTools);
copyFileSync("apps/linux/scripts/tauri-appimage-tools-x86_64.tsv", trustedToolsManifest);
copyFileSync("apps/linux/scripts/finalize-appimage.sh", trustedFinalizer);
copyFileSync("apps/linux/tests/packaged_runtime_smoke.py", trustedSmoke);
copyFileSync("apps/linux/tests/first_run.py", trustedFirstRun);
chmodSync(trustedTools, 0o755);
chmodSync(trustedFinalizer, 0o755);
const toolSources = new Map<string, Buffer>();
for (const toolName of toolNames) {
const contents =
toolName === "linuxdeploy-plugin-appimage.AppImage"
? Buffer.from(
[
"#!/usr/bin/env bash",
"set -euo pipefail",
'if [[ ${1:-} == "--appimage-offset" ]]; then',
" printf '16\\n'",
" exit 0",
"fi",
'[[ "$1" == "--appdir" && -d "$2" ]]',
'[[ "${ARCH:?}" == "${EXPECTED_ARCH:?}" ]]',
'[[ -f "${LDAI_RUNTIME_FILE:?}" ]]',
'printf "executed\\n" > "$PLUGIN_SENTINEL"',
'cat "$LDAI_RUNTIME_FILE" > "$LDAI_OUTPUT"',
`printf '\\n#!/bin/sh\\nexit 0\\n' >> "$LDAI_OUTPUT"`,
'chmod +x "$LDAI_OUTPUT"',
"",
].join("\n"),
)
: Buffer.from(`#!/bin/sh\n# synthetic ${toolName}\nexit 0\n`);
toolSources.set(toolName, contents);
writeFileSync(path.join(toolSourceDir, toolName), contents, { mode: 0o755 });
}
const preBuildLinuxdeploy = Buffer.from(
expectDefined(toolSources.get("linuxdeploy-x86_64.AppImage"), "linuxdeploy source"),
);
const postBuildLinuxdeploy = Buffer.from(preBuildLinuxdeploy);
postBuildLinuxdeploy.fill(0, 8, 11);
const digest = (contents: Buffer) => createHash("sha256").update(contents).digest("hex");
const writeSyntheticManifest = (wrongDigest = false) => {
writeFileSync(
trustedToolsManifest,
toolNames
.map((toolName, index) => {
const contents = expectDefined(toolSources.get(toolName), `${toolName} source`);
const preBuildDigest = wrongDigest && index === 0 ? "0".repeat(64) : digest(contents);
const postBuildDigest =
toolName === "linuxdeploy-x86_64.AppImage"
? digest(postBuildLinuxdeploy)
: digest(contents);
const mode = toolName === "linuxdeploy-x86_64.AppImage" ? "0755" : "0555";
return [
toolName,
`https://example.invalid/${toolName}`,
preBuildDigest,
postBuildDigest,
mode,
].join("\t");
})
.join("\n") + "\n",
);
};
writeFileSync(
path.join(fakeBin, "curl"),
[
"#!/usr/bin/env bash",
"set -euo pipefail",
"output=",
"url=",
"while [[ $# -gt 0 ]]; do",
' case "$1" in',
" --output) output=$2; shift 2 ;;",
" https://*) url=$1; shift ;;",
" *) shift ;;",
" esac",
"done",
'[[ -n "$output" && -n "$url" ]]',
'if [[ ${CACHE_RACE_TOOL:-} == "${url##*/}" ]]; then',
' mkdir -p "$XDG_CACHE_HOME/tauri"',
' printf "raced\\n" > "$XDG_CACHE_HOME/tauri/race-marker"',
"fi",
'cp "$TOOL_SOURCE_DIR/${url##*/}" "$output"',
"",
].join("\n"),
{ mode: 0o755 },
);
copyFileSync(path.join(architectureBin, "uname"), path.join(fakeBin, "uname"));
chmodSync(path.join(fakeBin, "uname"), 0o755);
const toolEnv = {
...process.env,
EXPECTED_ARCH: "x86_64",
PATH: `${fakeBin}${path.delimiter}${process.env.PATH ?? ""}`,
SYNTHETIC_UNAME_MACHINE: "x86_64",
SYNTHETIC_UNAME_SYSTEM: "Linux",
TOOL_SOURCE_DIR: toolSourceDir,
XDG_CACHE_HOME: cacheRoot,
};
writeSyntheticManifest(true);
const rejectedPrepare = spawnSync(trustedTools, ["prepare"], {
cwd: selectedTagRoot,
encoding: "utf8",
env: toolEnv,
});
expect(rejectedPrepare.status).not.toBe(0);
expect(existsSync(path.join(cacheRoot, "tauri"))).toBe(false);
writeSyntheticManifest();
const rejectedRacedPrepare = spawnSync(trustedTools, ["prepare"], {
cwd: selectedTagRoot,
encoding: "utf8",
env: {
...toolEnv,
CACHE_RACE_TOOL: "linuxdeploy-plugin-appimage.AppImage",
},
});
expect(
rejectedRacedPrepare.status,
`${rejectedRacedPrepare.stdout}${rejectedRacedPrepare.stderr}`,
).not.toBe(0);
expect(readFileSync(path.join(cacheRoot, "tauri/race-marker"), "utf8")).toBe("raced\n");
expect(globSync(path.join(cacheRoot, ".tauri-tools.*"))).toEqual([]);
rmSync(path.join(cacheRoot, "tauri"), { recursive: true });
const prepared = spawnSync(trustedTools, ["prepare"], {
cwd: selectedTagRoot,
encoding: "utf8",
env: toolEnv,
});
expect(prepared.status, `${prepared.stdout}${prepared.stderr}`).toBe(0);
expect(globSync(path.join(cacheRoot, ".tauri-tools.*"))).toEqual([]);
const verifiedPreBuild = spawnSync(trustedTools, ["verify", "pre-build"], {
cwd: selectedTagRoot,
encoding: "utf8",
env: toolEnv,
});
expect(verifiedPreBuild.status, `${verifiedPreBuild.stdout}${verifiedPreBuild.stderr}`).toBe(0);
const toolsDir = path.join(cacheRoot, "tauri");
const runtime = path.join(toolsDir, ".appimage-runtime-x86_64");
const appImagePlugin = expectDefined(
toolSources.get("linuxdeploy-plugin-appimage.AppImage"),
"AppImage plugin source",
);
expect(readFileSync(runtime)).toEqual(appImagePlugin.subarray(0, 16));
expect(statSync(runtime).mode & 0o777).toBe(0o444);
const rejectedStaleCache = spawnSync(trustedTools, ["prepare"], {
cwd: selectedTagRoot,
encoding: "utf8",
env: toolEnv,
});
expect(rejectedStaleCache.status).not.toBe(0);
const linuxdeploy = path.join(toolsDir, "linuxdeploy-x86_64.AppImage");
writeFileSync(linuxdeploy, postBuildLinuxdeploy);
chmodSync(linuxdeploy, 0o755);
const verifiedPostBuild = spawnSync(trustedTools, ["verify", "post-build"], {
cwd: selectedTagRoot,
encoding: "utf8",
env: toolEnv,
});
expect(verifiedPostBuild.status, `${verifiedPostBuild.stdout}${verifiedPostBuild.stderr}`).toBe(
0,
);
const resetBundle = () => {
rmSync(bundleDir, { force: true, recursive: true });
mkdirSync(path.join(appDir, "usr/lib"), { recursive: true });
writeFileSync(path.join(appDir, "usr/lib/libwayland-client.so.0"), "host-incompatible");
writeFileSync(appImage, "pre-finalized");
chmodSync(appImage, 0o755);
writeFileSync(`${appImage}.sig`, "stale-signature");
rmSync(pluginSentinel, { force: true });
};
const runFinalizer = () =>
spawnSync(trustedFinalizer, [bundleDir], {
cwd: selectedTagRoot,
encoding: "utf8",
env: {
...toolEnv,
PLUGIN_SENTINEL: pluginSentinel,
},
});
const restoreTool = (toolName: (typeof toolNames)[number]) => {
const contents =
toolName === "linuxdeploy-x86_64.AppImage"
? postBuildLinuxdeploy
: expectDefined(toolSources.get(toolName), `${toolName} source`);
rmSync(path.join(toolsDir, toolName), { force: true });
writeFileSync(path.join(toolsDir, toolName), contents, { mode: 0o755 });
chmodSync(
path.join(toolsDir, toolName),
toolName === "linuxdeploy-x86_64.AppImage" ? 0o755 : 0o555,
);
};
for (const toolName of toolNames) {
resetBundle();
const tool = path.join(toolsDir, toolName);
chmodSync(tool, 0o755);
writeFileSync(tool, Buffer.concat([readFileSync(tool), Buffer.from("tampered")]));
const rejected = runFinalizer();
expect(rejected.status, `${toolName}: ${rejected.stdout}${rejected.stderr}`).not.toBe(0);
expect(existsSync(pluginSentinel), `${toolName} plugin execution`).toBe(false);
expect(existsSync(path.join(appDir, "usr/lib/libwayland-client.so.0")), toolName).toBe(true);
expect(readFileSync(appImage, "utf8"), toolName).toBe("pre-finalized");
expect(readFileSync(`${appImage}.sig`, "utf8"), toolName).toBe("stale-signature");
restoreTool(toolName);
}
resetBundle();
const appRun = path.join(toolsDir, "AppRun-x86_64");
rmSync(appRun);
symlinkSync(path.join(toolSourceDir, "AppRun-x86_64"), appRun);
const rejectedSymlink = runFinalizer();
expect(rejectedSymlink.status).not.toBe(0);
expect(existsSync(pluginSentinel)).toBe(false);
restoreTool("AppRun-x86_64");
resetBundle();
const gtkPlugin = path.join(toolsDir, "linuxdeploy-plugin-gtk.sh");
chmodSync(gtkPlugin, 0o444);
const rejectedNonExecutable = runFinalizer();
expect(rejectedNonExecutable.status).not.toBe(0);
expect(existsSync(pluginSentinel)).toBe(false);
restoreTool("linuxdeploy-plugin-gtk.sh");
resetBundle();
chmodSync(runtime, 0o644);
writeFileSync(runtime, Buffer.concat([readFileSync(runtime), Buffer.from("tampered")]));
const rejectedRuntime = runFinalizer();
expect(rejectedRuntime.status).not.toBe(0);
expect(existsSync(pluginSentinel)).toBe(false);
expect(existsSync(path.join(appDir, "usr/lib/libwayland-client.so.0"))).toBe(true);
expect(readFileSync(appImage, "utf8")).toBe("pre-finalized");
writeFileSync(runtime, appImagePlugin.subarray(0, 16), { mode: 0o644 });
chmodSync(runtime, 0o444);
resetBundle();
const finalized = runFinalizer();
expect(finalized.status, `${finalized.stdout}${finalized.stderr}`).toBe(0);
expect(readFileSync(appImage).subarray(0, 16)).toEqual(appImagePlugin.subarray(0, 16));
expect(readFileSync(appImage, "utf8")).toContain("#!/bin/sh");
expect(existsSync(`${appImage}.sig`)).toBe(false);
expect(existsSync(path.join(appDir, "usr/lib/libwayland-client.so.0"))).toBe(false);
expect(readFileSync(pluginSentinel, "utf8")).toBe("executed\n");
writeFileSync(
path.join(appDir, "usr/lib/libwayland-client.so.0"),
"post-finalization-smoke-fixture",
);
expect(existsSync(path.join(selectedTagRoot, "apps/linux/scripts/finalize-appimage.sh"))).toBe(
false,
);
const smokeChild = spawnSync(
"python3",
[
"-c",
[
"from pathlib import Path",
"import subprocess",
"import sys",
'child = Path(sys.argv[1]).with_name("first_run.py")',
"assert child.is_file()",
'subprocess.run([sys.executable, str(child), "--help"], check=True)',
].join("; "),
trustedSmoke,
],
{ cwd: selectedTagRoot, encoding: "utf8" },
);
expect(smokeChild.status, `${smokeChild.stdout}${smokeChild.stderr}`).toBe(0);
const armToolNames = [
"AppRun-aarch64",
"linuxdeploy-aarch64.AppImage",
"linuxdeploy-plugin-gtk.sh",
"linuxdeploy-plugin-gstreamer.sh",
"linuxdeploy-plugin-appimage.AppImage",
] as const;
const armToolSourceDir = path.join(selectedTagRoot, "arm-tool-sources");
const armCacheRoot = path.join(selectedTagRoot, ".arm-cache");
const armBundleDir = path.join(selectedTagRoot, "arm-bundle");
const armAppDir = path.join(armBundleDir, "OpenClaw.AppDir");
const armAppImage = path.join(armBundleDir, "OpenClaw_2026.8.2_arm64.AppImage");
const armPluginSentinel = path.join(selectedTagRoot, "arm-plugin-executed");
mkdirSync(armToolSourceDir);
const armToolSources = new Map<string, Buffer>();
for (const toolName of armToolNames) {
const contents =
toolName === "linuxdeploy-plugin-appimage.AppImage"
? expectDefined(toolSources.get(toolName), `${toolName} source`)
: Buffer.from(`#!/bin/sh\n# synthetic ${toolName}\nexit 0\n`);
armToolSources.set(toolName, contents);
writeFileSync(path.join(armToolSourceDir, toolName), contents, { mode: 0o755 });
}
const armPostBuildLinuxdeploy = Buffer.from(
expectDefined(armToolSources.get("linuxdeploy-aarch64.AppImage"), "ARM linuxdeploy source"),
);
armPostBuildLinuxdeploy.fill(0, 8, 11);
writeFileSync(
trustedArmToolsManifest,
armToolNames
.map((toolName) => {
const contents = expectDefined(armToolSources.get(toolName), `${toolName} source`);
return [
toolName,
`https://example.invalid/${toolName}`,
digest(contents),
toolName === "linuxdeploy-aarch64.AppImage"
? digest(armPostBuildLinuxdeploy)
: digest(contents),
toolName === "linuxdeploy-aarch64.AppImage" ? "0755" : "0555",
].join("\t");
})
.join("\n") + "\n",
);
const armToolEnv = {
...toolEnv,
EXPECTED_ARCH: "aarch64",
SYNTHETIC_UNAME_MACHINE: "arm64",
TOOL_SOURCE_DIR: armToolSourceDir,
XDG_CACHE_HOME: armCacheRoot,
};
const armPrepared = spawnSync(trustedTools, ["prepare"], {
cwd: selectedTagRoot,
encoding: "utf8",
env: armToolEnv,
});
expect(armPrepared.status, `${armPrepared.stdout}${armPrepared.stderr}`).toBe(0);
const armRuntimePath = spawnSync(trustedTools, ["runtime-path"], {
cwd: selectedTagRoot,
encoding: "utf8",
env: armToolEnv,
});
expect(armRuntimePath.status, `${armRuntimePath.stdout}${armRuntimePath.stderr}`).toBe(0);
expect(armRuntimePath.stdout).toBe(`${armCacheRoot}/tauri/.appimage-runtime-aarch64\n`);
const armToolsDir = path.join(armCacheRoot, "tauri");
const armLinuxdeploy = path.join(armToolsDir, "linuxdeploy-aarch64.AppImage");
writeFileSync(armLinuxdeploy, armPostBuildLinuxdeploy);
chmodSync(armLinuxdeploy, 0o755);
mkdirSync(path.join(armAppDir, "usr/lib"), { recursive: true });
writeFileSync(path.join(armAppDir, "usr/lib/libwayland-client.so.0"), "host-incompatible");
writeFileSync(armAppImage, "pre-finalized", { mode: 0o755 });
writeFileSync(`${armAppImage}.sig`, "stale-signature");
const armFinalized = spawnSync(trustedFinalizer, [armBundleDir], {
cwd: selectedTagRoot,
encoding: "utf8",
env: {
...armToolEnv,
PLUGIN_SENTINEL: armPluginSentinel,
},
});
expect(armFinalized.status, `${armFinalized.stdout}${armFinalized.stderr}`).toBe(0);
expect(readFileSync(armAppImage).subarray(0, 16)).toEqual(appImagePlugin.subarray(0, 16));
expect(existsSync(`${armAppImage}.sig`)).toBe(false);
expect(existsSync(path.join(armAppDir, "usr/lib/libwayland-client.so.0"))).toBe(false);
expect(readFileSync(armPluginSentinel, "utf8")).toBe("executed\n");
const writeSigningToolFixtures = (root: string) => {
const bin = path.join(root, "bin");
const poisonBin = path.join(root, "poison-bin");
const tauri = path.join(bin, "cargo-tauri");
const tauriLog = path.join(root, "cargo-tauri.log");
const minisignLog = path.join(root, "minisign.log");
mkdirSync(bin, { recursive: true });
mkdirSync(poisonBin, { recursive: true });
writeFileSync(
tauri,
[
"#!/usr/bin/env bash",
"set -euo pipefail",
'[[ "$#" -eq 3 && "$1" == "signer" && "$2" == "sign" ]]',
'[[ -n "${TAURI_SIGNING_PRIVATE_KEY:-}" ]]',
'[[ -n "${TAURI_SIGNING_PRIVATE_KEY_PASSWORD:-}" ]]',
'printf "%s\\n" "$*" >> "$TAURI_SIGN_LOG"',
'printf "ephemeral-signature:%s" "$(basename "$3")" | base64 > "$3.sig"',
"",
].join("\n"),
{ mode: 0o755 },
);
writeFileSync(
path.join(bin, "minisign"),
[
"#!/usr/bin/env bash",
"set -euo pipefail",
'[[ "$#" -eq 6 && "$1" == "-Vm" && "$3" == "-x" && "$5" == "-p" ]]',
'[[ -z "${TAURI_SIGNING_PRIVATE_KEY+x}" ]]',
'[[ -z "${TAURI_SIGNING_PRIVATE_KEY_PASSWORD+x}" ]]',
'[[ -s "$2" && -s "$4" && -s "$6" ]]',
'[[ "$(cat "$6")" == "ephemeral-public-key" ]]',
'printf "%s\\n" "$(basename "$2")" >> "$MINISIGN_LOG"',
"",
].join("\n"),
{ mode: 0o755 },
);
for (const command of ["cargo-tauri", "minisign", "npm", "pnpm", "npx", "corepack"]) {
writeFileSync(path.join(poisonBin, command), "#!/bin/sh\nexit 97\n", { mode: 0o755 });
}
return {
minisignBinarySha256: createHash("sha256")
.update(readFileSync(path.join(bin, "minisign")))
.digest("hex"),
minisignLog,
path: `${poisonBin}${path.delimiter}${process.env.PATH ?? ""}`,
tauriBinarySha256: createHash("sha256").update(readFileSync(tauri)).digest("hex"),
tauriLog,
};
};
const signingRoot = tempDirs.make("openclaw-linux-signing-job-");
const signingInput = path.join(signingRoot, "dist/signing-input");
const finalizedArtifact = path.join(signingInput, "OpenClaw-2026.8.2-amd64.AppImage");
mkdirSync(signingInput, { recursive: true });
writeFileSync(finalizedArtifact, "trusted-finalized-bytes");
const linuxSigningTools = writeSigningToolFixtures(signingRoot);
const signed = spawnSync("bash", ["-c", signAppImage.run ?? ""], {
cwd: signingRoot,
encoding: "utf8",
env: {
...process.env,
PATH: linuxSigningTools.path,
RELEASE_TAG: "v2026.8.2",
RUNNER_TEMP: signingRoot,
TAG_SHA: "a".repeat(40),
MINISIGN_BINARY_SHA256: linuxSigningTools.minisignBinarySha256,
TAURI_CLI_BINARY_SHA256: linuxSigningTools.tauriBinarySha256,
TAURI_SIGN_LOG: linuxSigningTools.tauriLog,
TAURI_SIGNING_PRIVATE_KEY: "ephemeral-test-key",
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: "ephemeral-test-password",
UPDATER_PUBLIC_KEY: Buffer.from("ephemeral-public-key").toString("base64"),
MINISIGN_LOG: linuxSigningTools.minisignLog,
},
});
expect(signed.status, `${signed.stdout}${signed.stderr}`).toBe(0);
expect(readFileSync(finalizedArtifact, "utf8")).toBe("trusted-finalized-bytes");
expect(
readFileSync(
path.join(signingRoot, "dist/linux-app/release/OpenClaw-2026.8.2-amd64.AppImage"),
"utf8",
),
).toBe("trusted-finalized-bytes");
expect(
readFileSync(
path.join(signingRoot, "dist/linux-app/signatures/OpenClaw-2026.8.2-amd64.AppImage.sig"),
"utf8",
),
).toBe(
`${Buffer.from("ephemeral-signature:OpenClaw-2026.8.2-amd64.AppImage").toString("base64")}\n`,
);
expect(readFileSync(linuxSigningTools.tauriLog, "utf8")).toBe(
"signer sign dist/signing-input/OpenClaw-2026.8.2-amd64.AppImage\n",
);
expect(readFileSync(linuxSigningTools.minisignLog, "utf8")).toBe(
"OpenClaw-2026.8.2-amd64.AppImage\n",
);
expect(existsSync(path.join(signingRoot, ".release-tooling"))).toBe(false);
expect(existsSync(path.join(signingRoot, "apps"))).toBe(false);
const desktopSigningRoot = tempDirs.make("openclaw-desktop-signing-job-");
const macosInput = path.join(
desktopSigningRoot,
"dist/signing-input/macos/OpenClaw-2026.8.2-darwin-aarch64.app.tar.gz",
);
const windowsInput = path.join(
desktopSigningRoot,
"dist/signing-input/windows/OpenClaw-2026.8.2-windows-x86_64.exe",
);
mkdirSync(path.dirname(macosInput), { recursive: true });
mkdirSync(path.dirname(windowsInput), { recursive: true });
writeFileSync(macosInput, "finalized-macos-updater-bytes");
writeFileSync(windowsInput, "finalized-windows-updater-bytes");
const desktopSigningTools = writeSigningToolFixtures(desktopSigningRoot);
const desktopSigned = spawnSync("bash", ["-c", signDesktop.run ?? ""], {
cwd: desktopSigningRoot,
encoding: "utf8",
env: {
...process.env,
PATH: desktopSigningTools.path,
RELEASE_TAG: "v2026.8.2",
RUNNER_TEMP: desktopSigningRoot,
TAG_SHA: "a".repeat(40),
MINISIGN_BINARY_SHA256: desktopSigningTools.minisignBinarySha256,
TAURI_CLI_BINARY_SHA256: desktopSigningTools.tauriBinarySha256,
TAURI_SIGN_LOG: desktopSigningTools.tauriLog,
TAURI_SIGNING_PRIVATE_KEY: "ephemeral-test-key",
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: "ephemeral-test-password",
UPDATER_PUBLIC_KEY: Buffer.from("ephemeral-public-key").toString("base64"),
MINISIGN_LOG: desktopSigningTools.minisignLog,
},
});
expect(desktopSigned.status, `${desktopSigned.stdout}${desktopSigned.stderr}`).toBe(0);
expect(readFileSync(macosInput, "utf8")).toBe("finalized-macos-updater-bytes");
expect(readFileSync(windowsInput, "utf8")).toBe("finalized-windows-updater-bytes");
expect(
readFileSync(
path.join(
desktopSigningRoot,
"dist/desktop-test/macos/release/OpenClaw-2026.8.2-darwin-aarch64.app.tar.gz",
),
"utf8",
),
).toBe("finalized-macos-updater-bytes");
expect(
readFileSync(
path.join(
desktopSigningRoot,
"dist/desktop-test/windows/release/OpenClaw-2026.8.2-windows-x86_64.exe",
),
"utf8",
),
).toBe("finalized-windows-updater-bytes");
expect(
Buffer.from(
readFileSync(
path.join(
desktopSigningRoot,
"dist/desktop-test/macos/signatures/OpenClaw-2026.8.2-darwin-aarch64.app.tar.gz.sig",
),
"utf8",
),
"base64",
).toString(),
).toContain("OpenClaw-2026.8.2-darwin-aarch64.app.tar.gz");
expect(
Buffer.from(
readFileSync(
path.join(
desktopSigningRoot,
"dist/desktop-test/windows/signatures/OpenClaw-2026.8.2-windows-x86_64.exe.sig",
),
"utf8",
),
"base64",
).toString(),
).toContain("OpenClaw-2026.8.2-windows-x86_64.exe");
expect(readFileSync(desktopSigningTools.tauriLog, "utf8")).toBe(
"signer sign dist/signing-input/macos/OpenClaw-2026.8.2-darwin-aarch64.app.tar.gz\n" +
"signer sign dist/signing-input/windows/OpenClaw-2026.8.2-windows-x86_64.exe\n",
);
expect(readFileSync(desktopSigningTools.minisignLog, "utf8")).toBe(
"OpenClaw-2026.8.2-darwin-aarch64.app.tar.gz\nOpenClaw-2026.8.2-windows-x86_64.exe\n",
);
expect(existsSync(path.join(desktopSigningRoot, ".release-tooling"))).toBe(false);
expect(existsSync(path.join(desktopSigningRoot, "apps"))).toBe(false);
}
const linuxBuildBodies = linuxBuildSteps.map(({ run }) => run ?? "").join("\n");
for (const helper of [
"apps/linux/scripts/stage-appimage-gstreamer.sh",
"apps/linux/scripts/finalize-appimage.sh",
"apps/linux/tests/packaged_runtime_smoke.py",
]) {
expect(linuxBuildBodies).toContain(`.release-tooling/${helper}`);
expect(linuxBuildBodies).not.toMatch(new RegExp(`(^|\\s)${helper.replaceAll(".", "\\.")}`));
}
const linuxBody = expectDefined(
(linux.jobs.validate_release.steps as WorkflowStep[]).find(
({ name }) => name === workflows[0].validation,
)?.run,
"Linux release admission body",
);
expect(linuxBody).toContain('exec python3 -I -S "$CI_GIT_OWNER" --policy -');
expect(linuxBody.match(/timeout=120/gu)).toHaveLength(2);
expect(linuxBody).toContain('"+refs/heads/main:refs/remotes/origin/main"');
expect(linuxBody).toContain('run_git(workspace, "merge-base", "--is-ancestor", sha, ref)');
const macos = parse(readFileSync(workflows[1].file, "utf8"));
const macosBody = expectDefined(
(macos.jobs.validate_macos_release_request.steps as WorkflowStep[]).find(
({ name }) => name === workflows[1].validation,
)?.run,
"macOS release admission body",
);
expect(macosBody.match(/--git 0/gu)).toHaveLength(1);
expect(macosBody.match(/--checkout-git 120/gu)).toHaveLength(1);
expect(macosBody).toContain(
'"+refs/heads/${PUBLIC_RELEASE_BRANCH}:refs/remotes/origin/${PUBLIC_RELEASE_BRANCH}"',
);
expect(macosBody.indexOf("--checkout-git 120")).toBeLessThan(
macosBody.indexOf("pnpm release:openclaw:npm:check"),
);
const placeholder = parse(readFileSync(workflows[2].file, "utf8"));
const placeholderBody = expectDefined(
(placeholder.jobs.plan.steps as WorkflowStep[]).find(
({ name }) => name === workflows[2].validation,
)?.run,
"placeholder admission body",
);
expect(placeholderBody).toContain('exec python3 -I -S "$CI_GIT_OWNER" --policy -');
expect(placeholderBody.match(/timeout=120/gu)).toHaveLength(1);
expect(placeholderBody.match(/run_git\(workspace, "merge-base"/gu)).toHaveLength(2);
expect(placeholderBody).toContain('output.write(f"sha={source_ref}\\n")');
});
it("pins every Performance Git owner before checkout and preserves Git deadlines", () => {
const source = readFileSync(".github/workflows/openclaw-performance.yml", "utf8");
const workflow = parse(source);
const targets = [
["resolve_target", "Checkout target metadata", undefined, 10],
["kova", "Checkout OpenClaw", "Decide lane", 240],
["source_performance", "Checkout OpenClaw source target", undefined, 120],
["publish", "Checkout performance publisher helper", "Decide report publication lane", 30],
] as const;
for (const [jobId, checkout, decision, timeout] of targets) {
const job = workflow.jobs[jobId];
const steps = job.steps as WorkflowStep[];
const index = steps.findIndex(({ name }) => name === "Prepare Git owner");
expect(index).toBe(decision ? 1 : 0);
expect(steps[index + 1]?.name).toBe(checkout);
if (decision) {
expect(steps[index - 1]?.name).toBe(decision);
}
expect(steps[index]).toEqual({
name: "Prepare Git owner",
uses: "openclaw/openclaw/.github/actions/git-owner@a379bbd73e30b84a89aca4d54744ab9ca19082e7",
...(decision ? { if: "steps.lane.outputs.run == 'true'" } : {}),
});
expect(job["timeout-minutes"]).toBe(timeout);
const bodies = steps.map(({ run }) => run ?? "").join("\n");
expect(bodies).not.toMatch(/(?:^|[\s(])git\s/mu);
expect(bodies).not.toMatch(/(?:^|[\s(])timeout\s+[^\n]*\bgit\b/u);
const ownerDeadlines = [...bodies.matchAll(/--(?:checkout-)?git (\d+)/gu)].map((match) =>
Number(match[1]),
);
expect(ownerDeadlines.every((deadline) => deadline === 0)).toBe(true);
if (jobId !== "publish") {
expect(bodies).not.toMatch(/timeout=\d+/u);
} else {
expect(bodies.match(/timeout=120/g)).toHaveLength(2);
expect(bodies).not.toMatch(/timeout=(?!120)\d+/u);
expect(bodies.match(/for attempt in range\(1, 6\)/gu)).toHaveLength(1);
expect(bodies.match(/backoff\(attempt \* 2\)/gu)).toHaveLength(1);
expect(bodies).toContain('"push", "origin", "HEAD:main", timeout=120, reclaim_locks=True');
expect(
bodies.match(/"fetch", "--depth=1", "origin", "main", timeout=120, reclaim_locks=True/gu),
).toHaveLength(1);
expect(bodies).toContain('fetch(sys.argv[3], "main", max_attempts=3, retry_failures=True)');
expect(bodies).toContain("if error.code != 1:");
expect(bodies).toContain(
'"ls-tree", "--name-only", "FETCH_HEAD", "--", f"{dest}/report.json"',
);
}
}
expect(workflow.on.schedule).toEqual([{ cron: "11 5 * * *" }]);
expect(Object.keys(workflow.on.workflow_dispatch.inputs)).toEqual([
"mode",
"target_ref",
"baseline_ref",
"profile",
"repeat",
"deep_profile",
"live_openai_candidate",
"fail_on_regression",
"publish_reports",
"kova_ref",
"kova_config_contract",
"dispatch_id",
]);
expect(workflow.permissions).toEqual({ contents: "read" });
expect(workflow.jobs.publish.permissions).toEqual({ actions: "read", contents: "read" });
expect(workflow.concurrency).toEqual({
group:
"${{ github.event_name == 'workflow_dispatch' && format('{0}-{1}', github.workflow, github.run_id) || format('{0}-{1}', github.workflow, github.ref) }}",
"cancel-in-progress": false,
});
});
describe("frozen CI compatibility contracts", () => {
it("skips current-only launcher and QA contracts for frozen targets", () => {
const source = readFileSync(".github/workflows/ci.yml", "utf8");
expect(source).toContain(
`if: \${{ needs.preflight.outputs.frozen_target != 'true' }}\n run: |\n bun openclaw.mjs --help`,
);
expect(source).toContain(
"[skip] ${partId} is not declared by this checkout's legacy smoke plan",
);
expect(source).not.toContain('"control-ui-chat-flow-playwright",');
expect(source).toContain("if (!source.includes(marker)) process.exit(0);");
});
});