/**
 * @file Tests for lane lifecycle operations: preflight (what will be lost?),
 * reset, remove, and purge. Each operation is preceded by preflight to confirm
 * the counts and blocks before destructive action.
 * @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
 */

const { describe, it, before, after } = require("node:test");
const assert = require("node:assert/strict");
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const http = require("node:http");
const { execFileSync } = require("node:child_process");
const { PassThrough } = require("node:stream");
const { EventEmitter } = require("node:events");

const TEST_DB = path.join(os.tmpdir(), `dashboard-lifecycle-${Date.now()}-${process.pid}.db`);
process.env.DASHBOARD_DB_PATH = TEST_DB;
process.env.DASHBOARD_REMOTE_SYNC_MS = "0";
process.env.DASHBOARD_LIVENESS_PROBE = "0";

const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-lifecycle-"));
process.env.LANES_ROOT = path.join(ROOT, "lanes");

const { createApp, startServer } = require("../index");
const runs = require("../lib/pty-run");

let server;
let BASE;

function request(method, urlPath, body, extraHeaders = {}) {
  return new Promise((resolve, reject) => {
    const url = new URL(urlPath, BASE);
    const payload = body ? JSON.stringify(body) : null;
    const req = http.request(
      {
        hostname: url.hostname,
        port: url.port,
        path: url.pathname + url.search,
        method,
        headers: {
          ...(payload
            ? { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(payload) }
            : {}),
          ...extraHeaders,
        },
      },
      (res) => {
        let data = "";
        res.on("data", (c) => (data += c));
        res.on("end", () => {
          let parsed = null;
          try {
            parsed = JSON.parse(data);
          } catch {
            /* non-JSON body */
          }
          resolve({ status: res.statusCode, body: parsed });
        });
      }
    );
    req.on("error", reject);
    if (payload) req.write(payload);
    req.end();
  });
}

function makeRunChild({ exitsOnKill }) {
  const child = new EventEmitter();
  child.stdout = new PassThrough();
  child.stderr = new PassThrough();
  child.stdin = new PassThrough();
  child.killed = false;
  child.kill = function (signal) {
    if (signal === "SIGTERM") this.killed = true;
    if (exitsOnKill) setImmediate(() => this.emit("exit", 143, signal));
    return true;
  };
  return child;
}

// Puts a fake `claude` binary on PATH so a real `/start` spawns a real tmux
// session running THIS script instead of the system Claude Code CLI. Tests
// that mock tmux's own exec calls (to simulate a stuck/live session) still
// spawn this real process underneath — without the stub, that spawn launches
// the actual `claude` binary and, because the mock replaces the app's own
// kill-session call, the real process is never actually terminated, leaking
// a live tmux session + CLI process for good. Returns the restore function.
function stubClaudeBinary(name) {
  const bin = path.join(ROOT, `${name}-bin`);
  const claude = path.join(bin, "claude");
  fs.mkdirSync(bin, { recursive: true });
  fs.writeFileSync(
    claude,
    "#!/usr/bin/env node\nprocess.on('SIGTERM', () => process.exit(0));\nsetInterval(() => {}, 1000);\n"
  );
  fs.chmodSync(claude, 0o755);
  const originalPath = process.env.PATH;
  process.env.PATH = `${bin}${path.delimiter}${originalPath}`;
  return () => {
    process.env.PATH = originalPath;
  };
}

async function waitForProvisioning(id) {
  const deadline = Date.now() + 5000;
  let response;
  while (Date.now() < deadline) {
    response = await request("GET", `/api/lanes/${id}`);
    if (response.body.lane.status !== "provisioning") return response.body.lane;
    await new Promise((resolve) => setTimeout(resolve, 50));
  }
  assert.fail(
    `lane ${id} was still provisioning after 5 seconds; last response: ${JSON.stringify(response.body)}`
  );
}

// Git helper with clean environment (no hook vars)
const SRC = path.join(ROOT, "src-repo");
const g = (cwd, ...args) => {
  const env = { ...process.env };
  delete env.GIT_DIR;
  delete env.GIT_WORK_TREE;
  delete env.GIT_INDEX_FILE;
  delete env.GIT_COMMON_DIR;
  delete env.GIT_OBJECT_DIRECTORY;
  delete env.GIT_ALTERNATE_OBJECT_DIRECTORIES;
  delete env.GIT_PREFIX;
  delete env.GIT_NAMESPACE;
  delete env.GIT_CONFIG_PARAMETERS;
  env.GIT_TERMINAL_PROMPT = "0";
  return execFileSync("git", args, { cwd, encoding: "utf8", env });
};

before(async () => {
  // Create source repo
  fs.mkdirSync(SRC, { recursive: true });
  g(SRC, "init", "-b", "main");
  g(SRC, "config", "user.email", "test@example.com");
  g(SRC, "config", "user.name", "Test");
  fs.writeFileSync(path.join(SRC, "README.md"), "hello\n");
  fs.writeFileSync(path.join(SRC, ".gitignore"), "node_modules/\n.env\n");
  g(SRC, "add", "-A");
  g(SRC, "commit", "-m", "init");

  // Start server
  const app = createApp();
  server = await startServer(app, 0);
  BASE = `http://127.0.0.1:${server.address().port}`;
});

after(() => {
  if (server) server.close();
  fs.rmSync(ROOT, { recursive: true, force: true });
});

