// Telegram User Crabbox Proof tests cover telegram user crabbox proof script behavior.
import { spawn, spawnSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { setTimeout as delay } from "node:timers/promises";
import { pathToFileURL } from "node:url";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
  COMMAND_TIMEOUT_MS,
  createContainerizedSutSpawnSpec,
  createCrabboxWarmupArgs,
  createOpenClawGatewaySpawnSpec,
  parseArgs,
  processTargetExists,
  readCodexProxyPort,
  readLogTail,
  readTelegramUserProofLogTailBytes,
  recordProbeVideo,
  REMOTE_SETUP_COMMAND_TIMEOUT_MS,
  renderLaunchDesktop,
  renderRemoteProbe,
  renderRemoteSetup,
  renderSelectDesktopChat,
  renderTailscaleSshProxy,
  runCommand,
  runSutContainerAction,
  selectCrabboxSshPort,
  signalCommandTree,
  stageFullSessionArtifacts,
  startLocalSut,
  waitForLog,
  writeSutConfig,
} from "../../scripts/e2e/telegram-user-crabbox-proof.ts";
import { resolveWindowsTaskkillPath } from "../../scripts/lib/windows-taskkill.mjs";
import { cleanupTempDirs, makeTempDir } from "../helpers/temp-dir.js";

const tempDirs: string[] = [];
const posixIt = process.platform === "win32" ? it.skip : it;

function expectedTaskkillPath(): string {
  return resolveWindowsTaskkillPath();
}

function isProcessAlive(pid: number): boolean {
  try {
    process.kill(pid, 0);
    return true;
  } catch {
    return false;
  }
}

function hasLoneSurrogate(value: string): boolean {
  for (let index = 0; index < value.length; index += 1) {
    const codeUnit = value.charCodeAt(index);
    if (codeUnit >= 0xd800 && codeUnit <= 0xdbff) {
      const next = value.charCodeAt(index + 1);
      if (next >= 0xdc00 && next <= 0xdfff) {
        index += 1;
        continue;
      }
      return true;
    }
    if (codeUnit >= 0xdc00 && codeUnit <= 0xdfff) {
      return true;
    }
  }
  return false;
}

function writeExecutable(pathname: string, content: string): void {
  fs.writeFileSync(pathname, content, { mode: 0o755 });
}

function runProofCli(args: string[]) {
  return spawnSync(
    process.execPath,
    ["--import", "tsx", "scripts/e2e/telegram-user-crabbox-proof.ts", ...args],
    {
      cwd: process.cwd(),
      encoding: "utf8",
      env: {
        ...process.env,
        GIT_CONFIG_NOSYSTEM: "1",
        GIT_TERMINAL_PROMPT: "0",
      },
    },
  );
}

async function waitFor(predicate: () => boolean, timeoutMs = 5_000): Promise<void> {
  const started = Date.now();
  while (Date.now() - started < timeoutMs) {
    if (predicate()) {
      return;
    }
    await delay(5);
  }
  throw new Error("condition was not met before timeout");
}

afterEach(() => {
  vi.restoreAllMocks();
  vi.unstubAllEnvs();
  cleanupTempDirs(tempDirs);
});

