/**
 * E2E regression test for git-sync promotion / `use_individual_branch`.
 *
 * Guards the hub/28229 regression: the git-sync deployment callback must push
 * each deployed object to a dedicated `wm_deploy/<workspace>/<...>` branch when
 * `use_individual_branch` is set — NOT straight to the cloned base branch
 * (e.g. a protected `main`, which fails with GH006).
 *
 * Contract: `wmill sync git-deploy` does branch checkout + pull only. Commit
 * + push are the caller's job — the hub script does them in the same process
 * as `set_gpg_signing_secret` so the GPG agent's passphrase cache is still
 * warm at sign time (WIN-1974). This test replicates the caller half (git
 * add + commit + push) inline so the full promotion regression stays caught.
 */

import { expect, test } from "bun:test";
import { execFileSync } from "node:child_process";
import { mkdtemp, writeFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { withTestBackend } from "./test_backend.ts";
import { shouldSkipOnCI } from "./cargo_backend.ts";
import { addWorkspace } from "../workspace.ts";

// The HTTP-trigger promotion test creates an http_trigger, whose API routes are
// behind the `http_trigger` cargo feature — NOT in the default EE test feature
// set. The shared test backend reads TEST_FEATURES at construction (first
// `withTestBackend` call), so appending here at module load enables it. Guarded
// on shouldSkipOnCI() so we only widen the build when these EE tests actually
// run (i.e. EE_LICENSE_KEY present); minimal CI builds stay untouched.
if (!shouldSkipOnCI()) {
  process.env["TEST_FEATURES"] = [process.env["TEST_FEATURES"], "http_trigger"]
    .filter(Boolean)
    .join(",");
}

function git(cwd: string, ...args: string[]): string {
  return execFileSync("git", args, { cwd, encoding: "utf8" }).trim();
}

// Refs (branches) present on the bare remote.
function remoteBranches(bareDir: string): string[] {
  const out = execFileSync(
    "git",
    ["--git-dir", bareDir, "for-each-ref", "--format=%(refname)", "refs/heads/"],
    { encoding: "utf8" },
  ).trim();
  return out ? out.split("\n") : [];
}

function remoteHead(bareDir: string, branch: string): string {
  return execFileSync(
    "git",
    ["--git-dir", bareDir, "rev-parse", `refs/heads/${branch}`],
    { encoding: "utf8" },
  ).trim();
}

// True if `filePath` exists in the tree of `branch` on the bare remote.
function fileExistsOnBranch(
  bareDir: string,
  branch: string,
  filePath: string,
): boolean {
  try {
    execFileSync(
      "git",
      ["--git-dir", bareDir, "cat-file", "-e", `refs/heads/${branch}:${filePath}`],
      { stdio: "ignore" },
    );
    return true;
  } catch {
    return false;
  }
}

test.skipIf(shouldSkipOnCI())(
  "git-sync promotion: use_individual_branch pushes to wm_deploy branch, not main",
  async () => {
    await withTestBackend(async (backend, tempDir) => {
      const ws = backend.workspace; // "test"
      await addWorkspace(
        {
          remote: backend.baseUrl,
          workspaceId: ws,
          name: ws,
          token: backend.token,
        } as any,
        { force: true, configDir: backend.testConfigDir },
      );

      // --- 1. Local bare "remote" repo seeded with an initial `main` commit ---
      const bareDir = await mkdtemp(join(tmpdir(), "wmill_promo_bare_"));
      execFileSync("git", ["init", "--bare", "--initial-branch=main", bareDir]);
      const seedDir = await mkdtemp(join(tmpdir(), "wmill_promo_seed_"));
      git(seedDir, "init", "--initial-branch=main");
      git(seedDir, "config", "user.email", "seed@windmill.dev");
      git(seedDir, "config", "user.name", "seed");
      await writeFile(join(seedDir, "README.md"), "# promo test\n");
      git(seedDir, "add", "-A");
      git(seedDir, "commit", "-m", "seed");
      git(seedDir, "remote", "add", "origin", `file://${bareDir}`);
      git(seedDir, "push", "-u", "origin", "main");
      const seedMain = remoteHead(bareDir, "main");

      // --- 2. Workspace content to sync (a script in a folder) ---
      await backend.apiRequest!(`/api/w/${ws}/folders/create`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ name: "promo", owners: [], extra_perms: {} }),
      });
      await backend.apiRequest!(`/api/w/${ws}/scripts/create`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          path: "f/promo/foo",
          summary: "",
          description: "",
          content: "export async function main() { return 1 }",
          language: "bun",
        }),
      });

      // --- 3. git_repository resource + git-sync config pointing at the bare repo ---
      await backend.apiRequest!(`/api/w/${ws}/resources/create`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          path: "u/test/promo_repo",
          resource_type: "git_repository",
          value: { url: `file://${bareDir}`, branch: "main", token: "" },
        }),
      });
      await backend.updateGitSyncConfig!({
        git_sync_settings: {
          repositories: [
            {
              git_repo_resource_path: "u/test/promo_repo",
              script_path: "f/**",
              use_individual_branch: true,
              group_by_folder: false,
              settings: {
                include_path: ["f/**"],
                include_type: ["script"],
              },
            },
          ],
        },
      });

      const deployItems = JSON.stringify([
        { path_type: "script", path: "f/promo/foo", commit_msg: "deploy foo" },
      ]);

      // Caller-half: stage anything the CLI's pull dropped, commit on the
      // current branch (which the CLI just checked out), and push. Mirrors
      // what the hub script does in production after `wmill sync git-deploy`.
      const commitAndPush = (work: string) => {
        git(work, "config", "user.email", "test@windmill.dev");
        git(work, "config", "user.name", "test");
        git(work, "add", "-A");
        try {
          git(work, "diff", "--cached", "--quiet");
          // Exit 0 = nothing staged; nothing to commit. Still push the
          // (possibly new) branch ref so the assertions see it.
        } catch {
          git(work, "commit", "-m", "deploy foo");
        }
        git(work, "push", "--porcelain", "-u", "origin", "HEAD");
      };

      // --- Case A: use_individual_branch=true -> wm_deploy branch, main untouched ---
      const workA = await mkdtemp(join(tmpdir(), "wmill_promo_a_"));
      git(workA, "clone", `file://${bareDir}`, ".");
      await writeFile(
        join(workA, "wmill.yaml"),
        "defaultTs: bun\nincludes:\n  - f/**\nexcludes: []\n",
      );
      const resA = await backend.runCLICommand(
        [
          "sync",
          "git-deploy",
          "--repository",
          "u/test/promo_repo",
          "--use-individual-branch",
          "--git-deploy-items",
          deployItems,
        ],
        workA,
      );
      expect(resA.code).toBe(0);
      commitAndPush(workA);

      const branchesA = remoteBranches(bareDir);
      const expectedBranch = `refs/heads/wm_deploy/${ws}/script/f__promo__foo`;
      expect(branchesA).toContain(expectedBranch);
      // The regression: main MUST be untouched (no direct push to protected base).
      expect(remoteHead(bareDir, "main")).toBe(seedMain);

      // --- Case B: workspace-wide (no flag) -> pushes to main, no wm_deploy ---
      const workB = await mkdtemp(join(tmpdir(), "wmill_promo_b_"));
      git(workB, "clone", `file://${bareDir}`, ".");
      await writeFile(
        join(workB, "wmill.yaml"),
        "defaultTs: bun\nincludes:\n  - f/**\nexcludes: []\n",
      );
      const resB = await backend.runCLICommand(
        [
          "sync",
          "git-deploy",
          "--repository",
          "u/test/promo_repo",
          "--git-deploy-items",
          deployItems,
        ],
        workB,
      );
      expect(resB.code).toBe(0);
      commitAndPush(workB);

      expect(remoteHead(bareDir, "main")).not.toBe(seedMain);
      expect(
        remoteBranches(bareDir).filter((b) => b.includes("wm_deploy")).length,
      ).toBe(1); // only the one from Case A, none new

      await rm(bareDir, { recursive: true, force: true });
      await rm(seedDir, { recursive: true, force: true });
      await rm(workA, { recursive: true, force: true });
      await rm(workB, { recursive: true, force: true });
    });
  },
);