describe("preflight", () => {
  it("returns 404 for an unknown lane", async () => {
    const r = await request("GET", "/api/lanes/99999/preflight?action=reset");
    assert.equal(r.status, 404);
    assert.ok(r.body.error);
  });

  it("returns 400 for an unknown action", async () => {
    const c = await request("POST", "/api/lanes", { cwd: "/tmp/preflight-unknown-action" });
    const id = c.body.lane.id;
    const r = await request("GET", `/api/lanes/${id}/preflight?action=frobnicate`);
    assert.equal(r.status, 400);
    assert.ok(r.body.error);
    await request("DELETE", `/api/lanes/${id}`);
  });

  it("preflight on an adopted lane blocks with 'adopted'", async () => {
    const c = await request("POST", "/api/lanes", {
      cwd: "/tmp/preflight-adopted",
      kind: "adopted",
    });
    const id = c.body.lane.id;
    const r = await request("GET", `/api/lanes/${id}/preflight?action=reset`);
    assert.equal(r.status, 200);
    assert.ok(r.body.blocked);
    assert.ok(r.body.blocked.includes("adopted"));
    await request("DELETE", `/api/lanes/${id}`);
  });

  it("preflight counts dirty, untracked and unpushed for a managed lane", async () => {
    // Create a managed lane with a worktree
    const wt = require("../lib/worktree");
    const dir = path.join(process.env.LANES_ROOT, "src-repo__test-dirty");

    // Create the worktree
    await wt.addWorktree({
      sourceRepo: SRC,
      dir,
      branch: "feat/test-dirty",
      base: "main",
    });

    // Create the lane
    const c = await request("POST", "/api/lanes", {
      cwd: dir,
      kind: "managed",
      source_repo: SRC,
      base_branch: "main",
      branch: "feat/test-dirty",
      slug: "test-dirty",
    });
    const id = c.body.lane.id;

    // One commit of the lane's own work, so `unpushed` has something honest to
    // count: with no remote it is measured as base_branch..HEAD, not the whole
    // repository history.
    fs.writeFileSync(path.join(dir, "lane-work.txt"), "lane work\n");
    g(dir, "add", "lane-work.txt");
    g(dir, "commit", "-m", "lane work");

    // Dirty the worktree
    fs.appendFileSync(path.join(dir, "README.md"), "edit\n");
    fs.writeFileSync(path.join(dir, "scratch.txt"), "untracked\n");

    // Get preflight
    const r = await request("GET", `/api/lanes/${id}/preflight?action=reset`);
    assert.equal(r.status, 200);
    assert.equal(r.body.action, "reset");
    assert.equal(r.body.dirty, 1);
    assert.equal(r.body.untracked, 1);
    // Exactly the one commit ahead of main, not main's own commit as well.
    assert.equal(r.body.unpushed, 1);
    assert.ok(r.body.head);
    assert.ok(Array.isArray(r.body.blocked));
    // "unpushed-commits" is a hard blocker (force overrides it); "no-remote" is
    // informational only and must not appear in blocked.
    assert.ok(r.body.blocked.includes("unpushed-commits"));
    assert.ok(!r.body.blocked.includes("no-remote"));
    assert.ok(Array.isArray(r.body.warnings));
    assert.ok(r.body.warnings.includes("no-remote"));

    await request("DELETE", `/api/lanes/${id}`);
  });

  it("preflight for purge counts only eligible sessions and dependent rows", async () => {
    const cwd = "/tmp/preflight-purge";
    const c = await request("POST", "/api/lanes", { cwd });
    const id = c.body.lane.id;
    const { db } = require("../db");

    // The completed child is eligible; the active child, lane-bound session, and
    // string-prefix sibling each exercise one of purgeCandidateSessions' exclusions.
    db.prepare("INSERT INTO sessions (id, cwd, status) VALUES (?, ?, ?)").run(
      "preflight-purge-eligible",
      `${cwd}/child`,
      "completed"
    );
    db.prepare("INSERT INTO sessions (id, cwd, status) VALUES (?, ?, ?)").run(
      "preflight-purge-active",
      `${cwd}/active-child`,
      "active"
    );
    db.prepare("INSERT INTO sessions (id, cwd, status) VALUES (?, ?, ?)").run(
      "preflight-purge-bound",
      cwd,
      "completed"
    );
    db.prepare("INSERT INTO sessions (id, cwd, status) VALUES (?, ?, ?)").run(
      "preflight-purge-prefix-sibling",
      `${cwd}-other`,
      "completed"
    );
    db.prepare("UPDATE lanes SET session_id = ? WHERE id = ?").run("preflight-purge-bound", id);
    db.prepare("INSERT INTO events (session_id, event_type) VALUES (?, ?)").run(
      "preflight-purge-eligible",
      "PostToolUse"
    );
    db.prepare("INSERT INTO events (session_id, event_type) VALUES (?, ?)").run(
      "preflight-purge-eligible",
      "Stop"
    );
    db.prepare("INSERT INTO token_usage (session_id, model, input_tokens) VALUES (?, ?, ?)").run(
      "preflight-purge-eligible",
      "test-model",
      1
    );

    const r = await request("GET", `/api/lanes/${id}/preflight?action=purge`);
    assert.equal(r.status, 200);
    assert.equal(r.body.action, "purge");
    assert.equal(r.body.lane, id);
    assert.equal(r.body.sessions, 1);
    assert.equal(r.body.events, 2);
    assert.equal(r.body.tokenRows, 1);
    assert.equal(r.body.activeSessionSkipped, true);

    await request("DELETE", `/api/lanes/${id}`);
  });

  it("preflight returns 'missing' block when the lane directory vanishes", async () => {
    const wt = require("../lib/worktree");
    const dir = path.join(process.env.LANES_ROOT, "src-repo__test-missing");

    // Create the worktree
    await wt.addWorktree({
      sourceRepo: SRC,
      dir,
      branch: "feat/test-missing",
      base: "main",
    });

    // Create the lane
    const c = await request("POST", "/api/lanes", {
      cwd: dir,
      kind: "managed",
      source_repo: SRC,
      base_branch: "main",
      branch: "feat/test-missing",
      slug: "test-missing",
    });
    const id = c.body.lane.id;

    // Remove the directory
    fs.rmSync(dir, { recursive: true, force: true });

    // Get preflight - should report missing but not crash
    const r = await request("GET", `/api/lanes/${id}/preflight?action=reset`);
    assert.equal(r.status, 200);
    assert.ok(r.body.blocked);
    assert.ok(r.body.blocked.includes("missing"));

    await request("DELETE", `/api/lanes/${id}`);
  });

  it("counts only the lane's own commits as unpushed in a local-only repo, not the whole history", async () => {
    // Regression: with no remote, `unpushed` used to be `rev-list --count HEAD`,
    // so a freshly provisioned worktree in a repo with history reported every
    // commit in that repo as unpushed and demanded Force to discard commits a
    // `reset --hard <base>` would never touch.
    const deepRepo = path.join(ROOT, "deep-repo");
    fs.mkdirSync(deepRepo, { recursive: true });
    g(deepRepo, "init", "-b", "main");
    g(deepRepo, "config", "user.email", "test@example.com");
    g(deepRepo, "config", "user.name", "Test");
    for (let i = 1; i <= 5; i += 1) {
      fs.writeFileSync(path.join(deepRepo, `f${i}.txt`), `${i}\n`);
      g(deepRepo, "add", "-A");
      g(deepRepo, "commit", "-m", `commit ${i}`);
    }
    assert.equal(g(deepRepo, "rev-list", "--count", "HEAD").trim(), "5");
    assert.equal(g(deepRepo, "remote").trim(), "");

    const wt = require("../lib/worktree");
    const dir = path.join(process.env.LANES_ROOT, "deep-repo__fresh");
    await wt.addWorktree({ sourceRepo: deepRepo, dir, branch: "feat/fresh", base: "main" });
    const c = await request("POST", "/api/lanes", {
      cwd: dir,
      kind: "managed",
      source_repo: deepRepo,
      base_branch: "main",
      branch: "feat/fresh",
      slug: "fresh",
    });
    const id = c.body.lane.id;

    const fresh = await request("GET", `/api/lanes/${id}/preflight?action=reset`);
    assert.equal(fresh.status, 200);
    // Nothing of this lane's own has been committed yet.
    assert.equal(fresh.body.unpushed, 0);
    assert.ok(!fresh.body.blocked.includes("unpushed-commits"));
    // The user is still told nothing is backed up — that is the warning's job.
    assert.ok(fresh.body.warnings.includes("no-remote"));

    // One commit of its own now counts, and only that one.
    fs.writeFileSync(path.join(dir, "mine.txt"), "mine\n");
    g(dir, "add", "mine.txt");
    g(dir, "commit", "-m", "my work");
    const after = await request("GET", `/api/lanes/${id}/preflight?action=reset`);
    assert.equal(after.body.unpushed, 1);
    assert.ok(after.body.blocked.includes("unpushed-commits"));
    assert.ok(!after.body.blocked.includes("no-remote"));

    await request("DELETE", `/api/lanes/${id}`);
  });
});