describe("telegram user Crabbox proof log polling", () => {
  it("starts the local gateway through the repo pnpm runner", () => {
    const root = makeTempDir(tempDirs, "openclaw-telegram-proof-");
    const fakePnpm = path.join(root, "pnpm.cjs");
    fs.writeFileSync(fakePnpm, "#!/usr/bin/env node\n", { mode: 0o755 });

    const spec = createOpenClawGatewaySpawnSpec({
      env: { ...process.env, OPENCLAW_TELEGRAM_PROOF_SENTINEL: "1" },
      gatewayPort: 19042,
      nodeExecPath: "/opt/node/bin/node",
      npmExecPath: fakePnpm,
      repoRoot: root,
    });

    expect(spec.command).toBe("/opt/node/bin/node");
    expect(spec.args).toEqual([fakePnpm, "openclaw", "gateway", "--port", "19042"]);
    expect(spec.options.cwd).toBe(root);
    expect(spec.options.env?.OPENCLAW_TELEGRAM_PROOF_SENTINEL).toBe("1");
    expect(spec.options.shell).toBe(false);
  });

  it("uses an explicitly pinned pnpm executable for a worktree gateway", () => {
    const spec = createOpenClawGatewaySpawnSpec({
      env: { PATH: "/definitely-missing" },
      gatewayPort: 19042,
      pnpmExecPath: "/opt/mantis-toolchain/pnpm",
      repoRoot: "/repo",
    });

    expect(spec.command).toBe("/opt/mantis-toolchain/pnpm");
    expect(spec.args).toEqual(["openclaw", "gateway", "--port", "19042"]);
    expect(spec.options.cwd).toBe("/repo");
    expect(spec.options.shell).toBe(false);
  });

  it("routes fork SUT startup through the root-owned validating wrapper", () => {
    const repoRoot = makeTempDir(tempDirs, "openclaw-telegram-proof-");
    const runtimeRoot = makeTempDir(tempDirs, "openclaw-telegram-proof-");
    const spec = createContainerizedSutSpawnSpec({
      codexProxyPort: 43123,
      containerName: "openclaw-telegram-sut-test",
      gatewayEnv: {
        TELEGRAM_BOT_TOKEN: "telegram-burner-token",
      },
      gatewayPort: 19042,
      mockPort: 19043,
      mockResponseText: "streamed response",
      repoRoot,
      runtimeRoot,
      sutLane: "candidate",
    });

    expect(spec.command).toBe("sudo");
    expect(spec.args).toContain("/usr/local/sbin/openclaw-mantis-sut-container");
    expect(spec.args).toContain("run");
    expect(spec.args).toContain("candidate");
    expect(spec.args).not.toContain("docker");
    expect(spec.args.join("\n")).not.toContain("--preserve-env");
    expect(spec.args.join("\n")).not.toContain("CODEX_HOME");
    expect(spec.options.env).not.toHaveProperty("CODEX_HOME");
    expect(spec.options.env).not.toHaveProperty("OPENAI_API_KEY");
    expect(spec.options.env).not.toHaveProperty("TELEGRAM_BOT_TOKEN");
    expect(fs.statSync(spec.inputPath).mode & 0o777).toBe(0o600);
    expect(JSON.parse(fs.readFileSync(spec.inputPath, "utf8"))).toEqual({
      mockResponseText: "streamed response",
      telegramBotToken: "telegram-burner-token",
    });
  });

  it("reads only the loopback Responses proxy port from Codex config", () => {
    const codexHome = makeTempDir(tempDirs, "openclaw-telegram-proof-");
    fs.writeFileSync(
      path.join(codexHome, "config.toml"),
      '[model_providers.codex-action-responses-proxy]\nbase_url = "http://127.0.0.1:43123/v1"\n',
    );
    expect(readCodexProxyPort(codexHome)).toBe(43123);
    fs.writeFileSync(
      path.join(codexHome, "config.toml"),
      '[model_providers.codex-action-responses-proxy]\nbase_url = "https://api.openai.com/v1"\n',
    );
    expect(readCodexProxyPort(codexHome)).toBeUndefined();
  });

  it("requires successful privileged SUT teardown commands", () => {
    const run = vi.fn(() => ({ signal: null, status: 0, stderr: "" }));
    runSutContainerAction(
      "stop",
      "openclaw-telegram-sut-test",
      "/tmp/openclaw-tg-crabbox-sut-test",
      run,
    );
    expect(run).toHaveBeenCalledWith(
      "sudo",
      [
        "-n",
        "/usr/local/sbin/openclaw-mantis-sut-container",
        "stop",
        "openclaw-telegram-sut-test",
        "/tmp/openclaw-tg-crabbox-sut-test",
      ],
      expect.objectContaining({ encoding: "utf8", stdio: "pipe" }),
    );

    expect(() =>
      runSutContainerAction(
        "destroy",
        "openclaw-telegram-sut-test",
        "/tmp/openclaw-tg-crabbox-sut-test",
        () => ({ signal: null, status: 1, stderr: "destroy failed" }),
      ),
    ).toThrow("destroy failed with exit code 1.\ndestroy failed");
    expect(() =>
      runSutContainerAction(
        "stop",
        "openclaw-telegram-sut-test",
        "/tmp/openclaw-tg-crabbox-sut-test",
        () => ({ signal: "SIGKILL", status: null, stderr: "" }),
      ),
    ).toThrow("stop was terminated by SIGKILL");
    expect(() =>
      runSutContainerAction(
        "stop",
        "openclaw-telegram-sut-test",
        "/tmp/openclaw-tg-crabbox-sut-test",
        () => ({ error: new Error("spawn failed"), status: null }),
      ),
    ).toThrow("Failed to stop container-isolated SUT: spawn failed");
  });

  it("treats permission-denied process probes as alive", () => {
    const kill = vi.spyOn(process, "kill");
    kill.mockImplementation(() => {
      throw Object.assign(new Error("not permitted"), { code: "EPERM" });
    });
    expect(processTargetExists(1234)).toBe(true);

    kill.mockImplementation(() => {
      throw Object.assign(new Error("gone"), { code: "ESRCH" });
    });
    expect(processTargetExists(1234)).toBe(false);

    kill.mockImplementation(() => {
      throw Object.assign(new Error("unexpected"), { code: "EINVAL" });
    });
    expect(() => processTargetExists(1234)).toThrow("unexpected");
  });

  it("forces fork candidates into the isolated SUT container", () => {
    vi.stubEnv("MANTIS_CANDIDATE_TRUST", "fork-pr-head");
    expect(() => parseArgs(["start"])).toThrow(
      "container proof requires --sut-repo-root and --sut-lane.",
    );
    expect(
      parseArgs(["start", "--sut-lane", "candidate", "--sut-repo-root", "/prepared/candidate"]),
    ).toMatchObject({
      sutContainer: true,
      sutLane: "candidate",
      sutRepoRoot: "/prepared/candidate",
    });
    expect(() => parseArgs([])).toThrow("--sut-container requires the held-session start flow.");
    vi.stubEnv("MANTIS_CANDIDATE_TRUST", "open-pr-head");
    expect(parseArgs(["start"]).sutContainer).toBe(false);
    expect(() => parseArgs(["start", "--sut-container"])).toThrow(
      "container proof requires --sut-repo-root and --sut-lane.",
    );
  });

  it("allows cold remote setup to outlive ordinary command timeouts", () => {
    expect(REMOTE_SETUP_COMMAND_TIMEOUT_MS).toBeGreaterThan(COMMAND_TIMEOUT_MS);
    expect(REMOTE_SETUP_COMMAND_TIMEOUT_MS).toBeGreaterThanOrEqual(90 * 60 * 1000);
  });

  it.each([
    {
      inspect: { sshFallbackPorts: ["22", "2222", " 2200 ", "22"], sshPort: "2222" },
      ports: ["2222", "22", "2200"],
    },
    {
      inspect: { sshFallbackPorts: ["2200", "22", "2200"] },
      ports: ["22", "2200"],
    },
    { inspect: {}, ports: ["22"] },
  ])("selects ordered, deduplicated SSH candidates: $ports", async ({ inspect, ports }) => {
    const probes: string[] = [];

    await expect(
      selectCrabboxSshPort({
        inspect,
        probe: async (port) => {
          probes.push(port);
          if (port !== ports.at(-1)) {
            throw new Error("Connection refused");
          }
        },
      }),
    ).resolves.toBe(ports.at(-1));
    expect(probes).toEqual(ports);
  });

  it("does not try a fallback after a non-connection failure", async () => {
    const probes: string[] = [];

    await expect(
      selectCrabboxSshPort({
        inspect: { sshFallbackPorts: ["22"], sshPort: "2222" },
        probe: async (port) => {
          probes.push(port);
          throw new Error("Permission denied (publickey)");
        },
      }),
    ).rejects.toThrow("Permission denied");
    expect(probes).toEqual(["2222"]);
  });

  it("rejects loose numeric log tail limits instead of parsing prefixes", () => {
    expect(() =>
      readTelegramUserProofLogTailBytes({
        OPENCLAW_TELEGRAM_USER_PROOF_LOG_TAIL_BYTES: "1e3",
      }),
    ).toThrow("invalid OPENCLAW_TELEGRAM_USER_PROOF_LOG_TAIL_BYTES: 1e3");
    expect(() =>
      readTelegramUserProofLogTailBytes({
        OPENCLAW_TELEGRAM_USER_PROOF_LOG_TAIL_BYTES: "1000bytes",
      }),
    ).toThrow("invalid OPENCLAW_TELEGRAM_USER_PROOF_LOG_TAIL_BYTES: 1000bytes");
    expect(
      readTelegramUserProofLogTailBytes({
        OPENCLAW_TELEGRAM_USER_PROOF_LOG_TAIL_BYTES: "4096",
      }),
    ).toBe(4096);
  });

  it.each([
    ["loose gateway", "--gateway-port", "1e3", "--gateway-port must be a positive integer."],
    [
      "out-of-range gateway",
      "--gateway-port",
      "65536",
      "--gateway-port must be a TCP port from 1 to 65535.",
    ],
    [
      "out-of-range mock",
      "--mock-port",
      "65536",
      "--mock-port must be a TCP port from 1 to 65535.",
    ],
  ])("rejects %s proof ports before remote setup", (_label, flag, value, message) => {
    expect(() => parseArgs([flag, value, "--dry-run"])).toThrow(message);
  });

  it("rejects short flags as proof option values before dry-run planning", () => {
    const result = runProofCli(["--output-dir", "-h", "--dry-run"]);

    expect(result.status).toBe(1);
    expect(result.stderr).toContain("Usage:");
    expect(result.stdout).toBe("");
  });

  it("keeps hyphen-prefixed free-text proof values", () => {
    expect(parseArgs(["--text", "-ping"]).text).toBe("-ping");
  });

  it("accepts an explicit Telegram link-preview setting", () => {
    expect(parseArgs(["start", "--link-preview", "false"]).linkPreview).toBe(false);
    expect(parseArgs(["start", "--link-preview", "true"]).linkPreview).toBe(true);
    expect(parseArgs(["start"]).linkPreview).toBeUndefined();
    expect(() => parseArgs(["start", "--link-preview", "disabled"])).toThrow(
      "--link-preview must be true or false.",
    );
  });

  it("accepts a positive mock response chunk delay", () => {
    expect(
      parseArgs(["start", "--mock-response-chunk-delay-ms", "1200"]).mockResponseChunkDelayMs,
    ).toBe(1200);
    expect(() => parseArgs(["start", "--mock-response-chunk-delay-ms", "0"])).toThrow(
      "--mock-response-chunk-delay-ms must be a positive integer.",
    );
  });

  it("accepts only a positive fixed human delay", () => {
    expect(parseArgs(["start", "--human-delay-fixed-ms", "1200"]).humanDelayFixedMs).toBe(1200);
    expect(() => parseArgs(["start", "--human-delay-fixed-ms", "0"])).toThrow(
      "--human-delay-fixed-ms must be a positive integer.",
    );
    expect(() => parseArgs(["start", "--human-delay-fixed-ms", "1e3"])).toThrow(
      "--human-delay-fixed-ms must be a positive integer.",
    );
    expect(() =>
      parseArgs(["send", "--session", "session.json", "--human-delay-fixed-ms", "1200"]),
    ).toThrow("--human-delay-fixed-ms is available only for start sessions.");
  });

  it("rejects duplicate single-value proof controls while keeping repeated expectations", () => {
    expect(() =>
      parseArgs(["--output-dir", ".artifacts/one", "--output-dir", ".artifacts/two"]),
    ).toThrow("--output-dir was provided more than once");

    expect(parseArgs(["--expect", "OpenClaw", "--expect", "ready"]).expect).toEqual([
      "OpenClaw",
      "ready",
    ]);
  });

  it("uses unique default output dirs", () => {
    const firstOutputDir = parseArgs([]).outputDir;
    const secondOutputDir = parseArgs([]).outputDir;

    expect(path.dirname(firstOutputDir)).toBe(
      path.join(".artifacts", "qa-e2e", "telegram-user-crabbox"),
    );
    expect(path.basename(firstOutputDir)).toMatch(
      /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z-[a-f0-9]{8}$/u,
    );
    expect(secondOutputDir).not.toBe(firstOutputDir);
    expect(parseArgs(["--output-dir", ".artifacts/custom"]).outputDir).toBe(".artifacts/custom");
  });

  it("clamps proof timeout args before they reach Node timers", () => {
    expect(parseArgs(["--timeout-ms", String(MAX_TIMER_TIMEOUT_MS + 1)]).timeoutMs).toBe(
      MAX_TIMER_TIMEOUT_MS,
    );
  });

  it("enables the pinned MCP App fixture only when explicitly requested", () => {
    expect(parseArgs(["start", "--mcp-app-fixture"]).mcpAppFixture).toBe(true);
    expect(parseArgs([]).mcpAppFixture).toBe(false);

    const ordinaryArgs = createCrabboxWarmupArgs(parseArgs([]));
    const fixtureArgs = createCrabboxWarmupArgs(parseArgs(["start", "--mcp-app-fixture"]));
    expect(ordinaryArgs).not.toContain("--tailscale");
    expect(fixtureArgs).toContain("--tailscale");
    expect(() => parseArgs(["--mcp-app-fixture"])).toThrow(
      "--mcp-app-fixture is available only for start sessions.",
    );
    expect(() =>
      parseArgs([
        "start",
        "--mcp-app-fixture",
        "--sut-lane",
        "baseline",
        "--sut-repo-root",
        "/prepared/baseline",
      ]),
    ).toThrow("--mcp-app-fixture is unavailable for container-isolated SUT proof.");
    expect(() => parseArgs(["start", "--mcp-app-fixture", "--id", "cbx_reused"])).toThrow(
      "--mcp-app-fixture requires a fresh lifecycle-owned Crabbox lease.",
    );
  });

  it("writes an isolated Funnel and official MCP App fixture config", () => {
    const configRoot = writeSutConfig({
      gatewayPort: 19042,
      groupId: "group",
      mcpAppFixture: true,
      mockPort: 19043,
      outputDir: makeTempDir(tempDirs, "openclaw-telegram-proof-"),
      repoRoot: "/repo",
      testerId: "tester",
    });
    tempDirs.push(configRoot.tempRoot);
    const config = JSON.parse(fs.readFileSync(configRoot.configPath, "utf8"));

    expect(config.gateway).toMatchObject({
      auth: {
        mode: "password",
        password: { id: "OPENCLAW_GATEWAY_PASSWORD", source: "env" },
      },
      tailscale: { mode: "funnel", resetOnExit: true },
    });
    expect(config.mcp.servers.fixture).toEqual({
      args: ["/repo/scripts/e2e/mcp-app-conformance-server.mjs"],
      command: process.execPath,
    });
    expect(JSON.stringify(config)).not.toContain("companion-called");
    expect(JSON.stringify(config)).not.toContain("resource-ok");
  });

  it("injects the requested Telegram link-preview setting before startup", () => {
    const disabledConfigRoot = writeSutConfig({
      gatewayPort: 19042,
      groupId: "group",
      linkPreview: false,
      mockPort: 19043,
      outputDir: makeTempDir(tempDirs, "openclaw-telegram-proof-"),
      testerId: "tester",
    });
    const defaultConfigRoot = writeSutConfig({
      gatewayPort: 19044,
      groupId: "group",
      mockPort: 19045,
      outputDir: makeTempDir(tempDirs, "openclaw-telegram-proof-"),
      testerId: "tester",
    });
    tempDirs.push(disabledConfigRoot.tempRoot, defaultConfigRoot.tempRoot);

    const disabledConfig = JSON.parse(fs.readFileSync(disabledConfigRoot.configPath, "utf8"));
    const defaultConfig = JSON.parse(fs.readFileSync(defaultConfigRoot.configPath, "utf8"));

    expect(disabledConfig.channels.telegram.linkPreview).toBe(false);
    expect(defaultConfig.channels.telegram).not.toHaveProperty("linkPreview");
  });

  it("injects the requested fixed human delay before startup", () => {
    const delayedConfigRoot = writeSutConfig({
      gatewayPort: 19042,
      groupId: "group",
      humanDelayFixedMs: 1200,
      mockPort: 19043,
      outputDir: makeTempDir(tempDirs, "openclaw-telegram-proof-"),
      testerId: "tester",
    });
    const defaultConfigRoot = writeSutConfig({
      gatewayPort: 19044,
      groupId: "group",
      mockPort: 19045,
      outputDir: makeTempDir(tempDirs, "openclaw-telegram-proof-"),
      testerId: "tester",
    });
    tempDirs.push(delayedConfigRoot.tempRoot, defaultConfigRoot.tempRoot);

    const delayedConfig = JSON.parse(fs.readFileSync(delayedConfigRoot.configPath, "utf8"));
    const defaultConfig = JSON.parse(fs.readFileSync(defaultConfigRoot.configPath, "utf8"));

    expect(delayedConfig.agents.defaults.humanDelay).toEqual({
      maxMs: 1200,
      minMs: 1200,
      mode: "custom",
    });
    expect(defaultConfig.agents.defaults).not.toHaveProperty("humanDelay");
  });

  it("pins the browser fixture SDK and exposes only the required app capabilities", () => {
    const fixture = fs.readFileSync("scripts/e2e/mcp-app-conformance-server.mjs", "utf8");
    const uiPackage = JSON.parse(fs.readFileSync("ui/package.json", "utf8"));

    expect(uiPackage.dependencies["@modelcontextprotocol/ext-apps"]).toBe("1.7.5");
    expect(fixture).toContain(
      `@modelcontextprotocol/ext-apps@${uiPackage.dependencies["@modelcontextprotocol/ext-apps"]}`,
    );
    expect(fixture).toContain('server.tool("app_companion"');
    expect(fixture).toContain('visibility: ["app"]');
    expect(fixture).toContain('"data://conformance/value"');
    expect(fixture).toContain('text: "resource-ok"');
  });

  it("serves the fixture view, app-only tool, and resource over official MCP stdio", async () => {
    const transport = new StdioClientTransport({
      args: ["scripts/e2e/mcp-app-conformance-server.mjs"],
      command: process.execPath,
      cwd: process.cwd(),
      stderr: "pipe",
    });
    const client = new Client({ name: "telegram-proof-fixture-test", version: "1.0.0" });
    try {
      await client.connect(transport);
      const tools = await client.listTools();
      expect(tools.tools.find((tool) => tool.name === "show")?._meta).toMatchObject({
        ui: { resourceUri: "ui://conformance/app" },
      });
      expect(tools.tools.find((tool) => tool.name === "app_companion")?._meta).toMatchObject({
        ui: { visibility: ["app"] },
      });
      expect(await client.callTool({ arguments: {}, name: "app_companion" })).toMatchObject({
        structuredContent: { value: "companion-called" },
      });
      expect(await client.readResource({ uri: "data://conformance/value" })).toMatchObject({
        contents: [{ text: "resource-ok" }],
      });
    } finally {
      await Promise.allSettled([client.close(), transport.close()]);
    }
  });

  posixIt("limits the Funnel bridge proxy to the Gateway lifecycle commands", () => {
    const root = makeTempDir(tempDirs, "openclaw-telegram-proof-");
    const sshPath = path.join(root, "ssh");
    const argvPath = path.join(root, "ssh-argv.json");
    const proxyPath = path.join(root, "tailscale");
    writeExecutable(
      sshPath,
      `#!/usr/bin/env node\nimport fs from "node:fs";\nfs.writeFileSync(process.env.ARGV_PATH, JSON.stringify(process.argv.slice(2)));\n`,
    );
    writeExecutable(
      proxyPath,
      renderTailscaleSshProxy({
        gatewayPort: 19042,
        inspect: {
          host: "proof.example",
          sshHost: "",
          sshKey: "/tmp/proof-key",
          sshPort: "2222",
          sshUser: "proof",
        },
      }),
    );
    const env = {
      ...process.env,
      ARGV_PATH: argvPath,
      PATH: `${root}${path.delimiter}${process.env.PATH ?? ""}`,
    };

    const allowed = spawnSync(proxyPath, ["funnel", "--bg", "--yes", "19042"], {
      encoding: "utf8",
      env,
    });
    expect(allowed.status).toBe(0);
    const fallbackArgv = JSON.parse(fs.readFileSync(argvPath, "utf8"));
    expect(fallbackArgv).toContain("proof@proof.example");
    expect(fallbackArgv).toContain("'tailscale' 'funnel' '--bg' '--yes' '19042'");

    const rejected = spawnSync(proxyPath, ["serve", "--bg", "19042"], {
      encoding: "utf8",
      env,
    });
    expect(rejected.status).toBe(64);
    expect(rejected.stderr).toContain("unsupported proof Tailscale command");
  });

  posixIt("keeps the inspect SSH host when selecting a fallback port", async () => {
    const root = makeTempDir(tempDirs, "openclaw-telegram-proof-");
    const sshPath = path.join(root, "ssh");
    const argvPath = path.join(root, "ssh-argv.json");
    const proxyPath = path.join(root, "tailscale");
    const inspect = {
      host: "public.example",
      sshFallbackPorts: ["22"],
      sshHost: "ssh.example",
      sshKey: "/tmp/proof-key",
      sshPort: "2222",
      sshUser: "proof",
    };
    const probes: string[] = [];
    const sshPort = await selectCrabboxSshPort({
      inspect,
      probe: async (port) => {
        probes.push(port);
        if (port === "2222") {
          throw new Error("Connection refused");
        }
      },
    });
    writeExecutable(
      sshPath,
      `#!/usr/bin/env node\nimport fs from "node:fs";\nfs.writeFileSync(process.env.ARGV_PATH, JSON.stringify(process.argv.slice(2)));\n`,
    );
    writeExecutable(
      proxyPath,
      renderTailscaleSshProxy({
        gatewayPort: 19042,
        inspect: { ...inspect, sshPort },
      }),
    );

    const result = spawnSync(proxyPath, ["status", "--json"], {
      encoding: "utf8",
      env: {
        ...process.env,
        ARGV_PATH: argvPath,
        PATH: `${root}${path.delimiter}${process.env.PATH ?? ""}`,
      },
    });

    expect(result.status).toBe(0);
    const argv = JSON.parse(fs.readFileSync(argvPath, "utf8"));
    expect(probes).toEqual(["2222", "22"]);
    expect(argv).toContain("proof@ssh.example");
    expect(argv).not.toContain("proof@public.example");
    const portFlagIndex = argv.indexOf("-p");
    expect(argv.slice(portFlagIndex, portFlagIndex + 2)).toEqual(["-p", "22"]);
  });

  it("reads only the requested log tail", () => {
    const logPath = path.join(makeTempDir(tempDirs, "openclaw-telegram-proof-"), "gateway.log");
    fs.writeFileSync(logPath, `${"old\n".repeat(2000)}ready\n`, "utf8");

    const tail = readLogTail(logPath, 32);

    expect(tail).toContain("ready");
    expect(tail.length).toBeLessThanOrEqual(32);
    expect(tail).not.toContain("old\nold\nold\nold\nold\nold\nold\nold\nold");
  });

  it("keeps byte-cut log tails UTF-8 safe and reads at least one byte", () => {
    const logPath = path.join(makeTempDir(tempDirs, "openclaw-telegram-proof-"), "gateway.log");
    fs.writeFileSync(
      logPath,
      Buffer.concat([Buffer.from("x".repeat(100)), Buffer.from("😀"), Buffer.from("y".repeat(20))]),
    );

    expect(readLogTail(logPath, 23)).toBe("y".repeat(20));
    expect(readLogTail(logPath, 24)).toBe(`😀${"y".repeat(20)}`);
    expect(readLogTail(logPath, 0)).toBe("y");
  });

  it("keeps readiness timeout tails free of split surrogate pairs", async () => {
    const logPath = path.join(makeTempDir(tempDirs, "openclaw-telegram-proof-"), "gateway.log");
    fs.writeFileSync(logPath, `${"a".repeat(9)}😀${"b".repeat(3999)}`, "utf8");

    let message = "";
    try {
      await waitForLog(logPath, /\[gateway\] ready/u, "gateway", 0);
    } catch (error) {
      message = error instanceof Error ? error.message : String(error);
    }

    const tail = message.split("\n").at(-1) ?? "";
    expect(tail).toBe("b".repeat(3999));
    expect(hasLoneSurrogate(tail)).toBe(false);
  });

  it("honors short reads when a log shrinks during tailing", () => {
    vi.spyOn(fs, "statSync").mockReturnValue({
      isFile: () => true,
      size: 64,
    } as fs.Stats);
    vi.spyOn(fs, "openSync").mockReturnValue(123 as never);
    vi.spyOn(fs, "closeSync").mockImplementation(() => undefined);
    vi.spyOn(fs, "readSync").mockImplementation((_fd, buffer) => {
      if (!Buffer.isBuffer(buffer)) {
        throw new Error("expected buffer read");
      }
      buffer.write("ready");
      return 5;
    });

    expect(readLogTail("/tmp/truncated.log", 64)).toBe("ready");
  });

  it("does not reread the full log while waiting for readiness", async () => {
    const logPath = path.join(makeTempDir(tempDirs, "openclaw-telegram-proof-"), "mock-openai.log");
    fs.writeFileSync(logPath, `${"noise\n".repeat(2000)}mock-openai listening\n`, "utf8");
    const readFileSync = vi.spyOn(fs, "readFileSync").mockImplementation(() => {
      throw new Error("full log read");
    });

    await waitForLog(logPath, /mock-openai listening/u, "mock-openai", 100);

    expect(readFileSync).not.toHaveBeenCalled();
  });

  it("reports only a bounded log tail on timeout", async () => {
    const logPath = path.join(makeTempDir(tempDirs, "openclaw-telegram-proof-"), "gateway.log");
    fs.writeFileSync(logPath, `old-secret\n${"x".repeat(300_000)}recent failure\n`, "utf8");

    let message = "";
    try {
      await waitForLog(logPath, /\[gateway\] ready/u, "gateway", 0);
    } catch (error) {
      message = error instanceof Error ? error.message : String(error);
    }

    expect(message).toContain("recent failure");
    expect(message).not.toContain("old-secret");
  });

  it("bounds remote Telegram Desktop launch diagnostics", () => {
    const script = renderLaunchDesktop();

    expect(script).toContain("print_desktop_log_tail() {");
    expect(script).toContain('tail -c 262144 "$log_file"');
    expect(script).toContain("print_desktop_log_tail\n  exit 1");
    expect(script).not.toContain('cat "$root/telegram-desktop.log"');
  });

  it("shell-quotes generated remote setup and chat literals", () => {
    const payload = "name $(touch /tmp/openclaw-proof-injected) `touch /tmp/also-injected`";

    expect(renderRemoteSetup({ tdlibSha256: payload, tdlibUrl: payload })).toContain(
      `tdlib_url='${payload}'`,
    );
    expect(renderSelectDesktopChat({ chatTitle: payload })).toContain(`chat_title='${payload}'`);
  });

  it("stages full publish artifacts without session control files", () => {
    const outputDir = makeTempDir(tempDirs, "openclaw-telegram-proof-");
    const publishDir = path.join(outputDir, "publish-full-artifacts");
    fs.mkdirSync(publishDir);
    fs.writeFileSync(path.join(publishDir, "stale.txt"), "stale");
    fs.mkdirSync(path.join(outputDir, "publish-gif-only"));
    fs.writeFileSync(
      path.join(outputDir, "session.json"),
      '{"sshKey":"/private/tmp/openclaw/key"}',
    );
    fs.writeFileSync(path.join(outputDir, "lease.json"), '{"token":"secret"}');
    fs.writeFileSync(path.join(outputDir, "status.json"), '{"ok":true}');
    fs.writeFileSync(path.join(outputDir, "probe.json"), '{"ok":true}');
    fs.writeFileSync(path.join(outputDir, "probe-2026-06-20T16-47-48-123Z.json"), '{"ok":true}');
    fs.writeFileSync(path.join(outputDir, "probe-secret.json"), '{"token":"secret"}');
    fs.writeFileSync(path.join(outputDir, "telegram-user-crabbox-session-summary.json"), "{}");
    fs.writeFileSync(path.join(outputDir, "telegram-user-crabbox-proof.md"), "report");
    fs.writeFileSync(path.join(outputDir, "telegram-desktop.log"), "log");
    fs.writeFileSync(path.join(outputDir, "telegram-user-crabbox-session-motion.gif"), "gif");
    fs.writeFileSync(path.join(outputDir, "telegram-user-crabbox-session.mp4"), "video");

    const stagedDir = stageFullSessionArtifacts(outputDir);

    expect(stagedDir).toBe(publishDir);
    expect(fs.readdirSync(stagedDir).toSorted()).toEqual([
      "probe-2026-06-20T16-47-48-123Z.json",
      "probe.json",
      "status.json",
      "telegram-desktop.log",
      "telegram-user-crabbox-proof.md",
      "telegram-user-crabbox-session-motion.gif",
      "telegram-user-crabbox-session-summary.json",
      "telegram-user-crabbox-session.mp4",
    ]);
    expect(fs.existsSync(path.join(stagedDir, "session.json"))).toBe(false);
    expect(fs.existsSync(path.join(stagedDir, "lease.json"))).toBe(false);
    expect(fs.existsSync(path.join(stagedDir, "probe-secret.json"))).toBe(false);
    expect(fs.existsSync(path.join(stagedDir, "stale.txt"))).toBe(false);
  });

  it("requires finish to write the proof report before full artifact publishing", () => {
    const outputDir = makeTempDir(tempDirs, "openclaw-telegram-proof-");
    fs.writeFileSync(
      path.join(outputDir, "session.json"),
      '{"sshKey":"/private/tmp/openclaw/key"}',
    );
    fs.writeFileSync(path.join(outputDir, "status.json"), '{"ok":true}');
    fs.writeFileSync(path.join(outputDir, "telegram-desktop.log"), "log");

    expect(() => stageFullSessionArtifacts(outputDir)).toThrow(
      "Missing proof report. Run finish first: telegram-user-crabbox-proof.md",
    );
    expect(fs.existsSync(path.join(outputDir, "publish-full-artifacts"))).toBe(false);
  });

  posixIt("does not expand generated remote probe arguments in the shell", () => {
    const root = makeTempDir(tempDirs, "openclaw-telegram-proof-");
    const fakePython = path.join(root, "python3");
    const scriptPath = path.join(root, "remote-probe.sh");
    const argvPath = path.join(root, "argv.json");
    const injectedPath = path.join(root, "injected");
    const payload = `literal ' $(touch ${injectedPath})`;
    writeExecutable(
      fakePython,
      `#!/usr/bin/env node
import fs from "node:fs";
fs.writeFileSync(process.env.OPENCLAW_TEST_ARGV_PATH, JSON.stringify(process.argv.slice(1)));
`,
    );
    writeExecutable(
      scriptPath,
      renderRemoteProbe({
        expect: [payload],
        sutUsername: payload,
        text: payload,
        timeoutMs: 1000,
      }),
    );

    const result = spawnSync("bash", [scriptPath], {
      cwd: root,
      encoding: "utf8",
      env: {
        ...process.env,
        OPENCLAW_TEST_ARGV_PATH: argvPath,
        PATH: `${root}${path.delimiter}${process.env.PATH ?? ""}`,
      },
    });

    expect(result.status).toBe(0);
    expect(fs.existsSync(injectedPath)).toBe(false);
    expect(JSON.parse(fs.readFileSync(argvPath, "utf8"))).toContain(payload);
  });

  it("clamps oversized command timeouts before arming timers", async () => {
    const setTimeoutSpy = vi.spyOn(globalThis, "setTimeout");

    await expect(
      runCommand({
        args: ["--version"],
        command: process.execPath,
        cwd: process.cwd(),
        timeoutMs: MAX_TIMER_TIMEOUT_MS + 1,
      }),
    ).resolves.toMatchObject({ stderr: "" });

    expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), MAX_TIMER_TIMEOUT_MS);
    setTimeoutSpy.mockRestore();
  });

  it("keeps command failure tails free of split surrogate pairs", async () => {
    const root = makeTempDir(tempDirs, "openclaw-telegram-proof-");
    const scriptPath = path.join(root, "unicode-failure.mjs");
    fs.writeFileSync(
      scriptPath,
      `
await new Promise((resolve) => {
  process.stdout.write("a".repeat(3) + "😀" + "b".repeat(65_535), resolve);
});
await new Promise((resolve) => {
  process.stderr.write("😀" + "c".repeat(262_143), resolve);
});
process.exitCode = 2;
`,
    );
    let message = "";
    try {
      await runCommand({
        args: [scriptPath],
        command: process.execPath,
        cwd: root,
      });
    } catch (error) {
      message = error instanceof Error ? error.message : String(error);
    }

    const marker = "[stdout truncated to last 65536 characters]\n";
    const tail = message.split(marker).at(-1) ?? "";
    expect(message).toContain(marker);
    expect(tail.startsWith("b".repeat(100))).toBe(true);
    expect(tail.endsWith("c".repeat(100))).toBe(true);
    expect(tail).not.toContain("😀");
    expect(hasLoneSurrogate(tail)).toBe(false);
  });

  it("decodes command output statefully across split stream chunks", async () => {
    const script = [
      'const emoji = Buffer.from("😀", "utf8");',
      "process.stdout.write(emoji.subarray(0, 2));",
      "process.stderr.write(emoji.subarray(0, 2));",
      "setTimeout(() => {",
      "  process.stdout.write(emoji.subarray(2));",
      "  process.stderr.write(emoji.subarray(2));",
      "  process.exit(2);",
      "}, 100);",
    ].join("\n");
    let message = "";
    try {
      await runCommand({
        args: ["-e", script],
        command: process.execPath,
        cwd: makeTempDir(tempDirs, "openclaw-telegram-proof-"),
      });
    } catch (error) {
      message = error instanceof Error ? error.message : String(error);
    }

    const output = message.split("failed with exit code 2\n").at(-1) ?? "";
    expect(output.match(/😀/gu)).toHaveLength(2);
    expect(output).not.toContain("�");
  });

  posixIt("kills timed-out command process groups when the leader exits first", async () => {
    const root = makeTempDir(tempDirs, "openclaw-telegram-proof-");
    const scriptPath = path.join(root, "trap-term.mjs");
    const grandchildPidPath = path.join(root, "grandchild.pid");
    let grandchildPid = 0;

    fs.writeFileSync(
      scriptPath,
      `
import { spawn } from "node:child_process";
import fs from "node:fs";

const grandchild = spawn(process.execPath, [
  "-e",
  "process.on('SIGTERM', () => {}); setInterval(() => {}, 1000);",
], { stdio: "ignore" });
fs.writeFileSync(process.argv[2], String(grandchild.pid));
process.on("SIGTERM", () => process.exit(0));
setInterval(() => {}, 1000);
`,
      "utf8",
    );

    const runPromise = runCommand({
      args: [scriptPath, grandchildPidPath],
      command: process.execPath,
      cwd: root,
      timeoutKillGraceMs: 100,
      timeoutMs: 500,
    });
    const runResult = runPromise.then(
      () => ({ ok: true as const }),
      (error: unknown) => ({ error, ok: false as const }),
    );

    try {
      await waitFor(() => {
        if (!fs.existsSync(grandchildPidPath)) {
          return false;
        }
        grandchildPid = Number.parseInt(fs.readFileSync(grandchildPidPath, "utf8"), 10);
        return Number.isInteger(grandchildPid) && isProcessAlive(grandchildPid);
      });
      expect(Number.isInteger(grandchildPid)).toBe(true);

      const result = await runResult;
      expect(result).toMatchObject({
        error: {
          code: "ETIMEDOUT",
          message: expect.stringContaining("timed out after 500ms"),
        },
        ok: false,
      });
      await waitFor(() => !isProcessAlive(grandchildPid));
    } finally {
      await runResult.catch(() => {});
      if (grandchildPid && isProcessAlive(grandchildPid)) {
        process.kill(grandchildPid, "SIGKILL");
      }
    }
  });

  it("signals Windows proof command process trees with taskkill", () => {
    const child = {
      kill: vi.fn(),
      pid: 12345,
    };
    const runTaskkill = vi.fn(() => ({ error: undefined, status: 0 }));

    signalCommandTree(child, "SIGTERM", {
      platform: "win32",
      runTaskkill,
    });
    expect(runTaskkill).toHaveBeenNthCalledWith(
      1,
      expectedTaskkillPath(),
      ["/PID", "12345", "/T"],
      {
        stdio: "ignore",
      },
    );

    signalCommandTree(child, "SIGKILL", {
      platform: "win32",
      runTaskkill,
    });
    expect(runTaskkill).toHaveBeenNthCalledWith(
      2,
      expectedTaskkillPath(),
      ["/PID", "12345", "/T", "/F"],
      {
        stdio: "ignore",
      },
    );
    expect(child.kill).not.toHaveBeenCalled();
  });

  it("force-kills Windows proof command process trees when graceful taskkill fails", () => {
    const child = {
      kill: vi.fn(),
      pid: 12345,
    };
    const runTaskkill = vi
      .fn()
      .mockReturnValueOnce({ error: undefined, status: 1 })
      .mockReturnValueOnce({ error: undefined, status: 0 });

    signalCommandTree(child, "SIGTERM", {
      platform: "win32",
      runTaskkill,
    });

    expect(runTaskkill).toHaveBeenNthCalledWith(
      1,
      expectedTaskkillPath(),
      ["/PID", "12345", "/T"],
      {
        stdio: "ignore",
      },
    );
    expect(runTaskkill).toHaveBeenNthCalledWith(
      2,
      expectedTaskkillPath(),
      ["/PID", "12345", "/T", "/F"],
      {
        stdio: "ignore",
      },
    );
    expect(child.kill).not.toHaveBeenCalled();
  });

  posixIt("lets timed-out command descendants exit during kill grace", async () => {
    const root = makeTempDir(tempDirs, "openclaw-telegram-proof-");
    const scriptPath = path.join(root, "trap-term-grace.mjs");
    const readyPath = path.join(root, "descendant.ready");
    const donePath = path.join(root, "descendant.done");

    fs.writeFileSync(
      scriptPath,
      `
import { spawn } from "node:child_process";

const descendant = spawn(process.execPath, [
  "--input-type=module",
  "--eval",
  ${JSON.stringify(
    `import { writeFileSync } from "node:fs";
process.on("SIGTERM", () => {
  setTimeout(() => {
    writeFileSync(${JSON.stringify(donePath)}, "done");
    process.exit(0);
  }, 75);
});
writeFileSync(${JSON.stringify(readyPath)}, "ready");
setInterval(() => {}, 1000);`,
  )},
], { stdio: "ignore" });
descendant.unref();
process.on("SIGTERM", () => process.exit(0));
setInterval(() => {}, 1000);
`,
      "utf8",
    );

    const runPromise = runCommand({
      args: [scriptPath],
      command: process.execPath,
      cwd: root,
      timeoutKillGraceMs: 500,
      timeoutMs: 500,
    });

    await waitFor(() => fs.existsSync(readyPath));
    await expect(runPromise).rejects.toMatchObject({
      code: "ETIMEDOUT",
      message: expect.stringContaining("timed out after 500ms"),
    });
    expect(fs.readFileSync(donePath, "utf8")).toBe("done");
  });

  posixIt("keeps closed command groups tracked for parent cleanup", async () => {
    const root = makeTempDir(tempDirs, "openclaw-telegram-proof-");
    const commandPath = path.join(root, "closed-command.mjs");
    const runnerPath = path.join(root, "closed-command-runner.mjs");
    const commandSettledPath = path.join(root, "command-settled");
    const descendantPidPath = path.join(root, "closed-command-descendant.pid");
    const descendantTermPath = path.join(root, "closed-command-descendant.term");
    let descendantPid = 0;

    fs.writeFileSync(
      commandPath,
      `
import { spawn } from "node:child_process";
import fs from "node:fs";

const descendant = spawn(process.execPath, [
  "-e",
  ${JSON.stringify(
    `const fs = require("node:fs");
process.on("SIGTERM", () => {
  fs.writeFileSync(${JSON.stringify(descendantTermPath)}, "terminated");
  process.exit(0);
});
fs.writeFileSync(${JSON.stringify(descendantPidPath)}, String(process.pid));
setInterval(() => {}, 1000);`,
  )},
], { stdio: "ignore" });
descendant.unref();
`,
      "utf8",
    );
    fs.writeFileSync(
      runnerPath,
      `
import fs from "node:fs";

const proof = await import(${JSON.stringify(
        pathToFileURL(path.resolve("scripts/e2e/telegram-user-crabbox-proof.ts")).href,
      )});
await proof.runCommand({
  args: [${JSON.stringify(commandPath)}],
  command: process.execPath,
  cwd: ${JSON.stringify(root)},
  timeoutMs: 30_000,
});
fs.writeFileSync(${JSON.stringify(commandSettledPath)}, "1");
setInterval(() => {}, 1000);
`,
      "utf8",
    );

    const runner = spawn(process.execPath, ["--import", "tsx", runnerPath], {
      cwd: process.cwd(),
      stdio: "ignore",
    });
    try {
      await waitFor(() => {
        if (!fs.existsSync(descendantPidPath)) {
          return false;
        }
        descendantPid = Number.parseInt(fs.readFileSync(descendantPidPath, "utf8"), 10);
        return (
          Number.isInteger(descendantPid) && descendantPid > 1 && isProcessAlive(descendantPid)
        );
      });
      expect(Number.isInteger(descendantPid)).toBe(true);
      await waitFor(() => fs.existsSync(commandSettledPath));
      if (!runner.pid) {
        throw new Error("runner did not start");
      }

      process.kill(runner.pid, "SIGTERM");

      await waitFor(() => fs.existsSync(descendantTermPath));
      await waitFor(() => !isProcessAlive(descendantPid));
    } finally {
      if (runner.pid && isProcessAlive(runner.pid)) {
        process.kill(runner.pid, "SIGKILL");
      }
      if (descendantPid && isProcessAlive(descendantPid)) {
        process.kill(descendantPid, "SIGKILL");
      }
    }
  });

  posixIt("keeps local SUT startup tails Unicode-safe and cleans child processes", async () => {
    const root = makeTempDir(tempDirs, "openclaw-telegram-proof-");
    const outputDir = makeTempDir(tempDirs, "openclaw-telegram-proof-");
    const mockScript = path.join(root, "scripts/e2e/mock-openai-server.mjs");
    const gatewayScript = path.join(root, "gateway-fail.mjs");
    const mockPidPath = path.join(root, "mock.pid");
    const mockTermPath = path.join(root, "mock.term");
    fs.mkdirSync(path.dirname(mockScript), { recursive: true });
    writeExecutable(
      mockScript,
      `
import fs from "node:fs";

// Handler before the readiness line: SIGTERM arrives once the gateway spawn
// fails, and a late-registered handler can lose it to the default disposition.
process.on("SIGTERM", () => {
  fs.writeFileSync(${JSON.stringify(mockTermPath)}, "terminated");
  process.exit(0);
});
fs.writeFileSync(${JSON.stringify(mockPidPath)}, String(process.pid));
process.stdout.write("mock-openai listening\\n");
setInterval(() => {}, 1000);
`,
    );
    writeExecutable(
      gatewayScript,
      `
const output = "😀" + "x".repeat(7998) + "😀" + "y".repeat(3999);
process.stderr.write(output);
process.exit(2);
`,
    );

    let message = "";
    try {
      await startLocalSut(
        {
          gatewayPort: 19042,
          groupId: "group",
          mockPort: 19043,
          mockResponseText: "ok",
          outputDir,
          repoRoot: root,
          sutToken: "token",
          testerId: "tester",
        },
        {
          createGatewaySpawnSpec: () => ({
            args: [gatewayScript],
            command: process.execPath,
            options: { cwd: root, env: process.env },
          }),
          drainUpdates: async () => ({
            drained: 0,
            pendingAfter: undefined,
            pendingBefore: undefined,
            webhookUrlSet: false,
          }),
        },
      );
    } catch (error) {
      message = error instanceof Error ? error.message : String(error);
    }

    expect(message).toContain("gateway exited before ready");
    expect(message.endsWith("y".repeat(3999))).toBe(true);
    expect(hasLoneSurrogate(message)).toBe(false);
    await waitFor(() => fs.existsSync(mockTermPath));
    const mockPid = Number.parseInt(fs.readFileSync(mockPidPath, "utf8"), 10);
    await waitFor(() => !isProcessAlive(mockPid));
  });

  posixIt("cleans gateway descendants after a failed gateway leader exits", async () => {
    const root = makeTempDir(tempDirs, "openclaw-telegram-proof-");
    const outputDir = makeTempDir(tempDirs, "openclaw-telegram-proof-");
    const mockScript = path.join(root, "scripts/e2e/mock-openai-server.mjs");
    const gatewayScript = path.join(root, "gateway-leader-exits.mjs");
    const gatewayGrandchildPidPath = path.join(root, "gateway-grandchild.pid");
    let gatewayGrandchildPid = 0;
    fs.mkdirSync(path.dirname(mockScript), { recursive: true });
    writeExecutable(
      mockScript,
      `
process.stdout.write("mock-openai listening\\n");
process.on("SIGTERM", () => process.exit(0));
setInterval(() => {}, 1000);
`,
    );
    writeExecutable(
      gatewayScript,
      `
import { spawn } from "node:child_process";
import fs from "node:fs";

const grandchild = spawn(process.execPath, [
  "-e",
  "process.on('SIGTERM', () => process.exit(0)); setInterval(() => {}, 1000);",
], { stdio: "ignore" });
fs.writeFileSync(${JSON.stringify(gatewayGrandchildPidPath)}, String(grandchild.pid));
process.exit(2);
`,
    );

    try {
      await expect(
        startLocalSut(
          {
            gatewayPort: 19042,
            groupId: "group",
            mockPort: 19043,
            mockResponseText: "ok",
            outputDir,
            repoRoot: root,
            sutToken: "token",
            testerId: "tester",
          },
          {
            createGatewaySpawnSpec: () => ({
              args: [gatewayScript],
              command: process.execPath,
              options: { cwd: root, env: process.env },
            }),
            drainUpdates: async () => ({
              drained: 0,
              pendingAfter: undefined,
              pendingBefore: undefined,
              webhookUrlSet: false,
            }),
            waitForOutputReady: async (child, _pattern, output, label) => {
              if (label === "mock-openai") {
                await waitFor(() => output().includes("mock-openai listening"));
                return;
              }
              // Parse inside the poll: existsSync can observe writeFileSync's
              // 0-byte open-truncate window, and a NaN pid would skip both the
              // dead-check and the finally-block SIGKILL cleanup.
              await waitFor(() => {
                if (!fs.existsSync(gatewayGrandchildPidPath)) {
                  return false;
                }
                gatewayGrandchildPid = Number.parseInt(
                  fs.readFileSync(gatewayGrandchildPidPath, "utf8"),
                  10,
                );
                return Number.isInteger(gatewayGrandchildPid) && gatewayGrandchildPid > 1;
              });
              if (child.exitCode === null && child.signalCode === null) {
                await new Promise<void>((resolve) => {
                  child.once("exit", () => resolve());
                });
              }
              throw new Error("gateway exited before ready");
            },
          },
        ),
      ).rejects.toThrow("gateway exited before ready");

      await waitFor(() => !isProcessAlive(gatewayGrandchildPid));
    } finally {
      if (gatewayGrandchildPid && isProcessAlive(gatewayGrandchildPid)) {
        process.kill(gatewayGrandchildPid, "SIGKILL");
      }
    }
  });

  posixIt("stops Crabbox recording when the desktop probe fails", async () => {
    const root = makeTempDir(tempDirs, "openclaw-telegram-proof-");
    const recorderPath = path.join(root, "fake-crabbox-recorder.mjs");
    const recorderPidPath = path.join(root, "recorder.pid");
    const recorderTermPath = path.join(root, "recorder.term");
    writeExecutable(
      recorderPath,
      `#!/usr/bin/env node
import fs from "node:fs";

// Arm the SIGTERM handler before publishing the pid file: the probe throws as
// soon as the pid file exists and recordProbeVideo SIGTERMs the recorder in
// its finally, so a handler installed after publish can lose that signal to
// the default disposition and recorder.term is never written.
process.on("SIGTERM", () => {
  fs.writeFileSync(${JSON.stringify(recorderTermPath)}, "terminated");
  process.exit(0);
});
fs.writeFileSync(${JSON.stringify(recorderPidPath)}, String(process.pid));
setInterval(() => {}, 1000);
`,
    );

    await expect(
      recordProbeVideo({
        crabboxBin: recorderPath,
        cwd: root,
        durationSeconds: 30,
        leaseId: "cbx_test",
        outputPath: path.join(root, "proof.mp4"),
        provider: "aws",
        runProbe: async () => {
          await waitFor(() => fs.existsSync(recorderPidPath));
          throw new Error("probe failed");
        },
        startDelayMs: 0,
        target: "linux",
      }),
    ).rejects.toThrow("probe failed");

    await waitFor(() => fs.existsSync(recorderTermPath));
    const recorderPid = Number.parseInt(fs.readFileSync(recorderPidPath, "utf8"), 10);
    await waitFor(() => !isProcessAlive(recorderPid));
  });

  posixIt(
    "does not wait forever when Crabbox recording exits before the probe returns",
    async () => {
      const root = makeTempDir(tempDirs, "openclaw-telegram-proof-");
      const recorderPath = path.join(root, "fake-crabbox-recorder.mjs");
      const recorderExitPath = path.join(root, "recorder.exit");
      writeExecutable(
        recorderPath,
        `#!/usr/bin/env node
import fs from "node:fs";

fs.writeFileSync(${JSON.stringify(recorderExitPath)}, "exited");
`,
      );

      await expect(
        Promise.race([
          recordProbeVideo({
            crabboxBin: recorderPath,
            cwd: root,
            durationSeconds: 1,
            leaseId: "cbx_test",
            outputPath: path.join(root, "proof.mp4"),
            provider: "aws",
            runProbe: async () => {
              await waitFor(() => fs.existsSync(recorderExitPath));
              await delay(50);
            },
            startDelayMs: 0,
            target: "linux",
          }),
          delay(500, undefined, { ref: false }).then(() => {
            throw new Error("recordProbeVideo hung after the recorder had already exited");
          }),
        ]),
      ).resolves.toBeUndefined();
    },
  );
});