/**
 * Dev-workspace promotion. The backend passes `--dev-workspace-label` +
 * `--parent-workspace-id` for a dev workspace's deploys; with promotion on it
 * must behave like a root (per-item `wm_deploy/**`) for branchable objects, and
 * — critically — non-branchable objects (user/group) must fall back to the dev's
 * env-label branch, NEVER the cloned base (which for a dev is the parent's
 * tracked branch, so a leak would push dev content straight to prod).
 */
test.skipIf(shouldSkipOnCI())(
  "git-sync promotion (dev workspace): script -> wm_deploy branch; group -> env-label branch; main never touched",
  async () => {
    await withTestBackend(async (backend) => {
      const ws = backend.workspace; // "test"
      await addWorkspace(
        {
          remote: backend.baseUrl,
          workspaceId: ws,
          name: ws,
          token: backend.token,
        } as any,
        { force: true, configDir: backend.testConfigDir },
      );

      const bareDir = await mkdtemp(join(tmpdir(), "wmill_devpromo_bare_"));
      execFileSync("git", ["init", "--bare", "--initial-branch=main", bareDir]);
      const seedDir = await mkdtemp(join(tmpdir(), "wmill_devpromo_seed_"));
      git(seedDir, "init", "--initial-branch=main");
      git(seedDir, "config", "user.email", "seed@windmill.dev");
      git(seedDir, "config", "user.name", "seed");
      await writeFile(join(seedDir, "README.md"), "# dev promo\n");
      git(seedDir, "add", "-A");
      git(seedDir, "commit", "-m", "seed");
      git(seedDir, "remote", "add", "origin", `file://${bareDir}`);
      git(seedDir, "push", "-u", "origin", "main");
      const seedMain = remoteHead(bareDir, "main");

      await backend.apiRequest!(`/api/w/${ws}/folders/create`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ name: "promo", owners: [], extra_perms: {} }),
      });
      await backend.apiRequest!(`/api/w/${ws}/scripts/create`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          path: "f/promo/foo",
          summary: "",
          description: "",
          content: "export async function main() { return 1 }",
          language: "bun",
        }),
      });
      await backend.apiRequest!(`/api/w/${ws}/resources/create`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          path: "u/test/devpromo_repo",
          resource_type: "git_repository",
          value: { url: `file://${bareDir}`, branch: "main", token: "" },
        }),
      });
      await backend.updateGitSyncConfig!({
        git_sync_settings: {
          repositories: [
            {
              git_repo_resource_path: "u/test/devpromo_repo",
              script_path: "f/**",
              use_individual_branch: true,
              group_by_folder: false,
              settings: { include_path: ["f/**", "g/**"], include_type: ["script", "group"] },
            },
          ],
        },
      });

      // The dev flags the backend passes with every dev-workspace deploy.
      const devFlags = ["--dev-workspace-label", "dev", "--parent-workspace-id", "prod"];
      const commitAndPush = (work: string) => {
        git(work, "config", "user.email", "test@windmill.dev");
        git(work, "config", "user.name", "test");
        git(work, "add", "-A");
        try {
          git(work, "diff", "--cached", "--quiet");
        } catch {
          git(work, "commit", "-m", "deploy");
        }
        git(work, "push", "--porcelain", "-u", "origin", "HEAD");
      };

      // Script: gets its own wm_deploy branch (namespaced by the dev's id), main untouched.
      const workS = await mkdtemp(join(tmpdir(), "wmill_devpromo_s_"));
      git(workS, "clone", `file://${bareDir}`, ".");
      await writeFile(join(workS, "wmill.yaml"), "defaultTs: bun\nincludes:\n  - f/**\nexcludes: []\n");
      const resS = await backend.runCLICommand(
        [
          "sync", "git-deploy", "--repository", "u/test/devpromo_repo",
          "--use-individual-branch", ...devFlags,
          "--git-deploy-items", JSON.stringify([{ path_type: "script", path: "f/promo/foo", commit_msg: "deploy foo" }]),
        ],
        workS,
      );
      expect(resS.code).toBe(0);
      commitAndPush(workS);
      expect(remoteBranches(bareDir)).toContain(`refs/heads/wm_deploy/${ws}/script/f__promo__foo`);
      expect(remoteHead(bareDir, "main")).toBe(seedMain);

      // Group (non-branchable): must land on the dev env-label branch, NEVER main.
      const workG = await mkdtemp(join(tmpdir(), "wmill_devpromo_g_"));
      git(workG, "clone", `file://${bareDir}`, ".");
      await writeFile(join(workG, "wmill.yaml"), "defaultTs: bun\nincludes:\n  - f/**\n  - g/**\nexcludes: []\n");
      const resG = await backend.runCLICommand(
        [
          "sync", "git-deploy", "--repository", "u/test/devpromo_repo",
          "--use-individual-branch", ...devFlags,
          "--git-deploy-items", JSON.stringify([{ path_type: "group", path: "g/all", commit_msg: "deploy group" }]),
        ],
        workG,
      );
      expect(resG.code).toBe(0);
      commitAndPush(workG);
      // The anti-leak regression: a group deploy must not advance main.
      expect(remoteHead(bareDir, "main")).toBe(seedMain);
      expect(remoteBranches(bareDir)).toContain("refs/heads/dev");

      await rm(bareDir, { recursive: true, force: true });
      await rm(seedDir, { recursive: true, force: true });
      await rm(workS, { recursive: true, force: true });
      await rm(workG, { recursive: true, force: true });
    });
  },
);