describe("managed worktree provisioning", () => {
  it("creates a managed lane, returns 202 provisioning, then flips to idle when the worktree lands", async () => {
    const created = await request("POST", "/api/lanes/worktree", {
      sourceRepo: SRC,
      title: "Managed Lane",
      base: "main",
    });

    assert.equal(created.status, 202);
    assert.equal(created.body.lane.kind, "managed");
    assert.equal(created.body.lane.status, "provisioning");
    assert.equal(created.body.lane.branch, "feat/managed-lane");

    const lane = await waitForProvisioning(created.body.lane.id);
    assert.equal(lane.status, "idle");
    assert.equal(lane.base_branch, "main");
    assert.equal(fs.existsSync(lane.cwd), true);
    assert.equal(g(lane.cwd, "branch", "--show-current").trim(), "feat/managed-lane");
  });

  it("honours LANE_BASE_BRANCH as the default base when the request omits base", async () => {
    g(SRC, "branch", "alt-base");
    process.env.LANE_BASE_BRANCH = "alt-base";
    try {
      const created = await request("POST", "/api/lanes/worktree", {
        sourceRepo: SRC,
        title: "Base Env Lane",
      });
      assert.equal(created.status, 202);
      const lane = await waitForProvisioning(created.body.lane.id);
      assert.equal(lane.status, "idle");
      assert.equal(lane.base_branch, "alt-base");
    } finally {
      delete process.env.LANE_BASE_BRANCH;
    }
  });

  it("honours LANE_BRANCH_PREFIX for the managed lane's feature branch name", async () => {
    process.env.LANE_BRANCH_PREFIX = "wt/";
    try {
      const created = await request("POST", "/api/lanes/worktree", {
        sourceRepo: SRC,
        title: "Prefix Env Lane",
        base: "main",
      });
      assert.equal(created.status, 202);
      assert.equal(created.body.lane.branch, "wt/prefix-env-lane");
      const lane = await waitForProvisioning(created.body.lane.id);
      assert.equal(lane.status, "idle");
      assert.equal(g(lane.cwd, "branch", "--show-current").trim(), "wt/prefix-env-lane");
    } finally {
      delete process.env.LANE_BRANCH_PREFIX;
    }
  });

  it("rejects a sourceRepo that is not an absolute path or not a git repo", async () => {
    const cases = ["relative-repo", path.join(ROOT, "missing-repo"), ROOT];
    for (const sourceRepo of cases) {
      const response = await request("POST", "/api/lanes/worktree", {
        sourceRepo,
        title: "Invalid Source",
      });
      assert.equal(response.status, 400);
      assert.equal(response.body.error.code, "EBADSOURCEREPO");
    }
  });

  it("suffixes the slug when the directory already exists", async () => {
    fs.mkdirSync(path.join(process.env.LANES_ROOT, "src-repo__collision"), { recursive: true });
    const created = await request("POST", "/api/lanes/worktree", {
      sourceRepo: SRC,
      title: "Collision",
      base: "main",
    });

    assert.equal(created.status, 202);
    assert.equal(created.body.lane.slug, "collision-2");
    assert.equal(created.body.lane.cwd, path.join(process.env.LANES_ROOT, "src-repo__collision-2"));

    const lane = await waitForProvisioning(created.body.lane.id);
    assert.equal(lane.status, "idle");
  });

  it("returns 409 when no unique worktree directory is found in 50 attempts", async () => {
    const root = path.join(process.env.LANES_ROOT, "src-repo__collision-cap");
    for (let suffix = 1; suffix <= 51; suffix += 1) {
      const dir = suffix === 1 ? root : `${root}-${suffix}`;
      fs.mkdirSync(dir, { recursive: true });
    }

    const response = await request("POST", "/api/lanes/worktree", {
      sourceRepo: SRC,
      title: "Collision Cap",
      base: "main",
    });

    assert.equal(response.status, 409);
    assert.equal(response.body.error.code, "EWORKTREEDIRCOLLISION");
    assert.equal(
      response.body.error.message,
      "could not allocate a unique worktree directory after 50 attempts"
    );
  });

  it("marks the lane failed with git's message when provisioning fails", async () => {
    const emptyRepo = path.join(ROOT, "empty-repo");
    fs.mkdirSync(emptyRepo, { recursive: true });
    g(emptyRepo, "init", "-b", "main");

    const created = await request("POST", "/api/lanes/worktree", {
      sourceRepo: emptyRepo,
      title: "Unborn Repo",
      base: "main",
    });

    assert.equal(created.status, 202);
    assert.equal(created.body.lane.status, "provisioning");
    const lane = await waitForProvisioning(created.body.lane.id);
    assert.equal(lane.kind, "managed");
    assert.equal(lane.status, "failed");
    assert.match(lane.notes, /fatal:|ambiguous argument|unknown revision/i);
    assert.equal(fs.existsSync(lane.cwd), false);

    // A failed provisioning never created a worktree, so POST remove correctly
    // refuses it; DELETE remains the non-destructive "forget this lane row" path.
    const removed = await request("DELETE", `/api/lanes/${lane.id}`);
    assert.equal(removed.status, 200);
    const missing = await request("GET", `/api/lanes/${lane.id}`);
    assert.equal(missing.status, 404);
  });

  it("uses a caller-supplied branch name instead of deriving one from the slug", async () => {
    const created = await request("POST", "/api/lanes/worktree", {
      sourceRepo: SRC,
      title: "Custom Branch",
      base: "main",
      branch: "custom/my-branch",
    });

    assert.equal(created.status, 202);
    assert.equal(created.body.lane.branch, "custom/my-branch");
    const lane = await waitForProvisioning(created.body.lane.id);
    assert.equal(lane.status, "idle");
    assert.equal(g(lane.cwd, "branch", "--show-current").trim(), "custom/my-branch");
  });

  it("rejects an invalid caller-supplied branch name before creating anything", async () => {
    const response = await request("POST", "/api/lanes/worktree", {
      sourceRepo: SRC,
      title: "Bad Branch",
      base: "main",
      branch: "not a valid branch..name",
    });

    assert.equal(response.status, 400);
    assert.equal(response.body.error.code, "EBADBRANCH");
  });
});

