// Changed Lanes tests cover changed lanes script behavior.
import { execFileSync, spawnSync } from "node:child_process";
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
import { createRequire } from "node:module";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { expectDefined } from "@openclaw/normalization-core";
import { afterEach, describe, expect, it } from "vitest";
import {
  createEmptyChangedLanes,
  detectChangedLanes,
  hasDeadcodeScannedSource,
  isChangedLaneTestPath,
  isLiveDockerPackageScriptOnlyChange,
  isPackageScriptOnlyChange,
  listChangedPathsFromGit,
  listStagedChangedPaths,
} from "../../scripts/changed-lanes.mts";
import {
  buildChangedCheckCrabboxArgs,
  changedCheckLocalDependenciesReady,
  changedCheckRequiresRemote,
  cleanupCorepackPnpmShimDir,
  createChangedCheckChildEnv,
  createChangedCheckPlan,
  createPnpmManagedCommand,
  createTargetedCoreLintCommand,
  createTargetedExtensionLintCommand,
  createTargetedScriptLintCommand,
  shouldDelegateChangedCheckToCrabbox,
  shouldRunAppcastOwnerTest,
  shouldRunCanvasA2uiNativeResourceCheck,
  shouldRunControlUiI18nVerify,
  shouldRunPromptSnapshotCheck,
  shouldRunPromptSnapshotOwnerTest,
  shouldRunDoctorContractOwnerTests,
  shouldRunRuntimeSidecarBaselineCheck,
  shouldRunNpmLockGuard,
  shouldRunPluginSdkApiBaselineCheck,
  shouldRunDeprecationHygieneChecks,
  shouldRunPluginSdkSurfaceChecks,
  shouldRunSqliteSessionSchemaBaselineCheck,
  shouldRunTestTempCreationReport,
  shouldRunWrapperShadowingCheck,
  createNpmLockGuardCommand,
  delegationFailedBeforeRunning,
} from "../../scripts/check-changed.mts";
import { resolveOxfmtInvocation } from "../../scripts/format-docs.mts";
import { isDirectRunPath } from "../../scripts/lib/direct-run.mjs";
import { cleanupTempDirs, makeTempRepoRoot } from "../helpers/temp-repo.js";

const tempDirs: string[] = [];
const repoRoot = process.cwd();
const tsxImport = pathToFileURL(createRequire(import.meta.url).resolve("tsx")).href;
type ExecFileSyncFailure = Error & { status?: number | null; stderr?: Buffer };
const nestedGitEnvKeys = [
  "GIT_ALTERNATE_OBJECT_DIRECTORIES",
  "GIT_DIR",
  "GIT_INDEX_FILE",
  "GIT_OBJECT_DIRECTORY",
  "GIT_QUARANTINE_PATH",
  "GIT_WORK_TREE",
] as const;

function createNestedGitEnv(): NodeJS.ProcessEnv {
  const env: NodeJS.ProcessEnv = {
    ...process.env,
    GIT_CONFIG_NOSYSTEM: "1",
    GIT_TERMINAL_PROMPT: "0",
  };
  for (const key of nestedGitEnvKeys) {
    delete env[key];
  }
  return env;
}

const git = (cwd: string, args: string[]) =>
  execFileSync("git", args, {
    cwd,
    encoding: "utf8",
    env: createNestedGitEnv(),
  }).trim();

function commitAll(cwd: string, message: string): void {
  git(cwd, ["add", "."]);
  git(cwd, [
    "-c",
    "user.email=test@example.com",
    "-c",
    "user.name=Test User",
    "commit",
    "-q",
    "-m",
    message,
  ]);
}

function expectLanes(
  lanes: ReturnType<typeof createEmptyChangedLanes>,
  expected: Partial<ReturnType<typeof createEmptyChangedLanes>>,
) {
  expect(lanes).toEqual({ ...createEmptyChangedLanes(), ...expected });
}

function parseChangedLaneOutput(output: string): {
  paths: string[];
  lanes: ReturnType<typeof createEmptyChangedLanes>;
} {
  return JSON.parse(output) as {
    paths: string[];
    lanes: ReturnType<typeof createEmptyChangedLanes>;
  };
}

function runChangedLanesCli(cwd: string, args: string[]) {
  return parseChangedLaneOutput(
    execFileSync(process.execPath, [path.join(repoRoot, "scripts", "changed-lanes.mjs"), ...args], {
      cwd,
      encoding: "utf8",
      env: createNestedGitEnv(),
    }),
  );
}

function runRepoScript(script: string, args: string[], env = createNestedGitEnv()) {
  const nodeArgs = script.endsWith(".mts")
    ? ["--import", "tsx", script, ...args]
    : [script, ...args];
  return spawnSync(process.execPath, nodeArgs, {
    cwd: repoRoot,
    encoding: "utf8",
    env,
  });
}

function writeRepoFile(repoDir: string, filePath: string, contents: string): void {
  const absolutePath = path.join(repoDir, filePath);
  mkdirSync(path.dirname(absolutePath), { recursive: true });
  writeFileSync(absolutePath, contents, "utf8");
}

const prettyJson = (value: unknown) => `${JSON.stringify(value, null, 2)}\n`;

// Executes the exact "format changed files" plan command with the repo-pinned oxfmt,
// reconstructing `pnpm format:check <plan args>`. Guards the runtime verdict, not just
// plan construction: a misformatted added file must fail, deleted paths must not.
function runChangedFormatLaneWithRepoOxfmt(cwd: string, changedPaths: string[]) {
  const plan = createChangedCheckPlan(detectChangedLanes(changedPaths));
  const formatCommand = plan.commands.find((command) => command.name === "format changed files");
  expect(formatCommand?.args[0]).toBe("format:check");
  const packageJson = JSON.parse(readFileSync(path.join(repoRoot, "package.json"), "utf8")) as {
    scripts: Record<string, string>;
  };
  const formatScript = expectDefined(
    packageJson.scripts["format:check"],
    "format:check package script",
  );
  const [rawScriptBin, ...scriptArgs] = formatScript.split(" ");
  const scriptBin = expectDefined(rawScriptBin, "format:check script binary");
  expect(scriptBin).toBe("oxfmt");
  const invocation = resolveOxfmtInvocation(
    [...scriptArgs, ...(formatCommand?.args.slice(1) ?? [])],
    { repoRoot },
  );
  return spawnSync(invocation.command, invocation.args, {
    cwd,
    encoding: "utf8",
    shell: invocation.shell,
    windowsVerbatimArguments: invocation.windowsVerbatimArguments,
  });
}

function createSyntheticMergeRepo(prefix: string): { dir: string; staleBase: string } {
  const dir = makeTempRepoRoot(tempDirs, prefix);
  git(dir, ["init", "-q", "--initial-branch=main"]);
  writeRepoFile(dir, "README.md", "base\n");
  commitAll(dir, "base");
  const staleBase = git(dir, ["rev-parse", "HEAD"]);

  git(dir, ["switch", "-q", "-c", "feature"]);
  writeRepoFile(dir, "src/pr.ts", "export const pr = true;\n");
  commitAll(dir, "feature");

  git(dir, ["switch", "-q", "main"]);
  writeRepoFile(dir, "src/main-only.ts", "export const mainOnly = true;\n");
  commitAll(dir, "main only");
  git(dir, [
    "-c",
    "user.email=test@example.com",
    "-c",
    "user.name=Test User",
    "merge",
    "--no-ff",
    "feature",
    "-m",
    "synthetic merge",
  ]);

  return { dir, staleBase };
}

function classifyPackageJsonChange(
  prefix: string,
  before: Record<string, unknown>,
  after: Record<string, unknown>,
) {
  const dir = makeTempRepoRoot(tempDirs, prefix);
  git(dir, ["init", "-q", "--initial-branch=main"]);
  writeRepoFile(dir, "package.json", prettyJson(before));
  commitAll(dir, "initial");
  writeRepoFile(dir, "package.json", prettyJson(after));

  const output = execFileSync(
    process.execPath,
    [path.join(repoRoot, "scripts", "changed-lanes.mjs"), "--json", "--base", "HEAD"],
    { cwd: dir, encoding: "utf8", env: createNestedGitEnv() },
  );
  return parseChangedLaneOutput(output);
}

afterEach(() => {
  cleanupCorepackPnpmShimDir();
  cleanupTempDirs(tempDirs);
});

