Files
openclaw/test/scripts/plugin-prerelease-test-plan.test.ts
Peter Steinberger 6a50179657 ci: select protected owner coverage for pull requests
Use existing owner maps and runtime transitive imports instead of broad PR
fallbacks, with six fixed smoke files and protected regression coverage.
Keep audited zero-catch suites in the existing PR-exempt tier with direct-edit
and owner opt-in.

Select Windows, UI, build, Docker, and QA families through their owners.
Preserve all moved coverage hourly and in Full Release Verification through
the existing normal CI and Plugin Prerelease owners. Static correctness gates
remain blocking.

Supersedes the RV3 selection work in #158594. Linux owning-config, shared-helper,
selection, and static proof passed with focused corrections; P2 review completed.
2026-09-27 22:30:47 -07:00

1593 lines
71 KiB
TypeScript

// Plugin Prerelease Test Plan tests cover plugin prerelease test plan script behavior.
import { execFileSync, spawnSync } from "node:child_process";
import { existsSync, mkdirSync, readFileSync, symlinkSync, writeFileSync } from "node:fs";
import { join, resolve } from "node:path";
import { runInNewContext } from "node:vm";
import { expectDefined } from "@openclaw/normalization-core";
import { afterEach, describe, expect, it } from "vitest";
import { parse } from "yaml";
import { listAvailableExtensionIds } from "../../scripts/lib/changed-extensions.mts";
import { findLaneByName } from "../../scripts/lib/docker-e2e-plan.mts";
import { BUNDLED_PLUGIN_INSTALL_UNINSTALL_SHARDS } from "../../scripts/lib/docker-e2e-scenarios.mts";
import { resolveExtensionTestPlan } from "../../scripts/lib/extension-test-plan.mts";
import {
assertPluginPrereleaseTestPlanComplete,
createPluginPrereleaseTestPlan,
resolvePluginPrereleaseExtensionRuntime,
} from "../../scripts/lib/plugin-prerelease-test-plan.mts";
import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
import { evaluateWorkflowExpression, evaluateWorkflowRunner } from "./ci-workflow.test-support.js";
const CHECKOUT_V6 = "actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1";
const UPLOAD_ARTIFACT_V7 = "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a";
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
type WorkflowStep = {
id?: string;
env?: Record<string, string>;
name?: string;
run?: string;
uses?: string;
with?: Record<string, unknown>;
};
function readCiWorkflow() {
return parse(readFileSync(".github/workflows/ci.yml", "utf8"));
}
function readFullReleaseValidationWorkflow() {
return parse(readFileSync(".github/workflows/full-release-validation.yml", "utf8"));
}
function readPluginPrereleaseWorkflow() {
return parse(readFileSync(".github/workflows/plugin-prerelease.yml", "utf8"));
}
function getDockerLane(name: string) {
const lane = findLaneByName(name);
if (!lane) {
throw new Error(`Missing Docker E2E lane ${name}`);
}
return lane;
}
function pluginCandidateArtifactJson(selectedSha = "a".repeat(40)) {
return JSON.stringify({
packageArtifactName: "docker-e2e-package-123-1",
packageArtifactId: "456",
packageArtifactDigest: "b".repeat(64),
packageArtifactRunId: "123",
packageArtifactRunAttempt: "1",
packageFileName: "openclaw-current.tgz",
packageSourceSha: selectedSha,
packageSha256: "c".repeat(64),
packageVersion: "2026.8.1",
imageArtifactName: "docker-e2e-shared-images-123-1",
imageArtifactId: "789",
imageArtifactDigest: "d".repeat(64),
imageArtifactRunId: "123",
imageArtifactRunAttempt: "1",
imageArchiveSha256: "e".repeat(64),
});
}
function runPluginPhaseValidation(params: {
candidateArtifactJson?: string;
expectedSha?: string;
fullReleaseValidation?: boolean;
phase: string;
}) {
const workflow = readPluginPrereleaseWorkflow();
const step = workflow.jobs.preflight.steps.find(
(candidate: WorkflowStep) => candidate.name === "Validate phase inputs",
);
if (!step?.run) {
throw new Error("Missing plugin prerelease phase validation step");
}
return spawnSync("bash", ["-c", step.run], {
encoding: "utf8",
env: {
CANDIDATE_ARTIFACT_JSON: params.candidateArtifactJson ?? "",
EXPECTED_SHA: params.expectedSha ?? "",
FULL_RELEASE_VALIDATION: String(params.fullReleaseValidation ?? true),
PATH: process.env.PATH,
PHASE: params.phase,
},
});
}
function runPluginManifest(
phase: "all" | "candidate" | "independent",
eventName = "workflow_dispatch",
) {
const workflow = readPluginPrereleaseWorkflow();
const step = workflow.jobs.preflight.steps.find(
(candidate: WorkflowStep) => candidate.name === "Build plugin prerelease manifest",
);
if (!step?.run) {
throw new Error("Missing plugin prerelease manifest step");
}
const root = tempDirs.make("openclaw-plugin-prerelease-phase-");
const outputPath = join(root, "github-output");
const result = spawnSync("bash", ["-c", step.run], {
encoding: "utf8",
env: {
FULL_RELEASE_VALIDATION: eventName === "schedule" ? "false" : "true",
GITHUB_EVENT_NAME: eventName,
GITHUB_OUTPUT: outputPath,
PATH: process.env.PATH,
PHASE: phase,
},
});
return {
output: existsSync(outputPath) ? readFileSync(outputPath, "utf8") : "",
result,
};
}
function runPluginSummary(params: {
docker: string;
extensions: string;
inspector?: string;
node: string;
runDocker: boolean;
runExtensions: boolean;
runNode: boolean;
runNpmSecurity: boolean;
runStatic: boolean;
static: string;
}) {
const workflow = readPluginPrereleaseWorkflow();
const step = workflow.jobs["plugin-prerelease-suite"].steps.find(
(candidate: WorkflowStep) => candidate.name === "Verify plugin prerelease suite",
);
if (!step?.run) {
throw new Error("Missing plugin prerelease summary step");
}
return spawnSync("bash", ["-c", step.run], {
encoding: "utf8",
env: {
DOCKER_RESULT: params.docker,
EXTENSIONS_RESULT: params.extensions,
INSPECTOR_RESULT: params.inspector ?? "skipped",
NODE_RESULT: params.node,
PATH: process.env.PATH,
RUN_DOCKER: String(params.runDocker),
RUN_EXTENSIONS: String(params.runExtensions),
RUN_NODE: String(params.runNode),
RUN_NPM_SECURITY: String(params.runNpmSecurity),
RUN_STATIC: String(params.runStatic),
SECURITY_RESULT: "success",
STATIC_RESULT: params.static,
},
});
}
describe("scripts/lib/plugin-prerelease-test-plan.mts", () => {
it.each([
{
name: "ordinary plugin validation",
fullReleaseValidation: false,
memory: true,
vitestArgs: [],
requiresBun: false,
},
{
name: "full release memory and database-worker groups",
fullReleaseValidation: true,
memory: true,
vitestArgs: [],
requiresBun: true,
},
{
name: "full release with an exact exclusion",
fullReleaseValidation: true,
memory: true,
vitestArgs: ["--exclude=extensions/memory-lancedb/config.test.ts"],
requiresBun: true,
},
{
name: "full release with an explicit report",
fullReleaseValidation: true,
memory: true,
vitestArgs: ["--reporter=json", "--outputFile=report.json"],
requiresBun: false,
},
{
name: "full release database-worker groups",
fullReleaseValidation: true,
memory: false,
vitestArgs: [],
requiresBun: false,
},
])(
"selects runtime setup for $name",
async ({ fullReleaseValidation, memory, vitestArgs, requiresBun }) => {
const planGroups = [
{
config: "test/vitest/vitest.extension-database-workers.config.ts",
roots: ["extensions/memory-lancedb/index.test.ts"],
},
...(memory
? [
{
config: "test/vitest/vitest.extension-memory.config.ts",
roots: ["extensions/memory-lancedb"],
},
]
: []),
];
expect(
await resolvePluginPrereleaseExtensionRuntime({
planGroups,
fullReleaseValidation,
vitestArgs,
}),
).toEqual({ test_runtime_policy: requiresBun ? "dual" : "node", requires_bun: requiresBun });
},
);
it("runs the package and Docker product lanes through the existing scheduler", () => {
const plan = assertPluginPrereleaseTestPlanComplete();
expect(plan.dockerLanes).toEqual([
"npm-onboard-channel-agent",
"npm-onboard-discord-candidate-channel-agent",
"npm-onboard-slack-candidate-channel-agent",
"doctor-switch",
"update-channel-switch",
"plugins-offline",
"plugins",
"kitchen-sink-plugin",
"kitchen-sink-rpc",
"plugin-update",
"config-reload",
"gateway-network",
"mcp-channels",
"cron-mcp-cleanup",
...Array.from(
{ length: BUNDLED_PLUGIN_INSTALL_UNINSTALL_SHARDS },
(_, index) => `bundled-plugin-install-uninstall-${index}`,
),
]);
for (const lane of plan.dockerLanes) {
expect(getDockerLane(lane).name).toBe(lane);
}
const candidateLane = getDockerLane("npm-onboard-discord-candidate-channel-agent");
expect(candidateLane.command).toContain("OPENCLAW_DOCKER_E2E_TRUSTED_HARNESS_DIR");
expect(candidateLane.command).toContain(
'OPENCLAW_LIVE_DOCKER_REPO_ROOT="${OPENCLAW_DOCKER_E2E_REPO_ROOT:-$PWD}"',
);
});
it("keeps live-ish coverage outside provider-backed Docker lanes", () => {
const plan = createPluginPrereleaseTestPlan();
expect(plan.dockerLanes).not.toContain("openai-web-search-minimal");
expect(plan.dockerLanes.some((lane) => lane.startsWith("live-"))).toBe(false);
expect(plan.staticChecks[2]).toEqual({
check: "live-ish-availability",
checkName: "checks-plugin-prerelease-live-ish-availability",
command: "node --import tsx scripts/plugin-prerelease-liveish-matrix.mts",
surfaces: ["live-ish-availability"],
});
});
it("keeps SDK/package boundary checks inside the plugin prerelease suite", () => {
const plan = createPluginPrereleaseTestPlan();
expect(plan.staticChecks.map((check) => check.checkName)).toEqual([
"checks-plugin-prerelease-package-boundary-compile",
"checks-plugin-prerelease-package-boundary-canary",
"checks-plugin-prerelease-live-ish-availability",
]);
});
it("uses kitchen-sink npm and ClawHub scenarios as the registry install canary", () => {
const lane = getDockerLane("kitchen-sink-plugin");
const script = readFileSync("scripts/e2e/kitchen-sink-plugin-docker.sh", "utf8");
const sweepScript = readFileSync("scripts/e2e/lib/kitchen-sink-plugin/sweep.sh", "utf8");
const assertionsScript = readFileSync(
"scripts/e2e/lib/kitchen-sink-plugin/assertions.mjs",
"utf8",
);
expect(lane).toEqual({
command: "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:kitchen-sink-plugin",
e2eImageKind: "functional",
live: false,
name: "kitchen-sink-plugin",
resources: ["npm"],
stateScenario: "empty",
weight: 3,
});
expect(script).toContain("npm:@openclaw/kitchen-sink@latest");
expect(script).toContain("npm-latest-conformance");
expect(script).toContain("npm-latest-adversarial");
expect(script).toContain("npm:@openclaw/kitchen-sink@beta");
expect(script).toContain("clawhub:@openclaw/kitchen-sink@latest");
expect(script).toContain("clawhub:@openclaw/kitchen-sink@beta");
expect(script).toContain("OPENCLAW_KITCHEN_SINK_PLUGIN_MAX_MEMORY_MIB");
expect(script).toContain(
"npm-to-clawhub|clawhub:@openclaw/kitchen-sink@latest|openclaw-kitchen-sink-fixture|clawhub|success|basic||${KITCHEN_SINK_NPM_SPEC}",
);
expect(script).toContain("scripts/e2e/lib/kitchen-sink-plugin/sweep.sh");
expect(sweepScript).toContain('plugins install "$KITCHEN_SINK_SPEC" --force');
expect(sweepScript).toContain('plugins install "$KITCHEN_SINK_PREINSTALL_SPEC" --force');
expect(sweepScript).toContain("assert-cutover-preinstalled");
expect(sweepScript).toContain('install_args+=("--force")');
expect(sweepScript).toContain("KITCHEN_SINK_PERSONALITY");
expect(sweepScript).toContain("OPENCLAW_KITCHEN_SINK_PERSONALITY");
expect(sweepScript).toContain('plugins uninstall "$KITCHEN_SINK_SPEC" --force');
const successScenario = sweepScript.slice(
sweepScript.indexOf("run_success_scenario()"),
sweepScript.indexOf("run_failure_scenario()"),
);
expect(successScenario.indexOf('plugins install "${install_args[@]}" --force')).toBeLessThan(
successScenario.indexOf("configure_kitchen_sink_runtime"),
);
expect(successScenario.indexOf("configure_kitchen_sink_runtime")).toBeLessThan(
successScenario.indexOf('plugins enable "$KITCHEN_SINK_ID"'),
);
expect(successScenario).toContain('plugins inspect "$KITCHEN_SINK_ID" --runtime --json');
expect(successScenario).toContain("plugins inspect --all --runtime --json");
expect(sweepScript).toContain("run_failure_scenario");
expect(assertionsScript).toContain("assertCutoverPreinstalled");
expect(assertionsScript).toContain("record.source !== source");
expect(assertionsScript).toContain("record.clawhubPackage !== packageName");
expect(assertionsScript).toContain("record.artifactKind");
expect(assertionsScript).toContain("assertClawHubExternalInstallContract");
expect(assertionsScript).toContain("expectedErrorMessages");
expect(assertionsScript).toContain(
'const INVALID_PROBE_DIAGNOSTIC_SURFACE_MODES = new Set(["full", "adversarial"]);',
);
expect(assertionsScript).toContain("!INVALID_PROBE_DIAGNOSTIC_SURFACE_MODES.has(surfaceMode)");
expect(readFileSync("scripts/e2e/lib/clawhub-fixture-server.cjs", "utf8")).toContain(
'from "openclaw/plugin-sdk/plugin-entry"',
);
expect(readFileSync("scripts/e2e/lib/clawhub-fixture-server.cjs", "utf8")).toContain(
"X-ClawHub-Artifact-Sha256",
);
expect(script).toContain("docker_e2e_sample_stats_until_exit");
expect(script).toContain("scripts/e2e/lib/docker-stats/assert-resource-ceiling.mjs");
expect(sweepScript).toContain("scan_logs_for_unexpected_errors");
});
it("keeps kitchen-sink RPC coverage package-backed and resource-guarded", () => {
const lane = getDockerLane("kitchen-sink-rpc");
const script = readFileSync("scripts/e2e/kitchen-sink-rpc-docker.sh", "utf8");
const walkScript = readFileSync("scripts/e2e/kitchen-sink-rpc-walk.mts", "utf8");
expect(lane).toMatchObject({
command: "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:kitchen-sink-rpc",
e2eImageKind: "functional",
live: false,
name: "kitchen-sink-rpc",
resources: ["service", "npm"],
stateScenario: "empty",
timeoutMs: 1_500_000,
weight: 3,
});
expect(script).toContain("OPENCLAW_ENTRY=/app/openclaw.mjs");
expect(script).toContain("OPENCLAW_KITCHEN_SINK_COMMAND_MAX_RSS_MIB");
expect(script).toContain("docker_e2e_sample_stats_until_exit");
expect(script).toContain("scripts/e2e/lib/docker-stats/assert-resource-ceiling.mjs");
expect(script).toContain(
"openclaw_e2e_run_script_entrypoint scripts/e2e/kitchen-sink-rpc-walk",
);
expect(walkScript).toContain("commands.list");
expect(walkScript).toContain("tools.invoke");
expect(walkScript).toContain("tts.providers");
expect(walkScript).toContain("plugins.uiDescriptors");
expect(walkScript).toContain("loadCallGatewayModule(options.runner)");
expect(walkScript).toContain("usesBuiltOpenClawEntry(runner)");
expect(walkScript).toContain('"gateway"');
expect(walkScript).toContain('"call"');
expect(walkScript).not.toContain("src/gateway/call.ts");
expect(walkScript).toContain("^call(?:\\.runtime)?");
});
it("keeps the generic plugin Docker lane as an external install contract canary", () => {
const lane = getDockerLane("plugins");
const sweepScript = readFileSync("scripts/e2e/lib/plugins/sweep.sh", "utf8");
const clawhubScript = readFileSync("scripts/e2e/lib/plugins/clawhub.sh", "utf8");
const assertionsScript = readFileSync("scripts/e2e/lib/plugins/assertions.mjs", "utf8");
const fixtureServer = readFileSync("scripts/e2e/lib/clawhub-fixture-server.cjs", "utf8");
const prereleasePlan = createPluginPrereleaseTestPlan();
expect(lane).toEqual({
command: "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:plugins",
e2eImageKind: "functional",
live: false,
name: "plugins",
resources: ["npm", "service"],
stateScenario: "empty",
weight: 6,
});
expect(prereleasePlan.surfaces).toContain("external-install-boundary");
expect(sweepScript).toContain("run_plugins_clawhub_scenario");
expect(clawhubScript).toContain('plugins install "$CLAWHUB_PLUGIN_SPEC"');
expect(assertionsScript).toContain("assertClawHubExternalInstallContract");
expect(fixtureServer).toContain('"is-number": "7.0.0"');
expect(fixtureServer).toContain('openclaw: ">=2026.4.11"');
expect(fixtureServer).toContain("/versions/${fixture.version}/artifact");
});
it("forwards validated frozen-target omissions to the selected shard adapters", () => {
const workflow = readPluginPrereleaseWorkflow();
const preflight = workflow.jobs.preflight;
const nodeStep = workflow.jobs["plugin-prerelease-node-shard"].steps.find(
(step: WorkflowStep) => step.name === "Run release-only plugin Node shard",
);
const extensionStep = workflow.jobs["plugin-prerelease-extension-shard"].steps.find(
(step: WorkflowStep) => step.name === "Run extension shard",
);
expect(preflight.outputs.node_test_exclude_patterns_json).toBe(
"${{ steps.node_test_exclusions.outputs.patterns_json }}",
);
expect(nodeStep.env.NODE_TEST_EXCLUDE_PATTERNS_JSON).toBe(
"${{ needs.preflight.outputs.node_test_exclude_patterns_json }}",
);
expect(extensionStep.env.FRV_TEST_EXCLUDE_PATHS_JSON).toBe(
"${{ needs.preflight.outputs.extension_test_exclude_patterns_json }}",
);
expect(extensionStep.run).not.toContain("extensions/codex/src/app-server/run-attempt.test.ts");
});
it("keeps the trusted security scanner outside the candidate test process", () => {
const workflow = readPluginPrereleaseWorkflow();
const admission = workflow.jobs.resolve_target;
const securityPlan = workflow.jobs["plugin-npm-security-plan"];
const securityScan = workflow.jobs["plugin-npm-security-scan"];
const source = readFileSync(".github/workflows/plugin-prerelease.yml", "utf8");
expect(admission.steps).toEqual([
expect.objectContaining({
uses: CHECKOUT_V6,
with: expect.objectContaining({ ref: "${{ github.sha }}", path: "workflow" }),
}),
expect.objectContaining({
env: {
EXPECTED_SHA:
"${{ github.event_name == 'schedule' && github.sha || inputs.expected_sha }}",
OPENCLAW_REF_REMOTE: "${{ github.server_url }}/${{ github.repository }}.git",
TARGET_REF: "${{ github.event_name == 'schedule' && github.sha || inputs.target_ref }}",
},
run: expect.stringContaining("bash workflow/scripts/github/resolve-openclaw-ref.sh"),
}),
]);
expect(securityPlan.needs).toEqual(["resolve_target"]);
expect(securityPlan.if).toBe("github.event_name != 'schedule' && inputs.phase != 'candidate'");
expect(workflow.jobs["plugin-npm-security-package"]).toBeUndefined();
const securityPlanStepNames = securityPlan.steps.map((step: WorkflowStep) => step.name);
expect(securityPlanStepNames.indexOf("Install trusted scanner dependencies")).toBeLessThan(
securityPlanStepNames.indexOf("Checkout candidate as inert data"),
);
expect(evaluateWorkflowRunner(securityScan["runs-on"])).toBe("ubuntu-24.04");
expect(securityScan).toMatchObject({
name: "plugin-npm-security-scan",
needs: ["resolve_target", "plugin-npm-security-plan"],
permissions: { contents: "read" },
"runs-on": expect.any(String),
"timeout-minutes": 45,
});
const securityScanStepNames = securityScan.steps.map((step: WorkflowStep) => step.name);
expect(securityScanStepNames.indexOf("Install trusted scanner dependencies")).toBeLessThan(
securityScanStepNames.indexOf("Checkout candidate as inert package input"),
);
const packStep = securityScan.steps.find(
(step: WorkflowStep) => step.name === "Pack supplemental inert plugin inputs",
);
expect(packStep?.env?.EXPECTED_PACKAGES_JSON).toBe(
"${{ needs.plugin-npm-security-plan.outputs.packages_json }}",
);
expect(packStep?.run).toContain("plugin-npm-security-prepare.mts prepare");
expect(packStep?.run).toContain("if ! node --import tsx");
expect(packStep?.run).toContain("Package preparation failed");
expect(
securityScan.steps.find(
(step: WorkflowStep) => step.name === "Scan supplemental inert plugin inputs",
)?.run,
).toContain("node scripts/plugin-npm-security-scan-runner.mjs");
expect(
securityScan.steps.find(
(step: WorkflowStep) => step.name === "Scan supplemental inert plugin inputs",
)?.run,
).toContain('--target-context-ref "$TARGET_CONTEXT_REF"');
expect(source).not.toContain("actions/download-artifact@");
expect(source).not.toContain("plugin-npm-security-artifact-plan");
expect(source).not.toContain("npm-install-security-scan.release.test.ts");
expect(workflow.on.workflow_dispatch.inputs.target_context_ref).toEqual({
default: "",
description: "Canonical release context for an exact-SHA frozen-target validation",
required: false,
type: "string",
});
});
it("binds scanner identity independently of candidate-owned outputs", () => {
const workflow = readPluginPrereleaseWorkflow();
const root = tempDirs.make("openclaw-plugin-prerelease-identity-");
const admittedSha = "a".repeat(40);
const substitutedSha = "b".repeat(40);
mkdirSync(join(root, "scripts/lib"), { recursive: true });
mkdirSync(join(root, "workflow/scripts/github"), { recursive: true });
mkdirSync(join(root, "bin"));
symlinkSync(resolve("node_modules"), join(root, "node_modules"), "dir");
writeFileSync(
join(root, "workflow/scripts/github/resolve-openclaw-ref.sh"),
readFileSync("scripts/github/resolve-openclaw-ref.sh"),
);
writeFileSync(
join(root, "bin/git"),
`#!/bin/sh\n[ "$1" = rev-parse ] && [ "$2" = HEAD ] || exit 2\nprintf '%s\\n' '${admittedSha}'\n`,
{ mode: 0o755 },
);
writeFileSync(
join(root, "scripts/lib/plugin-prerelease-test-plan.mts"),
`import { appendFileSync } from "node:fs";
process.once("exit", () => appendFileSync(process.env.GITHUB_OUTPUT,
"checkout_revision=${substitutedSha}\\n"));
export function assertPluginPrereleaseTestPlanComplete() {
return { staticChecks: [{ checkName: "fixture", command: "true", check: "fixture" }], dockerLanes: [] };
}`,
);
writeFileSync(
join(root, "scripts/lib/extension-test-plan.mts"),
"export const DEFAULT_EXTENSION_TEST_SHARD_COUNT = 1; export function createExtensionTestShards() { return []; }",
);
writeFileSync(
join(root, "scripts/lib/ci-node-test-plan.mts"),
"export function createNodeTestShards() { return []; }",
);
const needs: Record<string, { outputs: Record<string, string> }> = {};
for (const [jobId, stepName] of [
["resolve_target", "Resolve target SHA"],
["preflight", "Build plugin prerelease manifest"],
] as const) {
const job = workflow.jobs[jobId];
if (!job) {
continue;
}
const step = job.steps.find((entry: WorkflowStep) => entry.name === stepName);
const outputPath = join(root, `${jobId}-output`);
const result = spawnSync("bash", ["-c", step.run], {
cwd: root,
encoding: "utf8",
env: {
EXPECTED_SHA: admittedSha,
FULL_RELEASE_VALIDATION: "true",
GITHUB_OUTPUT: outputPath,
PATH: `${join(root, "bin")}:${process.env.PATH}`,
PHASE: "independent",
TARGET_REF: admittedSha,
},
});
expect(result.status, result.stderr).toBe(0);
// Actions assigns each output in file order; the last duplicate wins.
const outputs = Object.fromEntries(
readFileSync(outputPath, "utf8")
.trim()
.split("\n")
.map((line) => [line.slice(0, line.indexOf("=")), line.slice(line.indexOf("=") + 1)]),
);
if (jobId === "preflight") {
expect(outputs.checkout_revision).toBe(substitutedSha);
}
needs[jobId] = {
outputs: Object.fromEntries(
Object.entries(job.outputs).map(([key, expression]) => [
key,
runInNewContext(String(expression).slice(3, -2), {
steps: { [step.id]: { outputs }, node_test_exclusions: { outputs: {} } },
}),
]),
),
};
}
for (const jobId of ["plugin-npm-security-plan", "plugin-npm-security-scan"]) {
for (const step of workflow.jobs[jobId].steps as WorkflowStep[]) {
const candidateRef = step.with?.path === ".release-candidate" ? step.with.ref : undefined;
for (const expression of [candidateRef, step.env?.CANDIDATE_SHA]) {
if (expression !== undefined) {
if (typeof expression !== "string") {
throw new Error("Candidate identity must be a workflow expression");
}
expect(runInNewContext(expression.slice(3, -2), { needs })).toBe(admittedSha);
}
}
}
}
});
it("wires the full plugin prerelease plan into its release workflow", () => {
const workflow = readCiWorkflow();
const preflight = workflow.jobs.preflight;
const pluginWorkflow = readPluginPrereleaseWorkflow();
const pluginPreflight = pluginWorkflow.jobs.preflight;
const securityScan = pluginWorkflow.jobs["plugin-npm-security-scan"];
const staticShard = pluginWorkflow.jobs["plugin-prerelease-static-shard"];
const nodeShard = pluginWorkflow.jobs["plugin-prerelease-node-shard"];
const extensionShard = pluginWorkflow.jobs["plugin-prerelease-extension-shard"];
const inspector = pluginWorkflow.jobs["plugin-prerelease-inspector"];
const dockerSuite = pluginWorkflow.jobs["plugin-prerelease-docker-suite"];
const suite = pluginWorkflow.jobs["plugin-prerelease-suite"];
const releaseWorkflow = readFullReleaseValidationWorkflow();
const releaseWorkflowSource = readFileSync(
".github/workflows/full-release-validation.yml",
"utf8",
);
const manifestScript = preflight.steps.find(
(step: WorkflowStep) => step.name === "Build CI manifest",
).run;
const manifestEnv = preflight.steps.find(
(step: WorkflowStep) => step.name === "Build CI manifest",
).env;
const pluginManifestScript = pluginPreflight.steps.find(
(step: WorkflowStep) => step.name === "Build plugin prerelease manifest",
).run;
const pluginManifestEnv = pluginPreflight.steps.find(
(step: WorkflowStep) => step.name === "Build plugin prerelease manifest",
).env;
const normalCiScript = releaseWorkflow.jobs.normal_ci.steps.find(
(step: WorkflowStep) => step.name === "Dispatch CI",
).run;
const pluginPrereleaseScript = releaseWorkflow.jobs.plugin_prerelease_candidate.steps.find(
(step: WorkflowStep) => step.name === "Dispatch plugin prerelease candidate phase",
).run;
const releaseChecksStep = releaseWorkflow.jobs.release_checks_candidate.steps.find(
(step: WorkflowStep) => step.name === "Dispatch release checks candidate phase",
);
const releaseChecksScript = releaseChecksStep.run;
const buildDistStep = workflow.jobs["build-artifacts"].steps.find(
(step: WorkflowStep) => step.name === "Build dist",
);
expect(workflow.jobs["plugin-prerelease-static-shard"]).toBeUndefined();
expect(workflow.jobs["plugin-prerelease-inspector"]).toBeUndefined();
expect(workflow.jobs["plugin-prerelease-docker-suite"]).toBeUndefined();
expect(workflow.jobs["plugin-prerelease-suite"]).toBeUndefined();
expect(workflow.jobs["checks-node-extensions-shard"]).toBeUndefined();
expect(preflight.outputs).not.toHaveProperty("run_plugin_prerelease_suite");
expect(preflight.outputs).not.toHaveProperty("run_checks_node_extensions");
expect(buildDistStep.env).toEqual({ NODE_OPTIONS: "--max-old-space-size=8192" });
expect(evaluateWorkflowRunner(staticShard["runs-on"])).toBe("ubuntu-24.04");
expect(evaluateWorkflowRunner(staticShard["runs-on"], { eventName: "push" })).toBe(
"blacksmith-8vcpu-ubuntu-2404",
);
expect(staticShard).toEqual({
if: "needs.preflight.outputs.run_plugin_prerelease_static == 'true'",
name: "${{ matrix.check_name || 'plugin-prerelease-static-shard' }}",
needs: ["resolve_target", "preflight"],
permissions: {
contents: "read",
},
"runs-on": expect.any(String),
steps: [
{
name: "Checkout",
uses: CHECKOUT_V6,
with: {
"fetch-depth": 1,
"fetch-tags": false,
"persist-credentials": false,
ref: "${{ needs.resolve_target.outputs.checkout_revision }}",
submodules: false,
},
},
{
name: "Setup Node environment",
uses: "./.github/actions/setup-node-env",
with: {
"cache-mode": "restore",
"install-bun": "false",
},
},
{
env: {
PLUGIN_PRERELEASE_COMMAND: "${{ matrix.command }}",
PLUGIN_PRERELEASE_TASK: "${{ matrix.task }}",
},
name: "Run plugin prerelease static shard",
run: [
"set -euo pipefail",
'echo "Running ${PLUGIN_PRERELEASE_TASK}: ${PLUGIN_PRERELEASE_COMMAND}"',
'bash -c "$PLUGIN_PRERELEASE_COMMAND"',
"",
].join("\n"),
shell: "bash",
},
],
strategy: {
"fail-fast": false,
matrix: "${{ fromJson(needs.preflight.outputs.plugin_prerelease_static_matrix) }}",
},
"timeout-minutes": 45,
});
expect(workflow.on.workflow_dispatch.inputs.full_release_validation).toBeUndefined();
expect(workflow.on.workflow_dispatch.inputs.include_android).toEqual({
default: false,
description: "Run Android lanes for this manual CI dispatch.",
required: false,
type: "boolean",
});
expect(workflow.on.workflow_dispatch.inputs.historical_target_tag).toEqual({
default: "",
description: "Semver release tag authorizing compatibility fallbacks for its exact commit",
required: false,
type: "string",
});
expect(workflow.on.workflow_dispatch.inputs.release_candidate_ref).toEqual({
default: "",
description:
"Canonical release branch authorizing compatibility fallbacks for its exact head",
required: false,
type: "string",
});
expect(workflow.on.workflow_dispatch.inputs.target_context_ref).toEqual({
default: "",
description:
"Canonical release branch context authorizing compatibility fallbacks for an exact-SHA target",
required: false,
type: "string",
});
expect(manifestEnv).toMatchObject({
OPENCLAW_CI_CHANGED_PATHS_JSON:
"${{ steps.changed_scope.outputs.changed_paths_json || 'null' }}",
OPENCLAW_CI_CHECKOUT_REVISION: "${{ steps.checkout_ref.outputs.sha }}",
OPENCLAW_CI_DOCS_CHANGED:
"${{ github.event_name == 'schedule' && 'true' || github.event_name == 'workflow_dispatch' && (steps.runner_profile.outputs.node_runner_backend != 'runson' && steps.runner_profile.outputs.ci_qualification != 'true') && 'true' || steps.docs_scope.outputs.docs_changed }}",
OPENCLAW_CI_DOCS_ONLY:
"${{ github.event_name == 'schedule' && 'false' || github.event_name == 'workflow_dispatch' && (steps.runner_profile.outputs.node_runner_backend != 'runson' && steps.runner_profile.outputs.ci_qualification != 'true') && 'false' || steps.docs_scope.outputs.docs_only }}",
OPENCLAW_CI_EVENT_NAME: "${{ github.event_name }}",
OPENCLAW_CI_HISTORICAL_TARGET: "${{ steps.historical_target.outputs.eligible || 'false' }}",
OPENCLAW_CI_RELEASE_GATE: "${{ inputs.release_gate && 'true' || 'false' }}",
OPENCLAW_CI_RELEASE_CANDIDATE_TARGET:
"${{ steps.release_candidate_target.outputs.eligible || 'false' }}",
OPENCLAW_CI_TARGET_CONTEXT_TARGET:
"${{ steps.target_context_target.outputs.eligible || 'false' }}",
OPENCLAW_CI_REPOSITORY: "${{ github.repository }}",
OPENCLAW_CI_RUNNER_PROFILE: "${{ steps.runner_profile.outputs.runner_profile }}",
OPENCLAW_CI_NODE_RUNNER_BACKEND: "${{ steps.runner_profile.outputs.node_runner_backend }}",
OPENCLAW_CI_QUALIFICATION: "${{ steps.runner_profile.outputs.ci_qualification }}",
OPENCLAW_CI_SHAPE: "${{ steps.runner_profile.outputs.ci_shape }}",
OPENCLAW_CI_RUN_ANDROID:
"${{ github.event_name == 'schedule' && 'true' || github.event_name == 'workflow_dispatch' && ((inputs.release_gate && (steps.runner_profile.outputs.node_runner_backend != 'runson' && steps.runner_profile.outputs.ci_qualification != 'true')) || inputs.include_android) && 'true' || steps.changed_scope.outputs.run_android || 'false' }}",
OPENCLAW_CI_RUN_CONTROL_UI_I18N:
"${{ github.event_name == 'schedule' && 'true' || github.event_name == 'workflow_dispatch' && (steps.runner_profile.outputs.node_runner_backend != 'runson' && steps.runner_profile.outputs.ci_qualification != 'true') && 'true' || steps.changed_scope.outputs.run_control_ui_i18n || 'false' }}",
OPENCLAW_CI_RUN_IOS_BUILD:
"${{ github.event_name == 'schedule' && 'true' || github.event_name == 'workflow_dispatch' && !inputs.release_gate && 'true' || steps.changed_scope.outputs.run_ios_build || 'false' }}",
OPENCLAW_CI_RUN_MACOS:
"${{ github.event_name == 'schedule' && 'true' || github.event_name == 'workflow_dispatch' && !inputs.release_gate && 'true' || steps.changed_scope.outputs.run_macos || 'false' }}",
OPENCLAW_CI_RUN_MACOS_NODE:
"${{ github.event_name == 'schedule' && 'true' || github.event_name == 'workflow_dispatch' && !inputs.release_gate && 'true' || steps.changed_scope.outputs.run_macos_node || 'false' }}",
OPENCLAW_CI_RUN_NATIVE_I18N:
"${{ github.event_name == 'schedule' && 'true' || github.event_name == 'workflow_dispatch' && (steps.runner_profile.outputs.node_runner_backend != 'runson' && steps.runner_profile.outputs.ci_qualification != 'true') && 'true' || steps.changed_scope.outputs.run_native_i18n || 'false' }}",
OPENCLAW_CI_RUN_NODE:
"${{ github.event_name == 'schedule' && 'true' || github.event_name == 'workflow_dispatch' && (steps.runner_profile.outputs.node_runner_backend != 'runson' && steps.runner_profile.outputs.ci_qualification != 'true') && 'true' || steps.changed_scope.outputs.run_node || 'false' }}",
OPENCLAW_CI_RUN_NODE_FAST_CI_ROUTING:
"${{ github.event_name == 'workflow_dispatch' && (steps.runner_profile.outputs.node_runner_backend != 'runson' && steps.runner_profile.outputs.ci_qualification != 'true') && 'false' || steps.changed_scope.outputs.run_node_fast_ci_routing || 'false' }}",
OPENCLAW_CI_RUN_NODE_FAST_ONLY:
"${{ github.event_name == 'workflow_dispatch' && (steps.runner_profile.outputs.node_runner_backend != 'runson' && steps.runner_profile.outputs.ci_qualification != 'true') && 'false' || steps.changed_scope.outputs.run_node_fast_only || 'false' }}",
OPENCLAW_CI_RUN_NODE_FAST_PLUGIN_CONTRACTS:
"${{ github.event_name == 'workflow_dispatch' && (steps.runner_profile.outputs.node_runner_backend != 'runson' && steps.runner_profile.outputs.ci_qualification != 'true') && 'false' || steps.changed_scope.outputs.run_node_fast_plugin_contracts || 'false' }}",
OPENCLAW_CI_RUN_SKILLS_PYTHON:
"${{ github.event_name == 'schedule' && 'true' || github.event_name == 'workflow_dispatch' && (steps.runner_profile.outputs.node_runner_backend != 'runson' && steps.runner_profile.outputs.ci_qualification != 'true') && 'true' || steps.changed_scope.outputs.run_skills_python || 'false' }}",
OPENCLAW_CI_RUN_UI_TESTS:
"${{ github.event_name == 'schedule' && 'true' || github.event_name == 'workflow_dispatch' && !inputs.release_gate && (steps.runner_profile.outputs.node_runner_backend != 'runson' && steps.runner_profile.outputs.ci_qualification != 'true') && 'true' || steps.changed_scope.outputs.run_ui_tests || 'false' }}",
OPENCLAW_CI_RUN_WINDOWS:
"${{ github.event_name == 'schedule' && 'true' || github.event_name == 'workflow_dispatch' && !inputs.release_gate && (steps.runner_profile.outputs.node_runner_backend != 'runson' && steps.runner_profile.outputs.ci_qualification != 'true') && 'true' || steps.changed_scope.outputs.run_windows || 'false' }}",
OPENCLAW_CI_WORKFLOW_REVISION: "${{ github.workflow_sha }}",
});
expect(manifestEnv).not.toHaveProperty("OPENCLAW_CI_FULL_RELEASE_VALIDATION");
expect(manifestScript).toContain("includeReleaseOnlyPluginShards: false");
expect(manifestScript).not.toContain("plugin-prerelease-test-plan.mts");
expect(
workflow.jobs["check-shard"].steps.find(
(step: WorkflowStep) => step.name === "Run check shard",
).run,
).toContain("pnpm deadcode:ci");
expect(releaseChecksStep.env?.TARGET_CONTEXT_REF).toBe("${{ inputs.target_context_ref }}");
expect(releaseChecksScript).toContain('-f ref="$TARGET_SHA"');
expect(releaseChecksScript).toContain('-f target_context_ref="$TARGET_CONTEXT_REF"');
expect(releaseChecksScript).toContain("args+=(-f allow_frozen_target_scenario_omissions=true)");
expect(releaseWorkflowSource).toContain('--arg targetContextRef "$TARGET_CONTEXT_REF"');
expect(releaseWorkflowSource).toContain("targetContextRef: $targetContextRef");
expect(normalCiScript).toContain('dispatch_child ci.yml "$dispatch_run_name" "${args[@]}"');
const normalCiDispatchCase = normalCiScript.match(/^\s*ci\)\n([\s\S]*?)^\s*;;$/mu)?.[1];
expect(normalCiDispatchCase).toContain('dispatch_child ci.yml "$dispatch_run_name"');
expect(normalCiDispatchCase).not.toContain("full_release_validation=true");
expect(pluginPrereleaseScript).toContain('-f phase="$PHASE"');
expect(pluginPrereleaseScript).toContain(
'args+=(-f candidate_artifact_json="$CANDIDATE_ARTIFACT_JSON")',
);
expect(pluginPrereleaseScript).toContain(
'dispatch_child plugin-prerelease.yml "$dispatch_run_name" "${args[@]}"',
);
expect(pluginManifestScript).toContain("await import(");
expect(pluginManifestScript).toContain('"./scripts/lib/plugin-prerelease-test-plan.mts"');
expect(pluginManifestScript).toContain('"./scripts/lib/extension-test-plan.mts"');
expect(pluginManifestScript).toContain('"./scripts/lib/ci-node-test-plan.mts"');
expect(pluginManifestScript).toContain("const { createNodeTestShards } = await import");
expect(pluginManifestScript).not.toContain("createNodeTestShardBundles");
expect(pluginManifestScript).not.toContain("compactMode");
expect(pluginManifestScript).not.toContain("runnerBackend");
expect(pluginManifestScript).toContain('shard.shardName === "agentic-plugins"');
expect(pluginManifestScript).toContain(
"Plugin prerelease plan unavailable in target ref; skipping static and Docker plugin prerelease lanes.",
);
const pluginNodeShardScript = pluginWorkflow.jobs["plugin-prerelease-node-shard"].steps.find(
(step: WorkflowStep) => step.name === "Run release-only plugin Node shard",
).run;
expect(pluginNodeShardScript).toContain(
'".frv-tooling/scripts/frv-test-exclusions.mjs", "run", "--", "pnpm", "test", "--", ...configs',
);
expect(pluginNodeShardScript).not.toContain("scripts/test-projects.mts");
expect(pluginWorkflow.on.workflow_dispatch.inputs.target_ref).toEqual({
default: "main",
description: "Branch, tag, or full commit SHA to validate",
required: false,
type: "string",
});
expect(pluginWorkflow.on.workflow_dispatch.inputs.full_release_validation).toEqual({
default: false,
description: "Enable release-only Docker prerelease lanes from Full Release Validation",
required: false,
type: "boolean",
});
expect(pluginWorkflow.on.workflow_dispatch.inputs.phase).toEqual({
default: "all",
description: "Plugin prerelease phase to run",
options: ["all", "independent", "candidate"],
required: false,
type: "choice",
});
expect(pluginWorkflow.on.workflow_dispatch.inputs.dispatch_id).toEqual({
description: "Optional parent workflow dispatch identifier",
required: false,
default: "",
type: "string",
});
expect(pluginManifestEnv).toEqual({
NODE_TEST_EXCLUDE_PATTERNS_JSON: "${{ steps.node_test_exclusions.outputs.patterns_json }}",
EXTENSION_TEST_EXCLUDE_PATTERNS_JSON:
"${{ steps.node_test_exclusions.outputs.extension_patterns_json }}",
FULL_RELEASE_VALIDATION: "${{ inputs.full_release_validation && 'true' || 'false' }}",
PHASE: "${{ github.event_name == 'schedule' && 'independent' || inputs.phase }}",
});
expect(pluginManifestScript).toContain(
'const fullReleaseValidation = process.env.FULL_RELEASE_VALIDATION === "true";',
);
expect(pluginManifestScript).toContain('const runIndependent = phase !== "candidate";');
expect(pluginManifestScript).toContain('const runCandidate = phase !== "independent";');
expect(pluginManifestScript).toContain(
"const runDocker = runCandidate && fullReleaseValidation && dockerLanes.length > 0;",
);
expect(pluginPreflight.outputs).toEqual({
plugin_prerelease_docker_lanes:
"${{ steps.manifest.outputs.plugin_prerelease_docker_lanes }}",
plugin_prerelease_extension_matrix:
"${{ steps.manifest.outputs.plugin_prerelease_extension_matrix }}",
plugin_prerelease_node_matrix: "${{ steps.manifest.outputs.plugin_prerelease_node_matrix }}",
node_test_exclude_patterns_json: "${{ steps.node_test_exclusions.outputs.patterns_json }}",
extension_test_exclude_patterns_json:
"${{ steps.node_test_exclusions.outputs.extension_patterns_json }}",
plugin_prerelease_static_matrix:
"${{ steps.manifest.outputs.plugin_prerelease_static_matrix }}",
run_plugin_prerelease_docker: "${{ steps.manifest.outputs.run_plugin_prerelease_docker }}",
run_plugin_prerelease_extensions:
"${{ steps.manifest.outputs.run_plugin_prerelease_extensions }}",
run_plugin_prerelease_inspector:
"${{ steps.manifest.outputs.run_plugin_prerelease_inspector }}",
run_plugin_prerelease_node: "${{ steps.manifest.outputs.run_plugin_prerelease_node }}",
run_plugin_prerelease_static: "${{ steps.manifest.outputs.run_plugin_prerelease_static }}",
run_plugin_prerelease_suite: "${{ steps.manifest.outputs.run_plugin_prerelease_suite }}",
});
expect(staticShard.strategy.matrix).toBe(
"${{ fromJson(needs.preflight.outputs.plugin_prerelease_static_matrix) }}",
);
expect(securityScan.needs).toEqual(["resolve_target", "plugin-npm-security-plan"]);
expect(nodeShard.strategy.matrix).toBe(
"${{ fromJson(needs.preflight.outputs.plugin_prerelease_node_matrix) }}",
);
expect(extensionShard.if).toBe(
"needs.preflight.outputs.run_plugin_prerelease_extensions == 'true'",
);
expect(extensionShard.strategy.matrix).toBe(
"${{ fromJson(needs.preflight.outputs.plugin_prerelease_extension_matrix) }}",
);
expect(
extensionShard.steps.find(
(step: WorkflowStep) => step.name === "Setup pinned Bun test runtime",
),
).toMatchObject({
if: "matrix.requires_bun == true",
uses: "./.github/actions/setup-test-bun",
});
expect(
extensionShard.steps.find((step: WorkflowStep) => step.name === "Run extension shard").env
.OPENCLAW_CI_TEST_RUNTIME_POLICY,
).toBe("${{ matrix.test_runtime_policy || 'node' }}");
expect(inspector.name).toBe("plugin-prerelease-inspector");
expect(inspector.needs).toEqual(["resolve_target", "preflight"]);
expect(inspector.if).toBe("needs.preflight.outputs.run_plugin_prerelease_inspector == 'true'");
expect(inspector["continue-on-error"]).toBe(true);
expect(evaluateWorkflowRunner(inspector["runs-on"])).toBe("ubuntu-24.04");
expect(inspector["timeout-minutes"]).toBe(30);
expect(
inspector.steps.find((step: WorkflowStep) => step.name === "Setup Node environment").with,
).toEqual({
"cache-mode": "restore",
"install-bun": "false",
});
const inspectorRun = inspector.steps.find(
(step: WorkflowStep) => step.name === "Run plugin inspector advisory sweep",
);
expect(inspectorRun.env).toEqual({
OPENCLAW_PLUGIN_INSPECTOR_ROOT: ".artifacts/plugin-inspector",
OPENCLAW_PLUGIN_INSPECTOR_VERSION: "0.3.26",
});
expect(inspectorRun.run).toContain("extensions/");
expect(inspectorRun.run).toContain(
'npm exec --yes "@openclaw/plugin-inspector@${OPENCLAW_PLUGIN_INSPECTOR_VERSION}" -- ci',
);
expect(inspectorRun.run).toContain("This job is informational");
expect(
inspector.steps.find(
(step: WorkflowStep) => step.name === "Upload plugin inspector advisory artifacts",
),
).toEqual({
if: "always()",
name: "Upload plugin inspector advisory artifacts",
uses: UPLOAD_ARTIFACT_V7,
with: {
"if-no-files-found": "warn",
name: "plugin-inspector-advisory",
path: ".artifacts/plugin-inspector/**",
},
});
expect(
staticShard.steps.find(
(step: WorkflowStep) => step.name === "Run plugin prerelease static shard",
).run,
).toContain('bash -c "$PLUGIN_PRERELEASE_COMMAND"');
expect(dockerSuite).toMatchObject({
if: "${{ inputs.full_release_validation && needs.preflight.outputs.run_plugin_prerelease_docker == 'true' }}",
name: "plugin-prerelease-docker-suite",
needs: ["resolve_target", "preflight"],
permissions: {
actions: "read",
contents: "read",
packages: "read",
"pull-requests": "read",
},
uses: "./.github/workflows/openclaw-live-and-e2e-checks-reusable.yml",
with: {
docker_lanes: "${{ needs.preflight.outputs.plugin_prerelease_docker_lanes }}",
include_live_suites: false,
include_openwebui: false,
include_release_path_suites: false,
include_repo_e2e: false,
live_models_only: false,
allow_unreleased_changelog: true,
ref: "${{ needs.resolve_target.outputs.checkout_revision }}",
shared_image_artifact_namespace: "plugin-prerelease",
shared_image_policy: "no-push-artifact",
targeted_docker_lane_group_size: 2,
},
});
expect(dockerSuite.with.enable_prepublish_plugin_registry).toBe(true);
expect(
Object.keys(dockerSuite.with).filter((key) => key.startsWith("prepublish_plugin_registry_")),
).toEqual([
"prepublish_plugin_registry_artifact_name",
"prepublish_plugin_registry_artifact_id",
"prepublish_plugin_registry_artifact_digest",
"prepublish_plugin_registry_artifact_run_id",
"prepublish_plugin_registry_artifact_run_attempt",
"prepublish_plugin_registry_manifest_sha256",
]);
expect(dockerSuite.with.package_artifact_id).toBe(
"${{ fromJSON(inputs.candidate_artifact_json || '{}').packageArtifactId || '' }}",
);
expect(dockerSuite.with.shared_image_artifact_id).toBe(
"${{ fromJSON(inputs.candidate_artifact_json || '{}').imageArtifactId || '' }}",
);
expect(dockerSuite.secrets).toBeUndefined();
expect(suite.needs).toEqual([
"preflight",
"plugin-npm-security-scan",
"plugin-prerelease-static-shard",
"plugin-prerelease-node-shard",
"plugin-prerelease-extension-shard",
"plugin-prerelease-inspector",
"plugin-prerelease-docker-suite",
]);
expect(
suite.steps.find((step: WorkflowStep) => step.name === "Verify plugin prerelease suite").run,
).toContain("plugin-prerelease-inspector advisory result");
expect(
suite.steps.find((step: WorkflowStep) => step.name === "Verify plugin prerelease suite").run,
).toContain('check_required "plugin-npm-security-scan" "$RUN_NPM_SECURITY" "$SECURITY_RESULT"');
});
it.each([
{
expected: {
docker: "true",
extensions: "true",
inspector: "true",
node: "true",
static: "true",
},
phase: "all",
},
{
expected: {
docker: "false",
extensions: "true",
inspector: "true",
node: "true",
static: "true",
},
phase: "independent",
},
{
expected: {
docker: "true",
extensions: "false",
inspector: "false",
node: "false",
static: "false",
},
phase: "candidate",
},
] as const)("routes only the $phase plugin prerelease phase", ({ expected, phase }) => {
const { output, result } = runPluginManifest(phase);
expect(result.status, result.stderr).toBe(0);
for (const [lane, scheduled] of Object.entries(expected)) {
expect(output).toContain(`run_plugin_prerelease_${lane}=${scheduled}\n`);
}
if (phase === "independent") {
const matrixLine = expectDefined(
output.split("\n").find((line) => line.startsWith("plugin_prerelease_extension_matrix=")),
"extension matrix output",
);
const matrix = JSON.parse(matrixLine.slice(matrixLine.indexOf("=") + 1)) as {
include: {
extensions_csv: string;
requires_bun?: boolean;
task: string;
test_runtime_policy?: string;
}[];
};
const bunRows = matrix.include.filter((row) => row.requires_bun);
expect(
bunRows
.flatMap((row) => row.extensions_csv.split(",").filter((id) => id.startsWith("memory-")))
.toSorted(),
).toEqual(["memory-lancedb", "memory-wiki"]);
expect(
bunRows.every(
(row) => row.task === "extensions-batch" && row.test_runtime_policy === "dual",
),
).toBe(true);
expect(
matrix.include
.filter((row) => row.task === "extensions-batch" && !row.requires_bun)
.every((row) => row.test_runtime_policy === "node"),
).toBe(true);
}
});
it("requires a complete immutable candidate for the plugin candidate phase", () => {
const selectedSha = "a".repeat(40);
const independent = runPluginPhaseValidation({
candidateArtifactJson: "",
phase: "independent",
});
const missing = runPluginPhaseValidation({
candidateArtifactJson: "",
expectedSha: selectedSha,
phase: "candidate",
});
const valid = runPluginPhaseValidation({
candidateArtifactJson: pluginCandidateArtifactJson(selectedSha),
expectedSha: selectedSha,
phase: "candidate",
});
expect(independent.status, independent.stderr).toBe(0);
expect(missing.status).toBe(1);
expect(missing.stderr).toContain(
"phase=candidate requires the complete immutable package and Docker image artifact tuple.",
);
expect(valid.status, valid.stderr).toBe(0);
});
it("keeps hourly extension coverage with its complete release owner and required result", () => {
const workflow = readPluginPrereleaseWorkflow();
const context = {
eventName: "schedule" as const,
repository: "openclaw/openclaw",
ref: "refs/heads/main",
sha: "a".repeat(40),
runAttempt: 1,
};
const evaluate = (value: string, overrides = {}) =>
evaluateWorkflowExpression(value.startsWith("${{") ? value : `\${{ ${value} }}`, {
...context,
...overrides,
});
expect(workflow.on.schedule).toEqual([{ cron: "37 * * * *" }]);
expect(workflow.permissions).toEqual({ contents: "read" });
expect(evaluate(workflow.jobs.resolve_target.if)).toBe(true);
expect(evaluate(workflow.jobs.resolve_target.if, { repository: "contributor/openclaw" })).toBe(
false,
);
expect(evaluate(workflow.jobs.resolve_target.if, { ref: "refs/heads/topic" })).toBe(false);
expect(
evaluate(workflow.jobs["plugin-prerelease-suite"].if, {
repository: "contributor/openclaw",
preflightOutputs: {},
}),
).toBe(false);
const resolver = workflow.jobs.resolve_target.steps.find(
(step: WorkflowStep) => step.id === "resolve",
);
expect(evaluate(resolver.env.TARGET_REF)).toBe(context.sha);
expect(evaluate(resolver.env.EXPECTED_SHA)).toBe(context.sha);
expect(evaluate(workflow.concurrency.group)).toBe("plugin-prerelease-hourly-main");
expect(evaluate(workflow.concurrency.group, { sha: "b".repeat(40) })).toBe(
"plugin-prerelease-hourly-main",
);
expect(evaluate(workflow.concurrency["cancel-in-progress"])).toBe(false);
expect(evaluate(workflow.jobs["plugin-npm-security-plan"].if)).toBe(false);
const validation = workflow.jobs.preflight.steps.find(
(step: WorkflowStep) => step.name === "Validate phase inputs",
);
expect(evaluate(validation.env.PHASE)).toBe("independent");
const exclusions = workflow.jobs.preflight.steps.find(
(step: WorkflowStep) => step.id === "node_test_exclusions",
);
expect(evaluate(exclusions.env.NODE_TEST_EXCLUDE_PATTERNS_JSON)).toBe("[]");
expect(evaluate(exclusions.env.EXTENSION_TEST_EXCLUDE_PATTERNS_JSON)).toBe("[]");
const summaryStep = workflow.jobs["plugin-prerelease-suite"].steps.find(
(step: WorkflowStep) => step.name === "Verify plugin prerelease suite",
);
expect(evaluate(summaryStep.env.RUN_NPM_SECURITY)).toBe("false");
expect(workflow.jobs["plugin-prerelease-extension-shard"].strategy["max-parallel"]).toBe(12);
const hourly = runPluginManifest("independent", "schedule");
const release = runPluginManifest("independent");
expect(hourly.result.status, hourly.result.stderr).toBe(0);
expect(release.result.status, release.result.stderr).toBe(0);
const outputs = (output: string) =>
Object.fromEntries(
output
.trim()
.split("\n")
.map((line) => [line.slice(0, line.indexOf("=")), line.slice(line.indexOf("=") + 1)]),
);
const selected = outputs(hourly.output);
const ownership = (matrix: string | undefined) =>
JSON.parse(expectDefined(matrix, "extension matrix")).include.map(
({ requires_bun: _bun, test_runtime_policy: _runtime, ...row }: Record<string, unknown>) =>
row,
);
expect(ownership(selected.plugin_prerelease_extension_matrix)).toEqual(
ownership(outputs(release.output).plugin_prerelease_extension_matrix),
);
const rows: {
task: string;
extensions_csv: string;
includePatterns?: string[];
requires_bun?: boolean;
test_runtime_policy?: string;
}[] = JSON.parse(
expectDefined(selected.plugin_prerelease_extension_matrix, "hourly extension matrix"),
).include;
expect(
rows.every((row) => !row.requires_bun && (row.test_runtime_policy ?? "node") === "node"),
).toBe(true);
const extensionIds = new Set(rows.flatMap((row) => row.extensions_csv.split(",")));
for (const id of ["device-pair", "active-memory", "talk-voice"]) {
expect(extensionIds, id).toContain(id);
}
expect(listAvailableExtensionIds()).toContain("image-generation-core");
expect(resolveExtensionTestPlan({ targetArg: "image-generation-core" })).toMatchObject({
hasTests: false,
testFileCount: 0,
});
expect(extensionIds).not.toContain("image-generation-core");
const fileTargets = rows
.filter((row) => row.task === "extension-file-shard")
.flatMap((row) => row.includePatterns ?? []);
expect(new Set(fileTargets).size).toBe(fileTargets.length);
expect(fileTargets).toContain("extensions/device-pair/doctor-contract-api.test.ts");
expect(
rows.filter((row) =>
row.includePatterns?.includes("extensions/plugin-entry.cli-laziness.test.ts"),
),
).toEqual([
expect.objectContaining({
task: "extension-file-shard",
vitest_config: "test/vitest/vitest.extensions.config.ts",
}),
]);
expect(selected.run_plugin_prerelease_extensions).toBe("true");
expect(selected.run_plugin_prerelease_suite).toBe("true");
for (const family of ["static", "node", "inspector", "docker"]) {
expect(selected[`run_plugin_prerelease_${family}`]).toBe("false");
}
for (const result of ["success", "failure", "cancelled", "skipped"]) {
const summary = runPluginSummary({
docker: "skipped",
extensions: result,
node: "skipped",
static: "skipped",
runDocker: false,
runExtensions: true,
runNode: false,
runNpmSecurity: false,
runStatic: false,
});
expect(summary.status).toBe(result === "success" ? 0 : 1);
}
});
it("validates only scheduled plugin jobs in each phase summary", () => {
const independent = runPluginSummary({
docker: "failure",
extensions: "success",
node: "success",
runDocker: false,
runExtensions: true,
runNode: true,
runNpmSecurity: true,
runStatic: true,
static: "success",
});
const candidate = runPluginSummary({
docker: "success",
extensions: "failure",
node: "failure",
runDocker: true,
runExtensions: false,
runNode: false,
runNpmSecurity: false,
runStatic: false,
static: "failure",
});
const failedCandidate = runPluginSummary({
docker: "failure",
extensions: "skipped",
node: "skipped",
runDocker: true,
runExtensions: false,
runNode: false,
runNpmSecurity: false,
runStatic: false,
static: "skipped",
});
expect(independent.status, independent.stderr).toBe(0);
expect(candidate.status, candidate.stderr).toBe(0);
expect(failedCandidate.status).toBe(1);
expect(`${failedCandidate.stdout}\n${failedCandidate.stderr}`).toContain(
"plugin-prerelease-docker ended with failure",
);
});
it("keeps exact release tuples independent without cancelling adopted children", () => {
const releaseChecksWorkflow = parse(
readFileSync(".github/workflows/openclaw-release-checks.yml", "utf8"),
);
const fullReleaseWorkflow = readFullReleaseValidationWorkflow();
expect(releaseChecksWorkflow.concurrency).toEqual({
group:
"openclaw-release-checks-${{ inputs.expected_sha || inputs.ref }}-${{ github.sha }}-${{ inputs.rerun_group }}-${{ inputs.phase }}-${{ inputs.release_profile == 'minimum' && 'beta' || inputs.release_profile }}-${{ inputs.run_release_soak || inputs.release_profile == 'stable' || inputs.release_profile == 'full' }}",
"cancel-in-progress": "${{ startsWith(github.ref, 'refs/heads/tideclaw/alpha/') }}",
});
expect(readPluginPrereleaseWorkflow().concurrency).toEqual({
group:
"${{ github.event_name == 'schedule' && 'plugin-prerelease-hourly-main' || format('plugin-prerelease-{0}-{1}-{2}', inputs.target_ref, github.sha, inputs.phase) }}",
"cancel-in-progress": "${{ github.event_name != 'schedule' && inputs.target_ref == 'main' }}",
});
expect(fullReleaseWorkflow.concurrency).toEqual({
group:
"full-release-validation-${{ inputs.expected_sha || inputs.ref }}-${{ github.sha }}-${{ inputs.rerun_group }}-${{ inputs.release_profile == 'minimum' && 'beta' || inputs.release_profile }}-${{ inputs.run_release_soak || inputs.release_profile == 'stable' || inputs.release_profile == 'full' }}",
"cancel-in-progress": false,
});
for (const workflow of [fullReleaseWorkflow, releaseChecksWorkflow]) {
const coverageKey = (profile: string, soak: boolean) =>
workflow.concurrency.group.replace(
/\$\{\{\s*([\s\S]*?)\s*\}\}/gu,
(_: string, expression: string) =>
String(
runInNewContext(expression, {
github: { sha: "a".repeat(40) },
inputs: {
expected_sha: "b".repeat(40),
rerun_group: "all",
phase: "candidate",
release_profile: profile,
run_release_soak: soak,
},
}),
),
);
expect(
new Set([
coverageKey("beta", false),
coverageKey("beta", true),
coverageKey("stable", false),
coverageKey("full", false),
]).size,
).toBe(4);
expect(coverageKey("minimum", false)).toBe(coverageKey("beta", false));
for (const profile of ["stable", "full"]) {
expect(coverageKey(profile, false)).toBe(coverageKey(profile, true));
}
}
expect(fullReleaseWorkflow.on.workflow_dispatch.inputs.expected_sha).toEqual({
description: "Optional full Validation SHA that ref must resolve to",
required: false,
default: "",
type: "string",
});
const resolveTargetStep = fullReleaseWorkflow.jobs.resolve_target.steps.find(
(step: WorkflowStep) => step.name === "Resolve target SHA",
);
const targetSummaryStep = fullReleaseWorkflow.jobs.resolve_target.steps.find(
(step: WorkflowStep) => step.name === "Summarize target",
);
expect(resolveTargetStep.env?.EXPECTED_SHA).toBe("${{ inputs.expected_sha }}");
expect(resolveTargetStep.run).toContain('--expected-sha "$EXPECTED_SHA"');
expect(targetSummaryStep.run).toContain("- Validation SHA:");
expect(targetSummaryStep.run).not.toContain("- Code SHA:");
expect(evaluateWorkflowRunner(releaseChecksWorkflow.jobs.resolve_target["runs-on"])).toBe(
"ubuntu-24.04",
);
expect(
evaluateWorkflowRunner(releaseChecksWorkflow.jobs.prepare_release_package["runs-on"]),
).toBe("ubuntu-24.04");
expect(evaluateWorkflowRunner(releaseChecksWorkflow.jobs.summary["runs-on"])).toBe(
"ubuntu-24.04",
);
for (const jobName of [
"docker_runtime_assets_preflight",
"normal_ci",
"plugin_prerelease_independent",
"plugin_prerelease_candidate",
"npm_telegram",
"summary",
]) {
expect(evaluateWorkflowRunner(fullReleaseWorkflow.jobs[jobName]["runs-on"])).toBe(
"ubuntu-24.04",
);
}
expect(fullReleaseWorkflow.jobs.normal_ci["timeout-minutes"]).toBe(15);
expect(fullReleaseWorkflow.jobs.normal_ci.needs).toEqual([
"resolve_target",
"plugin_compatibility_readiness",
"evidence_reuse",
]);
expect(fullReleaseWorkflow.jobs.normal_ci.if).toContain(
"needs.resolve_target.result == 'success'",
);
expect(fullReleaseWorkflow.jobs.normal_ci.if).toContain(
"needs.evidence_reuse.outputs.reuse != 'true'",
);
expect(fullReleaseWorkflow.jobs.docker_runtime_assets_preflight.if).toBe(
"${{ always() && github.run_attempt == 1 && needs.resolve_target.result == 'success' && contains(fromJSON('[\"success\",\"skipped\"]'), needs.plugin_compatibility_readiness.result) && contains(fromJSON('[\"success\",\"skipped\"]'), needs.evidence_reuse.result) && inputs.rerun_group == 'all' && contains(needs.resolve_target.outputs.target_version, '-alpha.') && needs.evidence_reuse.outputs.reuse != 'true' }}",
);
expect(fullReleaseWorkflow.jobs.docker_runtime_assets_preflight["timeout-minutes"]).toBe(20);
const dockerPreflightStep = fullReleaseWorkflow.jobs.docker_runtime_assets_preflight.steps.find(
(step: WorkflowStep) => step.name === "Verify Docker runtime-assets prune path",
);
expect(dockerPreflightStep).toBeDefined();
expect(dockerPreflightStep?.run).toContain("docker build");
expect(dockerPreflightStep?.run).toContain("--target runtime-assets");
expect(dockerPreflightStep?.run).toContain("timeout --kill-after=30s 15m docker build");
expect(dockerPreflightStep?.run).toContain(
'--build-arg OPENCLAW_EXTENSIONS="diagnostics-otel,codex"',
);
expect(
fullReleaseWorkflow.jobs.docker_runtime_assets_preflight.steps.some(
(step: WorkflowStep) => step.name === "Build and smoke test final Docker runtime image",
),
).toBe(false);
for (const jobName of [
"plugin_prerelease_independent",
"plugin_prerelease_candidate",
"release_checks_independent",
"release_checks_candidate",
"npm_telegram",
"performance",
]) {
expect(fullReleaseWorkflow.jobs[jobName]["timeout-minutes"], jobName).toBe(15);
}
const fullReleaseSource = readFileSync(".github/workflows/full-release-validation.yml", "utf8");
expect(fullReleaseWorkflow.on.workflow_dispatch.inputs.fail_fast).toEqual({
description:
"Cancel only an exact active child after its first blocking job; false drains all children and permits same-parent recovery",
required: false,
default: false,
type: "boolean",
});
for (const [jobName, kind] of [
["normal_ci", "ci"],
["plugin_prerelease_independent", "plugin-prerelease"],
["plugin_prerelease_candidate", "plugin-prerelease"],
["release_checks_independent", "release-checks"],
["release_checks_candidate", "release-checks"],
["npm_telegram", "npm-telegram"],
] as const) {
const dispatch = expectDefined(
fullReleaseWorkflow.jobs[jobName].steps.find(
(step: WorkflowStep) => step.id === "dispatch",
) as WorkflowStep | undefined,
`${jobName} dispatch step`,
);
expect(dispatch.env?.CHILD_WORKFLOW_KIND).toBe(kind);
if (jobName.startsWith("release_checks_")) {
expect(dispatch.env?.FAIL_FAST).toBe("${{ inputs.fail_fast }}");
expect(dispatch.run).toContain('-f fail_fast="$FAIL_FAST"');
} else {
expect(dispatch.env).not.toHaveProperty("FAIL_FAST");
}
}
expect(
fullReleaseWorkflow.jobs.performance.steps.find(
(step: WorkflowStep) => step.id === "dispatch",
)?.env,
).not.toHaveProperty("FAIL_FAST");
expect(fullReleaseSource).toContain('-f fail_fast="$FAIL_FAST"');
expect(fullReleaseSource).not.toContain(
"has failed child jobs before the workflow completed; cancelling the remaining run.",
);
expect(fullReleaseSource).not.toContain("trap cancel_child");
expect(fullReleaseSource).not.toContain("cancel_child_on_failure");
expect(fullReleaseSource).not.toContain("exit_on_parent_signal");
expect(fullReleaseSource).not.toContain("disable_child_cleanup");
expect(fullReleaseSource).not.toContain("cancel_child");
expect(fullReleaseSource).toContain(
'if [[ "$child_head_sha" != "$PARENT_WORKFLOW_SHA" ]]; then',
);
expect(releaseChecksWorkflow.on.workflow_dispatch.inputs.fail_fast).toEqual({
description: "Stop the Matrix QA lane after its first failed check or scenario",
required: false,
default: false,
type: "boolean",
});
expect(releaseChecksWorkflow.jobs.qa_live_release_checks.with.fail_fast).toBe(
"${{ fromJSON(needs.resolve_target.outputs.fail_fast) }}",
);
const qaLiveSource = readFileSync(".github/workflows/qa-live-transports-convex.yml", "utf8");
expect(qaLiveSource).toContain('if [[ "$FAIL_FAST" == "true" ]]');
});
it("allows Unreleased notes only for current-tree release checks", () => {
const workflow = parse(readFileSync(".github/workflows/openclaw-release-checks.yml", "utf8"));
const fullReleaseWorkflow = readFullReleaseValidationWorkflow();
const resolveTarget = workflow.jobs.resolve_target;
const captureInputs = resolveTarget.steps.find(
(step: WorkflowStep) => step.name === "Capture selected inputs",
);
const currentTreeAllowance =
"${{ needs.resolve_target.outputs.allow_unreleased_changelog == 'true' }}";
expect(workflow.on.workflow_dispatch.inputs.allow_unreleased_changelog).toEqual({
default: false,
description: "Allow explicitly opted-in current-tree packaging to use Unreleased notes",
required: false,
type: "boolean",
});
expect(resolveTarget.outputs.allow_unreleased_changelog).toBe(
"${{ steps.inputs.outputs.allow_unreleased_changelog }}",
);
expect(captureInputs?.run).toContain('RELEASE_REF_INPUT" == "main"');
expect(captureInputs?.run).toContain('RELEASE_REF_INPUT" == "refs/heads/main"');
expect(captureInputs?.run).toContain("release/[0-9]{4}");
expect(captureInputs?.run).toContain("extended-stable/[0-9]{4}");
expect(captureInputs?.run).toContain("tideclaw/alpha/");
expect(captureInputs?.run).toContain("refs/tags/");
expect(captureInputs?.run).toContain("RELEASE_ALLOW_UNRELEASED_CHANGELOG_INPUT");
expect(captureInputs?.run).toContain("allow_unreleased_changelog=false");
const explicitOptIn = captureInputs?.run.indexOf('"$allow_unreleased_changelog" == "true"');
const releaseRefGuard = captureInputs?.run.indexOf(
'"$RELEASE_REF_INPUT" =~ ^(refs/heads/)?(release/',
);
expect(explicitOptIn).toBeGreaterThanOrEqual(0);
expect(releaseRefGuard).toBeGreaterThan(explicitOptIn ?? -1);
expect(workflow.jobs.install_smoke_release_checks.with.allow_unreleased_changelog).toBe(
currentTreeAllowance,
);
expect(workflow.jobs.live_repo_e2e_release_checks.with.allow_unreleased_changelog).toBe(
currentTreeAllowance,
);
expect(workflow.jobs.docker_e2e_release_checks.with.allow_unreleased_changelog).toBe(
currentTreeAllowance,
);
const fullReleaseAllowance =
"${{ inputs.allow_unreleased_changelog || (inputs.target_context_ref == '' && (inputs.ref == 'main' || inputs.ref == 'refs/heads/main')) }}";
const summarizeTarget = fullReleaseWorkflow.jobs.resolve_target.steps.find(
(step: WorkflowStep) => step.name === "Summarize target",
);
const releaseChecksDispatch = fullReleaseWorkflow.jobs.release_checks_candidate.steps.find(
(step: WorkflowStep) => step.name === "Dispatch release checks candidate phase",
);
expect(summarizeTarget?.env?.ALLOW_UNRELEASED_CHANGELOG).toBe(fullReleaseAllowance);
expect(releaseChecksDispatch?.env?.ALLOW_UNRELEASED_CHANGELOG).toBe(fullReleaseAllowance);
});
it("keeps runtime tool coverage blocking in release checks", () => {
const releaseChecksSource = readFileSync(
".github/workflows/openclaw-release-checks.yml",
"utf8",
);
const releaseChecksWorkflow = parse(releaseChecksSource);
const runtimeToolCoverage = releaseChecksWorkflow.jobs.runtime_tool_coverage_release_checks;
expect(runtimeToolCoverage["continue-on-error"]).toBeUndefined();
expect(runtimeToolCoverage.needs).toEqual([
"resolve_target",
"qa_lab_runtime_parity_release_checks",
]);
expect(runtimeToolCoverage.steps).toEqual(
expect.arrayContaining([
expect.objectContaining({
name: "Enforce core runtime tool coverage",
run: expect.stringContaining("pnpm openclaw qa coverage"),
}),
]),
);
expect(runtimeToolCoverage.steps).toEqual(
expect.arrayContaining([
expect.objectContaining({
name: "Enforce core runtime tool coverage",
run: expect.stringContaining(
"--summary .artifacts/qa-e2e/runtime-pair-core/qa-suite-summary.json",
),
}),
]),
);
expect(releaseChecksWorkflow.jobs.summary.needs).toContain(
"runtime_tool_coverage_release_checks",
);
const verifyStep = releaseChecksWorkflow.jobs.summary.steps.find(
(step: { name?: string }) => step.name === "Verify release check results",
);
expect(verifyStep.env.RUNTIME_TOOL_COVERAGE_RELEASE_CHECKS_RESULT).toBe(
"${{ needs.runtime_tool_coverage_release_checks.result }}",
);
expect(verifyStep.run).toContain(
'"runtime_tool_coverage_release_checks=${RUNTIME_TOOL_COVERAGE_RELEASE_CHECKS_RESULT}"',
);
});
it("keeps the live-ish availability check redacted", () => {
const output = execFileSync(
process.execPath,
["--import", "tsx", "scripts/plugin-prerelease-liveish-matrix.mts"],
{
encoding: "utf8",
env: {
DISCORD_TOKEN: "discord-token-should-not-print",
OPENAI_API_KEY: "openai-token-should-not-print",
},
},
);
expect(output).toContain("provider-openai: present (OPENAI_API_KEY, OPENAI_BASE_URL)");
expect(output).toContain("channel-discord: present (DISCORD_TOKEN, OPENCLAW_DISCORD_TOKEN)");
expect(output).not.toContain("openai-token-should-not-print");
expect(output).not.toContain("discord-token-should-not-print");
});
});