/**
 * Regression test for the promotion trigger-include bug (fix/gitsync-promotion-
 * trigger-export): deploying a trigger (or any excluded-by-default kind:
 * schedule, group, user, settings, key) with `use_individual_branch` must still
 * land the object file on the `wm_deploy` branch.
 *
 * Root cause: `deriveGitSyncDeployIncludes` used to force the per-kind include
 * flags (`includeTriggers` etc.) to false in individual-branch mode. The
 * server-side tarball export STRIPS those object kinds entirely when their
 * include flag is false (`if include_triggers { … }` in workspaces_export.rs),
 * and `extraIncludes` is only a client-side filter over what the tarball
 * already contains — it can't recover a file the server never sent. So the
 * pull wrote no trigger file, the wm_deploy branch was created empty of the
 * trigger, and production's `git add '<path>**'` failed with "pathspec did not
 * match any files". A script (always-included kind) never hit this — hence the
 * dedicated trigger case here.
 *
 * Without the fix this test fails: the branch exists but the
 * `*.http_trigger.yaml` file is absent from it.
 */
test.skipIf(shouldSkipOnCI())(
  "git-sync promotion: use_individual_branch lands a trigger file on the wm_deploy branch",
  async () => {
    await withTestBackend(async (backend) => {
      const ws = backend.workspace; // "test"
      await addWorkspace(
        {
          remote: backend.baseUrl,
          workspaceId: ws,
          name: ws,
          token: backend.token,
        } as any,
        { force: true, configDir: backend.testConfigDir },
      );

      // --- 1. Bare "remote" seeded with an initial `main` commit ---
      const bareDir = await mkdtemp(join(tmpdir(), "wmill_promo_trig_bare_"));
      execFileSync("git", ["init", "--bare", "--initial-branch=main", bareDir]);
      const seedDir = await mkdtemp(join(tmpdir(), "wmill_promo_trig_seed_"));
      git(seedDir, "init", "--initial-branch=main");
      git(seedDir, "config", "user.email", "seed@windmill.dev");
      git(seedDir, "config", "user.name", "seed");
      await writeFile(join(seedDir, "README.md"), "# promo trigger test\n");
      git(seedDir, "add", "-A");
      git(seedDir, "commit", "-m", "seed");
      git(seedDir, "remote", "add", "origin", `file://${bareDir}`);
      git(seedDir, "push", "-u", "origin", "main");
      const seedMain = remoteHead(bareDir, "main");

      // --- 2. Workspace content: a script + an HTTP trigger pointing at it ---
      await backend.apiRequest!(`/api/w/${ws}/folders/create`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ name: "promo", owners: [], extra_perms: {} }),
      });
      await backend.apiRequest!(`/api/w/${ws}/scripts/create`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          path: "f/promo/foo",
          summary: "",
          description: "",
          content: "export async function main() { return 1 }",
          language: "bun",
        }),
      });
      const trigRes = await backend.apiRequest!(`/api/w/${ws}/http_triggers/create`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          path: "f/promo/hook",
          script_path: "f/promo/foo",
          route_path: "promo_hook",
          is_flow: false,
          http_method: "post",
          authentication_method: "none",
          is_static_website: false,
          request_type: "sync",
        }),
      });
      // Guard against the route silently 404ing (the http_trigger cargo feature
      // not being built) — otherwise the pull below would find nothing to sync
      // and the real assertion would fail with a confusing message.
      expect(trigRes.status).toBe(201);

      // --- 3. git_repository resource + git-sync config (triggers included) ---
      await backend.apiRequest!(`/api/w/${ws}/resources/create`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          path: "u/test/promo_repo",
          resource_type: "git_repository",
          value: { url: `file://${bareDir}`, branch: "main", token: "" },
        }),
      });
      await backend.updateGitSyncConfig!({
        git_sync_settings: {
          repositories: [
            {
              git_repo_resource_path: "u/test/promo_repo",
              script_path: "f/**",
              use_individual_branch: true,
              group_by_folder: false,
              settings: {
                include_path: ["f/**"],
                include_type: ["script", "trigger"],
              },
            },
          ],
        },
      });

      // The backend sets path_type "httptrigger" (no underscore) on the deploy
      // item — see DeployedObject::HttpTrigger => "httptrigger" in git_sync_ee.rs.
      const deployItems = JSON.stringify([
        {
          path_type: "httptrigger",
          path: "f/promo/hook",
          commit_msg: "deploy hook",
        },
      ]);

      const work = await mkdtemp(join(tmpdir(), "wmill_promo_trig_work_"));
      git(work, "clone", `file://${bareDir}`, ".");
      // Option B semantic: in promotion mode the deploy forces NOTHING — the
      // trigger lands only because THIS target's effective wmill.yaml opts
      // triggers in. (Reverting the source fix re-introduces the force-`false`
      // that clobbers this `includeTriggers: true`, so the file is dropped.)
      await writeFile(
        join(work, "wmill.yaml"),
        "defaultTs: bun\nincludes:\n  - f/**\nexcludes: []\nincludeTriggers: true\n",
      );
      const res = await backend.runCLICommand(
        [
          "sync",
          "git-deploy",
          "--repository",
          "u/test/promo_repo",
          "--use-individual-branch",
          "--git-deploy-items",
          deployItems,
        ],
        work,
      );
      expect(res.code).toBe(0);

      // Caller-half (mirrors the hub script): stage what the pull wrote, commit
      // on the checked-out wm_deploy branch, push.
      git(work, "config", "user.email", "test@windmill.dev");
      git(work, "config", "user.name", "test");
      git(work, "add", "-A");
      try {
        git(work, "diff", "--cached", "--quiet");
      } catch {
        git(work, "commit", "-m", "deploy hook");
      }
      git(work, "push", "--porcelain", "-u", "origin", "HEAD");

      const expectedBranch = `refs/heads/wm_deploy/${ws}/httptrigger/f__promo__hook`;
      expect(remoteBranches(bareDir)).toContain(expectedBranch);
      // The regression: the trigger file MUST be present on the branch. Without
      // the fix the include flag is false, the server strips the trigger from
      // the tarball, the pull writes nothing, and this file is absent.
      expect(
        fileExistsOnBranch(
          bareDir,
          `wm_deploy/${ws}/httptrigger/f__promo__hook`,
          "f/promo/hook.http_trigger.yaml",
        ),
      ).toBe(true);
      // Base branch untouched (individual-branch never pushes to the base).
      expect(remoteHead(bareDir, "main")).toBe(seedMain);

      await rm(bareDir, { recursive: true, force: true });
      await rm(seedDir, { recursive: true, force: true });
      await rm(work, { recursive: true, force: true });
    });
  },
);