describe("scripts/changed-lanes", () => {
  it("detects direct script execution from Windows argv paths", () => {
    expect(
      isDirectRunPath(
        "C:\\repo\\scripts\\check-changed.mjs",
        "c:\\repo\\scripts\\check-changed.mjs",
        "win32",
      ),
    ).toBe(true);
    expect(
      isDirectRunPath(
        "C:\\repo\\scripts\\changed-lanes.mjs",
        "C:\\repo\\scripts\\check-changed.mjs",
        "win32",
      ),
    ).toBe(false);
  });

  it.each([
    {
      name: "prints changed lane help without treating --help as a changed path",
      script: "scripts/changed-lanes.mjs",
      expected: {
        contains: "Usage: node scripts/changed-lanes.mjs",
        excludes: "--help: unknown surface",
      },
    },
    {
      name: "prints changed check help without running the changed gate",
      script: "scripts/check-changed.mjs",
      expected: { contains: "Usage: node scripts/check-changed.mjs", excludes: "[check:changed]" },
    },
  ])("$name", ({ script, expected }) => {
    const result = runRepoScript(script, ["--help"], {
      ...createNestedGitEnv(),
      OPENCLAW_TESTBOX: "1",
    });

    expect(result.status).toBe(0);
    expect(result.stderr).toBe("");
    expect(result.stdout).toContain(expected.contains);
    expect(result.stdout).not.toContain(expected.excludes);
  });

  it("exits cleanly for no changes without local dependencies", () => {
    const result = runRepoScript("scripts/check-changed.mjs", ["--no-changes"], {
      ...createNestedGitEnv(),
      PATH: "/nonexistent",
    });

    expect(result.status).toBe(0);
    expect(result.stdout).toBe("");
    expect(result.stderr.trim()).toBe("[check:changed] no changed paths; nothing to run");
  });

  it("delegates when the local checkout cannot resolve the default base ref", () => {
    const dir = makeTempRepoRoot(tempDirs, "openclaw-check-changed-missing-base-");
    git(dir, ["init", "-q", "--initial-branch=main"]);
    writeFileSync(path.join(dir, "README.md"), "initial\n", "utf8");
    commitAll(dir, "initial");
    const binDir = path.join(dir, "bin");
    mkdirSync(binDir, { recursive: true });
    writeFileSync(path.join(binDir, "node"), "#!/bin/sh\nexit 0\n", { mode: 0o755 });

    const result = spawnSync(process.execPath, [path.join(repoRoot, "scripts/check-changed.mjs")], {
      cwd: dir,
      encoding: "utf8",
      env: {
        ...createNestedGitEnv(),
        CI: "",
        GITHUB_ACTIONS: "",
        OPENCLAW_CHECK_CHANGED_REMOTE_CHILD: "",
        OPENCLAW_TESTBOX: "1",
        PATH: `${binDir}:${process.env.PATH ?? ""}`,
      },
    });

    expect(result.status).toBe(0);
    expect(result.stderr).toContain("delegating through Crabbox workload routing");
    expect(result.stderr).not.toContain("ambiguous argument");
  });

  it("delegates path-scoped release metadata when local diff refs are unavailable", () => {
    const dir = makeTempRepoRoot(tempDirs, "openclaw-check-changed-metadata-missing-base-");
    git(dir, ["init", "-q", "--initial-branch=main"]);
    writeFileSync(path.join(dir, "README.md"), "initial\n", "utf8");
    commitAll(dir, "initial");
    writeRepoFile(dir, "node_modules/.modules.yaml", "layoutVersion: 5\n");
    writeRepoFile(dir, "node_modules/.bin/oxfmt", "#!/bin/sh\n");
    writeRepoFile(dir, "node_modules/typescript/package.json", '{"name":"typescript"}\n');
    const binDir = path.join(dir, "bin");
    mkdirSync(binDir, { recursive: true });
    writeFileSync(path.join(binDir, "node"), "#!/bin/sh\nexit 0\n", { mode: 0o755 });

    const result = spawnSync(
      process.execPath,
      [path.join(repoRoot, "scripts/check-changed.mjs"), "--", "CHANGELOG.md"],
      {
        cwd: dir,
        encoding: "utf8",
        env: {
          ...createNestedGitEnv(),
          CI: "",
          GITHUB_ACTIONS: "",
          OPENCLAW_CHECK_CHANGED_REMOTE_CHILD: "",
          OPENCLAW_TESTBOX: "",
          PATH: `${binDir}:${process.env.PATH ?? ""}`,
        },
      },
    );

    expect(result.status).toBe(0);
    expect(result.stderr).toContain("delegating through Crabbox workload routing");
  });

  it.each([
    {
      name: "rejects unknown changed lane options before treating them as paths",
      script: "scripts/changed-lanes.mjs",
      option: "--jsno",
      expected: { stderr: "Unknown option: --jsno", excludes: [] },
    },
    {
      name: "rejects unknown changed check options before treating them as paths",
      script: "scripts/check-changed.mjs",
      option: "--dr-run",
      expected: {
        stderr: "Unknown option: --dr-run\n[check:changed] FAILED (exit 1)",
        excludes: [],
      },
    },
  ])("$name", ({ script, option, expected }) => {
    const result = runRepoScript(script, [option], {
      ...createNestedGitEnv(),
      OPENCLAW_TESTBOX: "1",
    });

    expect(result.status).toBe(1);
    expect(result.stdout).toBe("");
    expect(result.stderr.trim()).toBe(expected.stderr);
    expect(result.stderr).not.toContain("\n    at ");
    for (const excluded of expected.excludes) {
      expect(result.stderr).not.toContain(excluded);
    }
  });

  it("still accepts dash-prefixed explicit changed paths after the separator", () => {
    const result = runRepoScript("scripts/changed-lanes.mjs", ["--json", "--", "--github-output"]);

    expect(result.status).toBe(0);
    expect(result.stderr).toBe("");
    expect(parseChangedLaneOutput(result.stdout).paths).toEqual(["--github-output"]);
  });

  it("keeps changed check option-shaped paths intact after the separator", () => {
    const args = buildChangedCheckCrabboxArgs(["--staged", "--", "--no-changes"], {
      cwd: repoRoot,
    });

    expect(args.slice(args.indexOf("check:changed") + 1)).toEqual([
      "--staged",
      "--",
      "--no-changes",
    ]);
  });

  it("prints changed check dry-run commands", () => {
    const result = runRepoScript("scripts/check-changed.mjs", [
      "--dry-run",
      "--",
      "extensions/lmstudio/src/api.ts",
    ]);

    expect(result.status).toBe(0);
    expect(result.stderr).toContain("[check:changed:dry-run] lanes=extensions, extensionTests");
    expect(result.stderr).toContain(
      "[check:changed:dry-run] would run: node scripts/run-oxlint.mjs --tsconfig config/tsconfig/oxlint.extensions.json extensions/lmstudio/src/api.ts",
    );
  });

  it("includes untracked worktree files in the default local diff", () => {
    const dir = makeTempRepoRoot(tempDirs, "openclaw-changed-lanes-");
    git(dir, ["init", "-q", "--initial-branch=main"]);
    writeFileSync(path.join(dir, "README.md"), "initial\n", "utf8");
    commitAll(dir, "initial");

    mkdirSync(path.join(dir, "scripts"), { recursive: true });
    writeFileSync(path.join(dir, "scripts", "new-check.mjs"), "export {};\n", "utf8");

    const result = runChangedLanesCli(dir, ["--json", "--base", "HEAD"]);

    expect(result.paths).toEqual(["scripts/new-check.mjs"]);
    expectLanes(result.lanes, { tooling: true });
  });

  it("falls back to a two-dot diff when a delegated checkout has no merge base", () => {
    const dir = makeTempRepoRoot(tempDirs, "openclaw-changed-lanes-no-merge-base-");
    git(dir, ["init", "-q", "--initial-branch=main"]);
    writeFileSync(path.join(dir, "README.md"), "initial\n", "utf8");
    commitAll(dir, "initial");
    git(dir, ["update-ref", "refs/remotes/origin/main", "HEAD"]);
    git(dir, ["switch", "-q", "--orphan", "feature"]);
    writeFileSync(path.join(dir, "README.md"), "initial\n", "utf8");
    mkdirSync(path.join(dir, "src"), { recursive: true });
    writeFileSync(path.join(dir, "src", "committed.ts"), "export const committed = 1;\n", "utf8");
    commitAll(dir, "feature base");
    writeFileSync(path.join(dir, "src", "feature.ts"), "export const value = 1;\n", "utf8");

    expect(
      listChangedPathsFromGit({ base: "origin/main", cwd: dir, includeWorktree: false }),
    ).toEqual(["src/committed.ts"]);
    expect(listChangedPathsFromGit({ base: "origin/main", cwd: dir })).toEqual([
      "src/committed.ts",
      "src/feature.ts",
    ]);
  });

  it("prefers raw sync worktree paths over an implausibly broad no-merge-base diff", () => {
    const dir = makeTempRepoRoot(tempDirs, "openclaw-changed-lanes-raw-sync-");
    git(dir, ["init", "-q", "--initial-branch=main"]);
    for (let index = 0; index < 250; index += 1) {
      writeFileSync(path.join(dir, `baseline-${index}.txt`), "baseline\n", "utf8");
    }
    commitAll(dir, "initial");
    git(dir, ["update-ref", "refs/remotes/origin/main", "HEAD"]);
    git(dir, ["switch", "-q", "--orphan", "feature"]);
    git(dir, [
      "-c",
      "user.email=test@example.com",
      "-c",
      "user.name=Test User",
      "commit",
      "-q",
      "--allow-empty",
      "-m",
      "raw sync base",
    ]);
    mkdirSync(path.join(dir, "src"), { recursive: true });
    writeFileSync(path.join(dir, "src", "feature.ts"), "export const value = 1;\n", "utf8");

    const previousRawSync = process.env.OPENCLAW_CHANGED_LANES_RAW_SYNC;
    delete process.env.OPENCLAW_CHANGED_LANES_RAW_SYNC;
    try {
      const normalPaths = listChangedPathsFromGit({ base: "origin/main", cwd: dir });
      expect(normalPaths.length).toBeGreaterThan(200);
      expect(normalPaths).toContain("baseline-0.txt");
      expect(normalPaths).toContain("src/feature.ts");

      process.env.OPENCLAW_CHANGED_LANES_RAW_SYNC = "1";
      expect(listChangedPathsFromGit({ base: "origin/main", cwd: dir })).toEqual([
        "src/feature.ts",
      ]);
    } finally {
      if (previousRawSync === undefined) {
        delete process.env.OPENCLAW_CHANGED_LANES_RAW_SYNC;
      } else {
        process.env.OPENCLAW_CHANGED_LANES_RAW_SYNC = previousRawSync;
      }
    }
  });

  it("includes committed and untracked added files in the changed format check", () => {
    const dir = makeTempRepoRoot(tempDirs, "openclaw-changed-lanes-added-format-");
    git(dir, ["init", "-q", "--initial-branch=main"]);
    writeRepoFile(dir, "README.md", "initial\n");
    commitAll(dir, "initial");
    git(dir, ["update-ref", "refs/remotes/origin/main", "HEAD"]);
    git(dir, ["switch", "-q", "-c", "feature"]);
    writeRepoFile(dir, "src/committed.test.ts", "export const committed={value:1};\n");
    commitAll(dir, "add test");
    writeRepoFile(dir, "src/untracked.test.ts", "export const untracked={value:1};\n");
    writeRepoFile(dir, "--help", "ignored\n");

    const paths = listChangedPathsFromGit({ base: "origin/main", cwd: dir });
    const plan = createChangedCheckPlan(detectChangedLanes(paths));

    expect(paths).toEqual(["--help", "src/committed.test.ts", "src/untracked.test.ts"]);
    expect(plan.commands.find((command) => command.name === "format changed files")).toEqual({
      name: "format changed files",
      args: [
        "format:check",
        "--no-error-on-unmatched-pattern",
        "--",
        "--help",
        "src/committed.test.ts",
        "src/untracked.test.ts",
      ],
    });
  });

  it("includes staged added, modified, and deleted files in the changed format check", () => {
    const dir = makeTempRepoRoot(tempDirs, "openclaw-changed-lanes-staged-format-");
    git(dir, ["init", "-q", "--initial-branch=main"]);
    writeRepoFile(dir, "src/modified.ts", "export const modified = { value: 1 };\n");
    writeRepoFile(dir, "src/removed.ts", "export const removed = { value: 1 };\n");
    commitAll(dir, "initial");
    writeRepoFile(dir, "src/added.test.ts", "export const added={value:1};\n");
    writeRepoFile(dir, "src/modified.ts", "export const modified={value:2};\n");
    git(dir, ["add", "src/added.test.ts", "src/modified.ts"]);
    git(dir, ["rm", "-q", "src/removed.ts"]);

    const paths = listStagedChangedPaths(dir);
    const plan = createChangedCheckPlan(detectChangedLanes(paths));

    expect(paths).toEqual(["src/added.test.ts", "src/modified.ts", "src/removed.ts"]);
    expect(plan.commands.find((command) => command.name === "format changed files")).toEqual({
      name: "format changed files",
      args: [
        "format:check",
        "--no-error-on-unmatched-pattern",
        "--",
        "src/added.test.ts",
        "src/modified.ts",
        "src/removed.ts",
      ],
    });
  });

  it("fails the changed format check on a misformatted added file and passes once formatted", () => {
    const dir = makeTempRepoRoot(tempDirs, "openclaw-changed-format-added-");
    writeRepoFile(dir, "src/added.test.ts", "export const added={value:1};\n");

    const dirty = runChangedFormatLaneWithRepoOxfmt(dir, ["src/added.test.ts"]);
    expect(dirty.status).not.toBe(0);
    expect(`${dirty.stdout}${dirty.stderr}`).toContain("added.test.ts");

    writeRepoFile(dir, "src/added.test.ts", "export const added = { value: 1 };\n");
    const formatted = runChangedFormatLaneWithRepoOxfmt(dir, ["src/added.test.ts"]);
    expect(formatted.status).toBe(0);
  });

  it("fails the changed format check on a misformatted modified file", () => {
    const dir = makeTempRepoRoot(tempDirs, "openclaw-changed-format-modified-");
    writeRepoFile(dir, "src/modified.ts", "export const modified={value:2};\n");

    const result = runChangedFormatLaneWithRepoOxfmt(dir, ["src/modified.ts"]);
    expect(result.status).not.toBe(0);
    expect(`${result.stdout}${result.stderr}`).toContain("modified.ts");
  });

  it("does not fail the changed format check for deleted paths", () => {
    const dir = makeTempRepoRoot(tempDirs, "openclaw-changed-format-deleted-");
    writeRepoFile(dir, "src/kept.ts", "export const kept = { value: 1 };\n");

    const result = runChangedFormatLaneWithRepoOxfmt(dir, ["src/deleted.ts", "src/kept.ts"]);
    expect(result.status).toBe(0);
  });

  it("uses the merge commit first parent instead of a stale PR payload base", () => {
    const { dir, staleBase } = createSyntheticMergeRepo("openclaw-changed-lanes-merge-");

    expect(listChangedPathsFromGit({ base: staleBase, cwd: dir, includeWorktree: false })).toEqual([
      "src/main-only.ts",
      "src/pr.ts",
    ]);
    expect(
      listChangedPathsFromGit({
        base: staleBase,
        cwd: dir,
        includeWorktree: false,
        mergeHeadFirstParent: true,
      }),
    ).toEqual(["src/pr.ts"]);
  });

  it("ignores local Crabbox metadata in the default local diff", () => {
    const dir = makeTempRepoRoot(tempDirs, "openclaw-changed-lanes-crabbox-");
    git(dir, ["init", "-q", "--initial-branch=main"]);
    writeFileSync(path.join(dir, ".gitignore"), ".crabbox/\n", "utf8");
    writeFileSync(path.join(dir, "README.md"), "initial\n", "utf8");
    commitAll(dir, "initial");

    mkdirSync(path.join(dir, ".crabbox"), { recursive: true });
    writeFileSync(path.join(dir, ".crabbox", "capture-files.txt"), "stdout.log\n", "utf8");
    writeFileSync(path.join(dir, ".crabbox", "capture-manifest.txt"), "stdout.log\t12\n", "utf8");

    const result = runChangedLanesCli(dir, ["--json", "--base", "HEAD"]);

    expect(result.paths).toEqual([]);
    expectLanes(result.lanes, {});
  });

  it("includes deleted worktree files in the default local diff", () => {
    const dir = makeTempRepoRoot(tempDirs, "openclaw-changed-lanes-deleted-");
    git(dir, ["init", "-q", "--initial-branch=main"]);
    mkdirSync(path.join(dir, "src", "shared"), { recursive: true });
    writeFileSync(
      path.join(dir, "src", "shared", "obsolete.ts"),
      "export const value = 1;\n",
      "utf8",
    );
    commitAll(dir, "initial");

    unlinkSync(path.join(dir, "src", "shared", "obsolete.ts"));

    const result = runChangedLanesCli(dir, ["--json", "--base", "HEAD"]);

    expect(result.paths).toEqual(["src/shared/obsolete.ts"]);
    expectLanes(result.lanes, { core: true, coreTests: true });
  });

  it("includes deleted staged files in the staged diff", () => {
    const dir = makeTempRepoRoot(tempDirs, "openclaw-changed-lanes-staged-deleted-");
    git(dir, ["init", "-q", "--initial-branch=main"]);
    mkdirSync(path.join(dir, "src", "shared"), { recursive: true });
    writeFileSync(
      path.join(dir, "src", "shared", "obsolete.ts"),
      "export const value = 1;\n",
      "utf8",
    );
    commitAll(dir, "initial");

    unlinkSync(path.join(dir, "src", "shared", "obsolete.ts"));
    git(dir, ["add", "src/shared/obsolete.ts"]);

    const result = runChangedLanesCli(dir, ["--json", "--staged"]);

    expect(result.paths).toEqual(["src/shared/obsolete.ts"]);
    expectLanes(result.lanes, { core: true, coreTests: true });
  });

  it.each([
    { name: "core source", changedPaths: ["src/agents/api.ts"], expected: true },
    { name: "extension source", changedPaths: ["extensions/copilot/src/a.ts"], expected: true },
    { name: "ui source", changedPaths: ["ui/src/pages/a.ts"], expected: true },
    { name: "package source", changedPaths: ["packages/x/src/a.mts"], expected: true },
    // Matches the `[cm]?[jt]sx?` selector the lint lanes in check-changed.mts use.
    { name: "tsx source", changedPaths: ["ui/src/pages/Page.tsx"], expected: true },
    { name: "jsx source", changedPaths: ["ui/src/pages/Page.jsx"], expected: true },
    { name: "cjs source", changedPaths: ["src/agents/legacy.cjs"], expected: true },
    // An import-only edit can orphan a barrel re-export in a file this diff never
    // touches, so selection is by path; inspecting changed lines would miss it.
    { name: "import-only edit", changedPaths: ["src/agents/tool-surface-plan.ts"], expected: true },
    { name: "docs tree", changedPaths: ["docs/example.ts"], expected: false },
    { name: "scripts tree", changedPaths: ["scripts/check-changed.mjs"], expected: false },
    // knip never reads these, so they must not pull in the scan.
    { name: "markdown under src", changedPaths: ["src/README.md"], expected: false },
    { name: "sql under src", changedPaths: ["src/state/schema.sql"], expected: false },
  ])("selects the dead-export scan for $name", ({ changedPaths, expected }) => {
    expect(hasDeadcodeScannedSource(changedPaths)).toBe(expected);
  });

  it("ignores the explicit path separator", () => {
    const result = detectChangedLanes(["--", "scripts/test-live-acp-bind-docker.sh"]);

    expect(result.paths).toEqual(["scripts/test-live-acp-bind-docker.sh"]);
    expect(result.lanes.liveDockerTooling).toBe(true);
    expect(result.lanes.all).toBe(false);
  });

  it("routes a subagent-announce-only Docker diff through the live Docker lane", () => {
    const result = detectChangedLanes(["scripts/test-live-subagent-announce-docker.sh"]);

    expectLanes(result.lanes, { liveDockerTooling: true });
  });

  it.each([
    "extensions/whatsapp/src/config-ui-hints.ts",
    "extensions/mattermost/src/config-schema-core.ts",
    "extensions/telegram/openclaw.plugin.json",
    "extensions/discord/package.json",
    "extensions/slack/security-contract-api.ts",
    "src/config/zod-schema.core.ts",
    "src/channels/plugins/config-schema.ts",
    "scripts/load-channel-config-surface.ts",
  ])("routes %s through the bundled channel config metadata lane", (changedPath) => {
    const result = detectChangedLanes([changedPath]);
    const plan = createChangedCheckPlan(result);

    expect(result.lanes.bundledChannelConfigMetadata).toBe(true);
    expect(plan.commands.map((command) => command.args[0])).toContain(
      "check:bundled-channel-config-metadata",
    );
  });

  it("keeps unrelated plugin runtime changes out of the bundled channel metadata lane", () => {
    const result = detectChangedLanes(["extensions/whatsapp/src/monitor.ts"]);
    const plan = createChangedCheckPlan(result);

    expect(result.lanes.bundledChannelConfigMetadata).toBe(false);
    expect(plan.commands.map((command) => command.args[0])).not.toContain(
      "check:bundled-channel-config-metadata",
    );
  });

  it("includes bundled channel metadata in the fail-safe all plan", () => {
    const result = detectChangedLanes(["unknown-surface.foo"]);
    const plan = createChangedCheckPlan(result);

    expect(result.lanes.all).toBe(true);
    expect(plan.commands.map((command) => command.args[0])).toContain(
      "check:bundled-channel-config-metadata",
    );
  });

  it("exposes the shared changed-lane test path classifier", () => {
    expect(isChangedLaneTestPath("src/shared/string-normalization.test.ts")).toBe(true);
    expect(isChangedLaneTestPath("packages/foo/__tests__/helper.ts")).toBe(true);
    expect(isChangedLaneTestPath("src/example.ts")).toBe(false);
    expect(isChangedLaneTestPath("src/latest.ts")).toBe(false);
  });

  it("routes core production changes to core prod and core test lanes", () => {
    const result = detectChangedLanes(["packages/normalization-core/src/string-normalization.ts"]);
    const plan = createChangedCheckPlan(result, { env: { PATH: "/usr/bin" } });

    expectLanes(result.lanes, {
      core: true,
      coreTests: true,
    });
    expect(plan.commands.map((command) => command.args[0])).toContain(
      "check:database-first-legacy-stores",
    );
    expect(plan.commands.map((command) => command.args[0])).toContain("tsgo:core");
    expect(plan.commands.map((command) => command.args[0])).toContain("tsgo:core:test");
    expect(plan.commands.find((command) => command.args[0] === "tsgo:core")?.env).toEqual({
      PATH: "/usr/bin",
      OPENCLAW_OXLINT_SKIP_LOCK: "1",
      OPENCLAW_TEST_HEAVY_CHECK_LOCK_HELD: "1",
      OPENCLAW_TSGO_HEAVY_CHECK_LOCK_HELD: "1",
      OPENCLAW_TSGO_SPARSE_SKIP: "1",
    });
    expect(plan.commands.find((command) => command.name === "lint core changed file")).toEqual({
      name: "lint core changed file",
      bin: "node",
      args: [
        "scripts/run-oxlint.mjs",
        "--tsconfig",
        "config/tsconfig/oxlint.core.json",
        "packages/normalization-core/src/string-normalization.ts",
      ],
      env: {
        PATH: "/usr/bin",
        OPENCLAW_OXLINT_SKIP_LOCK: "1",
        OPENCLAW_TEST_HEAVY_CHECK_LOCK_HELD: "1",
        OPENCLAW_TSGO_HEAVY_CHECK_LOCK_HELD: "1",
      },
    });
  });

  it("targets mixed core, extension, and script lint without full-owner fan-out", () => {
    const result = detectChangedLanes([
      "src/gateway/node-registry.ts",
      "extensions/lmstudio/src/models.fetch.ts",
      "scripts/check-changed.mjs",
    ]);
    const plan = createChangedCheckPlan(result, { env: { PATH: "/usr/bin" } });

    expect(plan.commands).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          name: "lint core changed file",
          args: [
            "scripts/run-oxlint.mjs",
            "--tsconfig",
            "config/tsconfig/oxlint.core.json",
            "src/gateway/node-registry.ts",
          ],
        }),
        expect.objectContaining({
          name: "lint extension changed file",
          args: [
            "scripts/run-oxlint.mjs",
            "--tsconfig",
            "config/tsconfig/oxlint.extensions.json",
            "extensions/lmstudio/src/models.fetch.ts",
          ],
        }),
        expect.objectContaining({
          name: "lint script changed file",
          args: [
            "scripts/run-oxlint.mjs",
            "--tsconfig",
            "config/tsconfig/oxlint.scripts.json",
            "scripts/check-changed.mjs",
          ],
        }),
      ]),
    );
    const commandNames = plan.commands.map((command) => command.args[0]);
    for (const fullLane of ["lint:core", "lint:extensions", "lint:scripts"]) {
      expect(commandNames).not.toContain(fullLane);
    }
  });

  it.each([
    {
      owner: "core",
      paths: [
        "src/gateway/node-registry.ts",
        "src/gateway/node-registry.invoke-stream.ts",
        "src/gateway/server-methods/nodes.invoke.ts",
        "src/gateway/server-methods/nodes.invoke-deadline.ts",
        "src/node-host/runtime.ts",
        "src/node-host/runner.ts",
        "src/plugins/provider-self-hosted-setup.ts",
        "packages/gateway-client/src/timeouts.ts",
        "packages/normalization-core/src/number-coercion.ts",
      ],
      pluralName: "lint core changed files",
      singularName: "lint core changed file",
      fullLane: "lint:core",
    },
    {
      owner: "extension",
      paths: [
        "extensions/lmstudio/src/embedding-provider.ts",
        "extensions/lmstudio/src/stream.ts",
        "extensions/lmstudio/src/api.ts",
        "extensions/lmstudio/src/models.fetch.ts",
        "extensions/lmstudio/src/setup.ts",
        "extensions/lmstudio/src/defaults.ts",
        "extensions/lmstudio/src/provider-auth.ts",
        "extensions/lmstudio/src/runtime.ts",
        "extensions/lmstudio/src/models.ts",
      ],
      pluralName: "lint extension changed files",
      singularName: "lint extension changed file",
      fullLane: "lint:extensions",
    },
  ])("batches broad $owner changes without falling back to full lint", (testCase) => {
    const result = detectChangedLanes(testCase.paths);
    const plan = createChangedCheckPlan(result, { env: { PATH: "/usr/bin" } });
    const commands = plan.commands.filter(
      (command) => command.name === testCase.pluralName || command.name === testCase.singularName,
    );

    expect(commands).toHaveLength(2);
    expect(commands.map((command) => command.args.slice(3).length)).toEqual([8, 1]);
    expect(commands.flatMap((command) => command.args.slice(3)).toSorted()).toEqual(
      testCase.paths.toSorted(),
    );
    expect(plan.commands.map((command) => command.args[0])).not.toContain(testCase.fullLane);
  });

  it.each([
    {
      name: "routes UI production changes to UI prod and core test lanes",
      path: "ui/src/app.ts",
      expected: {
        includes: ["tsgo:ui", "tsgo:core:test", "lint:ui:i18n"],
        excludes: ["tsgo:core"],
      },
    },
    {
      name: "routes the UI production config to UI prod and core test lanes",
      path: "tsconfig.ui.json",
      expected: { includes: ["tsgo:ui", "tsgo:core:test"], excludes: [] },
    },
  ])("$name", ({ path: changedPath, expected }) => {
    const result = detectChangedLanes([changedPath]);
    const commands = createChangedCheckPlan(result, {
      env: { PATH: "/usr/bin" },
    }).commands.map((command) => command.args[0]);

    expectLanes(result.lanes, { coreTests: true, ui: true });
    for (const command of expected.includes) {
      expect(commands).toContain(command);
    }
    for (const command of expected.excludes) {
      expect(commands).not.toContain(command);
    }
  });

  it.each(["scripts/control-ui-i18n.ts", "scripts/lib/example.ts", "tsconfig.scripts.json"])(
    "routes %s to the scripts typecheck lane",
    (changedPath) => {
      const result = detectChangedLanes([changedPath]);
      const plan = createChangedCheckPlan(result);

      expect(result.lanes.scripts).toBe(true);
      expect(plan.commands.map((command) => command.args[0])).toContain("tsgo:scripts");
    },
  );

  it("keeps the scripts lane when another change selects the full lane", () => {
    const result = detectChangedLanes(["package.json", "scripts/example.mts"]);

    expect(result.lanes.all).toBe(true);
  });

  it("routes Control UI i18n tooling changes through keyless catalog verification", () => {
    const result = detectChangedLanes(["scripts/control-ui-i18n-verify.ts"]);
    const plan = createChangedCheckPlan(result);

    expect(shouldRunControlUiI18nVerify(result.paths)).toBe(true);
    expect(plan.commands.map((command) => command.args[0])).toContain("lint:ui:i18n");
    expect(shouldRunControlUiI18nVerify(["ui/config/control-ui-locales.ts"])).toBe(true);
    expect(shouldRunControlUiI18nVerify(["scripts/lib/example.ts"])).toBe(false);
  });

  it.each([
    ["test/vitest/foo.config.ts", true, true],
    ["test/vitest/vitest-runtime-helper.d.mts", true, true],
    ["test/fixtures/foo.ts", false, true],
    ["test/foo.mjs", false, true],
    ["test/tsconfig/tsconfig.test.root.json", true, true],
  ])(
    "routes %s to testRoot=%s and tooling=%s",
    (changedPath, expectedTestRoot, expectedTooling) => {
      const result = detectChangedLanes([changedPath]);
      const plan = createChangedCheckPlan(result);

      expect(result.lanes.testRoot).toBe(expectedTestRoot);
      expect(result.lanes.tooling).toBe(expectedTooling);
      expect(plan.commands.map((command) => command.args[0]).includes("tsgo:test:root")).toBe(
        expectedTestRoot,
      );
    },
  );

  it("falls back to full core lint for broad core diffs", () => {
    const targets = Array.from({ length: 9 }, (_, index) => `src/shared/file-${index}.ts`);
    const command = createTargetedCoreLintCommand(targets, { PATH: "/usr/bin" });

    expect(command).toBeNull();
  });

  it("falls back to full extension lint for broad extension diffs", () => {
    const targets = Array.from(
      { length: 9 },
      (_, index) => `extensions/discord/src/file-${index}.ts`,
    );
    const command = createTargetedExtensionLintCommand(targets, { PATH: "/usr/bin" });

    expect(command).toBeNull();
  });

  it("falls back to full core lint when a changed core target was deleted", () => {
    expect(
      createTargetedCoreLintCommand(
        ["src/shared/deleted.ts"],
        { PATH: "/usr/bin" },
        {
          fileExists: () => false,
        },
      ),
    ).toBeNull();
  });

  it("falls back to full core lint for mixed core lint configuration diffs", () => {
    expect(
      createTargetedCoreLintCommand(
        [
          "config/tsconfig/oxlint.core.json",
          "packages/normalization-core/src/string-normalization.ts",
        ],
        { PATH: "/usr/bin" },
        { fileExists: () => true },
      ),
    ).toBeNull();
  });

  it.each([
    {
      name: "targets small core lint diffs",
      create: createTargetedCoreLintCommand,
      targets: [
        ".github/workflows/ci.yml",
        "scripts/check-changed.mjs",
        "src/agents/auth-profiles/usage.ts",
        "test/scripts/changed-lanes.test.ts",
      ],
      expected: {
        name: "lint core changed file",
        tsconfig: "config/tsconfig/oxlint.core.json",
        path: "src/agents/auth-profiles/usage.ts",
      },
    },
    {
      name: "targets small extension lint diffs",
      create: createTargetedExtensionLintCommand,
      targets: ["extensions/lmstudio/src/api.ts", "docs/help/testing.md"],
      expected: {
        name: "lint extension changed file",
        tsconfig: "config/tsconfig/oxlint.extensions.json",
        path: "extensions/lmstudio/src/api.ts",
      },
    },
    {
      name: "targets small script lint diffs",
      create: createTargetedScriptLintCommand,
      targets: ["scripts/check-changed.mjs", "test/scripts/changed-lanes.test.ts"],
      expected: {
        name: "lint script changed file",
        tsconfig: "config/tsconfig/oxlint.scripts.json",
        path: "scripts/check-changed.mjs",
      },
    },
  ])("$name", ({ create, targets, expected }) => {
    expect(create(targets, { PATH: "/usr/bin" }, { fileExists: () => true })).toEqual({
      name: expected.name,
      bin: "node",
      args: ["scripts/run-oxlint.mjs", "--tsconfig", expected.tsconfig, expected.path],
      env: { PATH: "/usr/bin" },
    });
  });

  it("reenables local-check policy for changed typecheck commands", () => {
    const result = detectChangedLanes(["packages/normalization-core/src/string-normalization.ts"]);
    const plan = createChangedCheckPlan(result, {
      env: { OPENCLAW_LOCAL_CHECK: "0", PATH: "/usr/bin" },
    });

    expect(plan.commands.find((command) => command.args[0] === "tsgo:core")?.env).toEqual({
      OPENCLAW_LOCAL_CHECK: "1",
      OPENCLAW_OXLINT_SKIP_LOCK: "1",
      OPENCLAW_TEST_HEAVY_CHECK_LOCK_HELD: "1",
      OPENCLAW_TSGO_HEAVY_CHECK_LOCK_HELD: "1",
      OPENCLAW_TSGO_SPARSE_SKIP: "1",
      PATH: "/usr/bin",
    });
  });

  it("marks changed-check children as covered by the parent heavy-check lock", () => {
    expect(createChangedCheckChildEnv({ PATH: "/usr/bin" })).toEqual({
      OPENCLAW_OXLINT_SKIP_LOCK: "1",
      OPENCLAW_TEST_HEAVY_CHECK_LOCK_HELD: "1",
      OPENCLAW_TSGO_HEAVY_CHECK_LOCK_HELD: "1",
      PATH: "/usr/bin",
    });
  });

  it("runs CI changed-check children through Corepack pnpm", () => {
    const command = createPnpmManagedCommand(
      { name: "conflict markers", args: ["check:no-conflict-markers"] },
      { CI: "1", PATH: "/usr/bin" },
    );

    expect(command.bin).toBe("corepack");
    expect(command.args).toEqual(["pnpm", "check:no-conflict-markers"]);
  });

  it("cleans CI Corepack pnpm shim temp dirs", () => {
    const command = createPnpmManagedCommand(
      { name: "conflict markers", args: ["check:no-conflict-markers"] },
      { CI: "1", PATH: "/usr/bin" },
    );
    const shimDir = expectDefined(
      (command.env?.PATH ?? "").split(path.delimiter)[0],
      "CI Corepack pnpm shim directory",
    );

    expect(path.basename(shimDir)).toMatch(/^openclaw-corepack-pnpm-/u);
    expect(existsSync(path.join(shimDir, "pnpm"))).toBe(true);

    cleanupCorepackPnpmShimDir();

    expect(existsSync(shimDir)).toBe(false);
  });

  it("keeps local changed-check children on the repo pnpm shim", () => {
    const command = createPnpmManagedCommand(
      { name: "conflict markers", args: ["check:no-conflict-markers"] },
      { PATH: "/usr/bin" },
    );

    expect(command.bin).toBe("pnpm");
    expect(command.args).toEqual(["check:no-conflict-markers"]);
  });

  it("delegates heavy changed gates after classifying their lanes", () => {
    const result = detectChangedLanes(["src/config/config.ts"]);
    expect(
      shouldDelegateChangedCheckToCrabbox(
        ["--base", "origin/main"],
        { PATH: "/usr/bin" },
        { result },
      ),
    ).toBe(true);
    expect(changedCheckRequiresRemote(result)).toBe(true);

    expect(buildChangedCheckCrabboxArgs(["--base", "origin/main", "--head", "HEAD"])).toEqual([
      "scripts/crabbox-wrapper.mjs",
      "run",
      "--workload",
      "ci-fast",
      "--idle-timeout",
      "90m",
      "--ttl",
      "240m",
      "--timing-json",
      "--",
      "env",
      "OPENCLAW_CHECK_CHANGED_REMOTE_CHILD=1",
      "OPENCLAW_CHANGED_LANES_RAW_SYNC=1",
      "CI=1",
      "PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN=false",
      "corepack",
      "pnpm",
      "check:changed",
      "--base",
      "origin/main",
      "--head",
      "HEAD",
    ]);
  });

  it("routes a changed export signature remotely through its own source lane", () => {
    // Detection only fires for source files, and any such file already enables a
    // non-docs lane, so the dead export scan needs no special routing branch.
    const result = detectChangedLanes(["src/config/config.ts"]);

    expect(changedCheckRequiresRemote(result)).toBe(true);
    expect(shouldDelegateChangedCheckToCrabbox([], {}, { result })).toBe(true);
  });

  it("adds the dead export scan only for production source changes", () => {
    const command = {
      name: "dead export scan (skip with OPENCLAW_CHECK_CHANGED_SKIP_DEADCODE=1)",
      bin: "node",
      args: ["--import", "tsx", "scripts/check-deadcode-exports.mts"],
      env: expect.any(Object),
    };
    const sourceResult = detectChangedLanes(["src/config/config.ts"]);
    const toolingResult = detectChangedLanes(["scripts/check-changed.mjs"]);

    expect(createChangedCheckPlan(sourceResult).commands).toContainEqual(command);
    expect(createChangedCheckPlan(toolingResult).commands).not.toContainEqual(command);
    expect(
      createChangedCheckPlan(sourceResult, {
        env: { OPENCLAW_CHECK_CHANGED_SKIP_DEADCODE: "1" },
      }).commands,
    ).not.toContainEqual(command);
  });

  it("keeps small changed gates local only with a ready dependency install", () => {
    const dir = makeTempRepoRoot(tempDirs, "openclaw-check-changed-local-route-");
    const docsResult = detectChangedLanes(["docs/reference/test.md"]);
    const noChangesResult = detectChangedLanes([]);
    const metadataResult = detectChangedLanes(["CHANGELOG.md"]);
    const mixedResult = detectChangedLanes(["CHANGELOG.md", "src/config/config.ts"]);

    expect(changedCheckLocalDependenciesReady(dir)).toBe(false);
    expect(shouldDelegateChangedCheckToCrabbox([], {}, { cwd: dir, result: noChangesResult })).toBe(
      false,
    );
    expect(shouldDelegateChangedCheckToCrabbox([], {}, { cwd: dir, result: docsResult })).toBe(
      true,
    );

    writeRepoFile(dir, "node_modules/.modules.yaml", "layoutVersion: 5\n");
    writeRepoFile(dir, "node_modules/.bin/oxfmt", "#!/bin/sh\n");
    writeRepoFile(dir, "node_modules/typescript/package.json", '{"name":"typescript"}\n');

    expect(changedCheckLocalDependenciesReady(dir)).toBe(true);
    for (const result of [docsResult, noChangesResult, metadataResult]) {
      expect(changedCheckRequiresRemote(result)).toBe(false);
      expect(shouldDelegateChangedCheckToCrabbox([], {}, { cwd: dir, result })).toBe(false);
    }
    for (const result of [docsResult, metadataResult]) {
      expect(
        shouldDelegateChangedCheckToCrabbox([], { OPENCLAW_TESTBOX: "1" }, { cwd: dir, result }),
      ).toBe(true);
    }
    expect(changedCheckRequiresRemote(mixedResult)).toBe(true);
  });

  it("delegates generated docs baselines with heavy owner checks", () => {
    for (const changedPath of [
      "docs/.generated/plugin-sdk-api-baseline/core.json",
      "docs/.generated/sqlite-session-transcript-schema-baseline.sha256",
    ]) {
      const result = detectChangedLanes([changedPath]);
      expect(result.docsOnly).toBe(true);
      expect(changedCheckRequiresRemote(result)).toBe(true);
      expect(shouldDelegateChangedCheckToCrabbox([], {}, { cwd: repoRoot, result })).toBe(true);
    }
  });

  it("delegates staged changed gates as explicit remote paths", () => {
    const dir = makeTempRepoRoot(tempDirs, "openclaw-check-changed-staged-delegate-");
    git(dir, ["init", "-q", "--initial-branch=main"]);
    writeFileSync(path.join(dir, "README.md"), "initial\n", "utf8");
    commitAll(dir, "initial");
    mkdirSync(path.join(dir, "src"), { recursive: true });
    writeFileSync(path.join(dir, "src", "staged.ts"), "export const staged = 1;\n", "utf8");
    git(dir, ["add", "src/staged.ts"]);

    const args = buildChangedCheckCrabboxArgs(["--staged", "--timed"], { cwd: dir });
    expect(args.slice(args.indexOf("check:changed") + 1)).toEqual([
      "--timed",
      "--base",
      "HEAD",
      "--head",
      "HEAD",
      "--",
      "src/staged.ts",
    ]);
  });

  it("delegates empty staged changed gates without rediscovering unstaged paths", () => {
    const dir = makeTempRepoRoot(tempDirs, "openclaw-check-changed-empty-staged-delegate-");
    git(dir, ["init", "-q", "--initial-branch=main"]);
    writeFileSync(path.join(dir, "README.md"), "initial\n", "utf8");
    commitAll(dir, "initial");
    mkdirSync(path.join(dir, "src"), { recursive: true });
    writeFileSync(path.join(dir, "src", "unstaged.ts"), "export const unstaged = 1;\n", "utf8");

    const args = buildChangedCheckCrabboxArgs(["--staged", "--timed"], { cwd: dir });

    expect(args.slice(args.indexOf("check:changed") + 1)).toEqual(["--timed", "--no-changes"]);
  });

  it("does not delegate dry-run, CI, or remote-child changed gates", () => {
    expect(shouldDelegateChangedCheckToCrabbox(["--dry-run"], {})).toBe(false);
    expect(shouldDelegateChangedCheckToCrabbox([], { GITHUB_ACTIONS: "true" })).toBe(false);
    expect(shouldDelegateChangedCheckToCrabbox([], { CI: "1" })).toBe(false);
    expect(
      shouldDelegateChangedCheckToCrabbox([], { OPENCLAW_CHECK_CHANGED_REMOTE_CHILD: "1" }),
    ).toBe(false);
  });

  it("runs changed-check lint lanes under the parent heavy-check lock", () => {
    const result = detectChangedLanes(["extensions/lmstudio/src/api.ts"]);
    const plan = createChangedCheckPlan(result, { env: { PATH: "/usr/bin" } });
    const lintCommand = plan.commands.find(
      (command) => command.name === "lint extension changed file",
    );

    expect(lintCommand?.env).toEqual({
      OPENCLAW_OXLINT_SKIP_LOCK: "1",
      OPENCLAW_TEST_HEAVY_CHECK_LOCK_HELD: "1",
      OPENCLAW_TSGO_HEAVY_CHECK_LOCK_HELD: "1",
      PATH: "/usr/bin",
    });
  });

  it("runs changed-check app tests under the parent heavy-check lock", () => {
    const result = detectChangedLanes([
      "apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift",
    ]);
    const plan = createChangedCheckPlan(result, { env: { PATH: "/usr/bin" } });
    const testCommand = plan.commands.find((command) => command.args[0] === "test:macos:ci");

    expect(testCommand?.env).toEqual({
      OPENCLAW_OXLINT_SKIP_LOCK: "1",
      OPENCLAW_TEST_HEAVY_CHECK_LOCK_HELD: "1",
      OPENCLAW_TSGO_HEAVY_CHECK_LOCK_HELD: "1",
      PATH: "/usr/bin",
    });
  });

  it.each([
    {
      name: "routes core test-only changes to core test lanes only",
      path: "packages/normalization-core/src/string-normalization.test-support.ts",
      expected: {
        lanes: { coreTests: true },
        includes: ["tsgo:core:test"],
        excludes: ["tsgo:core"],
      },
    },
    {
      name: "routes extension production changes to extension prod and extension test lanes",
      path: "extensions/lmstudio/src/api.ts",
      expected: {
        lanes: { extensions: true, extensionTests: true },
        includes: ["tsgo:extensions", "tsgo:extensions:test"],
        excludes: [],
      },
    },
    {
      name: "routes extension test-only changes to extension test lanes only",
      path: "extensions/discord/src/index.test-helpers.ts",
      expected: {
        lanes: { extensionTests: true },
        includes: ["tsgo:extensions:test"],
        excludes: ["tsgo:extensions"],
      },
    },
  ])("$name", ({ path: changedPath, expected }) => {
    const result = detectChangedLanes([changedPath]);
    const commands = createChangedCheckPlan(result).commands.map((command) => command.args[0]);

    expectLanes(result.lanes, expected.lanes);
    for (const command of expected.includes) {
      expect(commands).toContain(command);
    }
    for (const command of expected.excludes) {
      expect(commands).not.toContain(command);
    }
  });

  it("expands public core/plugin contracts to extension validation", () => {
    const result = detectChangedLanes(["src/plugin-sdk/core.ts"]);
    const plan = createChangedCheckPlan(result);

    expect(result.extensionImpactFromCore).toBe(true);
    expectLanes(result.lanes, {
      core: true,
      coreTests: true,
      extensions: true,
      extensionTests: true,
    });
    expect(plan.commands.map((command) => command.args[0])).toContain("tsgo:core");
    expect(plan.commands.map((command) => command.args[0])).toContain("tsgo:extensions:test");
  });

  it("fails safe for root config changes", () => {
    const result = detectChangedLanes(["pnpm-lock.yaml"]);
    const plan = createChangedCheckPlan(result);

    expect(result.lanes.all).toBe(true);
    expect(plan.commands.map((command) => command.args[0])).toContain("tsgo:all");
    expect(plan.commands.map((command) => command.args[0])).not.toContain("test");
  });

  it.each([
    {
      name: "routes gitignore changes to tooling instead of all lanes",
      paths: [".gitignore"],
      excludesTests: true,
    },
    {
      name: "routes root hygiene config changes to tooling instead of all lanes",
      paths: [
        ".dockerignore",
        ".jscpd.json",
        ".npmignore",
        ".pre-commit-config.yaml",
        ".swiftformat",
        ".swiftlint.yml",
        "Makefile",
        "config/knip.config.ts",
        "config/markdownlint-cli2.jsonc",
        "config/shellcheckrc",
        "config/swiftformat",
        "config/swiftlint.yml",
        "deploy/fly.private.toml",
        "docker-setup.sh",
        "openclaw.podman.env",
        "setup-podman.sh",
        "skills/pyproject.toml",
      ],
      excludesTests: true,
    },
    {
      name: "routes VS Code workspace settings to tooling instead of all lanes",
      paths: [".vscode/settings.json", ".vscode/extensions.json"],
      excludesTests: true,
    },
    {
      name: "routes legacy root sandbox Dockerfile moves to tooling instead of all lanes",
      paths: [
        "Dockerfile.sandbox",
        "Dockerfile.sandbox-browser",
        "Dockerfile.sandbox-common",
        "scripts/docker/sandbox/Dockerfile",
        "scripts/docker/sandbox/Dockerfile.browser",
        "scripts/docker/sandbox/Dockerfile.common",
      ],
      excludesTests: true,
    },
    {
      name: "routes legacy root asset deletions as tooling during root cleanup",
      paths: ["assets/avatar-placeholder.svg", "assets/chrome-extension/icons/icon128.png"],
      excludesTests: false,
    },
  ])("$name", ({ paths, excludesTests }) => {
    const result = detectChangedLanes(paths);
    const commands = createChangedCheckPlan(result).commands.map((command) => command.args[0]);

    expectLanes(result.lanes, { tooling: true });
    expect(commands).toContain("lint:scripts");
    expect(commands).not.toContain("tsgo:all");
    if (excludesTests) {
      expect(commands).not.toContain("test");
    }
  });

  it("routes live Docker ACP tooling changes through a focused gate", () => {
    const result = detectChangedLanes([
      "scripts/lib/live-docker-auth.sh",
      "scripts/test-docker-all.mjs",
      "scripts/test-live-acp-bind-docker.sh",
      "src/gateway/gateway-acp-bind.live.test.ts",
      "docs/help/testing-live.md",
    ]);
    const plan = createChangedCheckPlan(result);

    expectLanes(result.lanes, {
      docs: true,
      liveDockerTooling: true,
    });
    expect(plan.commands.map((command) => command.name)).toEqual([
      "conflict markers",
      "environment variable count ratchet",
      "max-lines suppression ratchet",
      "changelog attributions",
      "doctor deprecation registry",
      "guarded extension wildcard re-exports",
      "plugin-sdk wildcard re-exports",
      "duplicate scan target coverage",
      "coercion helper declaration guard",
      "dependency pin guard",
      "format changed files",
      "deprecated API usage",
      "plugin boundaries",
      "wrapper shadowing",
      "package patch guard",
      // These live-Docker paths include `src/gateway/*.live.test.ts`, and the
      // full-tree knip scan sees test files, so a deleted last consumer can
      // orphan an export here too.
      "dead export scan (skip with OPENCLAW_CHECK_CHANGED_SKIP_DEADCODE=1)",
      "test temp creation report (warning-only)",
      "typecheck core tests",
      "lint core",
      "lint scripts",
      "live Docker shell syntax",
      "live Docker scheduler dry run",
    ]);
    expect(plan.commands.find((command) => command.name === "live Docker shell syntax")).toEqual({
      name: "live Docker shell syntax",
      bin: "bash",
      args: [
        "-n",
        "scripts/lib/live-docker-auth.sh",
        "scripts/test-live-acp-bind-docker.sh",
        "scripts/test-live-cli-backend-docker.sh",
        "scripts/test-live-codex-harness-docker.sh",
        "scripts/test-live-gateway-models-docker.sh",
        "scripts/test-live-models-docker.sh",
        "scripts/test-live-subagent-announce-docker.sh",
      ],
    });
    const schedulerDryRun = plan.commands.find(
      (command) => command.name === "live Docker scheduler dry run",
    );
    expect(schedulerDryRun?.bin).toBe("node");
    expect(schedulerDryRun?.args).toEqual(["scripts/test-docker-all.mjs"]);
    expect(schedulerDryRun?.env?.OPENCLAW_DOCKER_ALL_DRY_RUN).toBe("1");
    expect(schedulerDryRun?.env?.OPENCLAW_DOCKER_ALL_LIVE_MODE).toBe("only");
  });

  it("routes live Docker package script-only changes through the focused gate", () => {
    const before = prettyJson({
      name: "fixture",
      scripts: { "test:docker:all": "node scripts/test-docker-all.mjs" },
      dependencies: { leftpad: "1.0.0" },
    });
    const after = prettyJson({
      name: "fixture",
      scripts: {
        "test:docker:all": "node scripts/test-docker-all.mjs",
        "test:docker:live-acp-bind:droid":
          "OPENCLAW_LIVE_ACP_BIND_AGENT=droid bash scripts/test-live-acp-bind-docker.sh",
      },
      dependencies: { leftpad: "1.0.0" },
    });

    expect(isLiveDockerPackageScriptOnlyChange(before, after)).toBe(true);

    const result = detectChangedLanes(["package.json"], {
      packageJsonChangeKind: "liveDockerTooling",
    });
    const plan = createChangedCheckPlan(result);

    expectLanes(result.lanes, {
      liveDockerTooling: true,
    });
    expect(plan.commands.map((command) => command.name)).toContain("live Docker scheduler dry run");
  });

  it.each([
    {
      name: "classifies live Docker package script changes from the git diff",
      prefix: "openclaw-live-docker-package-",
      before: {
        name: "fixture",
        scripts: { "test:docker:all": "node scripts/test-docker-all.mjs" },
      },
      after: {
        name: "fixture",
        scripts: {
          "test:docker:all": "node scripts/test-docker-all.mjs",
          "test:docker:live-acp-bind:droid":
            "OPENCLAW_LIVE_ACP_BIND_AGENT=droid bash scripts/test-live-acp-bind-docker.sh",
        },
      },
      expected: { liveDockerTooling: true },
    },
    {
      name: "classifies normal package script changes from the git diff",
      prefix: "openclaw-package-scripts-",
      before: {
        name: "fixture",
        scripts: { test: "node --import tsx scripts/test-projects.mts" },
        dependencies: { leftpad: "1.0.0" },
      },
      after: {
        name: "fixture",
        scripts: {
          test: "node --import tsx scripts/test-projects.mts",
          "test:profile": "node scripts/profile-tests.mjs",
        },
        dependencies: { leftpad: "1.0.0" },
      },
      expected: { tooling: true },
    },
  ])("$name", ({ prefix, before, after, expected }) => {
    const result = classifyPackageJsonChange(prefix, before, after);

    expect(result.paths).toEqual(["package.json"]);
    expectLanes(result.lanes, expected);
  });

  it("keeps non-script package changes off the live Docker focused gate", () => {
    const before = prettyJson({
      name: "fixture",
      scripts: {},
      dependencies: { leftpad: "1.0.0" },
    });
    const after = prettyJson({
      name: "fixture",
      scripts: {
        "test:docker:live-acp-bind:droid":
          "OPENCLAW_LIVE_ACP_BIND_AGENT=droid bash scripts/test-live-acp-bind-docker.sh",
      },
      dependencies: { leftpad: "1.0.1" },
    });

    expect(isLiveDockerPackageScriptOnlyChange(before, after)).toBe(false);
  });

  it("routes package script-only changes through the tooling gate", () => {
    const before = prettyJson({
      name: "fixture",
      scripts: { test: "node test.js" },
      dependencies: { leftpad: "1.0.0" },
    });
    const after = prettyJson({
      name: "fixture",
      scripts: { test: "node test.js", "test:profile": "node scripts/profile-tests.mjs" },
      dependencies: { leftpad: "1.0.0" },
    });

    expect(isPackageScriptOnlyChange(before, after)).toBe(true);

    const result = detectChangedLanes(["package.json"], {
      packageJsonChangeKind: "tooling",
    });
    const plan = createChangedCheckPlan(result);

    expectLanes(result.lanes, {
      tooling: true,
    });
    expect(plan.commands.map((command) => command.args[0])).toContain("lint:scripts");
    expect(plan.commands.map((command) => command.args[0])).not.toContain("tsgo:all");
  });

  it("keeps release metadata commits off the full changed gate", () => {
    const result = detectChangedLanes([
      "CHANGELOG.md",
      "apps/android/CHANGELOG.md",
      "apps/android/Config/Version.properties",
      "apps/android/fastlane/metadata/android/en-US/release_notes.txt",
      "apps/android/version.json",
      "apps/ios/CHANGELOG.md",
      "apps/macos/Sources/OpenClaw/Resources/Info.plist",
      "docs/.generated/config-baseline.counts.json",
      "docs/.generated/config-baseline.sha256",
      "package.json",
    ]);
    const plan = createChangedCheckPlan(result, { staged: true });

    expectLanes(result.lanes, {
      docs: true,
      releaseMetadata: true,
    });
    const commands = plan.commands.map((command) => command.args[0]);
    expect(commands).toEqual([
      "check:no-conflict-markers",
      "check:changelog-attributions",
      "check:doctor-deprecation-registry",
      "lint:extensions:no-guarded-wildcard-reexports",
      "lint:extensions:no-plugin-sdk-wildcard-reexports",
      "dup:check:coverage",
      "check:coercion-helpers",
      "deps:pins:check",
      "format:check",
      "--import",
      "check:deprecated-api-usage",
      "plugins:boundary-report:ci",
      "check:wrapper-shadowing",
      "deps:patches:check",
      "release-metadata:check",
      "android:version:check",
      "config:schema:check",
      "config:docs:check",
      "deps:root-ownership:check",
    ]);
    expect(commands).not.toContain("ios:version:check");
    expect(
      plan.commands.find((command) => command.args[0] === "release-metadata:check")?.args,
    ).toEqual(["release-metadata:check", "--staged"]);
  });

  it("passes release metadata base and head refs as options", () => {
    const result = detectChangedLanes(["CHANGELOG.md"]);
    const plan = createChangedCheckPlan(result, { base: "main", head: "feature" });

    expect(
      plan.commands.find((command) => command.args[0] === "release-metadata:check")?.args,
    ).toEqual(["release-metadata:check", "--base", "main", "--head", "feature"]);
  });

  it("keeps docs plus changelog entries on the docs-only changed gate", () => {
    const result = detectChangedLanes(["CHANGELOG.md", "docs/tools/index.md"]);
    const plan = createChangedCheckPlan(result);

    expect(result.docsOnly).toBe(true);
    expectLanes(result.lanes, {
      docs: true,
    });
    expect(plan.commands.map((command) => command.args[0])).not.toContain("release-metadata:check");
  });

  it("runs the npm package-lock guard for dependency package surfaces", () => {
    expect(
      shouldRunNpmLockGuard([
        "extensions/slack/package.json",
        "extensions/slack/deps/local-runtime/package.json",
        "scripts/generate-npm-package-lock.mts",
      ]),
    ).toBe(true);

    const result = detectChangedLanes(["extensions/slack/package.json"]);
    const plan = createChangedCheckPlan(result);
    const npmLockGuard = createNpmLockGuardCommand(["extensions/slack/package.json"]);

    expect(npmLockGuard?.args.slice(0, 3)).toEqual([
      "--import",
      "tsx",
      "scripts/generate-npm-package-lock.mts",
    ]);
    expect(
      npmLockGuard?.args.some((arg) => arg.replaceAll("\\", "/").endsWith("extensions/slack")),
    ).toBe(true);
    expect(plan.commands.map((command) => command.name)).toContain("npm package-lock guard");
    expect(plan.commands.map((command) => command.args[0])).not.toContain("deps:npm-lock:check");
  });

  it.each([
    {
      name: "runs prompt snapshot drift checks for prompt snapshot generator surfaces",
      predicate: shouldRunPromptSnapshotCheck,
      predicatePaths: [
        "scripts/generate-prompt-snapshots.ts",
        "test/helpers/agents/happy-path-prompt-snapshots.ts",
        "test/fixtures/agents/prompt-snapshots/runtime-happy-path/telegram-direct-codex-message-tool.md",
      ],
      changedPath: "test/helpers/agents/happy-path-prompt-snapshots.ts",
      expected: {
        exact: [{ name: "prompt snapshot drift", args: ["prompt:snapshots:check"] }],
        partial: [
          {
            name: "prompt snapshot owner test",
            args: ["test:serial", "test/scripts/prompt-snapshots.test.ts"],
          },
        ],
      },
    },
    {
      name: "runs the prompt snapshot owner test for model fixture generator surfaces",
      predicate: shouldRunPromptSnapshotOwnerTest,
      predicatePaths: [
        "scripts/sync-codex-model-prompt-fixture.ts",
        "test/fixtures/agents/prompt-snapshots/codex-model-catalog/gpt-5.5.pragmatic.source.json",
      ],
      changedPath: "scripts/sync-codex-model-prompt-fixture.ts",
      expected: {
        exact: [],
        partial: [
          {
            name: "prompt snapshot owner test",
            args: ["test:serial", "test/scripts/prompt-snapshots.test.ts"],
          },
        ],
      },
    },
    {
      name: "runs runtime sidecar baseline checks for baseline owner surfaces",
      predicate: shouldRunRuntimeSidecarBaselineCheck,
      predicatePaths: [
        "scripts/generate-runtime-sidecar-paths-baseline.ts",
        "scripts/lib/bundled-runtime-sidecar-paths.json",
        "src/plugins/runtime-sidecar-paths-baseline.ts",
        "src/plugins/runtime-sidecar-paths.ts",
      ],
      changedPath: "scripts/lib/bundled-runtime-sidecar-paths.json",
      expected: {
        exact: [{ name: "runtime sidecar baseline", args: ["runtime-sidecars:check"] }],
        partial: [
          {
            name: "runtime sidecar owner test",
            args: ["test:serial", "src/plugins/bundled-plugin-metadata.test.ts"],
          },
        ],
      },
    },
    {
      name: "runs doctor contract owner tests for extension module and manifest changes",
      predicate: shouldRunDoctorContractOwnerTests,
      predicatePaths: [
        "extensions/telegram/doctor-contract-api.ts",
        "extensions/telegram/openclaw.plugin.json",
        "extensions/codex/src/migration/session-binding-sidecars.ts",
      ],
      changedPath: "extensions/telegram/doctor-contract-api.ts",
      expected: {
        exact: [],
        partial: [
          {
            name: "doctor contract declaration + closure guard tests",
            args: [
              "test:serial",
              "src/plugins/doctor-contract-declarations.test.ts",
              "src/plugins/doctor-contract-closure-guard.test.ts",
            ],
          },
        ],
      },
    },
    {
      name: "runs SQLite sessions/transcripts schema baseline checks for baseline owner surfaces",
      predicate: shouldRunSqliteSessionSchemaBaselineCheck,
      predicatePaths: [
        "src/state/openclaw-agent-schema.sql",
        "scripts/generate-sqlite-session-schema-baseline.ts",
        "scripts/lib/sqlite-session-schema-baseline.ts",
        "test/scripts/sqlite-session-schema-baseline.test.ts",
        "docs/.generated/sqlite-session-transcript-schema-baseline.sha256",
      ],
      changedPath: "src/state/openclaw-agent-schema.sql",
      expected: {
        exact: [
          {
            name: "SQLite sessions/transcripts schema baseline",
            args: ["sqlite:sessions-schema:check"],
          },
        ],
        partial: [],
      },
    },
  ])("$name", ({ predicate, predicatePaths, changedPath, expected }) => {
    expect(predicate(predicatePaths)).toBe(true);
    const commands = createChangedCheckPlan(detectChangedLanes([changedPath])).commands;
    for (const command of expected.exact) {
      expect(commands).toContainEqual(command);
    }
    for (const command of expected.partial) {
      expect(commands).toContainEqual(expect.objectContaining(command));
    }
  });

  it("runs Plugin SDK API checks for transitive public contract changes", () => {
    expect(
      shouldRunPluginSdkApiBaselineCheck([
        "src/config/sessions/session-accessor.ts",
        "packages/gateway-protocol/src/schema/approvals.ts",
        "extensions/memory-core/index.ts",
        "scripts/generate-plugin-sdk-api-baseline.ts",
        "scripts/lib/plugin-sdk-doc-metadata.ts",
        "scripts/lib/plugin-sdk-entries.mts",
        "docs/.generated/plugin-sdk-api-baseline/core.json",
      ]),
    ).toBe(true);
    expect(shouldRunPluginSdkApiBaselineCheck(["docs/help/troubleshooting.md"])).toBe(false);

    const result = detectChangedLanes(["src/config/sessions/session-accessor.ts"]);
    const plan = createChangedCheckPlan(result);

    expect(plan.commands).toContainEqual({
      name: "Plugin SDK API contract manifest",
      args: ["plugin-sdk:api:check"],
    });
    expect(plan.commands.map((command) => command.args[0])).not.toContain(
      "plugin-sdk:surface:check",
    );
  });

  it("runs Plugin SDK export and surface checks for direct SDK changes", () => {
    expect(
      shouldRunPluginSdkSurfaceChecks([
        "src/plugin-sdk/core.ts",
        "scripts/plugin-sdk-surface-report.mts",
        "scripts/sync-plugin-sdk-exports.mts",
        "scripts/lib/plugin-sdk-entries.mts",
        "scripts/lib/plugin-sdk-entrypoints.json",
        "package.json",
      ]),
    ).toBe(true);
    expect(shouldRunPluginSdkSurfaceChecks(["src/config/sessions/session-accessor.ts"])).toBe(
      false,
    );

    const result = detectChangedLanes(["src/plugin-sdk/core.ts"]);
    const plan = createChangedCheckPlan(result);

    expect(plan.commands).toContainEqual({
      name: "Plugin SDK API contract manifest",
      args: ["plugin-sdk:api:check"],
    });
    expect(plan.commands).toContainEqual({
      name: "Plugin SDK package exports",
      args: ["plugin-sdk:check-exports"],
    });
    expect(plan.commands).toContainEqual({
      name: "Plugin SDK surface budget",
      args: ["plugin-sdk:surface:check"],
    });

    const releaseMetadataPlan = createChangedCheckPlan(
      detectChangedLanes(["CHANGELOG.md", "package.json"]),
    );
    expect(releaseMetadataPlan.commands.map((command) => command.args[0])).not.toContain(
      "plugin-sdk:check-exports",
    );
  });

  it("runs deprecation hygiene checks for outcome-changing paths and all lanes", () => {
    expect(
      shouldRunDeprecationHygieneChecks([
        "src/plugin-sdk/core.ts",
        "extensions/slack/index.ts",
        "packages/gateway-protocol/src/index.ts",
        "scripts/lib/plugin-sdk-entries.mts",
        "scripts/check-deprecated-api-usage.mts",
        "scripts/plugin-boundary-report.ts",
        "src/plugins/compat/registry.ts",
        "package.json",
      ]),
    ).toBe(true);
    expect(shouldRunDeprecationHygieneChecks(["docs/plugins/sdk-migration.md"])).toBe(false);

    for (const result of [
      detectChangedLanes(["extensions/slack/index.ts"]),
      detectChangedLanes(["unknown-surface.foo"]),
    ]) {
      const plan = createChangedCheckPlan(result);
      expect(plan.commands).toContainEqual({
        name: "deprecated API usage",
        args: ["check:deprecated-api-usage"],
      });
      expect(plan.commands).toContainEqual({
        name: "plugin boundaries",
        args: ["plugins:boundary-report:ci"],
      });
    }
  });

  it("runs wrapper shadowing for source and guard-owner changes", () => {
    expect(
      shouldRunWrapperShadowingCheck([
        "src/channels/turn/run-channel-turn.ts",
        "scripts/check-wrapper-shadowing.mts",
        "scripts/check-export-name-collisions.mts",
        "scripts/lib/wrapper-shadowing-baseline.json",
        "scripts/lib/ts-guard-utils.mts",
        "package.json",
      ]),
    ).toBe(true);
    expect(shouldRunWrapperShadowingCheck(["docs/concepts/message-lifecycle.md"])).toBe(false);

    const plan = createChangedCheckPlan(
      detectChangedLanes(["scripts/check-wrapper-shadowing.mts"]),
    );
    expect(plan.commands).toContainEqual({
      name: "wrapper shadowing",
      args: ["check:wrapper-shadowing"],
    });
  });

  it("guards release metadata package changes to the top-level version field", () => {
    const dir = makeTempRepoRoot(tempDirs, "openclaw-release-metadata-");
    git(dir, ["init", "-q", "--initial-branch=main"]);
    writeFileSync(
      path.join(dir, "package.json"),
      `${JSON.stringify({ name: "fixture", version: "2026.4.20", dependencies: { leftpad: "1.0.0" } }, null, 2)}\n`,
      "utf8",
    );
    commitAll(dir, "initial");

    writeFileSync(
      path.join(dir, "package.json"),
      `${JSON.stringify({ name: "fixture", version: "2026.4.21", dependencies: { leftpad: "1.0.0" } }, null, 2)}\n`,
      "utf8",
    );
    git(dir, ["add", "package.json"]);
    expect(
      execFileSync(
        process.execPath,
        [
          "--import",
          tsxImport,
          path.join(repoRoot, "scripts", "check-release-metadata-only.mts"),
          "--staged",
        ],
        {
          cwd: dir,
          env: {
            ...createNestedGitEnv(),
            TSX_TSCONFIG_PATH: path.join(repoRoot, "tsconfig.json"),
          },
          stdio: "pipe",
        },
      ),
    ).toBeInstanceOf(Buffer);

    writeFileSync(
      path.join(dir, "package.json"),
      `${JSON.stringify({ name: "fixture", version: "2026.4.21", dependencies: { leftpad: "1.0.1" } }, null, 2)}\n`,
      "utf8",
    );
    git(dir, ["add", "package.json"]);
    let failure: ExecFileSyncFailure | undefined;
    try {
      execFileSync(
        process.execPath,
        [
          "--import",
          tsxImport,
          path.join(repoRoot, "scripts", "check-release-metadata-only.mts"),
          "--staged",
        ],
        {
          cwd: dir,
          env: {
            ...createNestedGitEnv(),
            TSX_TSCONFIG_PATH: path.join(repoRoot, "tsconfig.json"),
          },
          stdio: "pipe",
        },
      );
    } catch (error) {
      failure = error as ExecFileSyncFailure;
    }

    expect(failure?.status).toBe(1);
    expect(failure?.stderr?.toString("utf8")).toContain(
      "[release-metadata] package.json changed outside the top-level version field",
    );
  });

  it("routes root test/support changes to the tooling test lane instead of all lanes", () => {
    const result = detectChangedLanes([
      "test/git-hooks-pre-commit.test.ts",
      "test-fixtures/legacy-root-fixture.json",
    ]);
    const plan = createChangedCheckPlan(result);

    expectLanes(result.lanes, {
      testRoot: true,
      tooling: true,
    });
    expect(plan.commands.map((command) => command.args[0])).toContain("lint:scripts");
    expect(plan.commands.map((command) => command.args[0])).not.toContain("test");
  });

  it("routes legacy Swabble deletions as app surface during the app move", () => {
    const result = detectChangedLanes(["Swabble/Sources/SwabbleKit/WakeWordGate.swift"]);
    const plan = createChangedCheckPlan(result);

    expectLanes(result.lanes, {
      apps: true,
    });
    expect(plan.commands.map((command) => command.args[0])).not.toContain("tsgo:all");
  });

  it("runs macOS app CI tests for macOS app dependency changes", () => {
    for (const changedPath of [
      "apps/macos/Sources/OpenClawMac/AppDelegate.swift",
      "apps/macos-mlx-tts/Sources/OpenClawMLXTTS/main.swift",
      "apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift",
      "apps/swabble/Sources/SwabbleKit/WakeWordGate.swift",
      "Swabble/Sources/SwabbleKit/WakeWordGate.swift",
    ]) {
      const result = detectChangedLanes([changedPath]);
      const plan = createChangedCheckPlan(result, {
        env: { PATH: "/usr/bin" },
        platform: "linux",
        swiftlintAvailable: false,
      });

      expect(plan.commands.map((command) => command.args[0])).not.toContain("lint:apps");
      expect(plan.commands).toContainEqual(
        expect.objectContaining({
          name: "lint apps (swiftlint unavailable on this host)",
          bin: "node",
        }),
      );
      expect(plan.commands).toContainEqual(
        expect.objectContaining({
          name: "macOS app CI tests",
          args: ["test:macos:ci"],
        }),
      );
    }
  });

  it("runs macOS CI tests for workspace rsync receiver owners", () => {
    for (const changedPath of [
      "src/worker/workspace-rsync-receiver.ts",
      "src/gateway/worker-environments/workspace-sync.ts",
      "src/gateway/worker-environments/workspace-sync-helpers.ts",
      "src/gateway/worker-environments/workspace-accepted-sync.ts",
      "src/gateway/worker-environments/workspace-accepted-remote-script.ts",
      "src/gateway/worker-environments/workspace-mutation-remote-script.ts",
      "src/gateway/worker-environments/workspace-rsync-path.test.ts",
    ]) {
      const plan = createChangedCheckPlan(detectChangedLanes([changedPath]), {
        env: { PATH: "/usr/bin" },
        platform: "linux",
        swiftlintAvailable: false,
      });

      expect(plan.commands).toContainEqual(
        expect.objectContaining({
          name: "macOS app CI tests",
          args: ["test:macos:ci"],
        }),
      );
    }
  });

  it("runs the native state schema guard for either contract owner", () => {
    for (const changedPath of [
      "apps/shared/OpenClawKit/Sources/OpenClawNativeState/OpenClawNativeStateSQLite.swift",
      "src/state/openclaw-state-db-contract.ts",
    ]) {
      const plan = createChangedCheckPlan(detectChangedLanes([changedPath]), {
        env: { PATH: "/usr/bin" },
        platform: "linux",
        swiftlintAvailable: false,
      });

      expect(plan.commands).toContainEqual(
        expect.objectContaining({
          name: "native state schema version guard",
          bin: "node",
          args: ["scripts/check-native-state-schema-version.mjs"],
        }),
      );
    }
  });

  it("runs macOS app CI tests for macOS packaging scripts and owner tests", () => {
    for (const changedPath of [
      "scripts/codesign-mac-app.sh",
      "scripts/create-dmg.sh",
      "scripts/lib/plistbuddy.sh",
      "scripts/lib/swift-toolchain.sh",
      "scripts/notarize-mac-artifact.sh",
      "scripts/package-mac-app.sh",
      "scripts/package-mac-dist.sh",
      "test/scripts/codesign-mac-app.test.ts",
      "test/scripts/create-dmg.test.ts",
      "test/scripts/notarize-mac-artifact.test.ts",
      "test/scripts/package-mac-app.test.ts",
      "test/scripts/package-mac-dist.test.ts",
    ]) {
      const result = detectChangedLanes([changedPath]);
      const plan = createChangedCheckPlan(result, {
        env: { PATH: "/usr/bin" },
        platform: "linux",
        swiftlintAvailable: false,
      });

      expectLanes(result.lanes, {
        testRoot: changedPath.endsWith(".ts"),
        tooling: true,
      });
      expect(plan.commands.map((command) => command.args[0])).not.toContain("lint:apps");
      expect(plan.commands).toContainEqual(
        expect.objectContaining({
          name: "macOS app CI tests",
          args: ["test:macos:ci"],
        }),
      );
    }
  });

  it("routes appcast changes to appcast owner tests", () => {
    const result = detectChangedLanes(["appcast.xml"]);
    const plan = createChangedCheckPlan(result);

    expect(shouldRunAppcastOwnerTest(result.paths)).toBe(true);
    expect(plan.commands).toContainEqual(
      expect.objectContaining({
        name: "appcast owner tests",
        args: ["test:serial", "test/appcast.test.ts", "test/scripts/make-appcast.test.ts"],
      }),
    );
    expect(plan.commands.map((command) => command.name)).not.toContain("macOS app CI tests");
  });

  it("runs app lint when SwiftLint is available in Testbox", () => {
    const result = detectChangedLanes([
      "apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift",
    ]);
    const plan = createChangedCheckPlan(result, {
      env: { CI: "1", PATH: "/usr/bin" },
      platform: "linux",
      swiftlintAvailable: true,
    });

    expect(plan.commands.map((command) => command.args[0])).toContain("lint:apps");
    expect(plan.commands).toContainEqual(
      expect.objectContaining({
        name: "macOS app CI tests",
        args: ["test:macos:ci"],
      }),
    );
  });

  it("keeps macOS app CI tests out of Android-only app changes", () => {
    const result = detectChangedLanes(["apps/android/app/src/main/AndroidManifest.xml"]);
    const plan = createChangedCheckPlan(result, {
      env: { CI: "1", PATH: "/usr/bin" },
      platform: "linux",
      swiftlintAvailable: true,
    });

    expectLanes(result.lanes, {
      apps: true,
    });
    expect(plan.commands.map((command) => command.name)).not.toContain("macOS app CI tests");
  });

  it("routes A2UI bundle source changes as extension changes", () => {
    const result = detectChangedLanes([
      "extensions/canvas/src/host/a2ui-app/bootstrap.js",
      "extensions/canvas/src/host/a2ui-app/rolldown.config.mjs",
    ]);
    const plan = createChangedCheckPlan(result);

    expectLanes(result.lanes, {
      extensions: true,
      extensionTests: true,
    });
    expect(plan.commands.map((command) => command.args[0])).toContain("tsgo:extensions");
    expect(plan.commands.map((command) => command.args[0])).not.toContain("tsgo:all");
    expect(plan.commands).toContainEqual(
      expect.objectContaining({
        name: "Canvas A2UI native resource generation",
        bin: "node",
        args: ["--import", "tsx", "scripts/sync-native-a2ui.mts", "--check"],
      }),
    );
  });

  it("checks native A2UI resources when the copied resource tree changes", () => {
    const result = detectChangedLanes([
      "apps/shared/OpenClawKit/Sources/OpenClawKit/Resources/CanvasA2UI/a2ui.bundle.js",
    ]);
    const plan = createChangedCheckPlan(result);

    expectLanes(result.lanes, {
      apps: true,
    });
    expect(shouldRunCanvasA2uiNativeResourceCheck(result.paths)).toBe(true);
    expect(plan.commands).toContainEqual(
      expect.objectContaining({
        name: "Canvas A2UI native resource generation",
        bin: "node",
        args: ["--import", "tsx", "scripts/sync-native-a2ui.mts", "--check"],
      }),
    );
  });

  it("checks native A2UI resources when bundle inputs or generated outputs change", () => {
    const result = detectChangedLanes([
      "extensions/canvas/package.json",
      "extensions/canvas/src/host/a2ui/.bundle.hash",
      "extensions/canvas/src/host/a2ui/a2ui.bundle.js",
      "pnpm-lock.yaml",
    ]);
    const plan = createChangedCheckPlan(result);

    expect(shouldRunCanvasA2uiNativeResourceCheck(result.paths)).toBe(true);
    expect(plan.commands).toContainEqual(
      expect.objectContaining({
        name: "Canvas A2UI native resource generation",
        bin: "node",
        args: ["--import", "tsx", "scripts/sync-native-a2ui.mts", "--check"],
      }),
    );
  });

  it.each([
    "apps/android/app/build.gradle.kts",
    "apps/ios/project.yml",
    "apps/linux/src-tauri/build.rs",
    "apps/linux/src-tauri/src/canvas.rs",
  ])("checks native A2UI ownership when %s changes", (ownerPath) => {
    const result = detectChangedLanes([ownerPath]);
    const plan = createChangedCheckPlan(result);

    expect(shouldRunCanvasA2uiNativeResourceCheck(result.paths)).toBe(true);
    expect(plan.commands).toContainEqual(
      expect.objectContaining({
        name: "Canvas A2UI native resource generation",
        bin: "node",
        args: ["--import", "tsx", "scripts/sync-native-a2ui.mts", "--check"],
      }),
    );
  });

  it.each([
    {
      name: "keeps shared Vitest wiring changes out of check test execution",
      paths: ["test/vitest/vitest.shared.config.ts"],
      expected: "lint:scripts",
    },
    {
      name: "keeps setup changes out of check test execution",
      paths: ["test/setup.ts"],
      expected: "lint:scripts",
    },
    {
      name: "does not route generated plugin bundle artifacts as direct Vitest targets",
      paths: [
        "extensions/demo/src/host/assets/.bundle.hash",
        "extensions/canvas/scripts/bundle-a2ui.test.ts",
      ],
      expected: "tsgo:extensions",
    },
    {
      name: "routes changed extension Vitest configs to only their owning shard",
      paths: ["test/vitest/vitest.extension-discord.config.ts"],
      expected: "lint:scripts",
    },
  ])("$name", ({ paths, expected }) => {
    const commands = createChangedCheckPlan(detectChangedLanes(paths)).commands.map(
      (command) => command.args[0],
    );

    expect(commands).toContain(expected);
    expect(commands).not.toContain("test");
  });

  it("adds the warning-only temp creation report for changed test paths", () => {
    const result = detectChangedLanes(["test/helpers/temp-fixture.ts"]);
    const plan = createChangedCheckPlan(result, { base: "main", head: "feature" });
    const command = plan.commands.find(
      (candidate) => candidate.name === "test temp creation report (warning-only)",
    );

    expect(shouldRunTestTempCreationReport(result.paths)).toBe(true);
    expect(command).toMatchObject({
      bin: "node",
      args: ["scripts/report-test-temp-creations.mjs", "--base", "main", "--head", "feature"],
    });
  });

  it.each([
    {
      name: "adds the max-lines suppression ratchet with worktree and staged bases",
      commandName: "max-lines suppression ratchet",
      worktreeOptions: { base: "main", head: "feature" },
      expected: {
        worktree: ["check:max-lines-ratchet", "--base", "main"],
        staged: ["check:max-lines-ratchet", "--staged", "--base", "HEAD"],
      },
    },
    {
      name: "adds the environment variable count ratchet for production source",
      commandName: "environment variable count ratchet",
      worktreeOptions: { base: "main" },
      expected: {
        worktree: ["check:env-var-count", "--base", "main"],
        staged: ["check:env-var-count", "--staged", "--base", "HEAD"],
      },
    },
  ])("$name", ({ commandName, worktreeOptions, expected }) => {
    const result = detectChangedLanes(["src/runtime.ts"]);
    const worktreePlan = createChangedCheckPlan(result, worktreeOptions);
    const stagedPlan = createChangedCheckPlan(result, { staged: true });

    expect(worktreePlan.commands.find((command) => command.name === commandName)).toMatchObject({
      args: expected.worktree,
    });
    expect(stagedPlan.commands.find((command) => command.name === commandName)).toMatchObject({
      args: expected.staged,
    });
  });

  it("keeps the temp creation report out of non-test changed paths", () => {
    const result = detectChangedLanes(["scripts/check-changed.mjs"]);
    const plan = createChangedCheckPlan(result);

    expect(shouldRunTestTempCreationReport(result.paths)).toBe(false);
    expect(plan.commands.map((command) => command.name)).not.toContain(
      "test temp creation report (warning-only)",
    );
  });

  it("keeps an empty changed path list as a no-op", () => {
    const result = detectChangedLanes([]);
    const plan = createChangedCheckPlan(result);

    expect(result.lanes).toEqual({
      core: false,
      coreTests: false,
      ui: false,
      extensions: false,
      extensionTests: false,
      scripts: false,
      testRoot: false,
      apps: false,
      docs: false,
      tooling: false,
      liveDockerTooling: false,
      bundledChannelConfigMetadata: false,
      releaseMetadata: false,
      all: false,
    });
    expect(plan.commands).toEqual([
      { name: "conflict markers", args: ["check:no-conflict-markers"] },
      { name: "changelog attributions", args: ["check:changelog-attributions"] },
      { name: "doctor deprecation registry", args: ["check:doctor-deprecation-registry"] },
      {
        name: "guarded extension wildcard re-exports",
        args: ["lint:extensions:no-guarded-wildcard-reexports"],
      },
      {
        name: "plugin-sdk wildcard re-exports",
        args: ["lint:extensions:no-plugin-sdk-wildcard-reexports"],
      },
      { name: "duplicate scan target coverage", args: ["dup:check:coverage"] },
      { name: "coercion helper declaration guard", args: ["check:coercion-helpers"] },
      { name: "dependency pin guard", args: ["deps:pins:check"] },
      { name: "package patch guard", args: ["deps:patches:check"] },
    ]);
  });

  it("keeps docs-only changes cheap", () => {
    const result = detectChangedLanes(["docs/ci.md", "README.md"]);
    const plan = createChangedCheckPlan(result);

    expect(result.docsOnly).toBe(true);
    expect(plan.commands).toEqual([
      { name: "conflict markers", args: ["check:no-conflict-markers"] },
      { name: "changelog attributions", args: ["check:changelog-attributions"] },
      { name: "doctor deprecation registry", args: ["check:doctor-deprecation-registry"] },
      {
        name: "guarded extension wildcard re-exports",
        args: ["lint:extensions:no-guarded-wildcard-reexports"],
      },
      {
        name: "plugin-sdk wildcard re-exports",
        args: ["lint:extensions:no-plugin-sdk-wildcard-reexports"],
      },
      { name: "duplicate scan target coverage", args: ["dup:check:coverage"] },
      { name: "coercion helper declaration guard", args: ["check:coercion-helpers"] },
      { name: "dependency pin guard", args: ["deps:pins:check"] },
      {
        name: "format changed files",
        args: ["format:check", "--no-error-on-unmatched-pattern", "--", "docs/ci.md", "README.md"],
      },
      { name: "package patch guard", args: ["deps:patches:check"] },
    ]);
  });
});