describe("GET /api/lanes/browse", () => {
  it("lists a directory's immediate subdirectories, marking git repos", async () => {
    const response = await request("GET", `/api/lanes/browse?path=${encodeURIComponent(ROOT)}`);
    assert.equal(response.status, 200);
    assert.equal(response.body.path, ROOT);
    const names = response.body.entries.map((e) => e.name);
    assert.ok(names.includes("src-repo"));
    const srcRepoEntry = response.body.entries.find((e) => e.name === "src-repo");
    assert.equal(srcRepoEntry.isGitRepo, true);
  });

  it("reports the parent directory, or null at the filesystem root", async () => {
    const response = await request("GET", `/api/lanes/browse?path=${encodeURIComponent(ROOT)}`);
    assert.equal(response.body.parent, path.dirname(ROOT));

    const rootResponse = await request("GET", "/api/lanes/browse?path=/");
    assert.equal(rootResponse.body.parent, null);
  });

  it("rejects a path that does not exist or is not a directory", async () => {
    const missing = await request(
      "GET",
      `/api/lanes/browse?path=${encodeURIComponent(path.join(ROOT, "does-not-exist"))}`
    );
    assert.equal(missing.status, 400);
    assert.equal(missing.body.error.code, "ENOTFOUND");

    const filePath = path.join(ROOT, "a-file.txt");
    fs.writeFileSync(filePath, "hi\n");
    const notADir = await request("GET", `/api/lanes/browse?path=${encodeURIComponent(filePath)}`);
    assert.equal(notADir.status, 400);
    assert.equal(notADir.body.error.code, "ENOTADIR");
  });
});