/**
 * Same regression as the HTTP-trigger case above, for a `schedule` — a
 * different excluded-by-default kind that exercises a DISTINCT path: its own
 * include flag (`includeSchedules`), its own server-side `if include_schedules`
 * tarball-strip branch, and its own `.schedule.yaml` extension. Unlike triggers
 * it needs no extra cargo feature, so it guards the fix even where the
 * trigger-specific features aren't built.
 *
 * Without the fix this test fails: the branch exists but the
 * `*.schedule.yaml` file is absent from it.
 */
test.skipIf(shouldSkipOnCI())(
  "git-sync promotion: use_individual_branch lands a schedule file on the wm_deploy branch",
  async () => {
    await withTestBackend(async (backend) => {
      const ws = backend.workspace; // "test"
      await addWorkspace(
        {
          remote: backend.baseUrl,
          workspaceId: ws,
          name: ws,
          token: backend.token,
        } as any,
        { force: true, configDir: backend.testConfigDir },
      );

      // --- 1. Bare "remote" seeded with an initial `main` commit ---
      const bareDir = await mkdtemp(join(tmpdir(), "wmill_promo_sched_bare_"));
      execFileSync("git", ["init", "--bare", "--initial-branch=main", bareDir]);
      const seedDir = await mkdtemp(join(tmpdir(), "wmill_promo_sched_seed_"));
      git(seedDir, "init", "--initial-branch=main");
      git(seedDir, "config", "user.email", "seed@windmill.dev");
      git(seedDir, "config", "user.name", "seed");
      await writeFile(join(seedDir, "README.md"), "# promo schedule test\n");
      git(seedDir, "add", "-A");
      git(seedDir, "commit", "-m", "seed");
      git(seedDir, "remote", "add", "origin", `file://${bareDir}`);
      git(seedDir, "push", "-u", "origin", "main");
      const seedMain = remoteHead(bareDir, "main");

      // --- 2. Workspace content: a script + a (disabled) schedule for it ---
      await backend.apiRequest!(`/api/w/${ws}/folders/create`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ name: "promo", owners: [], extra_perms: {} }),
      });
      await backend.apiRequest!(`/api/w/${ws}/scripts/create`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          path: "f/promo/foo",
          summary: "",
          description: "",
          content: "export async function main() { return 1 }",
          language: "bun",
        }),
      });
      const schedRes = await backend.apiRequest!(`/api/w/${ws}/schedules/create`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          path: "f/promo/sched",
          schedule: "0 0 12 * * *",
          timezone: "UTC",
          script_path: "f/promo/foo",
          is_flow: false,
          args: {},
          enabled: false,
        }),
      });
      expect(schedRes.status).toBe(200);

      // --- 3. git_repository resource + git-sync config (schedules included) ---
      await backend.apiRequest!(`/api/w/${ws}/resources/create`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          path: "u/test/promo_repo",
          resource_type: "git_repository",
          value: { url: `file://${bareDir}`, branch: "main", token: "" },
        }),
      });
      await backend.updateGitSyncConfig!({
        git_sync_settings: {
          repositories: [
            {
              git_repo_resource_path: "u/test/promo_repo",
              script_path: "f/**",
              use_individual_branch: true,
              group_by_folder: false,
              settings: {
                include_path: ["f/**"],
                include_type: ["script", "schedule"],
              },
            },
          ],
        },
      });

      const deployItems = JSON.stringify([
        {
          path_type: "schedule",
          path: "f/promo/sched",
          commit_msg: "deploy sched",
        },
      ]);

      const work = await mkdtemp(join(tmpdir(), "wmill_promo_sched_work_"));
      git(work, "clone", `file://${bareDir}`, ".");
      // Option B semantic: in promotion mode the deploy forces NOTHING — the
      // schedule lands only because THIS target's effective wmill.yaml opts
      // schedules in. (Reverting the source fix re-introduces the force-`false`
      // that clobbers this `includeSchedules: true`, so the file is dropped.)
      await writeFile(
        join(work, "wmill.yaml"),
        "defaultTs: bun\nincludes:\n  - f/**\nexcludes: []\nincludeSchedules: true\n",
      );
      const res = await backend.runCLICommand(
        [
          "sync",
          "git-deploy",
          "--repository",
          "u/test/promo_repo",
          "--use-individual-branch",
          "--git-deploy-items",
          deployItems,
        ],
        work,
      );
      expect(res.code).toBe(0);

      // Caller-half (mirrors the hub script): stage what the pull wrote, commit
      // on the checked-out wm_deploy branch, push.
      git(work, "config", "user.email", "test@windmill.dev");
      git(work, "config", "user.name", "test");
      git(work, "add", "-A");
      try {
        git(work, "diff", "--cached", "--quiet");
      } catch {
        git(work, "commit", "-m", "deploy sched");
      }
      git(work, "push", "--porcelain", "-u", "origin", "HEAD");

      const expectedBranch = `refs/heads/wm_deploy/${ws}/schedule/f__promo__sched`;
      expect(remoteBranches(bareDir)).toContain(expectedBranch);
      // The regression: the schedule file MUST be present on the branch. Without
      // the fix the include flag is false, the server strips the schedule from
      // the tarball, the pull writes nothing, and this file is absent.
      expect(
        fileExistsOnBranch(
          bareDir,
          `wm_deploy/${ws}/schedule/f__promo__sched`,
          "f/promo/sched.schedule.yaml",
        ),
      ).toBe(true);
      // Base branch untouched (individual-branch never pushes to the base).
      expect(remoteHead(bareDir, "main")).toBe(seedMain);

      await rm(bareDir, { recursive: true, force: true });
      await rm(seedDir, { recursive: true, force: true });
      await rm(work, { recursive: true, force: true });
    });
  },
);

