Files
openclaw/test/scripts/docker-e2e-plan.test.ts
Peter Steinberger 6652f7eac8 refactor: remove Tasks and TaskFlow runtime (#159179)
Remove Tasks and TaskFlow runtime, APIs, CLI, SDK surfaces and panels after the Cron, session, native execution and media completion ownership cutovers. Preserve stored rows and import provable legacy native assignments through Doctor; ambiguous ownership stays untouched with a warning.

Follows #158221, #158217, #158225, #158222, #158702 and #158776. Related: #156532. Task-specific public APIs retire immediately; retained responsibilities use their existing owners.

Maintainer-authorized administrative landing after full CI run 36312986498 attempt 2 passed on 274595e2, with subsequent actual conflicts reviewed and focused checks passing. Current PR CI preflight hits the 64 KiB changed-path metadata limit before tests (run 36335042695); its duplicate security-review status mirrors that planning failure. Review and scoped proof are recorded in the PR. Published 9.4 native import is proven; remaining native completion and 9.4 rollback witnesses are explicitly unproven.
2026-09-27 10:40:29 -07:00

2447 lines
92 KiB
TypeScript

import { execFileSync, spawnSync } from "node:child_process";
import {
chmodSync,
copyFileSync,
existsSync,
mkdirSync,
readFileSync,
rmSync,
writeFileSync,
} from "node:fs";
import { dirname, join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import {
RELEASE_PATH_PROFILE,
findLaneByName,
parseLaneSelection,
resolveDockerE2ePlan,
} from "../../scripts/lib/docker-e2e-plan.mts";
import {
allReleasePathLanes,
BUNDLED_PLUGIN_INSTALL_UNINSTALL_SHARDS,
mainLanes,
} from "../../scripts/lib/docker-e2e-scenarios.mts";
import { createFrozenTargetSource } from "../../scripts/lib/frozen-target-source.mjs";
import {
listRecordedFirstHopSourceVersions,
updateFirstHopCompatLaneName,
} from "../../scripts/lib/update-first-hop-lanes.mjs";
import { resolveTestNodeExecPath } from "../../src/test-utils/node-process.js";
import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
const testNodeExecPath = resolveTestNodeExecPath();
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
const orderLanes = <T>(lanes: T[]) => lanes;
const packageJson = JSON.parse(readFileSync("package.json", "utf8")) as {
scripts?: Record<string, string>;
};
function writeFrozenScenarioContract(targetRoot: string, scenarios: string[]): string {
const assertionsFile = join(targetRoot, "scripts/e2e/lib/upgrade-survivor/assertions.mjs");
mkdirSync(dirname(assertionsFile), { recursive: true });
writeFileSync(
assertionsFile,
[
`const scenarios = ${JSON.stringify(scenarios)};`,
'if (process.argv[2] !== "list-scenarios") throw new Error("unknown command");',
"process.stdout.write(`${JSON.stringify(scenarios)}\\n`);",
].join("\n"),
);
return assertionsFile;
}
function copyCurrentScenarioMetadata(targetRoot: string) {
const paths = [
"scripts/e2e/lib/upgrade-survivor/assertions.mjs",
"scripts/lib/upgrade-survivor-policy.mjs",
"scripts/lib/upgrade-survivor-scenarios.json",
];
for (const relative of paths) {
mkdirSync(dirname(join(targetRoot, relative)), { recursive: true });
copyFileSync(relative, join(targetRoot, relative));
}
return {
assertionsFile: join(targetRoot, paths[0]!),
policyFile: join(targetRoot, paths[1]!),
catalogFile: join(targetRoot, paths[2]!),
};
}
const firstHopSourceVersions = listRecordedFirstHopSourceVersions();
const firstHopLaneNames = firstHopSourceVersions.map(updateFirstHopCompatLaneName);
function planFor(
overrides: Partial<Parameters<typeof resolveDockerE2ePlan>[0]> = {},
): ReturnType<typeof resolveDockerE2ePlan>["plan"] {
return resolveDockerE2ePlan({
allowFrozenTargetScenarioOmissions: true,
includeOpenWebUI: false,
liveMode: "all",
orderLanes,
planReleaseAll: false,
profile: "all",
releaseChunk: "core",
selectedLaneNames: [],
timingStore: undefined,
...overrides,
}).plan;
}
function requireFirstLane(plan: ReturnType<typeof planFor>) {
const [lane] = plan.lanes;
if (!lane) {
throw new Error("Expected at least one Docker E2E lane");
}
return lane;
}
function summarizeLane(lane: ReturnType<typeof planFor>["lanes"][number]) {
return {
command: lane.command,
imageKind: lane.imageKind,
live: lane.live,
name: lane.name,
resources: lane.resources,
...(lane.stateScenario ? { stateScenario: lane.stateScenario } : {}),
...(lane.timeoutMs !== undefined ? { timeoutMs: lane.timeoutMs } : {}),
weight: lane.weight,
};
}
function trustedUpgradeSurvivorCommand(
envPrefix = "",
shellPrelude = "",
harnessDir = ".",
): string {
const prefix = envPrefix ? `${envPrefix} ` : "";
const prelude = shellPrelude ? `${shellPrelude}; ` : "";
return `OPENCLAW_DOCKER_E2E_REPO_ROOT="\${OPENCLAW_DOCKER_E2E_REPO_ROOT:-$PWD}" ${prefix}OPENCLAW_SKIP_DOCKER_BUILD=1 bash -c '${prelude}harness="\${OPENCLAW_DOCKER_E2E_TRUSTED_HARNESS_DIR:-${harnessDir}}"; OPENCLAW_LIVE_DOCKER_REPO_ROOT="\${OPENCLAW_DOCKER_E2E_REPO_ROOT:-$PWD}" bash "$harness/scripts/e2e/upgrade-survivor-docker.sh"'`;
}
function publishedUpgradeSurvivorLane(
name: string,
baselineSpec: string,
scenario?: string,
): ReturnType<typeof summarizeLane> {
return {
command: `OPENCLAW_UPGRADE_SURVIVOR_ARTIFACT_DIR="$PWD/.artifacts/upgrade-survivor/${name}" OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC='${baselineSpec}' ${
scenario ? `OPENCLAW_UPGRADE_SURVIVOR_SCENARIO='${scenario}' ` : ""
}${trustedUpgradeSurvivorCommand(
"OPENCLAW_UPGRADE_SURVIVOR_PUBLISHED_BASELINE=1",
'export OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC="${OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC:-openclaw@latest}"; export OPENCLAW_UPGRADE_SURVIVOR_DOCKER_RUN_TIMEOUT="${OPENCLAW_UPGRADE_SURVIVOR_DOCKER_RUN_TIMEOUT:-1500s}"',
)}`,
imageKind: "bare",
live: false,
name,
resources: ["docker", "npm"],
stateScenario: "upgrade-survivor",
timeoutMs: 1_500_000,
weight: 3,
};
}
function updateMigrationLane(name: string, baselineSpec: string): ReturnType<typeof summarizeLane> {
return {
command: `OPENCLAW_UPGRADE_SURVIVOR_ARTIFACT_DIR="$PWD/.artifacts/upgrade-survivor/${name}" OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC='${baselineSpec}' OPENCLAW_UPGRADE_SURVIVOR_SCENARIO='plugin-deps-cleanup' ${trustedUpgradeSurvivorCommand(
"OPENCLAW_UPGRADE_SURVIVOR_PUBLISHED_BASELINE=1",
'export OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC="${OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC:-openclaw@latest}"; export OPENCLAW_UPGRADE_SURVIVOR_SCENARIO="${OPENCLAW_UPGRADE_SURVIVOR_SCENARIO:-plugin-deps-cleanup}"',
)}`,
imageKind: "bare",
live: false,
name,
resources: ["docker", "npm"],
stateScenario: "upgrade-survivor",
timeoutMs: 1_800_000,
weight: 3,
};
}
function bundledPluginSweepLane(index: number): ReturnType<typeof summarizeLane> {
return {
command: `OPENCLAW_BUNDLED_PLUGIN_SWEEP_TOTAL=24 OPENCLAW_BUNDLED_PLUGIN_SWEEP_INDEX=${index} OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:bundled-plugin-install-uninstall`,
imageKind: "functional",
live: false,
name: `bundled-plugin-install-uninstall-${index}`,
resources: ["docker", "npm"],
stateScenario: "empty",
weight: 1,
};
}
describe("scripts/lib/docker-e2e-plan", () => {
function commitTarget(root: string) {
const git = (...args: string[]) =>
execFileSync(
"git",
["-c", "commit.gpgsign=false", "-c", "core.hooksPath=/dev/null", ...args],
{ cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] },
).trim();
git("init", "-q");
git("add", ".");
git(
"-c",
"user.name=Test",
"-c",
"user.email=test@example.invalid",
"commit",
"-qm",
"fixture",
);
return { sha: git("rev-parse", "HEAD"), git };
}
it.each(["base", "missing-configured-plugin-migration"])(
"admits the current frozen catalog for %s",
(scenario) => {
const root = tempDirs.make("openclaw-current-inert-catalog-");
copyCurrentScenarioMetadata(root);
copyFileSync("package.json", join(root, "package.json"));
const { sha } = commitTarget(root);
const baseline = scenario === "missing-configured-plugin-migration" ? "2026.9.2" : "2026.9.4";
const plan = planFor({
selectedLaneNames: ["published-upgrade-survivor"],
upgradeSurvivorBaselines: baseline,
upgradeSurvivorScenarios: scenario,
upgradeSurvivorTargetRoot: root,
frozenTarget: { mode: "inert", source: createFrozenTargetSource(root, sha) },
});
expect(plan.lanes.map((lane) => lane.name)).toEqual([
`published-upgrade-survivor-${baseline}${scenario === "base" ? "" : `-${scenario}`}`,
]);
expect(plan.omittedUnsupportedLanes).toEqual([]);
},
);
it("rejects execution of the retired Teams JSON migration scenario", () => {
expect(() =>
planFor({
selectedLaneNames: ["published-upgrade-survivor"],
upgradeSurvivorBaselines: "2026.9.5",
upgradeSurvivorScenarios: "msteams-polls",
}),
).toThrow("invalid published upgrade survivor scenario");
});
it.each(["committed", "unapproved checkout"])(
"reads declared JSON capabilities without executing its %s modules",
(mode) => {
const root = tempDirs.make("openclaw-imported-inert-catalog-");
const { assertionsFile, policyFile, catalogFile } = copyCurrentScenarioMetadata(root);
const marker = join(root, "executed");
const markerCode = `\nimport { writeFileSync as markExecuted } from "node:fs"; markExecuted(${JSON.stringify(marker)}, "executed");\n`;
for (const file of [assertionsFile, policyFile]) {
writeFileSync(file, readFileSync(file, "utf8") + markerCode);
}
const { sha } = commitTarget(root);
const source = createFrozenTargetSource(root, sha);
if (mode === "committed") {
writeFileSync(catalogFile, "uncommitted catalog must not be read\n");
}
const plan = planFor({
selectedLaneNames: ["published-upgrade-survivor"],
upgradeSurvivorBaselines: "2026.9.4",
upgradeSurvivorScenarios: "legacy-operator-state",
upgradeSurvivorTargetRoot: root,
allowFrozenTargetScenarioOmissions: false,
...(mode === "committed" ? { frozenTarget: { mode: "inert" as const, source } } : {}),
});
expect(plan.lanes.map((lane) => lane.name)).toEqual([
"published-upgrade-survivor-2026.9.4-legacy-operator-state",
]);
expect(existsSync(marker)).toBe(false);
},
);
it.each([
["invalid JSON", "not JSON"],
["dynamic expression", "loadCatalog()"],
["wrong root", "[]"],
["missing list", '{"scenarios":["base"]}'],
["empty scenarios", '{"scenarios":[],"assertionOnlyScenarios":[]}'],
["non-array list", '{"scenarios":["base"],"assertionOnlyScenarios":{}}'],
["invalid entry", '{"scenarios":["bad name"],"assertionOnlyScenarios":[]}'],
["non-string entry", '{"scenarios":[42],"assertionOnlyScenarios":[]}'],
["duplicate entry", '{"scenarios":["base","base"],"assertionOnlyScenarios":[]}'],
["overlapping lists", '{"scenarios":["base"],"assertionOnlyScenarios":["base"]}'],
["unknown field", '{"scenarios":["base"],"assertionOnlyScenarios":[],"dynamic":true}'],
])("rejects unsupported catalog data: %s", (_shape, content) => {
const root = tempDirs.make("openclaw-unsupported-inert-catalog-");
const { catalogFile } = copyCurrentScenarioMetadata(root);
writeFileSync(catalogFile, content);
const { sha } = commitTarget(root);
expect(() =>
planFor({
selectedLaneNames: ["published-upgrade-survivor"],
upgradeSurvivorBaselines: "2026.9.4",
upgradeSurvivorScenarios: "base",
upgradeSurvivorTargetRoot: root,
frozenTarget: { mode: "inert", source: createFrozenTargetSource(root, sha) },
}),
).toThrow(/inert scenario catalog/);
});
it("keeps admission inert even when frozen omissions authorize executable legacy planning", () => {
const root = tempDirs.make("openclaw-inert-catalog-");
const marker = join(root, "executed");
const assertions = writeFrozenScenarioContract(root, ["base"]);
writeFileSync(
assertions,
`import { writeFileSync } from "node:fs"; writeFileSync(${JSON.stringify(marker)}, "executed");\n` +
readFileSync(assertions, "utf8"),
);
const { sha } = commitTarget(root);
expect(() =>
planFor({
selectedLaneNames: ["published-upgrade-survivor"],
upgradeSurvivorBaselines: "2026.6.11",
upgradeSurvivorTargetRoot: root,
frozenTarget: { mode: "inert", source: createFrozenTargetSource(root, sha) },
}),
).toThrow(/inert scenario catalog/);
expect(existsSync(marker)).toBe(false);
});
it.each(["dirty absent owner", "missing blob", "unselected unreadable owner"])(
"uses committed selected metadata for %s",
(shape) => {
const root = tempDirs.make("openclaw-inert-metadata-");
const relative = "src/cli/update-cli/update-command-plugin-preflight.ts";
mkdirSync(dirname(join(root, relative)), { recursive: true });
writeFileSync(join(root, "package.json"), "{}");
if (shape !== "dirty absent owner") {
writeFileSync(join(root, relative), "throw new Error('never execute');\n");
}
const { sha, git } = commitTarget(root);
if (shape === "dirty absent owner") {
writeFileSync(
join(root, relative),
"local modification must not imply committed support\n",
);
} else {
const oid = git("rev-parse", `${sha}:${relative}`);
rmSync(join(root, ".git/objects", oid.slice(0, 2), oid.slice(2)));
}
const selected =
shape === "unselected unreadable owner"
? "docker-package-install"
: "update-corrupt-plugin";
const run = () =>
planFor({
selectedLaneNames: [selected],
upgradeSurvivorTargetRoot: root,
frozenTarget: { mode: "inert", source: createFrozenTargetSource(root, sha) },
});
if (shape === "missing blob") {
expect(run).toThrow(/unable to read selected source/);
} else if (shape === "dirty absent owner") {
expect(run().omittedUnsupportedLanes).toEqual([selected]);
} else {
expect(run().lanes.map((lane) => lane.name)).toEqual([selected]);
}
},
);
const literalFirstHopPostbuild = String.raw`const LEGACY_CLI_EXIT_COMPAT_CHUNKS = [
// v2026.8.2 and the exact d413210 build load these after replacing dist/.
// Remove only after both source artifacts fall outside the supported upgrade window.
{
dest: "dist/shared-Y6bNiw2w.js",
contents: LEGACY_UPDATE_NODE_RUNNER_COMPAT_CHUNK,
},
{
dest: "dist/shared-DTaQo6Hi.js",
contents: LEGACY_UPDATE_NODE_RUNNER_COMPAT_CHUNK,
},
{
dest: "dist/memory-state-CcqRgDZU.js",
contents: "export function hasMemoryRuntime() {\n return false;\n}\n",
},
{
dest: "dist/memory-state-DwGdReW4.js",
contents: "export function hasMemoryRuntime() {\n return false;\n}\n",
},
];`;
it.each(["literal", "mapped"])(
"retains first-hop coverage for supported %s postbuild output declarations without executing target code",
(shape) => {
const targetRoot = tempDirs.make("openclaw-first-hop-target-");
mkdirSync(join(targetRoot, "scripts"));
const source =
shape === "literal"
? literalFirstHopPostbuild
: readFileSync("scripts/runtime-postbuild.mts", "utf8");
writeFileSync(
join(targetRoot, "scripts/runtime-postbuild.mts"),
`throw new Error("must not execute target");\n${source}`,
);
const plan = planFor({
selectedLaneNames: parseLaneSelection("update-first-hop-compat"),
upgradeSurvivorTargetRoot: targetRoot,
});
expect(plan.lanes.map((lane) => lane.name)).toEqual(firstHopLaneNames);
expect(plan.omittedUnsupportedLanes).toEqual([]);
},
);
it("omits only the first-hop sources a target's own inventory does not record", () => {
const targetRoot = tempDirs.make("openclaw-partial-first-hop-target-");
mkdirSync(join(targetRoot, "scripts/lib"), { recursive: true });
writeFileSync(
join(targetRoot, "scripts/runtime-postbuild.mts"),
readFileSync("scripts/runtime-postbuild.mts", "utf8"),
);
const [oldest, ...newer] = firstHopSourceVersions;
writeFileSync(
join(targetRoot, "scripts/lib/update-compat-inventory.json"),
JSON.stringify({ schemaVersion: 1, releases: [{ version: oldest }] }),
);
const plan = planFor({
selectedLaneNames: parseLaneSelection("update-first-hop-compat"),
upgradeSurvivorTargetRoot: targetRoot,
});
expect(plan.lanes.map((lane) => lane.name)).toEqual([updateFirstHopCompatLaneName(oldest)]);
expect(plan.omittedUnsupportedLanes).toEqual(newer.map(updateFirstHopCompatLaneName));
});
it.each([
["literal", "shared-Y6bNiw2w.js"],
["mapped", "shared-Y6bNiw2w.js"],
["unrelated", ""],
["absent", ""],
])(
"does not infer first-hop support from %s with missing or unrelated outputs %s",
(shape, missing) => {
const targetRoot = tempDirs.make("openclaw-missing-first-hop-target-");
mkdirSync(join(targetRoot, "scripts"));
if (shape !== "absent") {
const source =
shape === "mapped"
? readFileSync("scripts/runtime-postbuild.mts", "utf8")
: literalFirstHopPostbuild;
writeFileSync(
join(targetRoot, "scripts/runtime-postbuild.mts"),
shape === "unrelated"
? source.replace("const LEGACY_CLI_EXIT_COMPAT_CHUNKS", "const UNRELATED_OUTPUTS")
: source.replaceAll(missing, "missing.js"),
);
}
const plan = planFor({
selectedLaneNames: parseLaneSelection("update-first-hop-compat"),
upgradeSurvivorTargetRoot: targetRoot,
});
expect(plan.lanes).toEqual([]);
expect(plan.omittedUnsupportedLanes).toEqual(firstHopLaneNames);
},
);
it("omits corrupt-plugin update admission only for authorized targets without the owner", () => {
const targetRoot = tempDirs.make("openclaw-corrupt-update-target-");
const unsupported = planFor({
selectedLaneNames: ["update-corrupt-plugin"],
upgradeSurvivorTargetRoot: targetRoot,
});
expect(unsupported.lanes).toEqual([]);
expect(unsupported.omittedUnsupportedLanes).toEqual(["update-corrupt-plugin"]);
const unauthorized = planFor({
allowFrozenTargetScenarioOmissions: false,
selectedLaneNames: ["update-corrupt-plugin"],
upgradeSurvivorTargetRoot: targetRoot,
});
expect(unauthorized.lanes.map((lane) => lane.name)).toEqual(["update-corrupt-plugin"]);
const preflight = join(targetRoot, "src/cli/update-cli/update-command-plugin-preflight.ts");
mkdirSync(dirname(preflight), { recursive: true });
writeFileSync(preflight, "export {};\n");
const supported = planFor({
selectedLaneNames: ["update-corrupt-plugin"],
upgradeSurvivorTargetRoot: targetRoot,
});
expect(supported.lanes.map((lane) => lane.name)).toEqual(["update-corrupt-plugin"]);
});
it.each([
["catalog", "docker-package-install", {}, 0, ""],
["missing package", "docker-package-install", { needsPackage: false }, 1, "package Docker"],
[
"package-only reused image",
"docker-package-install",
{ command: "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:package-install" },
1,
"must force a local image build",
],
[
"source reused image",
"docker-selected-plugins",
{ command: "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:selected-plugins" },
1,
"must force a local image build",
],
[
"container activation reused image",
"container-image-upgrade",
{ command: "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:container-image-upgrade" },
1,
"must force a local image build",
],
["unapproved live package", "live-models", { needsPackage: true }, 1, "must not require"],
[
"shared package image",
"docker-package-install",
{
e2eImageKind: "bare",
command: "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:package-install",
},
0,
"",
],
] as const)("validates Docker boundary ownership: %s", (_label, name, overrides, exit, error) => {
const result = spawnSync(
testNodeExecPath,
[
"--import",
"./scripts/tsx.mjs",
"--input-type=module",
"-e",
`import { mainLanes } from './scripts/lib/docker-e2e-scenarios.mts';
Object.assign(mainLanes.find(lane => lane.name === ${JSON.stringify(name)}), ${JSON.stringify(overrides)});
await import('./scripts/check-docker-e2e-boundaries.mts');`,
],
{ encoding: "utf8" },
);
expect(result.status, result.stderr).toBe(exit);
if (error) {
expect(result.stderr).toContain(error);
}
});
it.each([
["codex-media-path", ["@openclaw/codex"]],
["npm-onboard-discord-candidate-channel-agent", ["@openclaw/codex", "@openclaw/discord"]],
["npm-onboard-slack-candidate-channel-agent", ["@openclaw/codex", "@openclaw/slack"]],
] as const)("requests only the matching companions for %s", (name, packages) => {
const plan = planFor({ selectedLaneNames: [name] });
expect(plan.lanes.map((lane) => lane.name)).toEqual([name]);
expect(plan.needs.prepublishPluginRegistry).toBe(true);
expect(plan.requiredPrepublishPluginPackages).toEqual(packages);
});
it("runs Fleet host proof only when explicitly selected", () => {
expect(planFor().lanes.map((lane) => lane.name)).not.toContain("fleet-cache");
const selected = planFor({ selectedLaneNames: ["fleet-cache"] });
expect(selected.lanes.map((lane) => lane.name)).toEqual(["fleet-cache"]);
expect(selected.needs.package).toBe(true);
expect(selected.needs.e2eImage).toBe(false);
expect(selected.needs.prepublishPluginRegistry).toBe(false);
expect(findLaneByName("fleet-cache")?.name).toBe("fleet-cache");
});
it("builds the container activation image without preparing a package image", () => {
const plan = planFor({ selectedLaneNames: ["container-image-upgrade"] });
expect(plan.lanes.map((lane) => lane.name)).toEqual(["container-image-upgrade"]);
expect(plan.needs.package).toBe(false);
expect(plan.needs.e2eImage).toBe(false);
expect(plan.needs.liveImage).toBe(false);
});
it("plans the package-backed sandbox browser sidecar lane", () => {
const plan = planFor({
selectedLaneNames: ["sandbox-browser-sidecar"],
});
expect(plan.lanes.map(summarizeLane)).toEqual([
{
command: "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:sandbox-browser-sidecar",
imageKind: "functional",
live: false,
name: "sandbox-browser-sidecar",
resources: ["docker", "service"],
stateScenario: "empty",
timeoutMs: 1_200_000,
weight: 4,
},
]);
expect(plan.needs.functionalImage).toBe(true);
});
it("routes trusted Docker scripts through the nested release harness", () => {
const trustedScripts = new Map([
["live-codex-npm-plugin", "e2e/codex-npm-plugin-live-docker.sh"],
["upgrade-survivor", "e2e/upgrade-survivor-docker.sh"],
["published-upgrade-survivor", "e2e/upgrade-survivor-docker.sh"],
["dreaming-cron-doctor", "e2e/upgrade-survivor-docker.sh"],
["root-managed-vps-upgrade", "e2e/upgrade-survivor-docker.sh"],
["update-migration", "e2e/upgrade-survivor-docker.sh"],
]);
const sourceLanes = [
...new Map(
[...allReleasePathLanes({ releaseProfile: "beta" }), ...mainLanes]
.filter((candidate) => trustedScripts.has(candidate.name))
.map((candidate) => [candidate.name, candidate]),
).values(),
];
const tempRoot = tempDirs.make("openclaw-release-harness-");
const nestedModule = join(
tempRoot,
".release-harness",
"scripts",
"lib",
"docker-e2e-scenarios.mts",
);
expect(sourceLanes).toHaveLength(trustedScripts.size);
for (const sourceLane of sourceLanes) {
expect(sourceLane.command).toContain(
'harness="${OPENCLAW_DOCKER_E2E_TRUSTED_HARNESS_DIR:-.}"',
);
}
mkdirSync(dirname(nestedModule), { recursive: true });
for (const fileName of [
"docker-e2e-scenarios.mts",
"update-compat-inventory.json",
"update-first-hop-lanes.mjs",
]) {
copyFileSync(join("scripts/lib", fileName), join(dirname(nestedModule), fileName));
}
const laneJson = execFileSync(
testNodeExecPath,
[
"--input-type=module",
"--eval",
`
import { pathToFileURL } from "node:url";
const scenarios = await import(pathToFileURL(process.argv[1]).href);
const names = ${JSON.stringify([...trustedScripts.keys()])};
const lanes = [
...new Map(
[
...scenarios.allReleasePathLanes({ releaseProfile: "beta" }),
...scenarios.mainLanes,
]
.filter((candidate) => names.includes(candidate.name))
.map((candidate) => [candidate.name, candidate]),
).values(),
];
process.stdout.write(JSON.stringify(lanes));
`,
nestedModule,
],
{
encoding: "utf8",
env: {
...process.env,
OPENCLAW_DOCKER_E2E_REPO_ROOT: tempRoot,
},
},
);
const lanes = JSON.parse(laneJson) as Array<{ command: string; name: string }>;
expect(lanes).toHaveLength(trustedScripts.size);
for (const lane of lanes) {
expect(lane.command).toContain(
'harness="${OPENCLAW_DOCKER_E2E_TRUSTED_HARNESS_DIR:-.release-harness}"',
);
expect(lane.command).toContain(`bash "$harness/scripts/${trustedScripts.get(lane.name)}"`);
expect(lane.command).not.toContain(`pnpm test:docker:${lane.name}`);
}
});
it.each([
{ name: "published-upgrade-survivor", baseline: "2026.7.2", scenario: "feishu-channel" },
{ name: "update-migration", baseline: "2026.6.1", scenario: "plugin-deps-cleanup" },
{ name: "update-migration", baseline: undefined, scenario: "plugin-deps-cleanup" },
{ name: "root-managed-vps-upgrade", baseline: undefined, scenario: "base" },
])(
"passes the $name baseline through the trusted harness wrapper ($baseline)",
({ name, baseline, scenario }) => {
const root = tempDirs.make("openclaw-survivor-wrapper-");
const harnessRoot = join(root, ".release-harness");
const script = join(harnessRoot, "scripts/e2e/upgrade-survivor-docker.sh");
const output = join(root, "survivor-env.txt");
mkdirSync(dirname(script), { recursive: true });
writeFileSync(
script,
[
"#!/usr/bin/env bash",
'printf "%s|%s\\n" "$OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC" "$OPENCLAW_UPGRADE_SURVIVOR_SCENARIO" > "$OPENCLAW_TEST_OUTPUT"',
].join("\n"),
);
chmodSync(script, 0o755);
const lane = requireFirstLane(
planFor({
selectedLaneNames: [name],
upgradeSurvivorBaselines: baseline,
upgradeSurvivorScenarios: scenario,
}),
);
execFileSync("/bin/bash", ["-c", lane.command], {
cwd: root,
env: {
...process.env,
OPENCLAW_DOCKER_E2E_REPO_ROOT: root,
OPENCLAW_DOCKER_E2E_TRUSTED_HARNESS_DIR: harnessRoot,
OPENCLAW_TEST_OUTPUT: output,
OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC: "",
OPENCLAW_UPGRADE_SURVIVOR_SCENARIO: scenario,
},
});
expect(readFileSync(output, "utf8")).toBe(`openclaw@${baseline ?? "latest"}|${scenario}\n`);
},
);
it("plans package-backed installer, Compose, and package artifact proofs", () => {
const plan = planFor({
selectedLaneNames: ["cli-installer-distribution", "compose-setup", "docker-package-install"],
});
expect(plan.lanes.map(summarizeLane)).toEqual([
{
command: "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:cli-installer-distribution",
imageKind: "bare",
live: false,
name: "cli-installer-distribution",
resources: ["docker", "npm"],
stateScenario: "empty",
timeoutMs: 1_800_000,
weight: 3,
},
{
command: "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:compose-setup",
imageKind: "functional",
live: false,
name: "compose-setup",
resources: ["docker", "service"],
stateScenario: "empty",
timeoutMs: 1_200_000,
weight: 3,
},
{
command: "OPENCLAW_SKIP_DOCKER_BUILD=0 pnpm test:docker:package-install",
imageKind: undefined,
live: false,
name: "docker-package-install",
resources: ["docker", "npm"],
stateScenario: "empty",
timeoutMs: 1_200_000,
weight: 3,
},
]);
expect(plan.needs).toEqual({
bareImage: true,
e2eImage: true,
functionalImage: true,
liveImage: false,
package: true,
prepublishPluginRegistry: false,
});
expect(planFor({ selectedLaneNames: ["docker-package-install"] }).needs).toMatchObject({
package: true,
bareImage: false,
functionalImage: false,
e2eImage: false,
});
});
it("plans the full release path against package-backed e2e images", () => {
const plan = planFor({
includeOpenWebUI: false,
planReleaseAll: true,
profile: RELEASE_PATH_PROFILE,
});
expect(plan.needs).toEqual({
bareImage: true,
e2eImage: true,
functionalImage: true,
liveImage: false,
package: true,
prepublishPluginRegistry: true,
});
expect(plan.credentials).toEqual(["anthropic-api-key", "openai"]);
expect(plan.lanes.map((lane) => lane.name)).not.toContain("install-e2e-openai");
expect(plan.lanes.map((lane) => lane.name)).toContain("openai-chat-tools");
expect(plan.lanes.map((lane) => lane.name)).toContain("live-codex-npm-plugin");
expect(plan.lanes.map((lane) => lane.name)).toContain("codex-on-demand");
expect(plan.lanes.map((lane) => lane.name)).not.toContain("install-e2e-anthropic");
expect(plan.lanes.map((lane) => lane.name)).toContain("mcp-channels");
expect(plan.lanes.map((lane) => lane.name)).toContain("plugin-binding-command-escape");
expect(plan.lanes.map((lane) => lane.name)).toContain("live-plugin-tool");
expect(plan.lanes.map((lane) => lane.name)).toContain("bundled-plugin-install-uninstall-0");
expect(plan.lanes.map((lane) => lane.name)).toContain("bundled-plugin-install-uninstall-23");
const countLane = (name: string) =>
plan.lanes.reduce((count, lane) => count + (lane.name === name ? 1 : 0), 0);
expect(countLane("install-e2e-openai")).toBe(0);
expect(countLane("bundled-plugin-install-uninstall-0")).toBe(1);
expect(plan.lanes.map((lane) => lane.name)).not.toContain("bundled-plugin-install-uninstall");
expect(plan.lanes.map((lane) => lane.name)).not.toContain("bundled-channel-deps");
expect(plan.lanes.map((lane) => lane.name)).not.toContain("openwebui");
});
it("includes deterministic packaged MCP code-mode proof in the unfiltered release core", () => {
const plan = planFor({
profile: RELEASE_PATH_PROFILE,
releaseChunk: "core",
});
const codeModeLanes = plan.lanes.filter((lane) => lane.name === "mcp-code-mode-gateway");
expect(plan.selectedLanes).toEqual([]);
expect(codeModeLanes.map(summarizeLane)).toEqual([
{
command: "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:mcp-code-mode-gateway",
imageKind: "functional",
live: false,
name: "mcp-code-mode-gateway",
resources: ["docker", "service", "npm"],
stateScenario: "empty",
weight: 3,
},
]);
expect(plan.lanes.map((lane) => lane.name)).not.toContain("live-mcp-code-mode-gateway");
});
it("selects isolated packaged Gateway concurrency proof without widening release-core coverage", () => {
const targeted = planFor({ selectedLaneNames: ["gateway-concurrency"] });
const core = planFor({ profile: RELEASE_PATH_PROFILE, releaseChunk: "core" });
expect(targeted.lanes.map(summarizeLane)).toEqual([
{
command:
'OPENCLAW_SKIP_DOCKER_BUILD=1 bash -c \'harness="${OPENCLAW_DOCKER_E2E_TRUSTED_HARNESS_DIR:-.}"; OPENCLAW_LIVE_DOCKER_REPO_ROOT="${OPENCLAW_DOCKER_E2E_REPO_ROOT:-$PWD}" bash "$harness/scripts/e2e/gateway-concurrency-docker.sh"\'',
imageKind: "functional",
live: false,
name: "gateway-concurrency",
resources: ["docker", "service"],
timeoutMs: 600_000,
weight: 3,
},
]);
expect(core.lanes.map((lane) => lane.name)).not.toContain("gateway-concurrency");
});
it.each(["stable", "full"] as const)(
"requires packaged live Anthropic cache proof in the %s release core",
(releaseProfile) => {
const plan = planFor({ profile: RELEASE_PATH_PROFILE, releaseChunk: "core", releaseProfile });
const cacheLane = plan.lanes.find((lane) => lane.name === "live-anthropic-cache");
expect(cacheLane).toMatchObject({
imageKind: "functional",
live: true,
resources: ["docker", "live", "live:claude"],
timeoutMs: 900_000,
});
expect(plan.needs.functionalImage).toBe(true);
expect(plan.needs.liveImage).toBe(false);
expect(plan.credentials).toContain("anthropic-api-key");
},
);
it("plans Open WebUI only when release-path coverage requests it", () => {
const withoutOpenWebUI = planFor({
includeOpenWebUI: false,
planReleaseAll: true,
profile: RELEASE_PATH_PROFILE,
});
const withOpenWebUI = planFor({
includeOpenWebUI: true,
planReleaseAll: true,
profile: RELEASE_PATH_PROFILE,
});
expect(withoutOpenWebUI.lanes.map((lane) => lane.name)).not.toContain("openwebui");
expect(withOpenWebUI.lanes.filter((lane) => lane.name === "openwebui")).toHaveLength(1);
});
it("keeps beta release-path coverage to install, provider, and update proof lanes", () => {
const plan = planFor({
includeOpenWebUI: true,
planReleaseAll: true,
profile: RELEASE_PATH_PROFILE,
releaseProfile: "beta",
});
const laneNames = plan.lanes.map((lane) => lane.name);
expect(plan.releaseProfile).toBe("beta");
expect(laneNames).not.toContain("install-e2e-openai");
expect(laneNames).toContain("openai-chat-tools");
expect(laneNames).toContain("live-codex-npm-plugin");
expect(laneNames).toContain("release-typed-onboarding");
expect(laneNames).not.toContain("install-e2e-anthropic");
expect(laneNames).toContain("update-channel-switch");
expect(laneNames).not.toContain("plugins");
expect(laneNames).not.toContain("live-plugin-tool");
expect(laneNames).not.toContain("bundled-plugin-install-uninstall-0");
expect(laneNames).not.toContain("openwebui");
});
it("still allows explicit selected lanes outside the beta release profile", () => {
const plan = planFor({
includeOpenWebUI: true,
profile: RELEASE_PATH_PROFILE,
releaseProfile: "beta",
selectedLaneNames: ["live-plugin-tool"],
});
expect(plan.lanes.map((lane) => lane.name)).toEqual(["live-plugin-tool"]);
});
it("keeps provider-backed install E2E lanes out of non-live package chunks", () => {
const plan = planFor({
includeOpenWebUI: true,
liveMode: "skip",
profile: RELEASE_PATH_PROFILE,
releaseChunk: "package-update",
});
const laneNames = plan.lanes.map((lane) => lane.name);
expect(laneNames).not.toContain("install-e2e-openai");
expect(laneNames).not.toContain("openai-chat-tools");
expect(laneNames).not.toContain("live-codex-npm-plugin");
expect(laneNames).not.toContain("install-e2e-anthropic");
expect(laneNames).toContain("codex-on-demand");
expect(laneNames).toContain("release-typed-onboarding");
expect(laneNames).toContain("update-channel-switch");
});
it("splits release-path package and plugin chunks across shorter CI jobs", () => {
const core = planFor({
includeOpenWebUI: true,
profile: RELEASE_PATH_PROFILE,
releaseChunk: "core",
});
const packageInstallOpenAi = planFor({
includeOpenWebUI: true,
profile: RELEASE_PATH_PROFILE,
releaseChunk: "package-update-openai",
});
const packageUpdateCore = planFor({
includeOpenWebUI: true,
profile: RELEASE_PATH_PROFILE,
releaseChunk: "package-update-core",
});
const pluginsRuntimePlugins = planFor({
includeOpenWebUI: true,
profile: RELEASE_PATH_PROFILE,
releaseChunk: "plugins-runtime-plugins",
});
const pluginsRuntimeServices = planFor({
includeOpenWebUI: true,
profile: RELEASE_PATH_PROFILE,
releaseChunk: "plugins-runtime-services",
});
const openWebUI = planFor({
includeOpenWebUI: true,
profile: RELEASE_PATH_PROFILE,
releaseChunk: "openwebui",
});
const pluginsRuntimeInstallA = planFor({
includeOpenWebUI: true,
profile: RELEASE_PATH_PROFILE,
releaseChunk: "plugins-runtime-install-a",
});
const pluginsRuntimeInstallB = planFor({
includeOpenWebUI: true,
profile: RELEASE_PATH_PROFILE,
releaseChunk: "plugins-runtime-install-b",
});
const pluginsRuntimeInstallC = planFor({
includeOpenWebUI: true,
profile: RELEASE_PATH_PROFILE,
releaseChunk: "plugins-runtime-install-c",
});
const pluginsRuntimeInstallD = planFor({
includeOpenWebUI: true,
profile: RELEASE_PATH_PROFILE,
releaseChunk: "plugins-runtime-install-d",
});
const pluginsRuntimeInstallE = planFor({
includeOpenWebUI: true,
profile: RELEASE_PATH_PROFILE,
releaseChunk: "plugins-runtime-install-e",
});
const pluginsRuntimeInstallF = planFor({
includeOpenWebUI: true,
profile: RELEASE_PATH_PROFILE,
releaseChunk: "plugins-runtime-install-f",
});
const pluginsRuntimeInstallG = planFor({
includeOpenWebUI: true,
profile: RELEASE_PATH_PROFILE,
releaseChunk: "plugins-runtime-install-g",
});
const pluginsRuntimeInstallH = planFor({
includeOpenWebUI: true,
profile: RELEASE_PATH_PROFILE,
releaseChunk: "plugins-runtime-install-h",
});
expect(core.lanes.map((lane) => lane.name)).toContain("plugin-binding-command-escape");
expect(packageInstallOpenAi.lanes.map((lane) => lane.name)).toEqual([
"openai-chat-tools",
"live-codex-npm-plugin",
"codex-on-demand",
"release-typed-onboarding",
"root-managed-vps-upgrade",
"update-restart-auth",
]);
expect(
packageInstallOpenAi.lanes
.filter((lane) => lane.name === "release-typed-onboarding")
.map(summarizeLane),
).toEqual([
{
command: "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:release-typed-onboarding",
imageKind: "bare",
live: false,
name: "release-typed-onboarding",
resources: ["docker", "npm", "service"],
stateScenario: "empty",
timeoutMs: 1_200_000,
weight: 3,
},
]);
expect(packageInstallOpenAi.lanes.slice(-2).map(summarizeLane)).toEqual([
{
command: trustedUpgradeSurvivorCommand(
"OPENCLAW_UPGRADE_SURVIVOR_PUBLISHED_BASELINE=1 OPENCLAW_UPGRADE_SURVIVOR_ROOT_MANAGED_VPS=1",
'export OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC="${OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC:-openclaw@latest}"; export OPENCLAW_UPGRADE_SURVIVOR_DOCKER_RUN_TIMEOUT="${OPENCLAW_UPGRADE_SURVIVOR_DOCKER_RUN_TIMEOUT:-1500s}"',
),
imageKind: "bare",
live: false,
name: "root-managed-vps-upgrade",
resources: ["docker", "npm"],
stateScenario: "upgrade-survivor",
timeoutMs: 1_500_000,
weight: 3,
},
{
command: trustedUpgradeSurvivorCommand(
"OPENCLAW_UPGRADE_SURVIVOR_PUBLISHED_BASELINE=1 OPENCLAW_UPGRADE_SURVIVOR_UPDATE_RESTART_MODE=auto-auth",
'export OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC="${OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC:-openclaw@latest}"; export OPENCLAW_UPGRADE_SURVIVOR_DOCKER_RUN_TIMEOUT="${OPENCLAW_UPGRADE_SURVIVOR_DOCKER_RUN_TIMEOUT:-1500s}"',
),
imageKind: "bare",
live: false,
name: "update-restart-auth",
resources: ["docker", "npm"],
stateScenario: "upgrade-survivor",
timeoutMs: 1_500_000,
weight: 3,
},
]);
expect(packageUpdateCore.lanes.map(summarizeLane)).toEqual([
{
command: "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:npm-onboard-channel-agent",
imageKind: "bare",
live: false,
name: "npm-onboard-channel-agent",
resources: ["docker", "npm", "service"],
stateScenario: "empty",
weight: 3,
},
{
command:
"OPENCLAW_NPM_ONBOARD_CHANNEL=discord OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:npm-onboard-channel-agent",
imageKind: "bare",
live: false,
name: "npm-onboard-discord-channel-agent",
resources: ["docker", "npm", "service"],
stateScenario: "empty",
weight: 3,
},
{
command:
"OPENCLAW_NPM_ONBOARD_CHANNEL=slack OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:npm-onboard-channel-agent",
imageKind: "bare",
live: false,
name: "npm-onboard-slack-channel-agent",
resources: ["docker", "npm", "service"],
stateScenario: "empty",
weight: 3,
},
{
command: "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:doctor-switch",
imageKind: "bare",
live: false,
name: "doctor-switch",
resources: ["docker", "npm"],
stateScenario: "empty",
weight: 3,
},
{
command: "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:skill-install",
imageKind: "bare",
live: false,
name: "skill-install",
resources: ["docker", "npm"],
stateScenario: "empty",
timeoutMs: 600_000,
weight: 2,
},
{
command: "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:update-channel-switch",
imageKind: "bare",
live: false,
name: "update-channel-switch",
resources: ["docker", "npm"],
stateScenario: "update-stable",
timeoutMs: 1_800_000,
weight: 3,
},
{
command: trustedUpgradeSurvivorCommand(
"OPENCLAW_UPGRADE_SURVIVOR_PUBLISHED_BASELINE=1",
'export OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC="${OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC:-openclaw@latest}"; export OPENCLAW_UPGRADE_SURVIVOR_DOCKER_RUN_TIMEOUT="${OPENCLAW_UPGRADE_SURVIVOR_DOCKER_RUN_TIMEOUT:-1500s}"',
),
imageKind: "bare",
live: false,
name: "published-upgrade-survivor",
resources: ["docker", "npm"],
stateScenario: "upgrade-survivor",
timeoutMs: 1_500_000,
weight: 3,
},
{
command: trustedUpgradeSurvivorCommand(),
imageKind: "bare",
live: false,
name: "upgrade-survivor",
resources: ["docker", "npm"],
stateScenario: "upgrade-survivor",
timeoutMs: 1_200_000,
weight: 3,
},
...firstHopSourceVersions.map((version) => ({
command: `OPENCLAW_QA_ALLOW_UPDATE_FIRST_HOP=1 OPENCLAW_UPDATE_FIRST_HOP_SOURCE_VERSIONS=${version} OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:update-first-hop-compat`,
imageKind: "bare",
live: false,
name: updateFirstHopCompatLaneName(version),
resources: ["docker", "npm", "service"],
stateScenario: "upgrade-survivor",
timeoutMs: 1_500_000,
weight: 1,
})),
]);
expect(pluginsRuntimePlugins.lanes.map((lane) => lane.name)).toEqual(["plugins"]);
expect(pluginsRuntimeServices.lanes.map(summarizeLane)).toEqual([
{
command: "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:cron-mcp-cleanup",
imageKind: "functional",
live: false,
name: "cron-mcp-cleanup",
resources: ["docker", "service", "npm"],
stateScenario: "empty",
weight: 3,
},
{
command: "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:kitchen-sink-rpc",
imageKind: "functional",
live: false,
name: "kitchen-sink-rpc",
resources: ["docker", "service", "npm"],
stateScenario: "empty",
timeoutMs: 1_500_000,
weight: 3,
},
{
command: "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:openai-web-search-minimal",
imageKind: "functional",
live: false,
name: "openai-web-search-minimal",
resources: ["docker", "service"],
stateScenario: "empty",
timeoutMs: 480_000,
weight: 2,
},
{
command:
"OPENCLAW_LIVE_PLUGIN_TOOL_TIMEOUT_SECONDS=300 OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:live-plugin-tool",
imageKind: "bare",
live: true,
name: "live-plugin-tool",
resources: ["docker", "live", "live:openai", "npm"],
stateScenario: "empty",
timeoutMs: 1_200_000,
weight: 3,
},
]);
expect(openWebUI.lanes.map(summarizeLane)).toEqual([
{
command:
"OPENCLAW_OPENWEBUI_MODEL=openai/gpt-5.4-mini OPENCLAW_OPENWEBUI_PROVIDER_TIMEOUT_SECONDS=300 OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:openwebui",
imageKind: "functional",
live: true,
name: "openwebui",
resources: ["docker", "live", "live:openai", "service"],
timeoutMs: 1_200_000,
weight: 5,
},
]);
expect(pluginsRuntimePlugins.lanes.map((lane) => lane.name)).not.toContain(
"bundled-plugin-install-uninstall-0",
);
expect(pluginsRuntimeInstallA.lanes.map((lane) => lane.name)).toEqual([
"bundled-plugin-install-uninstall-0",
"bundled-plugin-install-uninstall-1",
"bundled-plugin-install-uninstall-2",
]);
expect(pluginsRuntimeInstallB.lanes.map((lane) => lane.name)).toEqual([
"bundled-plugin-install-uninstall-3",
"bundled-plugin-install-uninstall-4",
"bundled-plugin-install-uninstall-5",
]);
expect(pluginsRuntimeInstallC.lanes.map((lane) => lane.name)).toEqual([
"bundled-plugin-install-uninstall-6",
"bundled-plugin-install-uninstall-7",
"bundled-plugin-install-uninstall-8",
]);
expect(pluginsRuntimeInstallD.lanes.map((lane) => lane.name)).toEqual([
"bundled-plugin-install-uninstall-9",
"bundled-plugin-install-uninstall-10",
"bundled-plugin-install-uninstall-11",
]);
expect(pluginsRuntimeInstallE.lanes.map((lane) => lane.name)).toEqual([
"bundled-plugin-install-uninstall-12",
"bundled-plugin-install-uninstall-13",
"bundled-plugin-install-uninstall-14",
]);
expect(pluginsRuntimeInstallF.lanes.map((lane) => lane.name)).toEqual([
"bundled-plugin-install-uninstall-15",
"bundled-plugin-install-uninstall-16",
"bundled-plugin-install-uninstall-17",
]);
expect(pluginsRuntimeInstallG.lanes.map((lane) => lane.name)).toEqual([
"bundled-plugin-install-uninstall-18",
"bundled-plugin-install-uninstall-19",
"bundled-plugin-install-uninstall-20",
]);
expect(pluginsRuntimeInstallH.lanes.map((lane) => lane.name)).toEqual([
"bundled-plugin-install-uninstall-21",
"bundled-plugin-install-uninstall-22",
"bundled-plugin-install-uninstall-23",
]);
});
it("keeps planned pnpm docker lanes backed by package scripts", () => {
const plan = planFor({
includeOpenWebUI: true,
planReleaseAll: true,
profile: RELEASE_PATH_PROFILE,
});
const scripts = packageJson.scripts ?? {};
const missing = plan.lanes
.flatMap((lane) =>
Array.from(lane.command.matchAll(/\bpnpm\s+(test:docker:[\w:-]+)/gu)).flatMap((match) =>
match[1] === undefined ? [] : [{ lane: lane.name, script: match[1] }],
),
)
.filter(({ script }) => !scripts[script]);
expect(missing).toStrictEqual([]);
});
it.each(["minimum", "stable"] as const)(
"partitions package/update core across three jobs without losing or duplicating %s proof",
(releaseProfile) => {
const options = { profile: RELEASE_PATH_PROFILE, releaseProfile };
const aggregate = planFor({ ...options, releaseChunk: "package-update-core" });
const partitions = [
"package-update-onboarding",
"package-update-migrations",
"package-update-self-upgrade",
].map((releaseChunk) => planFor({ ...options, releaseChunk }));
const lanes = partitions.flatMap((partition) => partition.lanes);
expect(partitions.map((partition) => partition.lanes.length)).toEqual([
5,
2,
1 + firstHopLaneNames.length,
]);
expect(new Set(lanes.map((lane) => lane.name)).size).toBe(8 + firstHopLaneNames.length);
expect(lanes.map(summarizeLane)).toEqual(aggregate.lanes.map(summarizeLane));
const complete = planFor({ ...options, planReleaseAll: true });
const packageNames = new Set(lanes.map((lane) => lane.name));
expect(complete.lanes.filter((lane) => packageNames.has(lane.name))).toEqual(lanes);
},
);
it("keeps legacy release chunk names as aggregate aliases", () => {
const packageUpdate = planFor({
includeOpenWebUI: true,
profile: RELEASE_PATH_PROFILE,
releaseChunk: "package-update",
});
const pluginsRuntime = planFor({
includeOpenWebUI: true,
profile: RELEASE_PATH_PROFILE,
releaseChunk: "plugins-runtime",
});
const legacy = planFor({
includeOpenWebUI: true,
profile: RELEASE_PATH_PROFILE,
releaseChunk: "plugins-integrations",
});
const bundledPluginSweepLanes = Array.from(
{ length: BUNDLED_PLUGIN_INSTALL_UNINSTALL_SHARDS },
(_, index) => `bundled-plugin-install-uninstall-${index}`,
);
expect(packageUpdate.lanes.map((lane) => lane.name)).toEqual([
"openai-chat-tools",
"live-codex-npm-plugin",
"codex-on-demand",
"release-typed-onboarding",
"root-managed-vps-upgrade",
"update-restart-auth",
"npm-onboard-channel-agent",
"npm-onboard-discord-channel-agent",
"npm-onboard-slack-channel-agent",
"doctor-switch",
"skill-install",
"update-channel-switch",
"published-upgrade-survivor",
"upgrade-survivor",
...firstHopLaneNames,
]);
expect(pluginsRuntime.lanes.map((lane) => lane.name)).toEqual([
"plugins",
...bundledPluginSweepLanes,
"cron-mcp-cleanup",
"kitchen-sink-rpc",
"openai-web-search-minimal",
"live-plugin-tool",
"openwebui",
]);
expect(legacy.lanes.map((lane) => lane.name)).toEqual([
"plugins",
...bundledPluginSweepLanes,
"cron-mcp-cleanup",
"kitchen-sink-rpc",
"openai-web-search-minimal",
"live-plugin-tool",
"plugin-update",
"openwebui",
]);
});
it("includes OpenWebUI exactly once in each legacy plugin aggregate", () => {
for (const releaseChunk of [
"plugins-runtime-core",
"plugins-runtime",
"plugins-integrations",
]) {
const withOpenWebUI = planFor({
includeOpenWebUI: true,
profile: RELEASE_PATH_PROFILE,
releaseChunk,
});
const withoutOpenWebUI = planFor({
includeOpenWebUI: false,
profile: RELEASE_PATH_PROFILE,
releaseChunk,
});
expect(
withOpenWebUI.lanes.filter((lane) => lane.name === "openwebui"),
releaseChunk,
).toHaveLength(1);
expect(
withoutOpenWebUI.lanes.map((lane) => lane.name),
releaseChunk,
).not.toContain("openwebui");
}
});
it("expands the published upgrade survivor lane across deduped baselines", () => {
const plan = planFor({
selectedLaneNames: ["published-upgrade-survivor"],
upgradeSurvivorBaselines: "openclaw@2026.6.11 2026.6.1 openclaw@2026.6.1 openclaw@2026.6.1-1",
});
expect(plan.lanes.map(summarizeLane)).toEqual([
publishedUpgradeSurvivorLane("published-upgrade-survivor-2026.6.11", "openclaw@2026.6.11"),
publishedUpgradeSurvivorLane("published-upgrade-survivor-2026.6.1", "openclaw@2026.6.1"),
publishedUpgradeSurvivorLane("published-upgrade-survivor-2026.6.1-1", "openclaw@2026.6.1-1"),
]);
});
it.each([undefined, "."])(
"rejects pre-June baselines before scheduling against target %s",
(upgradeSurvivorTargetRoot) => {
expect(() =>
planFor({
selectedLaneNames: ["published-upgrade-survivor", "update-migration"],
upgradeSurvivorBaselines: "2026.6.1 openclaw@2026.5.35",
upgradeSurvivorTargetRoot,
}),
).toThrow("must be 2026.6.1 or newer");
},
);
it("expands the published upgrade survivor lane across scenarios", () => {
const plan = planFor({
selectedLaneNames: ["published-upgrade-survivor"],
upgradeSurvivorBaselines: "2026.6.11 2026.6.1",
upgradeSurvivorScenarios: "base feishu-channel tilde-log-path sqlite-volume",
});
expect(plan.lanes.map(summarizeLane)).toEqual([
publishedUpgradeSurvivorLane(
"published-upgrade-survivor-2026.6.11",
"openclaw@2026.6.11",
"base",
),
publishedUpgradeSurvivorLane(
"published-upgrade-survivor-2026.6.11-feishu-channel",
"openclaw@2026.6.11",
"feishu-channel",
),
publishedUpgradeSurvivorLane(
"published-upgrade-survivor-2026.6.11-tilde-log-path",
"openclaw@2026.6.11",
"tilde-log-path",
),
publishedUpgradeSurvivorLane(
"published-upgrade-survivor-2026.6.11-sqlite-volume",
"openclaw@2026.6.11",
"sqlite-volume",
),
publishedUpgradeSurvivorLane(
"published-upgrade-survivor-2026.6.1",
"openclaw@2026.6.1",
"base",
),
publishedUpgradeSurvivorLane(
"published-upgrade-survivor-2026.6.1-feishu-channel",
"openclaw@2026.6.1",
"feishu-channel",
),
publishedUpgradeSurvivorLane(
"published-upgrade-survivor-2026.6.1-tilde-log-path",
"openclaw@2026.6.1",
"tilde-log-path",
),
publishedUpgradeSurvivorLane(
"published-upgrade-survivor-2026.6.1-sqlite-volume",
"openclaw@2026.6.1",
"sqlite-volume",
),
]);
});
it.each([
{ baseline: "2026.7.1", scenario: "mobile-pairing-reconnect" },
{ baseline: "2026.8.1", scenario: "watchos-direct-node" },
{ baseline: "2026.9.4", scenario: "abandoned-update" },
{ baseline: "2026.9.3", scenario: "abandoned-update" },
{ baseline: "2026.7.1-2", scenario: "prerelease-plugin-registry" },
{ baseline: "2026.7.1-2", scenario: "auth-profile-v2026-7-2-beta-5" },
{ baseline: "2026.7.1-2", scenario: "recovery-cleanup" },
])("plans $scenario only when explicitly requested", ({ baseline, scenario }) => {
const laneName = `published-upgrade-survivor-${baseline}-${scenario}`;
const explicitPlan = planFor({
selectedLaneNames: ["published-upgrade-survivor"],
upgradeSurvivorBaselines: baseline,
upgradeSurvivorScenarios: scenario,
});
expect(explicitPlan.lanes.map(summarizeLane)).toEqual([
publishedUpgradeSurvivorLane(laneName, `openclaw@${baseline}`, scenario),
]);
if (scenario === "abandoned-update") {
expect(explicitPlan.requiredPrepublishPluginPackages).toEqual([]);
}
if (scenario === "recovery-cleanup") {
expect(explicitPlan.requiredPrepublishPluginPackages).toEqual([
"@openclaw/codex",
"@openclaw/discord",
"@openclaw/whatsapp",
]);
}
for (const aggregateScenario of ["reported-issues", "far-reaching"]) {
const aggregateLaneNames = planFor({
selectedLaneNames: ["published-upgrade-survivor"],
upgradeSurvivorBaselines: baseline,
upgradeSurvivorScenarios: aggregateScenario,
}).lanes.map((lane) => lane.name);
expect(aggregateLaneNames).not.toContain(laneName);
}
});
it("expands reported upgrade issue scenarios", () => {
const plan = planFor({
selectedLaneNames: ["published-upgrade-survivor"],
upgradeSurvivorBaselines: "2026.6.1",
upgradeSurvivorScenarios: "reported-issues",
});
expect(plan.lanes.map((lane) => lane.name)).toEqual([
"published-upgrade-survivor-2026.6.1",
"published-upgrade-survivor-2026.6.1-acpx-openclaw-tools-bridge",
"published-upgrade-survivor-2026.6.1-feishu-channel",
"published-upgrade-survivor-2026.6.1-bootstrap-persona",
"published-upgrade-survivor-2026.6.1-channel-post-core-restore",
"published-upgrade-survivor-2026.6.1-plugin-deps-cleanup",
"published-upgrade-survivor-2026.6.1-configured-plugin-installs",
"published-upgrade-survivor-2026.6.1-stale-source-plugin-shadow",
"published-upgrade-survivor-2026.6.1-tilde-log-path",
"published-upgrade-survivor-2026.6.1-meeting-transcripts-sqlite",
"published-upgrade-survivor-2026.6.1-versioned-runtime-deps",
"published-upgrade-survivor-2026.6.1-cron-scheduled-authority",
]);
});
it("plans legacy operator state with native, tracked, and formerly bundled plugins and serial admission", () => {
const plan = planFor({
selectedLaneNames: ["published-upgrade-survivor"],
upgradeSurvivorBaselines: "2026.6.33 2026.6.34 2026.9.1 2026.9.4 2026.9.6",
upgradeSurvivorScenarios: "legacy-operator-state",
});
expect(plan.lanes.map((lane) => lane.name)).toEqual([
"published-upgrade-survivor-2026.6.34-legacy-operator-state",
"published-upgrade-survivor-2026.9.1-legacy-operator-state",
"published-upgrade-survivor-2026.9.4-legacy-operator-state",
"published-upgrade-survivor-2026.9.6-legacy-operator-state",
]);
expect(plan.requiredPrepublishPluginPackages).toEqual([
"@openclaw/codex",
"@openclaw/discord",
"@openclaw/duckduckgo-plugin",
]);
expect(plan.lanes.every((lane) => lane.weight === 3)).toBe(true);
for (const alias of ["reported-issues", "far-reaching"]) {
expect(
planFor({
selectedLaneNames: ["published-upgrade-survivor"],
upgradeSurvivorBaselines: "2026.6.34",
upgradeSurvivorScenarios: alias,
}).lanes.map((lane) => lane.name),
).toContain("published-upgrade-survivor-2026.6.34-legacy-operator-state");
}
});
it.each([
["workshop-doctor-recovery", "2026.9.4", "2026.9.3 2026.9.4 2026.9.5"],
["update-report-recovery", "2026.9.6", "2026.9.5 2026.9.6 2026.9.7"],
])("pins opt-in %s to published %s without credentials", (scenario, baseline, baselines) => {
const plan = planFor({
selectedLaneNames: ["published-upgrade-survivor"],
upgradeSurvivorBaselines: baselines,
upgradeSurvivorScenarios: scenario,
});
const name = `published-upgrade-survivor-${baseline}-${scenario}`;
expect(plan.lanes.map(summarizeLane)).toEqual([
publishedUpgradeSurvivorLane(name, `openclaw@${baseline}`, scenario),
]);
expect(plan.requiredPrepublishPluginPackages).toEqual([]);
expect(plan.credentials).toEqual([]);
for (const alias of ["reported-issues", "far-reaching"]) {
expect(
planFor({
selectedLaneNames: ["published-upgrade-survivor"],
upgradeSurvivorBaselines: baseline,
upgradeSurvivorScenarios: alias,
}).lanes.map((lane) => lane.name),
).not.toContain(name);
}
});
it("runs sibling-source canaries from published 9.4 without provider or registry fixtures", () => {
const plan = planFor({
selectedLaneNames: ["published-upgrade-survivor"],
upgradeSurvivorBaselines: "2026.9.3 2026.9.4",
upgradeSurvivorScenarios: "custom-plugin-siblings",
});
const name = "published-upgrade-survivor-2026.9.4-custom-plugin-siblings";
expect(plan.lanes.map(summarizeLane)).toEqual([
publishedUpgradeSurvivorLane(name, "openclaw@2026.9.4", "custom-plugin-siblings"),
]);
expect(plan.requiredPrepublishPluginPackages).toEqual([]);
expect(plan.credentials).toEqual([]);
expect(
planFor({
selectedLaneNames: ["published-upgrade-survivor"],
upgradeSurvivorBaselines: "2026.9.4",
upgradeSurvivorScenarios: "reported-issues",
}).lanes.map((lane) => lane.name),
).toContain(name);
});
it.each([
{ scenario: "projects-doctor", baseline: "2026.9.4" },
{ scenario: "projects-startup-migration", baseline: "2026.9.4" },
{ scenario: "dreaming-cron-doctor", baseline: "2026.9.6" },
])(
"plans $scenario only for its exact published writer without registry or credential fixtures",
({ scenario, baseline }) => {
const plan = planFor({
selectedLaneNames: ["published-upgrade-survivor"],
upgradeSurvivorBaselines: "2026.9.3 2026.9.4 2026.9.5 2026.9.6 2026.9.7 latest",
upgradeSurvivorScenarios: scenario,
});
const name = `published-upgrade-survivor-${baseline}-${scenario}`;
expect(plan.lanes.map(summarizeLane)).toEqual([
publishedUpgradeSurvivorLane(name, `openclaw@${baseline}`, scenario),
]);
expect(plan.requiredPrepublishPluginPackages).toEqual([]);
expect(plan.credentials).toEqual([]);
for (const alias of ["reported-issues", "far-reaching"]) {
expect(
planFor({
selectedLaneNames: ["published-upgrade-survivor"],
upgradeSurvivorBaselines: baseline,
upgradeSurvivorScenarios: alias,
}).lanes.map((lane) => lane.name),
).not.toContain(name);
}
},
);
it("keeps platform survivors out of release aliases", () => {
const scenariosFor = (
upgradeSurvivorScenarios: string,
upgradeSurvivorBaselines = "2026.7.1-2",
) =>
planFor({
selectedLaneNames: ["published-upgrade-survivor"],
upgradeSurvivorBaselines,
upgradeSurvivorScenarios,
}).lanes.map((lane) => lane.name);
expect(scenariosFor("reported-issues")).not.toContain(
"published-upgrade-survivor-2026.7.1-2-sqlite-volume",
);
expect(scenariosFor("far-reaching")).toContain(
"published-upgrade-survivor-2026.7.1-2-sqlite-volume",
);
for (const alias of ["reported-issues", "far-reaching"]) {
expect(scenariosFor(alias, "2026.7.1")).not.toContain(
"published-upgrade-survivor-2026.7.1-mobile-pairing-reconnect",
);
expect(scenariosFor(alias, "2026.8.1")).not.toContain(
"published-upgrade-survivor-2026.8.1-watchos-direct-node",
);
}
});
it("runs the watchOS direct-node survivor only from its shipped gateway floor", () => {
const plan = planFor({
selectedLaneNames: ["published-upgrade-survivor"],
upgradeSurvivorBaselines: "2026.7.1 2026.8.1",
upgradeSurvivorScenarios: "watchos-direct-node",
});
expect(plan.lanes.map((lane) => lane.name)).toEqual([
"published-upgrade-survivor-2026.8.1-watchos-direct-node",
]);
});
it("runs mobile pairing reconnect from the 2026.7.1 baseline floor", () => {
const supported = planFor({
selectedLaneNames: ["published-upgrade-survivor"],
upgradeSurvivorBaselines: "2026.7.1",
upgradeSurvivorScenarios: "mobile-pairing-reconnect",
});
const unsupported = planFor({
selectedLaneNames: ["published-upgrade-survivor"],
upgradeSurvivorBaselines: "2026.6.34",
upgradeSurvivorScenarios: "mobile-pairing-reconnect",
});
expect(supported.lanes.map((lane) => lane.name)).toEqual([
"published-upgrade-survivor-2026.7.1-mobile-pairing-reconnect",
]);
expect(unsupported.lanes).toEqual([]);
expect(unsupported.omittedUnsupportedLanes).toEqual([]);
});
it("omits trusted-current scenarios unsupported by a frozen target harness", () => {
const targetRoot = tempDirs.make("openclaw-frozen-upgrade-harness-");
writeFrozenScenarioContract(targetRoot, [
"base",
"feishu-channel",
"bootstrap-persona",
"channel-post-core-restore",
"plugin-deps-cleanup",
"configured-plugin-installs",
"stale-source-plugin-shadow",
"tilde-log-path",
"versioned-runtime-deps",
]);
const plan = planFor({
selectedLaneNames: ["published-upgrade-survivor"],
upgradeSurvivorBaselines: "2026.6.11",
upgradeSurvivorScenarios: "reported-issues",
upgradeSurvivorTargetRoot: targetRoot,
});
expect(plan.lanes.map((lane) => lane.name)).toEqual([
"published-upgrade-survivor-2026.6.11",
"published-upgrade-survivor-2026.6.11-feishu-channel",
"published-upgrade-survivor-2026.6.11-bootstrap-persona",
"published-upgrade-survivor-2026.6.11-channel-post-core-restore",
"published-upgrade-survivor-2026.6.11-plugin-deps-cleanup",
"published-upgrade-survivor-2026.6.11-configured-plugin-installs",
"published-upgrade-survivor-2026.6.11-stale-source-plugin-shadow",
"published-upgrade-survivor-2026.6.11-tilde-log-path",
"published-upgrade-survivor-2026.6.11-versioned-runtime-deps",
]);
expect(plan.omittedUnsupportedLanes).toEqual([
"published-upgrade-survivor-2026.6.11-acpx-openclaw-tools-bridge",
"published-upgrade-survivor-2026.6.11-meeting-transcripts-sqlite",
"published-upgrade-survivor-2026.6.11-cron-scheduled-authority",
]);
});
it("reads content-addressed scenario catalogs from pre-command frozen targets", () => {
const targetRoot = tempDirs.make("openclaw-legacy-frozen-upgrade-harness-");
const assertionsFile = join(targetRoot, "scripts/e2e/lib/upgrade-survivor/assertions.mjs");
const legacyScenarios = [
"base",
"feishu-channel",
"bootstrap-persona",
"channel-post-core-restore",
"plugin-deps-cleanup",
"configured-plugin-installs",
"stale-source-plugin-shadow",
"tilde-log-path",
"versioned-runtime-deps",
];
mkdirSync(dirname(assertionsFile), { recursive: true });
writeFileSync(
assertionsFile,
[
"const SCENARIOS = new Set([",
...legacyScenarios.map((scenario) => ` "${scenario}",`),
"]);",
'throw new Error("unknown upgrade-survivor assertion command: list-scenarios");',
].join("\n"),
);
const { sha } = commitTarget(targetRoot);
const plan = planFor({
selectedLaneNames: ["published-upgrade-survivor"],
upgradeSurvivorBaselines: "2026.6.11",
upgradeSurvivorScenarios: "reported-issues",
upgradeSurvivorTargetRoot: targetRoot,
frozenTarget: { mode: "inert", source: createFrozenTargetSource(targetRoot, sha) },
});
expect(plan.omittedUnsupportedLanes).toEqual([
"published-upgrade-survivor-2026.6.11-acpx-openclaw-tools-bridge",
"published-upgrade-survivor-2026.6.11-meeting-transcripts-sqlite",
"published-upgrade-survivor-2026.6.11-cron-scheduled-authority",
]);
const catalogFile = join(targetRoot, "scripts/lib/upgrade-survivor-scenarios.json");
mkdirSync(dirname(catalogFile), { recursive: true });
writeFileSync(catalogFile, "invalid current catalog");
expect(() =>
planFor({
selectedLaneNames: ["published-upgrade-survivor"],
upgradeSurvivorBaselines: "2026.6.11",
upgradeSurvivorScenarios: "base",
upgradeSurvivorTargetRoot: targetRoot,
allowFrozenTargetScenarioOmissions: false,
}),
).toThrow(/unrecognized scenario contract/);
});
it("recognizes the frozen combined mobile and watch scenario catalog", () => {
const targetRoot = tempDirs.make("openclaw-platform-survivors-frozen-harness-");
const assertionsFile = join(targetRoot, "scripts/e2e/lib/upgrade-survivor/assertions.mjs");
const scenarios = [
"base",
"mobile-pairing-reconnect",
"acpx-openclaw-tools-bridge",
"feishu-channel",
"bootstrap-persona",
"channel-post-core-restore",
"codex-allowlist-survival",
"plugin-deps-cleanup",
"configured-plugin-installs",
"stale-source-plugin-shadow",
"prerelease-plugin-registry",
"tilde-log-path",
"meeting-transcripts-sqlite",
"versioned-runtime-deps",
"cron-scheduled-authority",
"sqlite-volume",
"recovery-cleanup",
"auth-profile-v2026-7-2-beta-5",
"watchos-direct-node",
];
mkdirSync(dirname(assertionsFile), { recursive: true });
writeFileSync(
assertionsFile,
[
"const SCENARIOS = new Set([",
...scenarios.map((scenario) => ` "${scenario}",`),
"]);",
'throw new Error("unknown upgrade-survivor assertion command: list-scenarios");',
].join("\n"),
);
const plan = planFor({
selectedLaneNames: ["update-migration"],
upgradeSurvivorBaselines: "2026.7.1 2026.8.1",
upgradeSurvivorScenarios: "mobile-pairing-reconnect watchos-direct-node",
upgradeSurvivorTargetRoot: targetRoot,
});
expect(plan.lanes.map((lane) => lane.name)).toEqual([
"update-migration-2026.8.1-watchos-direct-node",
]);
expect(plan.omittedUnsupportedLanes).toEqual([
"update-migration-2026.7.1-mobile-pairing-reconnect",
"update-migration-2026.8.1-mobile-pairing-reconnect",
]);
});
it("keeps mobile pairing when an unapproved target lacks a source-qualified omission", () => {
const targetRoot = tempDirs.make("openclaw-frozen-mobile-package-target-");
writeFrozenScenarioContract(targetRoot, ["base"]);
const plan = planFor({
selectedLaneNames: ["update-migration"],
upgradeSurvivorBaselines: "2026.7.1",
upgradeSurvivorScenarios: "mobile-pairing-reconnect",
upgradeSurvivorTargetRoot: targetRoot,
allowFrozenTargetScenarioOmissions: false,
});
expect(plan.lanes.map((lane) => lane.name)).toEqual([
"update-migration-2026.7.1-mobile-pairing-reconnect",
]);
expect(plan.omittedUnsupportedLanes).toEqual([]);
});
it("keeps mobile pairing when the selected Gateway admits iPhone watch relay", () => {
const targetRoot = tempDirs.make("openclaw-frozen-mobile-watch-relay-");
writeFrozenScenarioContract(targetRoot, ["base"]);
const policy = join(targetRoot, "src/gateway/node-command-policy.ts");
mkdirSync(dirname(policy), { recursive: true });
writeFileSync(
policy,
'const commands = ["watch.status", "watch.notify"];\nplatformId === "ios";\nnormalizeDeviceMetadataForPolicy(node?.deviceFamily) === "iphone";\nnew Set([...watchRelayCommands]);\n',
);
const plan = planFor({
selectedLaneNames: ["update-migration"],
upgradeSurvivorBaselines: "2026.7.1",
upgradeSurvivorScenarios: "mobile-pairing-reconnect",
upgradeSurvivorTargetRoot: targetRoot,
});
expect(plan.lanes.map((lane) => lane.name)).toEqual([
"update-migration-2026.7.1-mobile-pairing-reconnect",
]);
});
it("omits mobile pairing when the selected Gateway does not admit its declared relay commands", () => {
const targetRoot = tempDirs.make("openclaw-frozen-mobile-watch-relay-unadmitted-");
writeFrozenScenarioContract(targetRoot, ["base"]);
const policy = join(targetRoot, "src/gateway/node-command-policy.ts");
mkdirSync(dirname(policy), { recursive: true });
writeFileSync(
policy,
'const commands = ["watch.status", "watch.notify"];\nplatformId === "ios";\nnormalizeDeviceMetadataForPolicy(node?.deviceFamily) === "iphone";\n',
);
const plan = planFor({
selectedLaneNames: ["update-migration"],
upgradeSurvivorBaselines: "2026.7.1",
upgradeSurvivorScenarios: "mobile-pairing-reconnect",
upgradeSurvivorTargetRoot: targetRoot,
});
expect(plan.lanes).toEqual([]);
expect(plan.omittedUnsupportedLanes).toEqual([
"update-migration-2026.7.1-mobile-pairing-reconnect",
]);
});
it("omits survivor lanes when the target exposes none of the requested scenarios", () => {
const targetRoot = tempDirs.make("openclaw-frozen-empty-upgrade-harness-");
writeFrozenScenarioContract(targetRoot, ["unrelated"]);
const plan = planFor({
selectedLaneNames: ["published-upgrade-survivor"],
upgradeSurvivorBaselines: "2026.6.11",
upgradeSurvivorScenarios: "reported-issues",
upgradeSurvivorTargetRoot: targetRoot,
});
expect(plan.lanes).toEqual([]);
expect(plan.omittedUnsupportedLanes).toHaveLength(12);
expect(plan.omittedUnsupportedLanes).toContain("published-upgrade-survivor-2026.6.11");
expect(plan.omittedUnsupportedLanes).toContain(
"published-upgrade-survivor-2026.6.11-versioned-runtime-deps",
);
});
it("omits baseline-only survivor lanes when the target lacks the implicit base scenario", () => {
const targetRoot = tempDirs.make("openclaw-frozen-no-base-upgrade-harness-");
writeFrozenScenarioContract(targetRoot, ["unrelated"]);
const plan = planFor({
selectedLaneNames: ["published-upgrade-survivor"],
upgradeSurvivorBaselines: "2026.6.11",
upgradeSurvivorTargetRoot: targetRoot,
});
expect(plan.lanes).toEqual([]);
expect(plan.omittedUnsupportedLanes).toEqual(["published-upgrade-survivor-2026.6.11"]);
});
it("omits an unconfigured survivor lane when the target lacks the implicit base scenario", () => {
const targetRoot = tempDirs.make("openclaw-frozen-default-base-harness-");
writeFrozenScenarioContract(targetRoot, ["unrelated"]);
const plan = planFor({
selectedLaneNames: ["published-upgrade-survivor"],
upgradeSurvivorTargetRoot: targetRoot,
});
expect(plan.lanes).toEqual([]);
expect(plan.omittedUnsupportedLanes).toEqual(["published-upgrade-survivor"]);
});
it("reports an unsupported survivor lane beside runnable selected lanes", () => {
const targetRoot = tempDirs.make("openclaw-frozen-mixed-upgrade-harness-");
writeFrozenScenarioContract(targetRoot, ["unrelated"]);
const plan = planFor({
selectedLaneNames: ["published-upgrade-survivor", "plugin-binding-command-escape"],
upgradeSurvivorScenarios: "reported-issues",
upgradeSurvivorTargetRoot: targetRoot,
});
expect(plan.lanes.map((lane) => lane.name)).toEqual(["plugin-binding-command-escape"]);
expect(plan.omittedUnsupportedLanes).toHaveLength(14);
expect(plan.omittedUnsupportedLanes).toContain("published-upgrade-survivor");
expect(plan.omittedUnsupportedLanes).toContain(
"published-upgrade-survivor-versioned-runtime-deps",
);
});
it("reports an explicitly selected expanded survivor lane as unsupported", () => {
const targetRoot = tempDirs.make("openclaw-frozen-expanded-upgrade-harness-");
writeFrozenScenarioContract(targetRoot, ["unrelated"]);
const selectedLane = "published-upgrade-survivor-2026.6.11";
const plan = planFor({
selectedLaneNames: [selectedLane],
upgradeSurvivorBaselines: "2026.6.11",
upgradeSurvivorTargetRoot: targetRoot,
});
expect(plan.lanes).toEqual([]);
expect(plan.omittedUnsupportedLanes).toEqual([selectedLane]);
});
it("does not fall back to base when an unsupported scenario is baseline-incompatible", () => {
const targetRoot = tempDirs.make("openclaw-frozen-incompatible-scenario-harness-");
writeFrozenScenarioContract(targetRoot, ["unrelated"]);
const plan = planFor({
selectedLaneNames: ["published-upgrade-survivor"],
upgradeSurvivorBaselines: "2026.6.33",
upgradeSurvivorScenarios: "legacy-operator-state",
upgradeSurvivorTargetRoot: targetRoot,
});
expect(plan.lanes).toEqual([]);
expect(plan.omittedUnsupportedLanes).toEqual([]);
});
it("fails closed when an unknown legacy scenario catalog lacks the command", () => {
const targetRoot = tempDirs.make("openclaw-frozen-failed-scenario-harness-");
const assertionsFile = join(targetRoot, "scripts/e2e/lib/upgrade-survivor/assertions.mjs");
mkdirSync(dirname(assertionsFile), { recursive: true });
writeFileSync(
assertionsFile,
[
'const SCENARIOS = new Set(["base"]);',
'throw new Error("unknown upgrade-survivor assertion command: list-scenarios");',
].join("\n"),
);
expect(() =>
planFor({
selectedLaneNames: ["published-upgrade-survivor"],
upgradeSurvivorScenarios: "reported-issues",
upgradeSurvivorTargetRoot: targetRoot,
}),
).toThrow("unknown upgrade-survivor assertion command: list-scenarios");
});
it("fails closed when a frozen target scenario command returns non-JSON output", () => {
const targetRoot = tempDirs.make("openclaw-frozen-non-json-scenario-harness-");
const assertionsFile = join(targetRoot, "scripts/e2e/lib/upgrade-survivor/assertions.mjs");
mkdirSync(dirname(assertionsFile), { recursive: true });
writeFileSync(assertionsFile, 'process.stdout.write("base");\n');
expect(() =>
planFor({
selectedLaneNames: ["published-upgrade-survivor"],
upgradeSurvivorScenarios: "reported-issues",
upgradeSurvivorTargetRoot: targetRoot,
}),
).toThrow("list-scenarios did not return JSON");
});
it("fails closed when a frozen target scenario command returns an invalid catalog", () => {
const targetRoot = tempDirs.make("openclaw-frozen-invalid-scenario-harness-");
const assertionsFile = join(targetRoot, "scripts/e2e/lib/upgrade-survivor/assertions.mjs");
mkdirSync(dirname(assertionsFile), { recursive: true });
writeFileSync(assertionsFile, 'process.stdout.write("[\\"base\\",\\"base\\"]");\n');
expect(() =>
planFor({
selectedLaneNames: ["published-upgrade-survivor"],
upgradeSurvivorScenarios: "reported-issues",
upgradeSurvivorTargetRoot: targetRoot,
}),
).toThrow("list-scenarios returned an invalid catalog");
});
it("does not inspect a frozen survivor contract for unrelated selected lanes", () => {
const targetRoot = tempDirs.make("openclaw-frozen-unrelated-lane-harness-");
const assertionsFile = join(targetRoot, "scripts/e2e/lib/upgrade-survivor/assertions.mjs");
mkdirSync(dirname(assertionsFile), { recursive: true });
writeFileSync(assertionsFile, 'throw new Error("must not run");\n');
const plan = planFor({
selectedLaneNames: ["plugin-binding-command-escape"],
upgradeSurvivorScenarios: "reported-issues",
upgradeSurvivorTargetRoot: targetRoot,
});
expect(plan.lanes.map((lane) => lane.name)).toEqual(["plugin-binding-command-escape"]);
expect(plan.omittedUnsupportedLanes).toEqual([]);
});
it("expands update migration across baselines and cleanup scenarios", () => {
const plan = planFor({
selectedLaneNames: ["update-migration"],
upgradeSurvivorBaselines: "2026.6.11 2026.6.1",
upgradeSurvivorScenarios: "plugin-deps-cleanup",
});
expect(plan.lanes.map(summarizeLane)).toEqual([
updateMigrationLane("update-migration-2026.6.11-plugin-deps-cleanup", "openclaw@2026.6.11"),
updateMigrationLane("update-migration-2026.6.1-plugin-deps-cleanup", "openclaw@2026.6.1"),
]);
});
it("plans a live-only selected lane without package e2e images", () => {
const plan = planFor({ selectedLaneNames: ["live-models"] });
expect(plan.credentials).toEqual(["anthropic", "gemini"]);
expect(plan.lanes.map((lane) => lane.name)).toEqual(["live-models"]);
expect(plan.needs).toEqual({
bareImage: false,
e2eImage: false,
functionalImage: false,
liveImage: true,
package: false,
prepublishPluginRegistry: false,
});
});
it("runs the gateway lane with the scheduler's shared live image and plugins", () => {
const root = tempDirs.make("openclaw-live-gateway-image-");
const script = join(root, "scripts/test-live-gateway-models-docker.sh");
mkdirSync(dirname(script), { recursive: true });
writeFileSync(
script,
'printf "%s|%s|%s\\n" "$OPENCLAW_IMAGE" "$OPENCLAW_DOCKER_BUILD_EXTENSIONS" "$OPENCLAW_SKIP_DOCKER_BUILD"',
);
const lane = requireFirstLane(planFor({ selectedLaneNames: ["live-gateway"] }));
const output = execFileSync("/bin/bash", ["-c", lane.command], {
encoding: "utf8",
env: {
...process.env,
OPENCLAW_DOCKER_E2E_TRUSTED_HARNESS_DIR: root,
OPENCLAW_IMAGE: "openclaw:prepared-candidate",
OPENCLAW_DOCKER_BUILD_EXTENSIONS: "matrix acpx codex",
OPENCLAW_SKIP_DOCKER_BUILD: "1",
},
});
expect(output.trim()).toBe("openclaw:prepared-candidate|matrix acpx codex|1");
});
it("derives live Docker credentials from lane resources", () => {
const cases = [
{ credentials: ["anthropic", "gemini"], name: "live-models" },
{ credentials: ["anthropic", "gemini"], name: "live-gateway" },
{ credentials: ["anthropic-api-key"], name: "live-anthropic-cache" },
{ credentials: ["anthropic"], name: "live-cli-backend-claude" },
{ credentials: ["gemini"], name: "live-cli-backend-gemini" },
{ credentials: ["openai"], name: "live-codex-harness" },
{ credentials: ["openai"], name: "live-codex-media-path" },
{ credentials: ["openai"], name: "live-mcp-code-mode-gateway" },
{ credentials: ["openai"], name: "live-subagent-announce" },
{ credentials: ["openai"], name: "live-codex-bind" },
{ credentials: ["anthropic"], name: "live-acp-bind-claude" },
{ credentials: ["codex", "openai"], name: "live-acp-bind-codex" },
{ credentials: ["factory"], name: "live-acp-bind-droid" },
{ credentials: ["gemini"], name: "live-acp-bind-gemini" },
{ credentials: ["opencode"], name: "live-acp-bind-opencode" },
{ credentials: ["openai", "telegram"], name: "npm-telegram-live" },
] as const;
for (const { credentials, name } of cases) {
expect(planFor({ selectedLaneNames: [name] }).credentials, name).toEqual(credentials);
}
});
it("requires the aggregate Gemini CLI backend lane to report failures", () => {
const plan = planFor({ selectedLaneNames: ["live-cli-backend-gemini"] });
const lane = requireFirstLane(plan);
expect(lane.command).not.toContain("OPENCLAW_LIVE_CLI_BACKEND_ADVISORY");
expect(lane.command).not.toContain("OPENCLAW_LIVE_CLI_BACKEND_ALLOW_PROVIDER_SKIP");
expect(lane.command).toContain(
"OPENCLAW_LIVE_CLI_BACKEND_MODEL=google-gemini-cli/gemini-3-flash-preview",
);
});
it("plans Codex harness Docker-all lanes for API-key Testbox auth", () => {
for (const name of ["live-codex-harness", "live-codex-bind"]) {
const plan = planFor({ selectedLaneNames: [name] });
const lane = requireFirstLane(plan);
expect(plan.credentials, name).toEqual(["openai"]);
expect(lane.command, name).toContain("OPENCLAW_LIVE_CODEX_HARNESS_AUTH=api-key");
expect(lane.resources, name).toContain("live:openai");
expect(lane.resources, name).not.toContain("live:codex");
}
});
it("plans the Codex npm plugin live lane as package-backed OpenAI proof", () => {
const plan = planFor({ selectedLaneNames: ["live-codex-npm-plugin"] });
expect(plan.credentials).toEqual(["openai"]);
expect(plan.lanes.map(summarizeLane)).toEqual([
{
command:
'OPENCLAW_SKIP_DOCKER_BUILD=1 bash -c \'harness="${OPENCLAW_DOCKER_E2E_TRUSTED_HARNESS_DIR:-.}"; OPENCLAW_LIVE_DOCKER_REPO_ROOT="${OPENCLAW_DOCKER_E2E_REPO_ROOT:-$PWD}" bash "$harness/scripts/e2e/codex-npm-plugin-live-docker.sh"\'',
imageKind: "bare",
live: true,
name: "live-codex-npm-plugin",
resources: ["docker", "live", "live:openai", "npm"],
stateScenario: "empty",
timeoutMs: 1_800_000,
weight: 3,
},
]);
expect(plan.needs).toEqual({
bareImage: true,
e2eImage: true,
functionalImage: false,
liveImage: false,
package: true,
prepublishPluginRegistry: false,
});
});
it("plans the Codex on-demand onboarding lane as package-backed npm proof", () => {
const plan = planFor({ selectedLaneNames: ["codex-on-demand"] });
expect(plan.lanes).toHaveLength(1);
const lane = requireFirstLane(plan);
expect(lane.command).toBe("OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:codex-on-demand");
expect(lane.imageKind).toBe("bare");
expect(lane.live).toBe(false);
expect(lane.name).toBe("codex-on-demand");
expect(lane.resources).toEqual(["docker", "npm", "service"]);
expect(lane.stateScenario).toBe("empty");
expect(lane.timeoutMs).toBe(1_800_000);
expect(plan.needs.bareImage).toBe(true);
expect(plan.needs.package).toBe(true);
expect(plan.requiredPrepublishPluginPackages).toEqual(["@openclaw/codex"]);
expect(plan.needs.prepublishPluginRegistry).toBe(true);
});
it("plans the plugin binding command escape lane as source Docker proof", () => {
const plan = planFor({ selectedLaneNames: ["plugin-binding-command-escape"] });
expect(plan.lanes).toHaveLength(1);
const lane = requireFirstLane(plan);
expect(lane.command).toBe(
"OPENCLAW_SKIP_DOCKER_BUILD=0 pnpm test:docker:plugin-binding-command-escape",
);
expect(lane.imageKind).toBeUndefined();
expect(lane.live).toBe(false);
expect(lane.name).toBe("plugin-binding-command-escape");
expect(lane.resources).toEqual(["docker", "npm"]);
expect(lane.stateScenario).toBe("empty");
expect(plan.needs.e2eImage).toBe(false);
expect(plan.needs.package).toBe(false);
});
it("plans the live plugin tool lane as package-backed OpenAI proof", () => {
const plan = planFor({ selectedLaneNames: ["live-plugin-tool"] });
expect(plan.credentials).toEqual(["openai"]);
expect(plan.lanes).toHaveLength(1);
const lane = requireFirstLane(plan);
expect(lane.command).toBe(
"OPENCLAW_LIVE_PLUGIN_TOOL_TIMEOUT_SECONDS=300 OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:live-plugin-tool",
);
expect(lane.imageKind).toBe("bare");
expect(lane.live).toBe(true);
expect(lane.name).toBe("live-plugin-tool");
expect(lane.resources).toEqual(["docker", "live", "live:openai", "npm"]);
expect(lane.stateScenario).toBe("empty");
expect(plan.needs.bareImage).toBe(true);
expect(plan.needs.liveImage).toBe(false);
expect(plan.needs.package).toBe(true);
});
it("dedupes scheduler resources from lane wrappers and explicit lane metadata", () => {
const plan = planFor({
selectedLaneNames: ["release-user-journey", "release-plugin-marketplace"],
});
expect(plan.lanes.map((lane) => ({ name: lane.name, resources: lane.resources }))).toEqual([
{
name: "release-user-journey",
resources: ["docker", "npm", "service"],
},
{
name: "release-plugin-marketplace",
resources: ["docker", "npm"],
},
]);
});
it("plans the Droid ACP bind live lane as Factory-auth proof", () => {
const plan = planFor({ selectedLaneNames: ["live-acp-bind-droid"] });
expect(plan.credentials).toEqual(["factory"]);
expect(plan.lanes).toHaveLength(1);
const lane = requireFirstLane(plan);
expect(lane.command).toBe(
'OPENCLAW_LIVE_ACP_BIND_AGENT=droid OPENCLAW_LIVE_ACP_BIND_REQUIRE_TRANSCRIPT=1 OPENCLAW_SKIP_DOCKER_BUILD=1 bash -c \'harness="${OPENCLAW_DOCKER_E2E_TRUSTED_HARNESS_DIR:-.}"; OPENCLAW_LIVE_DOCKER_REPO_ROOT="${OPENCLAW_DOCKER_E2E_REPO_ROOT:-$PWD}" bash "$harness/scripts/test-live-acp-bind-docker.sh"\'',
);
expect(lane.imageKind).toBeUndefined();
expect(lane.live).toBe(true);
expect(lane.name).toBe("live-acp-bind-droid");
expect(lane.resources).toEqual(["docker", "live", "live:droid", "npm"]);
expect(lane.timeoutMs).toBe(1_200_000);
expect(plan.needs.liveImage).toBe(true);
});
it("plans Docker package scripts that were previously only directly runnable", () => {
const plan = planFor({
selectedLaneNames: ["browser-cdp-snapshot", "multi-node-update", "npm-telegram-live"],
});
expect(plan.credentials).toEqual(["openai", "telegram"]);
expect(plan.lanes.map(summarizeLane)).toEqual([
{
command: "pnpm test:docker:browser-cdp-snapshot",
imageKind: "functional",
live: false,
name: "browser-cdp-snapshot",
resources: ["docker", "service"],
stateScenario: "empty",
timeoutMs: 1_200_000,
weight: 3,
},
{
command: "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:multi-node-update",
imageKind: "bare",
live: false,
name: "multi-node-update",
resources: ["docker", "npm"],
stateScenario: "empty",
timeoutMs: 900_000,
weight: 3,
},
{
command: "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:npm-telegram-live",
imageKind: "bare",
live: true,
name: "npm-telegram-live",
resources: ["docker", "live", "live:openai", "live:telegram", "npm", "service"],
timeoutMs: 1_800_000,
weight: 3,
},
]);
expect(plan.needs).toEqual({
bareImage: true,
e2eImage: true,
functionalImage: true,
liveImage: false,
package: true,
prepublishPluginRegistry: false,
});
});
it("plans Open WebUI as a live-auth functional image lane", () => {
const plan = planFor({
includeOpenWebUI: true,
selectedLaneNames: ["openwebui"],
});
expect(plan.credentials).toEqual(["openai"]);
expect(plan.lanes.map(summarizeLane)).toEqual([
{
command:
"OPENCLAW_OPENWEBUI_MODEL=openai/gpt-5.4-mini OPENCLAW_OPENWEBUI_PROVIDER_TIMEOUT_SECONDS=300 OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:openwebui",
imageKind: "functional",
live: true,
name: "openwebui",
resources: ["docker", "live", "live:openai", "service"],
timeoutMs: 1_200_000,
weight: 5,
},
]);
expect(plan.needs).toEqual({
bareImage: false,
e2eImage: true,
functionalImage: true,
liveImage: false,
package: true,
prepublishPluginRegistry: false,
});
});
it("excludes Open WebUI from skip-live Docker all plans", () => {
const plan = planFor({
liveMode: "skip",
});
expect(plan.lanes.map((lane) => lane.name)).not.toContain("openwebui");
});
it("derives prerelease npm companions from selected survivor recipes", () => {
for (const laneName of [
"upgrade-survivor",
"published-upgrade-survivor",
"root-managed-vps-upgrade",
"update-restart-auth",
"update-migration",
]) {
const plan = planFor({ selectedLaneNames: [laneName] });
expect(plan.requiredPrepublishPluginPackages).toEqual([
"@openclaw/codex",
"@openclaw/discord",
"@openclaw/whatsapp",
]);
expect(plan.needs.prepublishPluginRegistry).toBe(true);
}
const feishuPlan = planFor({
selectedLaneNames: ["published-upgrade-survivor"],
upgradeSurvivorBaselines: "2026.7.2",
upgradeSurvivorScenarios: "base feishu-channel",
});
expect(feishuPlan.requiredPrepublishPluginPackages).toEqual([
"@openclaw/codex",
"@openclaw/discord",
"@openclaw/feishu",
"@openclaw/whatsapp",
]);
});
it("stages the ACP recipe companion from the oldest supported baseline", () => {
const plan = planFor({
selectedLaneNames: ["published-upgrade-survivor"],
upgradeSurvivorBaselines: "2026.6.1",
upgradeSurvivorScenarios: "acpx-openclaw-tools-bridge",
});
expect(plan.requiredPrepublishPluginPackages).toEqual([
"@openclaw/acpx",
"@openclaw/codex",
"@openclaw/discord",
"@openclaw/whatsapp",
]);
});
it("does not request a prerelease plugin registry for unrelated lanes", () => {
const plan = planFor({ selectedLaneNames: ["doctor-switch"] });
expect(plan.requiredPrepublishPluginPackages).toEqual([]);
expect(plan.needs.prepublishPluginRegistry).toBe(false);
});
it("maps installer E2E to provider-specific package install lanes", () => {
const selectedLaneNames = parseLaneSelection("install-e2e");
const plan = planFor({ selectedLaneNames });
expect(selectedLaneNames).toEqual(["install-e2e-openai", "install-e2e-anthropic"]);
expect(
plan.lanes.map((lane) => ({
imageKind: lane.imageKind,
live: lane.live,
name: lane.name,
resources: lane.resources,
timeoutMs: lane.timeoutMs,
weight: lane.weight,
})),
).toEqual([
{
imageKind: "bare",
live: true,
name: "install-e2e-openai",
resources: ["docker", "live", "live:openai", "npm", "service"],
timeoutMs: 900_000,
weight: 3,
},
{
imageKind: "bare",
live: true,
name: "install-e2e-anthropic",
resources: ["docker", "live", "live:claude", "npm", "service"],
weight: 3,
},
]);
expect(plan.credentials).toEqual(["anthropic", "openai"]);
});
it("maps bundled plugin install/uninstall to package-backed shards", () => {
const selectedLaneNames = parseLaneSelection("bundled-plugin-install-uninstall");
const plan = planFor({ selectedLaneNames });
expect(selectedLaneNames).toEqual(
Array.from(
{ length: BUNDLED_PLUGIN_INSTALL_UNINSTALL_SHARDS },
(_, index) => `bundled-plugin-install-uninstall-${index}`,
),
);
expect(plan.lanes).toHaveLength(BUNDLED_PLUGIN_INSTALL_UNINSTALL_SHARDS);
const firstLane = plan.lanes[0];
const lastLane = plan.lanes[23];
if (!firstLane || !lastLane) {
throw new Error("Expected bundled plugin sweep boundary lanes");
}
expect(summarizeLane(firstLane)).toEqual(bundledPluginSweepLane(0));
expect(summarizeLane(lastLane)).toEqual(bundledPluginSweepLane(23));
expect(plan.needs).toEqual({
bareImage: false,
e2eImage: true,
functionalImage: true,
liveImage: false,
package: true,
prepublishPluginRegistry: false,
});
});
it("rejects unknown selected lanes with the available lane names", () => {
expect(() => planFor({ selectedLaneNames: ["missing-lane"] })).toThrow(
/OPENCLAW_DOCKER_ALL_LANES unknown lane\(s\): missing-lane/u,
);
});
});