describe("destructive lane lifecycle actions", () => {
  async function createManagedLane(slug) {
    const wt = require("../lib/worktree");
    const dir = path.join(process.env.LANES_ROOT, `src-repo__${slug}`);
    const branch = `feat/${slug}`;
    await wt.addWorktree({ sourceRepo: SRC, dir, branch, base: "main" });
    const created = await request("POST", "/api/lanes", {
      cwd: dir,
      kind: "managed",
      source_repo: SRC,
      base_branch: "main",
      branch,
      slug,
    });
    assert.equal(created.status, 201);
    return created.body.lane;
  }

  function destructiveExpect(report) {
    return {
      head: report.head,
      dirty: report.dirty,
      untracked: report.untracked,
      unpushed: report.unpushed,
    };
  }

  it("reset requires confirm, restores the branch from base and clears lane state", async () => {
    const lane = await createManagedLane("reset-clean");
    fs.appendFileSync(path.join(lane.cwd, "README.md"), "changed\n");
    fs.writeFileSync(path.join(lane.cwd, "untracked.txt"), "remove me\n");
    fs.mkdirSync(path.join(lane.cwd, "node_modules"));
    fs.writeFileSync(path.join(lane.cwd, "node_modules", "preserved.txt"), "keep me\n");
    const preflight = await request("GET", `/api/lanes/${lane.id}/preflight?action=reset`);

    const unconfirmed = await request("POST", `/api/lanes/${lane.id}/reset`, {});
    assert.equal(unconfirmed.status, 400);
    assert.equal(unconfirmed.body.error.code, "ECONFIRM");

    const reset = await request("POST", `/api/lanes/${lane.id}/reset`, {
      confirm: true,
      force: true,
      expect: destructiveExpect(preflight.body),
    });
    assert.equal(reset.status, 200);
    assert.equal(reset.body.lane.stage, "idle");
    assert.equal(reset.body.lane.status, "idle");
    assert.equal(reset.body.lane.run_id, null);
    assert.equal(fs.readFileSync(path.join(lane.cwd, "README.md"), "utf8"), "hello\n");
    assert.equal(fs.existsSync(path.join(lane.cwd, "untracked.txt")), false);
    assert.equal(
      fs.readFileSync(path.join(lane.cwd, "node_modules", "preserved.txt"), "utf8"),
      "keep me\n"
    );
  });

  it("reset refuses with 409 when unpushed commits exist, and proceeds with force", async () => {
    const lane = await createManagedLane("reset-force");
    fs.writeFileSync(path.join(lane.cwd, "committed.txt"), "commit me\n");
    g(lane.cwd, "add", "committed.txt");
    g(lane.cwd, "commit", "-m", "unpushed work");
    const preflight = await request("GET", `/api/lanes/${lane.id}/preflight?action=reset`);
    // One commit ahead of main — main's own commit is the base, not lane work.
    assert.equal(preflight.body.unpushed, 1);

    const refused = await request("POST", `/api/lanes/${lane.id}/reset`, {
      confirm: true,
      expect: destructiveExpect(preflight.body),
    });
    assert.equal(refused.status, 409);
    assert.equal(refused.body.error.code, "EUNPUSHED");

    const reset = await request("POST", `/api/lanes/${lane.id}/reset`, {
      confirm: true,
      force: true,
      expect: destructiveExpect(preflight.body),
    });
    assert.equal(reset.status, 200);
    assert.equal(g(lane.cwd, "rev-list", "--count", "HEAD").trim(), "1");
  });

  it("reset returns 409 ESTALE when the head moved since preflight", async () => {
    const lane = await createManagedLane("reset-stale");
    const preflight = await request("GET", `/api/lanes/${lane.id}/preflight?action=reset`);
    fs.writeFileSync(path.join(lane.cwd, "head-moved.txt"), "moved\n");
    g(lane.cwd, "add", "head-moved.txt");
    g(lane.cwd, "commit", "-m", "move head");

    const reset = await request("POST", `/api/lanes/${lane.id}/reset`, {
      confirm: true,
      force: true,
      expect: destructiveExpect(preflight.body),
    });
    assert.equal(reset.status, 409);
    assert.equal(reset.body.error.code, "ESTALE");
    assert.equal(g(lane.cwd, "log", "-1", "--format=%s").trim(), "move head");
  });

  it("remove tears down the worktree and the branch, and deletes the lane row", async () => {
    const lane = await createManagedLane("remove-managed");
    const preflight = await request("GET", `/api/lanes/${lane.id}/preflight?action=remove`);
    const removed = await request("POST", `/api/lanes/${lane.id}/remove`, {
      confirm: true,
      force: true,
      expect: destructiveExpect(preflight.body),
    });
    assert.equal(removed.status, 200);
    assert.equal(removed.body.ok, true);
    assert.equal(fs.existsSync(lane.cwd), false);
    assert.equal(g(SRC, "branch", "--list", lane.branch).trim(), "");
    const missing = await request("GET", `/api/lanes/${lane.id}`);
    assert.equal(missing.status, 404);
  });

  it("reset refuses an adopted lane, while remove forgets its row without touching its directory", async () => {
    const adoptedDir = path.join(ROOT, "adopted-preserved");
    fs.mkdirSync(adoptedDir, { recursive: true });
    const adoptedFile = path.join(adoptedDir, "do-not-delete.txt");
    fs.writeFileSync(adoptedFile, "real project contents\n");
    const created = await request("POST", "/api/lanes", { cwd: adoptedDir });
    const lane = created.body.lane;
    const resetPreflight = await request("GET", `/api/lanes/${lane.id}/preflight?action=reset`);
    const reset = await request("POST", `/api/lanes/${lane.id}/reset`, {
      confirm: true,
      force: true,
      expect: destructiveExpect(resetPreflight.body),
    });
    assert.equal(reset.status, 400);
    assert.equal(reset.body.error.code, "ENOTMANAGED");
    assert.equal(fs.existsSync(adoptedDir), true);
    assert.equal(fs.readFileSync(adoptedFile, "utf8"), "real project contents\n");

    const removePreflight = await request("GET", `/api/lanes/${lane.id}/preflight?action=remove`);
    const removed = await request("POST", `/api/lanes/${lane.id}/remove`, {
      confirm: true,
      force: true,
      expect: destructiveExpect(removePreflight.body),
    });
    assert.equal(removed.status, 200);
    assert.equal((await request("GET", `/api/lanes/${lane.id}`)).status, 404);
    assert.equal(fs.existsSync(adoptedDir), true);
    assert.equal(fs.readFileSync(adoptedFile, "utf8"), "real project contents\n");
  });

  it("forgets an adopted lane with unpushed commits without force, while managed removal still requires it", async () => {
    const adoptedDir = path.join(ROOT, "adopted-unpushed");
    fs.mkdirSync(adoptedDir, { recursive: true });
    g(adoptedDir, "init");
    g(adoptedDir, "config", "user.email", "test@example.com");
    g(adoptedDir, "config", "user.name", "Test User");
    fs.writeFileSync(path.join(adoptedDir, "README.md"), "adopted\n");
    g(adoptedDir, "add", "README.md");
    g(adoptedDir, "commit", "-m", "adopted commit");
    const adopted = (await request("POST", "/api/lanes", { cwd: adoptedDir })).body.lane;
    const adoptedPreflight = await request(
      "GET",
      `/api/lanes/${adopted.id}/preflight?action=remove`
    );
    assert.ok(adoptedPreflight.body.unpushed > 0);
    const forgotten = await request("POST", `/api/lanes/${adopted.id}/remove`, {
      confirm: true,
      expect: destructiveExpect(adoptedPreflight.body),
    });
    assert.equal(forgotten.status, 200);
    assert.equal(fs.existsSync(adoptedDir), true);

    const managed = await createManagedLane("remove-unpushed-force");
    fs.writeFileSync(path.join(managed.cwd, "committed.txt"), "commit me\n");
    g(managed.cwd, "add", "committed.txt");
    g(managed.cwd, "commit", "-m", "unpushed managed work");
    const managedPreflight = await request(
      "GET",
      `/api/lanes/${managed.id}/preflight?action=remove`
    );
    assert.ok(managedPreflight.body.unpushed > 0);
    const refused = await request("POST", `/api/lanes/${managed.id}/remove`, {
      confirm: true,
      expect: destructiveExpect(managedPreflight.body),
    });
    assert.equal(refused.status, 409);
    assert.equal(refused.body.error.code, "EUNPUSHED");
    await request("DELETE", `/api/lanes/${managed.id}`);
  });

  it("requires a complete expect object and returns stale diagnostics", async () => {
    const lane = await createManagedLane("expect-required");
    const resetPreflight = await request("GET", `/api/lanes/${lane.id}/preflight?action=reset`);
    const omitted = await request("POST", `/api/lanes/${lane.id}/reset`, {
      confirm: true,
      force: true,
    });
    assert.equal(omitted.status, 400);
    assert.equal(omitted.body.error.code, "EEXPECT");
    const partial = await request("POST", `/api/lanes/${lane.id}/reset`, {
      confirm: true,
      force: true,
      expect: { head: resetPreflight.body.head },
    });
    assert.equal(partial.status, 400);
    assert.equal(partial.body.error.code, "EEXPECT");

    fs.writeFileSync(path.join(lane.cwd, "changed-after-preflight.txt"), "changed\n");
    const stale = await request("POST", `/api/lanes/${lane.id}/reset`, {
      confirm: true,
      force: true,
      expect: destructiveExpect(resetPreflight.body),
    });
    assert.equal(stale.status, 409);
    assert.equal(stale.body.error.code, "ESTALE");
    assert.deepEqual(stale.body.error.expected, destructiveExpect(resetPreflight.body));
    assert.equal(stale.body.error.current.untracked, 1);

    const purgePreflight = await request("GET", `/api/lanes/${lane.id}/preflight?action=purge`);
    const purgePartial = await request("POST", `/api/lanes/${lane.id}/purge`, {
      confirm: true,
      expect: { sessions: purgePreflight.body.sessions, events: purgePreflight.body.events },
    });
    assert.equal(purgePartial.status, 400);
    assert.equal(purgePartial.body.error.code, "EEXPECT");
    await request("DELETE", `/api/lanes/${lane.id}`);
  });

  it("waits for the tmux session to exit before resetting its worktree", async () => {
    const lane = await createManagedLane("await-real-exit");
    fs.writeFileSync(path.join(lane.cwd, "written-by-run.txt"), "run output\n");
    const bin = path.join(ROOT, "run-exit-bin");
    const claude = path.join(bin, "claude");
    fs.mkdirSync(bin, { recursive: true });
    fs.writeFileSync(
      claude,
      "#!/usr/bin/env node\nprocess.on('SIGTERM', () => process.exit(0));\nsetInterval(() => {}, 1000);\n"
    );
    fs.chmodSync(claude, 0o755);
    const originalPath = process.env.PATH;
    process.env.PATH = `${bin}${path.delimiter}${originalPath}`;
    try {
      const started = await request("POST", `/api/lanes/${lane.id}/start`, {
        prompt: "keep running",
      });
      assert.equal(started.status, 200);
      const running = await request("GET", `/api/lanes/${lane.id}`);
      const runId = running.body.lane.run_id;
      assert.equal(typeof runId, "string");
      const preflight = await request("GET", `/api/lanes/${lane.id}/preflight?action=reset`);

      const reset = await request("POST", `/api/lanes/${lane.id}/reset`, {
        confirm: true,
        force: true,
        expect: destructiveExpect(preflight.body),
      });
      assert.equal(reset.status, 200);
      assert.equal(runs.getRun(runId).status, "gone");
      assert.equal(fs.existsSync(path.join(lane.cwd, "written-by-run.txt")), false);
    } finally {
      process.env.PATH = originalPath;
    }
    await request("DELETE", `/api/lanes/${lane.id}`);
  });

  it("returns ERUNTIMEOUT and leaves the worktree untouched when a tmux session never exits", async () => {
    const tmux = require("../lib/tmux");
    const lane = await createManagedLane("await-timeout");
    const sentinel = path.join(lane.cwd, "must-survive-timeout.txt");
    fs.writeFileSync(sentinel, "still here\n");

    // Start a run for the lane
    const restorePath = stubClaudeBinary("await-timeout");
    const started = await request("POST", `/api/lanes/${lane.id}/start`, { prompt: "stuck" });
    assert.equal(started.status, 200);
    const runId = started.body.lane.run_id;

    // Mock tmux so has-session always returns 0 (session exists), simulating a stuck session
    tmux.__setExecImpl(async (cmd, args) => {
      if (cmd === "tmux" && args[0] === "has-session" && args[1] === "-t" && args[2] === runId) {
        return { code: 0, stdout: "", stderr: "" };
      }
      if (cmd === "tmux" && args[0] === "list-sessions") {
        return { code: 0, stdout: "", stderr: "" };
      }
      if (cmd === "tmux" && args[0] === "kill-session" && args[1] === "-t" && args[2] === runId) {
        return { code: 0, stdout: "", stderr: "" };
      }
      return { code: 1, stdout: "", stderr: "" };
    });

    try {
      const preflight = await request("GET", `/api/lanes/${lane.id}/preflight?action=reset`);
      const reset = await request("POST", `/api/lanes/${lane.id}/reset`, {
        confirm: true,
        force: true,
        expect: destructiveExpect(preflight.body),
      });
      assert.equal(reset.status, 500);
      assert.equal(reset.body.error.code, "ERUNTIMEOUT");
      assert.equal(fs.readFileSync(sentinel, "utf8"), "still here\n");
    } finally {
      tmux.__reset();
      // The mocked kill-session above only fools the app's own check — the
      // real tmux session + claude stub spawned above is still alive and
      // must be killed for real, or it leaks past this test run.
      try {
        execFileSync("tmux", ["kill-session", "-t", runId], { stdio: "ignore" });
      } catch {
        // already gone
      }
      restorePath();
    }
    await request("DELETE", `/api/lanes/${lane.id}`);
  });

  it("removes a lane whose worktree was deleted by hand, taking the prune path", async () => {
    // The design promises "the lane reports `missing` and only `remove` is
    // offered, taking the prune path". Before this, `remove` hit check 2, which
    // maps a non-existent path to EOUTSIDEROOT → 400, so the lane row could
    // never be removed through the destructive action at all.
    const lane = await createManagedLane("remove-hand-deleted");
    fs.rmSync(lane.cwd, { recursive: true, force: true });

    const preflight = await request("GET", `/api/lanes/${lane.id}/preflight?action=remove`);
    assert.equal(preflight.status, 200);
    assert.ok(preflight.body.blocked.includes("missing"));
    assert.equal(preflight.body.head, null);

    const removed = await request("POST", `/api/lanes/${lane.id}/remove`, {
      confirm: true,
      expect: destructiveExpect(preflight.body),
    });
    assert.equal(removed.status, 200, JSON.stringify(removed.body));
    assert.equal(removed.body.ok, true);
    assert.equal((await request("GET", `/api/lanes/${lane.id}`)).status, 404);
    // git's stale record and the lane's branch are both gone.
    const wt = require("../lib/worktree");
    assert.equal(
      (await wt.listWorktrees(SRC)).some((w) => w.path === lane.cwd),
      false
    );
    assert.equal(g(SRC, "branch", "--list", lane.branch).trim(), "");
  });

  it("refuses a second start while the first run is still live, so no tmux session is orphaned", async () => {
    // Overwriting run_id while its tmux session is alive orphans that session: a later
    // reset kills and awaits only the RECORDED run's session, then `git clean -fd` the
    // directory the orphan session is still using.
    const tmux = require("../lib/tmux");
    const lane = await createManagedLane("start-twice");

    // Start a run for the lane
    const restorePath = stubClaudeBinary("start-twice");
    const started = await request("POST", `/api/lanes/${lane.id}/start`, { prompt: "first" });
    assert.equal(started.status, 200);
    const runId = started.body.lane.run_id;
    assert.equal(runs.getRun(runId).status, "running");

    // Mock tmux so the session appears live
    tmux.__setExecImpl(async (cmd, args) => {
      if (cmd === "tmux" && args[0] === "has-session" && args[1] === "-t" && args[2] === runId) {
        return { code: 0, stdout: "", stderr: "" };
      }
      if (cmd === "tmux" && args[0] === "list-sessions") {
        return { code: 0, stdout: "", stderr: "" };
      }
      return { code: 1, stdout: "", stderr: "" };
    });

    try {
      // Try to start a second run — should be refused
      const second = await request("POST", `/api/lanes/${lane.id}/start`, { prompt: "me too" });
      assert.equal(second.status, 409);
      assert.equal(second.body.error.code, "ERUNLIVE");
      // The first run is still the recorded one — nothing was overwritten.
      const after = await request("GET", `/api/lanes/${lane.id}`);
      assert.equal(after.body.lane.run_id, runId);
    } finally {
      tmux.__reset();
      // The real tmux session behind the "first" run is never reset/killed
      // in this test, mocked or otherwise — kill it for real so it doesn't
      // leak past this test run.
      try {
        execFileSync("tmux", ["kill-session", "-t", runId], { stdio: "ignore" });
      } catch {
        // already gone
      }
      restorePath();
    }

    await request("DELETE", `/api/lanes/${lane.id}`);
  });

  it("start is serialized behind the per-lane lock, not left atomic only by accident", async () => {
    // The check-then-spawn in the "start" case has no `await` between reading
    // run_id and writing it, so it happens to be atomic today regardless of
    // locking. Wrapping it in withLaneLock is what keeps that true even after
    // a future edit adds one — provable by holding the SAME lock from outside
    // the route and confirming `start` genuinely queues behind it rather than
    // running immediately.
    const { withLaneLock } = require("../lib/lane-lock");
    const lane = await createManagedLane("start-lock-order");

    const bin = path.join(ROOT, "start-lock-bin");
    const claude = path.join(bin, "claude");
    fs.mkdirSync(bin, { recursive: true });
    fs.writeFileSync(
      claude,
      "#!/usr/bin/env node\nprocess.on('SIGTERM', () => process.exit(0));\nsetInterval(() => {}, 1000);\n"
    );
    fs.chmodSync(claude, 0o755);
    const originalPath = process.env.PATH;
    process.env.PATH = `${bin}${path.delimiter}${originalPath}`;

    let releaseHeldLock;
    const held = withLaneLock(
      lane.id,
      () =>
        new Promise((resolve) => {
          releaseHeldLock = resolve;
        })
    );
    // Give the held lock a moment to actually become the chain's current entry.
    await new Promise((resolve) => setTimeout(resolve, 20));

    let startSettled = false;
    const startPromise = request("POST", `/api/lanes/${lane.id}/start`, {
      prompt: "queued behind the held lock",
    }).then((res) => {
      startSettled = true;
      return res;
    });

    await new Promise((resolve) => setTimeout(resolve, 50));
    assert.equal(startSettled, false, "start must still be queued behind the held lock");
    // Nothing was spawned while queued.
    assert.equal((await request("GET", `/api/lanes/${lane.id}`)).body.lane.run_id, null);

    releaseHeldLock();
    await held;
    const started = await startPromise;

    try {
      assert.equal(started.status, 200, JSON.stringify(started.body));
      const runId = started.body.lane.run_id;
      assert.equal(typeof runId, "string");
      runs.killRun(runId);
      const deadline = Date.now() + 2000;
      while (runs.getRun(runId).status !== "gone" && Date.now() < deadline) {
        await new Promise((resolve) => setTimeout(resolve, 10));
      }
      assert.equal(runs.getRun(runId).status, "gone");
    } finally {
      process.env.PATH = originalPath;
    }
    await request("DELETE", `/api/lanes/${lane.id}`);
  });

  it("PATCH with an invalid kind returns 400, not a 500 from Express's default handler", async () => {
    const created = await request("POST", "/api/lanes", { cwd: "/tmp/patch-bad-kind" });
    const id = created.body.lane.id;
    const patched = await request("PATCH", `/api/lanes/${id}`, { kind: "gremlin" });
    assert.equal(patched.status, 400);
    assert.equal(patched.body.error.code, "EBADKIND");
    // And a valid kind is silently ignored rather than rewriting check 1.
    const rejected = await request("PATCH", `/api/lanes/${id}`, { kind: "managed" });
    assert.equal(rejected.status, 200);
    assert.equal(rejected.body.lane.kind, "adopted");
    await request("DELETE", `/api/lanes/${id}`);
  });

  it("purge deletes the lane's sessions and reports the counts", async () => {
    const cwd = path.join(ROOT, "purge-counts");
    const created = await request("POST", "/api/lanes", { cwd });
    const lane = created.body.lane;
    const { db } = require("../db");
    db.prepare("INSERT INTO sessions (id, cwd, status) VALUES (?, ?, ?)").run(
      "purge-action-session",
      cwd,
      "completed"
    );
    db.prepare("INSERT INTO events (session_id, event_type) VALUES (?, ?)").run(
      "purge-action-session",
      "Stop"
    );
    db.prepare("INSERT INTO token_usage (session_id, model, input_tokens) VALUES (?, ?, ?)").run(
      "purge-action-session",
      "test-model",
      3
    );
    const preflight = await request("GET", `/api/lanes/${lane.id}/preflight?action=purge`);
    assert.equal(preflight.body.sessions, 1);
    assert.equal(preflight.body.events, 1);
    assert.equal(preflight.body.tokenRows, 1);

    const purged = await request("POST", `/api/lanes/${lane.id}/purge`, {
      confirm: true,
      expect: {
        sessions: preflight.body.sessions,
        events: preflight.body.events,
        tokenRows: preflight.body.tokenRows,
        bytesEstimate: preflight.body.bytesEstimate,
        activeSessionSkipped: preflight.body.activeSessionSkipped,
      },
    });
    assert.equal(purged.status, 200);
    assert.deepEqual(purged.body.purged, { sessions: 1, events: 1, tokenRows: 1 });
    assert.equal(
      db.prepare("SELECT COUNT(*) AS count FROM sessions WHERE id = ?").get("purge-action-session")
        .count,
      0
    );
    assert.equal(
      db
        .prepare("SELECT COUNT(*) AS count FROM events WHERE session_id = ?")
        .get("purge-action-session").count,
      0
    );
    assert.equal(
      db
        .prepare("SELECT COUNT(*) AS count FROM token_usage WHERE session_id = ?")
        .get("purge-action-session").count,
      0
    );
  });
});