/**
 * Regression test for WIN-2052: a script with companion modules
 * (workflows-as-code) is stored on disk under a `__mod/` folder
 * (`<path>__mod/script.ts`, `<path>__mod/script.yaml`, `<path>__mod/<module>`)
 * — NOT under the dotted `<path>.script.*` layout a module-less script uses.
 *
 * Root cause: `gitSyncIncludePattern`'s script (default) case returned only
 * `<path>.*`, which the `ignoreF` extra-includes filter (minimatch) does NOT
 * match against any `__mod/` file (literal dot, no dot after the name). So the
 * pull filtered out every module file, the wm_deploy branch was created with
 * nothing staged, and production's `git add '<path>**'` failed with "pathspec
 * did not match any files". The fix adds `<path>__mod/**` to the pattern.
 *
 * The nested `utils/math.ts` module guards the `**` (vs `*`) choice: a single
 * star would not match files in module subdirectories.
 *
 * Without the fix this test fails: the branch exists but the `__mod/` files
 * (entry point + modules) are absent from it.
 */
test.skipIf(shouldSkipOnCI())(
  "git-sync promotion: use_individual_branch lands a module script's __mod/ files on the wm_deploy branch",
  async () => {
    await withTestBackend(async (backend) => {
      const ws = backend.workspace; // "test"
      await addWorkspace(
        {
          remote: backend.baseUrl,
          workspaceId: ws,
          name: ws,
          token: backend.token,
        } as any,
        { force: true, configDir: backend.testConfigDir },
      );

      // --- 1. Bare "remote" seeded with an initial `main` commit ---
      const bareDir = await mkdtemp(join(tmpdir(), "wmill_promo_mod_bare_"));
      execFileSync("git", ["init", "--bare", "--initial-branch=main", bareDir]);
      const seedDir = await mkdtemp(join(tmpdir(), "wmill_promo_mod_seed_"));
      git(seedDir, "init", "--initial-branch=main");
      git(seedDir, "config", "user.email", "seed@windmill.dev");
      git(seedDir, "config", "user.name", "seed");
      await writeFile(join(seedDir, "README.md"), "# promo module test\n");
      git(seedDir, "add", "-A");
      git(seedDir, "commit", "-m", "seed");
      git(seedDir, "remote", "add", "origin", `file://${bareDir}`);
      git(seedDir, "push", "-u", "origin", "main");
      const seedMain = remoteHead(bareDir, "main");

      // --- 2. Workspace content: a script WITH companion modules (one flat,
      //        one nested) — this is what triggers the `__mod/` folder layout. ---
      await backend.apiRequest!(`/api/w/${ws}/folders/create`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ name: "promo", owners: [], extra_perms: {} }),
      });
      const scriptRes = await backend.apiRequest!(`/api/w/${ws}/scripts/create`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          path: "f/promo/mod_script",
          summary: "",
          description: "",
          content: "export async function main() { return 1 }",
          language: "bun",
          modules: {
            "helper.ts": {
              content: "export function help() { return 2 }\n",
              language: "bun",
            },
            "utils/math.ts": {
              content: "export function add(a: number, b: number) { return a + b }\n",
              language: "bun",
            },
          },
        }),
      });
      expect(scriptRes.status).toBe(201);

      // --- 3. git_repository resource + git-sync config (scripts included) ---
      await backend.apiRequest!(`/api/w/${ws}/resources/create`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          path: "u/test/promo_repo",
          resource_type: "git_repository",
          value: { url: `file://${bareDir}`, branch: "main", token: "" },
        }),
      });
      await backend.updateGitSyncConfig!({
        git_sync_settings: {
          repositories: [
            {
              git_repo_resource_path: "u/test/promo_repo",
              script_path: "f/**",
              use_individual_branch: true,
              group_by_folder: false,
              settings: {
                include_path: ["f/**"],
                include_type: ["script"],
              },
            },
          ],
        },
      });

      // A script always has path_type "script" — modules are a property of the
      // script, not a distinct object kind — so this hits gitSyncIncludePattern's
      // default case, the one the fix amends.
      const deployItems = JSON.stringify([
        {
          path_type: "script",
          path: "f/promo/mod_script",
          commit_msg: "deploy mod_script",
        },
      ]);

      const work = await mkdtemp(join(tmpdir(), "wmill_promo_mod_work_"));
      git(work, "clone", `file://${bareDir}`, ".");
      await writeFile(
        join(work, "wmill.yaml"),
        "defaultTs: bun\nincludes:\n  - f/**\nexcludes: []\n",
      );
      const res = await backend.runCLICommand(
        [
          "sync",
          "git-deploy",
          "--repository",
          "u/test/promo_repo",
          "--use-individual-branch",
          "--git-deploy-items",
          deployItems,
        ],
        work,
      );
      expect(res.code).toBe(0);

      // Caller-half (mirrors the hub script): stage what the pull wrote, commit
      // on the checked-out wm_deploy branch, push.
      git(work, "config", "user.email", "test@windmill.dev");
      git(work, "config", "user.name", "test");
      git(work, "add", "-A");
      try {
        git(work, "diff", "--cached", "--quiet");
      } catch {
        git(work, "commit", "-m", "deploy mod_script");
      }
      git(work, "push", "--porcelain", "-u", "origin", "HEAD");

      const branch = `wm_deploy/${ws}/script/f__promo__mod_script`;
      expect(remoteBranches(bareDir)).toContain(`refs/heads/${branch}`);
      // The regression: every `__mod/` file MUST be present on the branch — the
      // entry point, the flat module, and the nested module. Without the fix the
      // extra-includes pattern matches none of them, the pull writes nothing, and
      // these files are absent (and `git add` would have failed in production).
      for (const f of [
        "f/promo/mod_script__mod/script.ts",
        "f/promo/mod_script__mod/helper.ts",
        "f/promo/mod_script__mod/utils/math.ts",
      ]) {
        expect(fileExistsOnBranch(bareDir, branch, f)).toBe(true);
      }
      // Base branch untouched (individual-branch never pushes to the base).
      expect(remoteHead(bareDir, "main")).toBe(seedMain);

      await rm(bareDir, { recursive: true, force: true });
      await rm(seedDir, { recursive: true, force: true });
      await rm(work, { recursive: true, force: true });
    });
  },
);

