// Docker E2E Plan tests cover docker e2e plan script behavior.
import { execFileSync } from "node:child_process";
import { chmodSync, copyFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import {
  DEFAULT_LIVE_RETRIES,
  RELEASE_PATH_PROFILE,
  findLaneByName,
  parseLaneSelection,
  requiredPrepublishPluginPackagesForLanes,
  resolveDockerE2ePlan,
} from "../../scripts/lib/docker-e2e-plan.mts";
import {
  allReleasePathLanes,
  BUNDLED_PLUGIN_INSTALL_UNINSTALL_SHARDS,
  mainLanes,
} from "../../scripts/lib/docker-e2e-scenarios.mts";
import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";

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 planFor(
  overrides: Partial<Parameters<typeof resolveDockerE2ePlan>[0]> = {},
): ReturnType<typeof resolveDockerE2ePlan>["plan"] {
  return resolveDockerE2ePlan({
    allowFrozenTargetScenarioOmissions: true,
    includeOpenWebUI: false,
    liveMode: "all",
    liveRetries: DEFAULT_LIVE_RETRIES,
    orderLanes,
    planReleaseAll: false,
    profile: "all",
    releaseChunk: "core",
    selectedLaneNames: [],
    timingStore: undefined,
    ...overrides,
  }).plan;
}

function writeCandidatePackage(scripts: Record<string, string>): string {
  const root = tempDirs.make("openclaw-docker-plan-candidate-");
  writeFileSync(join(root, "package.json"), JSON.stringify({ scripts }));
  return root;
}

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@2026.4.23}"; 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", () => {
  it("omits a package-script lane unavailable from the candidate", () => {
    const plan = planFor({
      candidatePackageRoot: writeCandidatePackage({}),
      selectedLaneNames: ["update-run-package-self-upgrade"],
    });

    expect(plan.lanes).toEqual([]);
  });

  it("keeps a package-script lane available from the candidate", () => {
    const plan = planFor({
      candidatePackageRoot: writeCandidatePackage({
        "test:docker:update-run-package-self-upgrade": "node test.mjs",
      }),
      selectedLaneNames: ["update-run-package-self-upgrade"],
    });

    expect(plan.lanes.map((lane) => lane.name)).toEqual(["update-run-package-self-upgrade"]);
  });

  it("keeps trusted upgrade harness lanes without candidate package scripts", () => {
    const plan = planFor({
      candidatePackageRoot: writeCandidatePackage({}),
      selectedLaneNames: [
        "upgrade-survivor",
        "published-upgrade-survivor",
        "root-managed-vps-upgrade",
        "update-migration",
        "update-run-package-self-upgrade",
      ],
    });

    expect(plan.lanes.map((lane) => lane.name)).toEqual([
      "upgrade-survivor",
      "published-upgrade-survivor",
      "root-managed-vps-upgrade",
      "update-migration",
    ]);
  });

  it("fails when the selected candidate package manifest is missing", () => {
    expect(() =>
      planFor({
        candidatePackageRoot: tempDirs.make("openclaw-docker-plan-missing-package-"),
        selectedLaneNames: ["update-run-package-self-upgrade"],
      }),
    ).toThrow(/package\.json/);
  });

  it("fails when the selected candidate package manifest is malformed", () => {
    const root = tempDirs.make("openclaw-docker-plan-malformed-package-");
    writeFileSync(join(root, "package.json"), "{");

    expect(() =>
      planFor({
        candidatePackageRoot: root,
        selectedLaneNames: ["update-run-package-self-upgrade"],
      }),
    ).toThrow(SyntaxError);
  });

  it("finds a named lane through the expanded catalog", () => {
    expect(findLaneByName("plugin-binding-command-escape")?.name).toBe(
      "plugin-binding-command-escape",
    );
  });

  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"],
      ["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 });
    copyFileSync("scripts/lib/docker-e2e-scenarios.mts", nestedModule);

    const laneJson = execFileSync(
      process.execPath,
      [
        "--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("preserves expanded survivor env through the trusted harness wrapper", () => {
    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: ["published-upgrade-survivor"],
        upgradeSurvivorBaselines: "2026.7.2",
        upgradeSurvivorScenarios: "feishu-channel",
      }),
    );
    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,
      },
    });

    expect(readFileSync(output, "utf8")).toBe("openclaw@2026.7.2|feishu-channel\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=1 pnpm test:docker:package-install",
        imageKind: "bare",
        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,
    });
  });

  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: true,
      package: true,
      prepublishPluginRegistry: true,
    });
    expect(plan.credentials).toEqual(["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("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",
    ]);
    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(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:update-channel-switch",
        imageKind: "bare",
        live: false,
        name: "update-channel-switch",
        resources: ["docker", "npm"],
        stateScenario: "update-stable",
        timeoutMs: 1_800_000,
        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: trustedUpgradeSurvivorCommand(),
        imageKind: "bare",
        live: false,
        name: "upgrade-survivor",
        resources: ["docker", "npm"],
        stateScenario: "upgrade-survivor",
        timeoutMs: 1_200_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(
          "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@2026.5.7}"; 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,
      },
      {
        command:
          "OPENCLAW_QA_ALLOW_UPDATE_RUN_SELF=1 OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:update-run-package-self-upgrade",
        imageKind: "bare",
        live: false,
        name: "update-run-package-self-upgrade",
        resources: ["docker", "npm", "service"],
        stateScenario: "upgrade-survivor",
        timeoutMs: 2_700_000,
        weight: 3,
      },
    ]);
    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("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",
      "npm-onboard-channel-agent",
      "npm-onboard-discord-channel-agent",
      "npm-onboard-slack-channel-agent",
      "doctor-switch",
      "update-channel-switch",
      "skill-install",
      "upgrade-survivor",
      "published-upgrade-survivor",
      "root-managed-vps-upgrade",
      "update-restart-auth",
      "update-run-package-self-upgrade",
    ]);
    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.4.29 2026.4.23 openclaw@2026.4.23 openclaw@2026.3.13-1",
    });

    expect(plan.lanes.map(summarizeLane)).toEqual([
      publishedUpgradeSurvivorLane("published-upgrade-survivor-2026.4.29", "openclaw@2026.4.29"),
      publishedUpgradeSurvivorLane("published-upgrade-survivor-2026.4.23", "openclaw@2026.4.23"),
      publishedUpgradeSurvivorLane(
        "published-upgrade-survivor-2026.3.13-1",
        "openclaw@2026.3.13-1",
      ),
    ]);
  });

  it("expands the published upgrade survivor lane across scenarios", () => {
    const plan = planFor({
      selectedLaneNames: ["published-upgrade-survivor"],
      upgradeSurvivorBaselines: "2026.4.29 2026.4.23",
      upgradeSurvivorScenarios: "base feishu-channel tilde-log-path",
    });

    expect(plan.lanes.map(summarizeLane)).toEqual([
      publishedUpgradeSurvivorLane(
        "published-upgrade-survivor-2026.4.29",
        "openclaw@2026.4.29",
        "base",
      ),
      publishedUpgradeSurvivorLane(
        "published-upgrade-survivor-2026.4.29-feishu-channel",
        "openclaw@2026.4.29",
        "feishu-channel",
      ),
      publishedUpgradeSurvivorLane(
        "published-upgrade-survivor-2026.4.29-tilde-log-path",
        "openclaw@2026.4.29",
        "tilde-log-path",
      ),
      publishedUpgradeSurvivorLane(
        "published-upgrade-survivor-2026.4.23",
        "openclaw@2026.4.23",
        "base",
      ),
      publishedUpgradeSurvivorLane(
        "published-upgrade-survivor-2026.4.23-feishu-channel",
        "openclaw@2026.4.23",
        "feishu-channel",
      ),
      publishedUpgradeSurvivorLane(
        "published-upgrade-survivor-2026.4.23-tilde-log-path",
        "openclaw@2026.4.23",
        "tilde-log-path",
      ),
    ]);
  });

  it("expands reported upgrade issue scenarios", () => {
    const plan = planFor({
      selectedLaneNames: ["published-upgrade-survivor"],
      upgradeSurvivorBaselines: "2026.4.29",
      upgradeSurvivorScenarios: "reported-issues",
    });

    expect(plan.lanes.map((lane) => lane.name)).toEqual([
      "published-upgrade-survivor-2026.4.29",
      "published-upgrade-survivor-2026.4.29-acpx-openclaw-tools-bridge",
      "published-upgrade-survivor-2026.4.29-feishu-channel",
      "published-upgrade-survivor-2026.4.29-bootstrap-persona",
      "published-upgrade-survivor-2026.4.29-channel-post-core-restore",
      "published-upgrade-survivor-2026.4.29-plugin-deps-cleanup",
      "published-upgrade-survivor-2026.4.29-configured-plugin-installs",
      "published-upgrade-survivor-2026.4.29-stale-source-plugin-shadow",
      "published-upgrade-survivor-2026.4.29-tilde-log-path",
      "published-upgrade-survivor-2026.4.29-meeting-transcripts-sqlite",
      "published-upgrade-survivor-2026.4.29-versioned-runtime-deps",
      "published-upgrade-survivor-2026.4.29-cron-scheduled-authority",
    ]);
  });

  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 plan = planFor({
      selectedLaneNames: ["published-upgrade-survivor"],
      upgradeSurvivorBaselines: "2026.6.11",
      upgradeSurvivorScenarios: "reported-issues",
      upgradeSurvivorTargetRoot: targetRoot,
    });

    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("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(12);
    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("omits unsupported scenario-only survivor lanes without explicit baselines", () => {
    const targetRoot = tempDirs.make("openclaw-frozen-scenario-only-harness-");
    writeFrozenScenarioContract(targetRoot, ["unrelated"]);

    const plan = planFor({
      selectedLaneNames: ["published-upgrade-survivor"],
      upgradeSurvivorScenarios: "reported-issues",
      upgradeSurvivorTargetRoot: targetRoot,
    });

    expect(plan.lanes).toEqual([]);
  });

  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.4.21",
      upgradeSurvivorScenarios: "acpx-openclaw-tools-bridge",
      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("skips plugin dependency cleanup for baselines without packaged plugin dirs", () => {
    const plan = planFor({
      selectedLaneNames: ["published-upgrade-survivor"],
      upgradeSurvivorBaselines: "2026.4.29 2026.4.22 2026.4.21 2026.3.13",
      upgradeSurvivorScenarios: "reported-issues",
    });

    expect(plan.lanes.map((lane) => lane.name)).toEqual([
      "published-upgrade-survivor-2026.4.29",
      "published-upgrade-survivor-2026.4.29-acpx-openclaw-tools-bridge",
      "published-upgrade-survivor-2026.4.29-feishu-channel",
      "published-upgrade-survivor-2026.4.29-bootstrap-persona",
      "published-upgrade-survivor-2026.4.29-channel-post-core-restore",
      "published-upgrade-survivor-2026.4.29-plugin-deps-cleanup",
      "published-upgrade-survivor-2026.4.29-configured-plugin-installs",
      "published-upgrade-survivor-2026.4.29-stale-source-plugin-shadow",
      "published-upgrade-survivor-2026.4.29-tilde-log-path",
      "published-upgrade-survivor-2026.4.29-meeting-transcripts-sqlite",
      "published-upgrade-survivor-2026.4.29-versioned-runtime-deps",
      "published-upgrade-survivor-2026.4.29-cron-scheduled-authority",
      "published-upgrade-survivor-2026.4.22",
      "published-upgrade-survivor-2026.4.22-acpx-openclaw-tools-bridge",
      "published-upgrade-survivor-2026.4.22-feishu-channel",
      "published-upgrade-survivor-2026.4.22-bootstrap-persona",
      "published-upgrade-survivor-2026.4.22-channel-post-core-restore",
      "published-upgrade-survivor-2026.4.22-configured-plugin-installs",
      "published-upgrade-survivor-2026.4.22-stale-source-plugin-shadow",
      "published-upgrade-survivor-2026.4.22-tilde-log-path",
      "published-upgrade-survivor-2026.4.22-meeting-transcripts-sqlite",
      "published-upgrade-survivor-2026.4.22-versioned-runtime-deps",
      "published-upgrade-survivor-2026.4.22-cron-scheduled-authority",
      "published-upgrade-survivor-2026.4.21",
      "published-upgrade-survivor-2026.4.21-feishu-channel",
      "published-upgrade-survivor-2026.4.21-bootstrap-persona",
      "published-upgrade-survivor-2026.4.21-channel-post-core-restore",
      "published-upgrade-survivor-2026.4.21-configured-plugin-installs",
      "published-upgrade-survivor-2026.4.21-stale-source-plugin-shadow",
      "published-upgrade-survivor-2026.4.21-tilde-log-path",
      "published-upgrade-survivor-2026.4.21-meeting-transcripts-sqlite",
      "published-upgrade-survivor-2026.4.21-versioned-runtime-deps",
      "published-upgrade-survivor-2026.4.21-cron-scheduled-authority",
      "published-upgrade-survivor-2026.3.13",
      "published-upgrade-survivor-2026.3.13-feishu-channel",
      "published-upgrade-survivor-2026.3.13-bootstrap-persona",
      "published-upgrade-survivor-2026.3.13-channel-post-core-restore",
      "published-upgrade-survivor-2026.3.13-configured-plugin-installs",
      "published-upgrade-survivor-2026.3.13-stale-source-plugin-shadow",
      "published-upgrade-survivor-2026.3.13-tilde-log-path",
      "published-upgrade-survivor-2026.3.13-meeting-transcripts-sqlite",
      "published-upgrade-survivor-2026.3.13-versioned-runtime-deps",
      "published-upgrade-survivor-2026.3.13-cron-scheduled-authority",
    ]);
  });

  it("expands update migration across baselines and cleanup scenarios", () => {
    const plan = planFor({
      selectedLaneNames: ["update-migration"],
      upgradeSurvivorBaselines: "2026.4.29 2026.4.23",
      upgradeSurvivorScenarios: "plugin-deps-cleanup",
    });

    expect(plan.lanes.map(summarizeLane)).toEqual([
      updateMigrationLane("update-migration-2026.4.29-plugin-deps-cleanup", "openclaw@2026.4.29"),
      updateMigrationLane("update-migration-2026.4.23-plugin-deps-cleanup", "openclaw@2026.4.23"),
    ]);
  });

  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("derives live Docker credentials from lane resources", () => {
    const cases = [
      { credentials: ["anthropic", "gemini"], name: "live-models" },
      { credentials: ["anthropic", "gemini"], name: "live-gateway" },
      { 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("marks the aggregate Gemini CLI backend lane advisory for auth drift", () => {
    const plan = planFor({ selectedLaneNames: ["live-cli-backend-gemini"] });
    const lane = requireFirstLane(plan);

    expect(lane.command).toContain("OPENCLAW_LIVE_CLI_BACKEND_ADVISORY=1");
    expect(lane.command).toContain("OPENCLAW_LIVE_CLI_BACKEND_ALLOW_PROVIDER_SKIP=1");
    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: true,
      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);
  });

  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(true);
    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: true,
      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("surfaces Docker lane test-state scenarios in plan JSON", () => {
    const plan = planFor({
      selectedLaneNames: [
        "onboard",
        "agents-delete-shared-workspace",
        "browser-cdp-snapshot",
        "doctor-switch",
        "openai-image-auth",
        "openai-web-search-minimal",
        "mcp-channels",
        "mcp-code-mode-gateway",
        "cron-mcp-cleanup",
        "agent-bundle-mcp-tools",
        "system-agent-first-run",
        "system-agent-rescue",
        "config-reload",
        "plugin-update",
        "plugins",
        "kitchen-sink-plugin",
        "kitchen-sink-rpc",
        "bundled-plugin-install-uninstall-0",
        "multi-node-update",
        "update-channel-switch",
        "skill-install",
        "upgrade-survivor",
      ],
    });

    expect(
      plan.lanes.map((lane) => ({ name: lane.name, stateScenario: lane.stateScenario })),
    ).toEqual([
      { name: "onboard", stateScenario: "empty" },
      { name: "agents-delete-shared-workspace", stateScenario: "empty" },
      { name: "browser-cdp-snapshot", stateScenario: "empty" },
      { name: "doctor-switch", stateScenario: "empty" },
      { name: "openai-image-auth", stateScenario: "empty" },
      { name: "openai-web-search-minimal", stateScenario: "empty" },
      { name: "mcp-channels", stateScenario: "empty" },
      { name: "mcp-code-mode-gateway", stateScenario: "empty" },
      { name: "cron-mcp-cleanup", stateScenario: "empty" },
      { name: "agent-bundle-mcp-tools", stateScenario: "empty" },
      { name: "system-agent-first-run", stateScenario: "empty" },
      { name: "system-agent-rescue", stateScenario: "empty" },
      { name: "config-reload", stateScenario: "empty" },
      { name: "plugin-update", stateScenario: "empty" },
      { name: "plugins", stateScenario: "empty" },
      { name: "kitchen-sink-plugin", stateScenario: "empty" },
      { name: "kitchen-sink-rpc", stateScenario: "empty" },
      { name: "bundled-plugin-install-uninstall-0", stateScenario: "empty" },
      { name: "multi-node-update", stateScenario: "empty" },
      { name: "update-channel-switch", stateScenario: "update-stable" },
      { name: "skill-install", stateScenario: "empty" },
      { name: "upgrade-survivor", stateScenario: "upgrade-survivor" },
    ]);
  });

  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",
    ]);
    const legacyFeishuPlan = planFor({
      selectedLaneNames: ["published-upgrade-survivor"],
      upgradeSurvivorBaselines: "2026.3.13",
      upgradeSurvivorScenarios: "feishu-channel",
    });
    expect(legacyFeishuPlan.requiredPrepublishPluginPackages).toEqual([
      "@openclaw/codex",
      "@openclaw/discord",
      "@openclaw/whatsapp",
    ]);
    const selfUpgradeLane = findLaneByName("update-run-package-self-upgrade");
    expect(selfUpgradeLane).toBeDefined();
    expect(requiredPrepublishPluginPackagesForLanes([selfUpgradeLane!])).toEqual([]);
  });

  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,
    );
  });
});