describe("lane ensure, start mode, lane_id and releasing a finished run", () => {
  const { db } = require("../db");

  async function adoptedLane(name) {
    const cwd = path.join(ROOT, `ensure-${name}`);
    fs.mkdirSync(cwd, { recursive: true });
    const created = await request("POST", "/api/lanes/ensure", { cwd, title: name });
    assert.equal(created.status, 201, JSON.stringify(created.body));
    return created.body.lane;
  }

  it("creates an adopted lane when no lane owns the cwd", async () => {
    const cwd = path.join(ROOT, "ensure-fresh");
    const r = await request("POST", "/api/lanes/ensure", { cwd, title: "Fresh" });
    assert.equal(r.status, 201, JSON.stringify(r.body));
    assert.equal(r.body.created, true);
    assert.equal(r.body.lane.cwd, cwd);
    assert.equal(r.body.lane.kind, "adopted");
    assert.equal(r.body.lane.title, "Fresh");
  });

  it("returns the existing lane for an exact cwd without creating a second one", async () => {
    const cwd = path.join(ROOT, "ensure-exact");
    const first = await request("POST", "/api/lanes/ensure", { cwd });
    assert.equal(first.status, 201);
    const second = await request("POST", "/api/lanes/ensure", { cwd, title: "ignored" });
    assert.equal(second.status, 200);
    assert.equal(second.body.created, false);
    assert.equal(second.body.lane.id, first.body.lane.id);
    assert.equal(db.prepare("SELECT COUNT(*) AS count FROM lanes WHERE cwd = ?").get(cwd).count, 1);
  });

  it("returns the owning lane for a cwd nested inside it", async () => {
    const parent = path.join(ROOT, "ensure-parent");
    const nested = path.join(parent, "packages", "app");
    const owner = await request("POST", "/api/lanes/ensure", { cwd: parent });
    assert.equal(owner.status, 201);
    const r = await request("POST", "/api/lanes/ensure", { cwd: nested });
    assert.equal(r.status, 200);
    assert.equal(r.body.created, false);
    assert.equal(r.body.lane.id, owner.body.lane.id);
    assert.equal(r.body.lane.cwd, parent);
    // A path-boundary sibling is NOT owned by it.
    const sibling = await request("POST", "/api/lanes/ensure", { cwd: `${parent}-sibling` });
    assert.equal(sibling.status, 201);
    assert.equal(sibling.body.created, true);
    assert.notEqual(sibling.body.lane.id, owner.body.lane.id);
  });

  it("yields exactly one lane for two concurrent ensure calls on the same path", async () => {
    const cwd = path.join(ROOT, "ensure-concurrent");
    const [a, b] = await Promise.all([
      request("POST", "/api/lanes/ensure", { cwd }),
      request("POST", "/api/lanes/ensure", { cwd }),
    ]);
    assert.equal(db.prepare("SELECT COUNT(*) AS count FROM lanes WHERE cwd = ?").get(cwd).count, 1);
    assert.equal(a.body.lane.id, b.body.lane.id);
    assert.deepEqual([a.status, b.status].sort(), [200, 201]);
    assert.deepEqual([a.body.created, b.body.created].sort(), [false, true]);
  });

  it("rejects a relative cwd with 400 EBADCWD", async () => {
    const r = await request("POST", "/api/lanes/ensure", { cwd: "relative/path" });
    assert.equal(r.status, 400);
    assert.equal(r.body.error.code, "EBADCWD");
  });

  it("refuses a cross-origin ensure", async () => {
    const cwd = path.join(ROOT, "ensure-cross-origin");
    const r = await request("POST", "/api/lanes/ensure", { cwd }, { Origin: "http://evil.test" });
    assert.equal(r.status, 403);
    assert.equal(r.body.error.code, "EBADORIGIN");
    assert.equal(db.prepare("SELECT COUNT(*) AS count FROM lanes WHERE cwd = ?").get(cwd).count, 0);
  });

  it("leaves a lane that already has a live run_id untouched during healing", async () => {
    const tmux = require("../lib/tmux");
    const lane = await adoptedLane("release-moved-on");

    // Create a run for this lane.
    const restorePath = stubClaudeBinary("release-moved-on");
    const started = await request("POST", `/api/lanes/${lane.id}/start`, { prompt: "test" });
    assert.equal(started.status, 200, JSON.stringify(started.body));
    const runId = started.body.lane.run_id;

    // Mock tmux so the run appears to be live.
    tmux.__setExecImpl(async (cmd, args) => {
      if (cmd === "tmux" && args[0] === "has-session" && args[1] === "-t" && args[2] === runId) {
        return { code: 0, stdout: "", stderr: "" };
      }
      if (cmd === "tmux" && args[0] === "list-sessions") {
        return { code: 0, stdout: "", stderr: "" };
      }
      return { code: 1, stdout: "", stderr: "" };
    });

    try {
      // Read the lane — it should NOT clear the run_id since it's still live.
      const before = (await request("GET", `/api/lanes/${lane.id}`)).body.lane;
      assert.equal(before.run_id, runId);
      assert.equal(before.status, "running");

      // Read again — same result, healing preserves live runs.
      const after = (await request("GET", `/api/lanes/${lane.id}`)).body.lane;
      assert.equal(after.run_id, runId);
      assert.equal(after.status, "running");
    } finally {
      tmux.__reset();
      // The app never calls kill-session here (healing preserves the "live"
      // run) — kill the real tmux session directly so it doesn't leak.
      try {
        execFileSync("tmux", ["kill-session", "-t", runId], { stdio: "ignore" });
      } catch {
        // already gone
      }
      restorePath();
    }
  });

  it("clears a stale run_id and sets status to idle when the tmux session is gone", async () => {
    const tmux = require("../lib/tmux");
    const lane = await adoptedLane("release-stale-run");

    // Start a run for this lane.
    const restorePath = stubClaudeBinary("release-stale-run");
    const started = await request("POST", `/api/lanes/${lane.id}/start`, { prompt: "test" });
    assert.equal(started.status, 200, JSON.stringify(started.body));
    const runId = started.body.lane.run_id;
    assert.equal(typeof runId, "string");
    assert.equal(started.body.lane.status, "running");

    // Mock tmux so the session appears to be gone (has-session fails with status 1).
    tmux.__setExecImpl((args) => {
      if (args[0] === "has-session" && args[1] === "-t" && args[2] === runId) {
        const e = new Error("no such session");
        e.status = 1;
        throw e;
      }
      if (args[0] === "list-sessions") {
        return "";
      }
      return "";
    });

    try {
      // Read the lane — it should clear the run_id and set status to idle.
      const after = (await request("GET", `/api/lanes/${lane.id}`)).body.lane;
      assert.equal(after.run_id, null, "run_id should be cleared for stale session");
      assert.equal(after.status, "idle", "status should be idle after run is gone");
    } finally {
      tmux.__reset();
      // The app believes the session is already gone and never calls
      // kill-session — kill the real tmux session directly so it doesn't leak.
      try {
        execFileSync("tmux", ["kill-session", "-t", runId], { stdio: "ignore" });
      } catch {
        // already gone
      }
      restorePath();
    }
  });
});