describe("delegationFailedBeforeRunning", () => {
  // The wrapper only prints a run summary once the command reached the box, so
  // the summary is the evidence that a verdict exists at all.
  it("treats a lease or network failure as never having run", () => {
    const output = [
      'request failed: Get "https://backend.blacksmith.sh/api/testbox/list?all=true": context deadline exceeded',
      "blacksmith testbox run exited 1",
    ].join("\n");

    expect(delegationFailedBeforeRunning(output)).toBe(true);
  });

  it("treats a reported command exit as a real check failure", () => {
    const output = [
      "  64.95s  failed:1   typecheck core tests",
      '{"provider":"blacksmith-testbox","runStatus":"failed","errorKind":"command-exit","exitCode":1}',
    ].join("\n");

    // Falling back locally here would re-run on macOS and could pass a lane
    // whose truth is Linux, turning a red gate green.
    expect(delegationFailedBeforeRunning(output)).toBe(false);
  });

  it("treats a full workload-routing provider outage as never having run", () => {
    // Provider selection happens before any dispatch, so an exhausted routing
    // chain (every doctor failing) can never carry a remote verdict.
    const output = [
      "[crabbox] no ready provider for workload=ci-fast",
      "[crabbox] provider readiness blacksmith-testbox:doctor exited 1,daytona:doctor exited 124,azure:doctor exited 124,aws:doctor exited 124",
    ].join("\n");

    expect(delegationFailedBeforeRunning(output)).toBe(true);
  });

  it("does not mistake an infrastructure error kind for a command verdict", () => {
    const output = [
      "failed to acquire lease for testbox",
      '{"provider":"blacksmith-testbox","runStatus":"failed","errorKind":"lease-timeout","exitCode":1}',
    ].join("\n");

    expect(delegationFailedBeforeRunning(output)).toBe(true);
  });

  // A crash after dispatch produces no summary either, so absence of one cannot
  // be read as "never ran" — that is how an unknown Linux result would go green.
  it("fails closed when the wrapper dies without saying why", () => {
    expect(delegationFailedBeforeRunning("node: killed\n")).toBe(false);
    expect(delegationFailedBeforeRunning("")).toBe(false);
  });

  it("keeps a command verdict authoritative even alongside network noise", () => {
    const output = [
      'request failed: Get "https://backend.blacksmith.sh/api/testbox/list": context deadline exceeded',
      '{"provider":"blacksmith-testbox","runStatus":"failed","errorKind":"command-exit","exitCode":1}',
    ].join("\n");

    expect(delegationFailedBeforeRunning(output)).toBe(false);
  });
});