/**
 * Regression test for WIN-1997: forking a workspace with git sync configured
 * must publish a `wm-fork/<branch>/<id>` branch to the remote.
 *
 * The fork-branch callback runs the sync script with `only_create_branch:
 * true` and no items. The hub script delegates branch checkout + push of that
 * empty ref to `wmill sync git-deploy --only-create-branch` — its own
 * in-process commit+push runs ONLY for the `!only_create_branch` path. So if
 * the CLI doesn't push the freshly checked-out branch here, nothing does and
 * the fork branch never reaches the remote (the symptom that broke the e2e
 * test after #9284 moved commit+push to the caller). This guards that the CLI
 * owns the push for the branch-only case.
 */
test.skipIf(shouldSkipOnCI())(
  "git-sync fork: only_create_branch publishes the wm-fork branch (CLI owns the push)",
  async () => {
    await withTestBackend(async (backend) => {
      // Bare "remote" seeded with an initial `main` commit.
      const bareDir = await mkdtemp(join(tmpdir(), "wmill_fork_bare_"));
      execFileSync("git", ["init", "--bare", "--initial-branch=main", bareDir]);
      const seedDir = await mkdtemp(join(tmpdir(), "wmill_fork_seed_"));
      git(seedDir, "init", "--initial-branch=main");
      git(seedDir, "config", "user.email", "seed@windmill.dev");
      git(seedDir, "config", "user.name", "seed");
      await writeFile(join(seedDir, "README.md"), "# fork test\n");
      git(seedDir, "add", "-A");
      git(seedDir, "commit", "-m", "seed");
      git(seedDir, "remote", "add", "origin", `file://${bareDir}`);
      git(seedDir, "push", "-u", "origin", "main");
      const seedMain = remoteHead(bareDir, "main");

      // The CWD the hub script runs git-deploy in: a clone of the repo on main.
      const work = await mkdtemp(join(tmpdir(), "wmill_fork_work_"));
      git(work, "clone", `file://${bareDir}`, ".");
      await writeFile(
        join(work, "wmill.yaml"),
        "defaultTs: bun\nincludes:\n  - f/**\nexcludes: []\n",
      );

      // Branch creation happens BEFORE the fork workspace exists (step 1 of the
      // fork flow), so we pass the fork workspace id straight through — whoami
      // returns synthetic superadmin info for it. No items, only_create_branch.
      const forkWs = "wm-fork-clitest";
      const res = await backend.runCLICommand(
        [
          "sync",
          "git-deploy",
          "--repository",
          "u/test/unused_on_branch_only_path",
          "--git-deploy-items",
          "[]",
          "--only-create-branch",
        ],
        work,
        { workspace: forkWs },
      );
      expect(res.code).toBe(0);

      // The regression: with NO caller-side commit/push, the fork branch must
      // already be on the remote because the CLI pushed it.
      expect(remoteBranches(bareDir)).toContain("refs/heads/wm-fork/main/clitest");
      // Base branch untouched — branch-only publish creates no commit.
      expect(remoteHead(bareDir, "main")).toBe(seedMain);

      await rm(bareDir, { recursive: true, force: true });
      await rm(seedDir, { recursive: true, force: true });
      await rm(work, { recursive: true, force: true });
    });
  },
);
