Ctrl K
ComunidadGuías…

tests/test_worker.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""Unit tests for the worker — pure functions + cancel-signal e2e with a real subprocess."""

import threading
import time

import jobd.worker.job_worker as job_worker
from jobd.worker.job_worker import (
    _missing_launcher_path,
    command_has_concurrent_ok,
    compute_unregistered_vram,
    effective_vram_request_gb_from_job,
    gpu_admission_check,
    pick_resource_snapshot_mock,
    run_job,
)


def test_compute_unregistered_zero_when_all_tracked():
    nvidia_procs = [(1000, 4096), (1001, 2048)]  # (pid, MiB)
    tracked_pids = {1000, 1001}
    assert compute_unregistered_vram(nvidia_procs, tracked_pids) == 0.0


def test_compute_unregistered_counts_untracked():
    nvidia_procs = [(1000, 4096), (9999, 8192)]
    tracked_pids = {1000}
    mib = 8192
    expected_gb = round(mib / 1024.0, 2)
    assert compute_unregistered_vram(nvidia_procs, tracked_pids) == expected_gb


def _stub_nvml(monkeypatch, free_gb: float, procs: list[tuple[int, int]]):
    """Force NVML on and stub the two callable seams."""
    monkeypatch.setattr(job_worker, "_NVML_OK", True)
    monkeypatch.setattr(job_worker, "nvidia_free_vram_gb", lambda: free_gb)
    monkeypatch.setattr(job_worker, "nvidia_processes", lambda: procs)


def test_admission_check_no_gpu_required_returns_unblocked(monkeypatch):
    """required_gb<=0 short-circuits — non-GPU jobs don't gate on this."""
    monkeypatch.setattr(job_worker, "_NVML_OK", True)
    out = gpu_admission_check(0.0, tracked_pids=set())
    assert out["blocked"] is False
    assert out["required_gb"] == 0.0


def test_admission_check_nvml_unavailable_returns_unblocked(monkeypatch):
    """Hosts without NVML (e.g. server CPU-only) skip the gate cleanly."""
    monkeypatch.setattr(job_worker, "_NVML_OK", False)
    out = gpu_admission_check(8.0, tracked_pids=set())
    assert out["blocked"] is False
    assert out["foreign_pids"] == []


def test_admission_check_unblocked_when_free_meets_required(monkeypatch):
    _stub_nvml(monkeypatch, free_gb=12.0, procs=[(1000, 4096)])
    out = gpu_admission_check(8.0, tracked_pids={1000})
    assert out["blocked"] is False
    assert out["free_gb"] == 12.0
    assert out["required_gb"] == 8.0
    assert out["foreign_pids"] == []  # 1000 is owned by us
    assert out["foreign_vram_gb"] == 0.0


def test_admission_check_blocked_when_foreign_holds_vram(monkeypatch):
    """Foreign holder pushes free below required — the load-bearing case."""
    _stub_nvml(monkeypatch, free_gb=2.0, procs=[(1000, 4096), (9999, 10240)])
    out = gpu_admission_check(8.0, tracked_pids={1000})
    assert out["blocked"] is True
    assert out["free_gb"] == 2.0
    assert out["required_gb"] == 8.0
    assert out["foreign_pids"] == [9999]
    assert out["foreign_vram_gb"] == 10.0


def test_admission_check_foreign_diagnostics_even_when_unblocked(monkeypatch):
    """A foreign holder that doesn't push free below required: blocked=False
    but the diagnostic fields still surface the foreign PID — useful for
    co-routing telemetry later (#42).
    """
    _stub_nvml(monkeypatch, free_gb=20.0, procs=[(9999, 8192)])
    out = gpu_admission_check(8.0, tracked_pids=set())
    assert out["blocked"] is False
    assert out["foreign_pids"] == [9999]
    assert out["foreign_vram_gb"] == 8.0


def test_admission_check_sums_multiple_foreign_holders(monkeypatch):
    _stub_nvml(monkeypatch, free_gb=1.0, procs=[(8001, 5120), (8002, 6144)])
    out = gpu_admission_check(8.0, tracked_pids=set())
    assert out["blocked"] is True
    assert out["foreign_pids"] == [8001, 8002]  # sorted
    assert out["foreign_vram_gb"] == 11.0


def test_effective_vram_explicit_wins():
    """#41d worker mirror: `vram_gb` overrides any tier-tag / gpu fallback."""
    job = {"vram_gb": 16.0, "requires": {"gpu": True, "needs": ["cuda-8gb"]}}
    assert effective_vram_request_gb_from_job(job) == 16.0


def test_effective_vram_tier_tag_picks_max():
    job = {"requires": {"gpu": True, "needs": ["cuda-12gb", "cuda-32gb", "R"]}}
    assert effective_vram_request_gb_from_job(job) == 32.0


def test_effective_vram_implicit_floor_when_only_gpu_true():
    """gpu=True but no explicit vram + no tier tag → 2 GB floor."""
    job = {"requires": {"gpu": True, "needs": []}}
    assert effective_vram_request_gb_from_job(job) == 2.0


def test_effective_vram_zero_for_non_gpu_job():
    """No requires.gpu, no tier tags, no explicit → 0 (gate is a no-op)."""
    job = {"requires": {"gpu": False, "needs": []}}
    assert effective_vram_request_gb_from_job(job) == 0.0


def test_effective_vram_handles_missing_requires():
    """Old job dicts without `requires` key shouldn't crash."""
    assert effective_vram_request_gb_from_job({}) == 0.0


def test_effective_vram_malformed_tier_tag_ignored():
    """`cuda-bigboygb` falls through ValueError; valid tag still wins."""
    job = {"requires": {"gpu": True, "needs": ["cuda-bigboygb", "cuda-12gb"]}}
    assert effective_vram_request_gb_from_job(job) == 12.0


def test_concurrent_ok_marker_detected_in_bash_c():
    """`bash -c "python train.py # CONCURRENT_OK"` form."""
    cmd = ["bash", "-c", "python train.py # CONCURRENT_OK"]
    assert command_has_concurrent_ok(cmd) is True


def test_concurrent_ok_marker_detected_with_no_space():
    cmd = ["bash", "-c", "python train.py #CONCURRENT_OK"]
    assert command_has_concurrent_ok(cmd) is True


def test_concurrent_ok_marker_absent():
    cmd = ["bash", "-c", "python train.py"]
    assert command_has_concurrent_ok(cmd) is False


def test_concurrent_ok_marker_substring_does_not_false_match():
    """Word-boundary anchor: `CONCURRENT_OKAY` mustn't trip the bypass."""
    cmd = ["bash", "-c", "echo CONCURRENT_OKAY"]
    assert command_has_concurrent_ok(cmd) is False


def test_concurrent_ok_marker_handles_empty_cmd():
    assert command_has_concurrent_ok(None) is False
    assert command_has_concurrent_ok([]) is False


def test_pick_resource_snapshot_mock_shape():
    snap = pick_resource_snapshot_mock()
    for key in (
        "host",
        "free_vram_gb",
        "unregistered_vram_gb",
        "free_ram_gb",
        "idle_cpus",
        "host_aliases",
        "arch",
        "os",
        "gpu",
        "tags",
    ):
        assert key in snap


class _FakeResp:
    def __init__(self, status_code=200, data=None):
        self.status_code = status_code
        self._data = data or {}

    def json(self):
        return self._data


class _FakeClient:
    """Minimal stand-in for httpx.Client. Records POSTs, answers GETs.

    cancel_after_s: wall-time elapsed from construction before /signal
    endpoints return {"signal": "cancel"}.
    """

    def __init__(self, cancel_after_s: float):
        self.cancel_after_s = cancel_after_s
        self.t0 = time.monotonic()
        self.posts: list[tuple[str, dict]] = []
        self.lock = threading.Lock()

    def get(self, path, **kw):
        if path.endswith("/signal"):
            elapsed = time.monotonic() - self.t0
            if elapsed >= self.cancel_after_s:
                return _FakeResp(200, {"signal": "cancel"})
            return _FakeResp(200, {"signal": None})
        return _FakeResp(200, {})

    def post(self, path, json=None, content=None, **kw):
        with self.lock:
            self.posts.append((path, json if json is not None else {"_raw": len(content or b"")}))
        return _FakeResp(200, {})


def test_run_job_cancel_signal_terminates_child(tmp_path, monkeypatch):
    """run_job must honor /signal=cancel: SIGTERM the child, exit with
    nonzero rc, and POST /complete with final_state=cancelled.

    Real subprocess (bash -c sleep 10); fake client returns cancel after
    1s. Whole test should finish in ~2s, well under the 10s sleep and
    the 60s grace window.

    Bypass the systemd-run wrap: the wrapper runs the workload in a
    user-scope cgroup, and signal delivery via `systemctl --user kill`
    is environment-dependent (CI containers often lack a user bus). Here
    we care about the signal chain inside run_job, not the scope
    integration — that's covered by tests/integration/test_cancel_latency.py.
    """
    import jobd.worker.job_worker as job_worker

    monkeypatch.setattr(job_worker.shutil, "which", lambda _name: None)

    job = {"id": 9001, "cmd": ["bash", "-c", "sleep 10"], "cwd": str(tmp_path)}
    client = _FakeClient(cancel_after_s=1.0)
    tracked: set[int] = set()

    t0 = time.monotonic()
    run_job(client, job, tracked)
    elapsed = time.monotonic() - t0

    assert elapsed < 8.0, f"run_job should exit quickly after cancel, took {elapsed:.1f}s"
    assert not tracked, "pid should be discarded after completion"

    completes = [p for p in client.posts if p[0].endswith("/complete")]
    assert len(completes) == 1, f"expected one /complete, got {client.posts}"
    body = completes[0][1]
    assert body["final_state"] == "cancelled"
    assert body["exit_code"] != 0, f"cancelled process should have nonzero rc, got {body}"


def test_run_job_fast_path_skips_systemd_run_wrap(tmp_path, monkeypatch):
    """fast_path=true must bypass the systemd-run scope wrap even when
    systemd-run exists on the worker. Regression guard: a fast-path job
    wrapped in systemd-run --scope pays a ~300ms startup tax per invocation
    and defeats the whole point."""
    import subprocess

    import jobd.worker.job_worker as job_worker

    monkeypatch.setattr(
        job_worker.shutil,
        "which",
        lambda name: f"/fake/bin/{name}" if name == "systemd-run" else None,
    )
    captured: dict[str, list[str]] = {}

    orig_popen = subprocess.Popen

    class _StubProc:
        def __init__(self, cmd, **kw):
            captured["cmd"] = cmd
            self.pid = 12345
            self.stdout = type("F", (), {"read": lambda _s, _n: b"", "close": lambda _s: None})()

        def send_signal(self, _sig):
            pass

        def wait(self, timeout=None):
            return 0

        def kill(self):
            pass

    monkeypatch.setattr(subprocess, "Popen", _StubProc)

    job = {"id": 9100, "cmd": ["echo", "hi"], "cwd": str(tmp_path), "fast_path": True}
    run_job(_FakeClient(cancel_after_s=999.0), job, set())

    assert captured["cmd"] == ["echo", "hi"], (
        f"fast_path should skip systemd-run wrap, got {captured['cmd']}"
    )
    # sanity: suppress unused warning
    _ = orig_popen


def test_run_job_non_fast_path_uses_named_scope(tmp_path, monkeypatch):
    """Non-fast-path jobs must be wrapped in `systemd-run --scope` with
    a deterministic --unit name keyed off job id and explicit memory caps.
    The scope name is what the cancel path signals via `systemctl --user
    kill`, so it must match the worker's _signal_workload contract.
    Regression for cancel-latency bug (2026-04-26): SIGTERM-to-pid was a
    no-op because pid pointed at the systemd-run client, not at bash."""
    import subprocess

    import jobd.worker.job_worker as job_worker

    monkeypatch.setattr(
        job_worker.shutil,
        "which",
        lambda name: f"/fake/bin/{name}" if name == "systemd-run" else None,
    )
    captured: dict[str, list[str]] = {}

    class _StubProc:
        def __init__(self, cmd, **kw):
            captured["cmd"] = cmd
            self.pid = 12346
            self.stdout = type("F", (), {"read": lambda _s, _n: b"", "close": lambda _s: None})()

        def send_signal(self, _sig):
            pass

        def wait(self, timeout=None):
            return 0

        def kill(self):
            pass

    monkeypatch.setattr(subprocess, "Popen", _StubProc)
    job = {"id": 9101, "cmd": ["python", "train.py"], "cwd": str(tmp_path), "fast_path": False}
    run_job(_FakeClient(cancel_after_s=999.0), job, set())
    assert captured["cmd"] == [
        "/fake/bin/systemd-run",
        "--user",
        "--scope",
        "--quiet",
        "--same-dir",
        f"--unit={job_worker.scope_unit_name(9101)}",
        "-p",
        "MemoryMax=14G",
        "-p",
        "MemorySwapMax=4G",
        "--",
        "python",
        "train.py",
    ], f"unexpected wrap shape: {captured['cmd']}"


def test_run_job_no_systemd_run_runs_bare(tmp_path, monkeypatch):
    """When systemd-run is unavailable (CI containers, minimal images),
    the worker must run the workload directly — no wrap, no crash."""
    import subprocess

    import jobd.worker.job_worker as job_worker

    monkeypatch.setattr(job_worker.shutil, "which", lambda _name: None)
    captured: dict[str, list[str]] = {}

    class _StubProc:
        def __init__(self, cmd, **kw):
            captured["cmd"] = cmd
            self.pid = 12347
            self.stdout = type("F", (), {"read": lambda _s, _n: b"", "close": lambda _s: None})()

        def send_signal(self, _sig):
            pass

        def wait(self, timeout=None):
            return 0

        def kill(self):
            pass

    monkeypatch.setattr(subprocess, "Popen", _StubProc)
    job = {"id": 9102, "cmd": ["echo", "x"], "cwd": str(tmp_path), "fast_path": False}
    run_job(_FakeClient(cancel_after_s=999.0), job, set())
    assert captured["cmd"] == ["echo", "x"], f"expected bare cmd, got {captured['cmd']}"


def test_run_job_cancel_signals_scope_unit_not_pid(tmp_path, monkeypatch):
    """The cancel signal path must `systemctl --user kill --signal=TERM
    jobd-{id}.scope` — NOT `proc.send_signal`. proc.pid points at the
    systemd-run client, which exits as soon as the scope is set up;
    signaling it is a silent no-op (root cause of the 2026-04-26 cancel-
    latency bug). This test verifies the signaling channel."""
    import subprocess

    import jobd.worker.job_worker as job_worker

    monkeypatch.setattr(
        job_worker.shutil,
        "which",
        lambda name: f"/fake/bin/{name}" if name == "systemd-run" else None,
    )
    systemctl_calls: list[list[str]] = []
    proc_signals: list[int] = []
    cancel_seen = threading.Event()

    class _StubStdout:
        def __init__(self):
            self._first = True

        def read(self, _n):
            # Block long enough for the signal-poll thread to observe cancel
            # and call _signal_workload. Without this, the main loop ends
            # before the poll thread gets scheduled.
            if self._first:
                self._first = False
                cancel_seen.wait(timeout=3.0)
            return b""

        def close(self):
            pass

    class _StubProc:
        def __init__(self, _cmd, **_kw):
            self.pid = 22222
            self.stdout = _StubStdout()

        def send_signal(self, sig):
            proc_signals.append(sig)

        def wait(self, timeout=None):
            return -15

        def kill(self):
            pass

    orig_run = subprocess.run

    def _fake_run(args, **kw):
        if args and args[0] == "systemctl":
            systemctl_calls.append(args)
            cancel_seen.set()

            class R:
                returncode = 0

            return R()
        return orig_run(args, **kw)

    monkeypatch.setattr(subprocess, "Popen", _StubProc)
    monkeypatch.setattr(subprocess, "run", _fake_run)

    job = {"id": 9201, "cmd": ["bash", "-c", "sleep 10"], "cwd": str(tmp_path)}
    client = _FakeClient(cancel_after_s=0.0)
    run_job(client, job, set())

    assert any(
        c[0] == "systemctl"
        and "--user" in c
        and "kill" in c
        and c[-1] == job_worker.scope_unit_name(9201)
        for c in systemctl_calls
    ), f"expected systemctl --user kill on scope, got {systemctl_calls}"
    assert proc_signals == [], (
        f"cancel must NOT fall through to proc.send_signal when scope is set, got {proc_signals}"
    )
    completes = [p for p in client.posts if p[0].endswith("/complete")]
    assert completes and completes[0][1]["final_state"] == "cancelled"


def test_run_job_posts_started_after_popen(tmp_path, monkeypatch):
    """Right after subprocess.Popen returns, the worker must POST /started so
    the broker can flip assigned -> running. Without this, jobs stay in
    ASSIGNED for their entire run and MCP cancel signal_sent reads as null."""
    import subprocess

    import jobd.worker.job_worker as job_worker

    monkeypatch.setattr(job_worker.shutil, "which", lambda _name: None)

    class _StubProc:
        def __init__(self, _cmd, **_kw):
            self.pid = 12347
            self.stdout = type("F", (), {"read": lambda _s, _n: b"", "close": lambda _s: None})()

        def send_signal(self, _sig):
            pass

        def wait(self, timeout=None):
            return 0

        def kill(self):
            pass

    monkeypatch.setattr(subprocess, "Popen", _StubProc)
    client = _FakeClient(cancel_after_s=999.0)
    job = {"id": 9200, "cmd": ["echo", "hi"], "cwd": str(tmp_path)}
    run_job(client, job, set())

    started_posts = [p for p in client.posts if p[0] == "/jobs/9200/started"]
    assert len(started_posts) == 1, (
        f"expected exactly one /started POST, got {[p[0] for p in client.posts]}"
    )


def _quiet_stub_proc_pair(rc_on_term: int = -15):
    """Build (StubProc, StubStdout) where stdout blocks until release_event
    is set, then returns b"" (EOF). Lets the test choose when the workload
    "finishes" — used to exercise watchdog-driven kills.
    """
    import subprocess as _subprocess  # noqa: F401

    release_event = threading.Event()

    class _StubStdout:
        def read(self, _n):
            release_event.wait(timeout=10.0)
            return b""

        def close(self):
            pass

    class _StubProc:
        def __init__(self, _cmd, **_kw):
            self.pid = 33333
            self.stdout = _StubStdout()

        def send_signal(self, _sig):
            release_event.set()

        def wait(self, timeout=None):
            release_event.wait(timeout=10.0)
            return rc_on_term

        def kill(self):
            release_event.set()

    return _StubProc, release_event


def test_run_job_idle_timeout_kills_silent_workload(tmp_path, monkeypatch):
    """A job that produces zero output for idle_timeout_s seconds must be
    killed via the scope cancel path and reported as failed with
    termination_reason='idle_timeout'. This is the 2026-04-25 hung-project-c
    failure mode: liveness != progress."""
    import subprocess

    import jobd.worker.job_worker as job_worker

    monkeypatch.setattr(job_worker, "SIGNAL_POLL_INTERVAL_S", 0.05)
    monkeypatch.setattr(
        job_worker.shutil,
        "which",
        lambda name: f"/fake/bin/{name}" if name == "systemd-run" else None,
    )
    StubProc, release = _quiet_stub_proc_pair()
    monkeypatch.setattr(subprocess, "Popen", StubProc)
    systemctl_calls: list[list[str]] = []

    def _fake_run(args, **_kw):
        if args and args[0] == "systemctl":
            systemctl_calls.append(args)
            release.set()

            class R:
                returncode = 0

            return R()

        class R:
            returncode = 0

        return R()

    monkeypatch.setattr(subprocess, "run", _fake_run)

    job = {
        "id": 9301,
        "cmd": ["bash", "-c", "sleep 60"],
        "cwd": str(tmp_path),
        "idle_timeout_s": 1,
    }
    client = _FakeClient(cancel_after_s=999.0)  # never user-cancel
    t0 = time.monotonic()
    run_job(client, job, set())
    elapsed = time.monotonic() - t0

    assert elapsed < 4.0, f"idle watchdog should fire promptly, took {elapsed:.1f}s"
    assert any(
        "kill" in c and c[-1] == job_worker.scope_unit_name(9301) for c in systemctl_calls
    ), f"expected systemctl kill on scope unit, got {systemctl_calls}"
    completes = [p for p in client.posts if p[0].endswith("/complete")]
    assert len(completes) == 1
    body = completes[0][1]
    assert body["final_state"] == "failed", (
        f"watchdog timeout must surface as 'failed' so depends_on cascade fails children, got {body}"
    )
    assert body["termination_reason"] == "idle_timeout"


def test_run_job_max_wall_kills_long_running_workload(tmp_path, monkeypatch):
    """A job whose total runtime exceeds max_wall_s must be killed and
    reported as failed with termination_reason='wall_timeout' — even if
    it's producing output (i.e. not idle)."""
    import subprocess

    import jobd.worker.job_worker as job_worker

    monkeypatch.setattr(job_worker, "SIGNAL_POLL_INTERVAL_S", 0.05)
    monkeypatch.setattr(
        job_worker.shutil,
        "which",
        lambda name: f"/fake/bin/{name}" if name == "systemd-run" else None,
    )
    StubProc, release = _quiet_stub_proc_pair()
    monkeypatch.setattr(subprocess, "Popen", StubProc)
    systemctl_calls: list[list[str]] = []

    def _fake_run(args, **_kw):
        if args and args[0] == "systemctl":
            systemctl_calls.append(args)
            release.set()

            class R:
                returncode = 0

            return R()

        class R:
            returncode = 0

        return R()

    monkeypatch.setattr(subprocess, "run", _fake_run)

    job = {
        "id": 9302,
        "cmd": ["bash", "-c", "yes"],
        "cwd": str(tmp_path),
        "max_wall_s": 1,
    }
    client = _FakeClient(cancel_after_s=999.0)
    t0 = time.monotonic()
    run_job(client, job, set())
    elapsed = time.monotonic() - t0

    assert elapsed < 4.0, f"wall watchdog should fire promptly, took {elapsed:.1f}s"
    completes = [p for p in client.posts if p[0].endswith("/complete")]
    assert len(completes) == 1
    body = completes[0][1]
    assert body["final_state"] == "failed"
    assert body["termination_reason"] == "wall_timeout"


def test_run_job_env_default_idle_timeout(tmp_path, monkeypatch):
    """JOBD_WORKER_IDLE_TIMEOUT_S env var supplies a default when the job
    doesn't specify idle_timeout_s. Lets ops set a fleet-wide watchdog
    without changing every call site."""
    import subprocess

    import jobd.worker.job_worker as job_worker

    monkeypatch.setenv("JOBD_WORKER_IDLE_TIMEOUT_S", "1")
    monkeypatch.setattr(job_worker, "SIGNAL_POLL_INTERVAL_S", 0.05)
    monkeypatch.setattr(job_worker.shutil, "which", lambda _name: None)
    StubProc, release = _quiet_stub_proc_pair()
    monkeypatch.setattr(subprocess, "Popen", StubProc)

    def _fake_run(args, **_kw):
        release.set()

        class R:
            returncode = 0

        return R()

    monkeypatch.setattr(subprocess, "run", _fake_run)

    job = {"id": 9303, "cmd": ["bash", "-c", "sleep 60"], "cwd": str(tmp_path)}
    client = _FakeClient(cancel_after_s=999.0)
    t0 = time.monotonic()
    run_job(client, job, set())
    elapsed = time.monotonic() - t0
    assert elapsed < 4.0, f"env-default idle watchdog should fire promptly, took {elapsed:.1f}s"
    completes = [p for p in client.posts if p[0].endswith("/complete")]
    assert len(completes) == 1
    assert completes[0][1]["termination_reason"] == "idle_timeout"


def test_user_cancel_surfaces_as_cancelled_not_failed(tmp_path, monkeypatch):
    """Regression guard: the watchdog logic flips final_state from
    'cancelled' to 'failed' only when termination_reason is set. A
    user-initiated /cancel must still land as 'cancelled' so the manual-
    abort surface stays clean."""
    import subprocess

    import jobd.worker.job_worker as job_worker

    monkeypatch.setattr(job_worker, "SIGNAL_POLL_INTERVAL_S", 0.05)
    monkeypatch.setattr(job_worker.shutil, "which", lambda _name: None)
    StubProc, release = _quiet_stub_proc_pair()
    monkeypatch.setattr(subprocess, "Popen", StubProc)

    def _fake_run(_args, **_kw):
        release.set()

        class R:
            returncode = 0

        return R()

    monkeypatch.setattr(subprocess, "run", _fake_run)

    job = {"id": 9304, "cmd": ["bash", "-c", "sleep 5"], "cwd": str(tmp_path)}
    client = _FakeClient(cancel_after_s=0.05)  # cancel almost immediately
    run_job(client, job, set())

    completes = [p for p in client.posts if p[0].endswith("/complete")]
    assert len(completes) == 1
    body = completes[0][1]
    assert body["final_state"] == "cancelled", body
    # No termination_reason should be set for user cancels
    assert "termination_reason" not in body or body.get("termination_reason") is None


def test_run_job_watchdog_escalates_to_sigkill_when_sigterm_ignored(tmp_path, monkeypatch):
    """A watchdog kill must escalate SIGTERM -> SIGKILL for a workload that
    ignores SIGTERM and keeps stdout open. The wall/idle/first-output branches
    route through _initiate_termination, which arms a kill timer; without it
    (bare _signal_workload('TERM')) poll_signals returns, the stdout-read loop
    blocks forever on the still-open pipe, and the post-loop
    proc.wait(timeout=grace) escalation is never reached — the job pins its
    slot (e.g. a GPU) indefinitely. Here only the SIGKILL lets the stub finish,
    so reaching /complete promptly proves the timer fired."""
    import subprocess

    import jobd.worker.job_worker as job_worker

    monkeypatch.setattr(job_worker, "SIGNAL_POLL_INTERVAL_S", 0.05)
    monkeypatch.setattr(job_worker, "WATCHDOG_KILL_GRACE_S", 0.3)
    monkeypatch.setattr(
        job_worker.shutil,
        "which",
        lambda name: f"/fake/bin/{name}" if name == "systemd-run" else None,
    )

    # Released ONLY by the SIGKILL escalation. A SIGTERM leaves the workload
    # alive with stdout still blocked, modelling the ignore-SIGTERM case.
    killed = threading.Event()

    class _StubStdout:
        def read(self, _n):
            killed.wait(timeout=10.0)
            return b""

        def close(self):
            pass

    class _StubProc:
        def __init__(self, _cmd, **_kw):
            self.pid = 34444
            self.stdout = _StubStdout()

        def send_signal(self, _sig):
            pass

        def poll(self):
            return None if not killed.is_set() else -9

        def wait(self, timeout=None):
            killed.wait(timeout=10.0)
            return -9

        def kill(self):
            killed.set()

    monkeypatch.setattr(subprocess, "Popen", _StubProc)
    systemctl_calls: list[list[str]] = []

    def _fake_run(args, **_kw):
        if args and args[0] == "systemctl":
            systemctl_calls.append(args)
            if any(a == "--signal=KILL" for a in args):
                killed.set()

        class R:
            returncode = 0

        return R()

    monkeypatch.setattr(subprocess, "run", _fake_run)

    job = {
        "id": 9401,
        "cmd": ["bash", "-c", "sleep 60"],
        "cwd": str(tmp_path),
        "idle_timeout_s": 1,
    }
    client = _FakeClient(cancel_after_s=999.0)  # never user-cancel
    t0 = time.monotonic()
    run_job(client, job, set())
    elapsed = time.monotonic() - t0

    assert elapsed < 6.0, f"escalation should force-kill promptly, took {elapsed:.1f}s"
    kill_calls = [
        c
        for c in systemctl_calls
        if any(a == "--signal=KILL" for a in c) and c[-1] == job_worker.scope_unit_name(9401)
    ]
    assert kill_calls, f"watchdog must escalate to SIGKILL on the scope unit, got {systemctl_calls}"
    completes = [p for p in client.posts if p[0].endswith("/complete")]
    assert len(completes) == 1
    assert completes[0][1]["termination_reason"] == "idle_timeout"


def test_run_job_does_not_post_started_when_popen_fails(tmp_path, monkeypatch):
    """If Popen raises (bad cmd, missing executable), the worker reports
    /complete with exit 127 — and must NOT POST /started, because there's
    no live subprocess to be 'running'."""
    import subprocess

    import jobd.worker.job_worker as job_worker

    monkeypatch.setattr(job_worker.shutil, "which", lambda _name: None)

    def _boom(*_a, **_kw):
        raise FileNotFoundError("nope")

    monkeypatch.setattr(subprocess, "Popen", _boom)
    client = _FakeClient(cancel_after_s=999.0)
    job = {"id": 9201, "cmd": ["does-not-exist"], "cwd": str(tmp_path)}
    run_job(client, job, set())

    started_posts = [p for p in client.posts if p[0].endswith("/started")]
    assert started_posts == [], f"unexpected /started POST after Popen failure: {client.posts}"
    complete_posts = [p for p in client.posts if p[0].endswith("/complete")]
    assert len(complete_posts) == 1
    assert complete_posts[0][1]["exit_code"] == 127


class _PreemptClient:
    """FakeClient variant: returns signal=preempt after preempt_after_s.

    Records all POSTs (raw bytes for /log, parsed JSON for everything
    else) so tests can verify both the side effects of preempt handling
    and the bodies of /complete and /checkpoint-complete.
    """

    def __init__(self, preempt_after_s: float):
        self.preempt_after_s = preempt_after_s
        self.t0 = time.monotonic()
        self.posts: list[tuple[str, object]] = []
        self.lock = threading.Lock()

    def get(self, path, **kw):
        if path.endswith("/signal"):
            elapsed = time.monotonic() - self.t0
            if elapsed >= self.preempt_after_s:
                return _FakeResp(200, {"signal": "preempt"})
            return _FakeResp(200, {"signal": None})
        return _FakeResp(200, {})

    def post(self, path, json=None, content=None, **kw):
        with self.lock:
            payload: object = json if json is not None else (content or b"")
            self.posts.append((path, payload))
        return _FakeResp(200, {})


def test_run_job_preempt_with_checkpoint_token_emits_checkpoint_complete(tmp_path, monkeypatch):
    """#37: when the workload prints `jobd-checkpoint-complete` after
    SIGTERM during a preempt, the worker POSTs /checkpoint-complete and
    reports final_state=preempted."""
    import jobd.worker.job_worker as job_worker

    monkeypatch.setattr(job_worker.shutil, "which", lambda _name: None)

    job = {
        "id": 9301,
        "cmd": [
            "python3",
            "-c",
            "import signal,sys,time\n"
            "signal.signal(signal.SIGTERM, lambda *a: ("
            "print('jobd-checkpoint-complete', flush=True), sys.exit(0)))\n"
            "time.sleep(30)\n",
        ],
        "cwd": str(tmp_path),
        "checkpoint_grace_s": 30,
    }
    client = _PreemptClient(preempt_after_s=1.0)
    run_job(client, job, set())

    paths = [p for p, _ in client.posts]
    assert any(p.endswith("/checkpoint-complete") for p in paths), (
        f"expected /checkpoint-complete POST, got {paths}"
    )
    completes = [
        p
        for p in client.posts
        if p[0].endswith("/complete") and not p[0].endswith("/checkpoint-complete")
    ]
    assert len(completes) == 1
    assert completes[0][1]["final_state"] == "preempted"


def test_run_job_preempt_without_token_skips_checkpoint_complete(tmp_path, monkeypatch):
    """#37: a preempted workload that exits without printing the token
    still completes as preempted, but no /checkpoint-complete POST fires."""
    import jobd.worker.job_worker as job_worker

    monkeypatch.setattr(job_worker.shutil, "which", lambda _name: None)

    job = {
        "id": 9302,
        "cmd": ["bash", "-c", "sleep 30"],
        "cwd": str(tmp_path),
    }
    client = _PreemptClient(preempt_after_s=1.0)
    run_job(client, job, set())

    paths = [p for p, _ in client.posts]
    assert not any(p.endswith("/checkpoint-complete") for p in paths), (
        f"unexpected /checkpoint-complete POST: {paths}"
    )
    completes = [p for p in client.posts if p[0].endswith("/complete")]
    assert len(completes) == 1
    assert completes[0][1]["final_state"] == "preempted"


def test_run_job_preempt_honors_checkpoint_grace_s(tmp_path, monkeypatch):
    """#37: an explicit checkpoint_grace_s overrides the worker's 60s
    default — a workload that ignores SIGTERM gets SIGKILLed after the
    configured grace window, not after a minute."""
    import jobd.worker.job_worker as job_worker

    monkeypatch.setattr(job_worker.shutil, "which", lambda _name: None)

    job = {
        "id": 9303,
        "cmd": [
            "python3",
            "-c",
            "import signal,time\nsignal.signal(signal.SIGTERM, lambda *a: None)\ntime.sleep(30)\n",
        ],
        "cwd": str(tmp_path),
        "checkpoint_grace_s": 2,
    }
    client = _PreemptClient(preempt_after_s=1.0)

    t0 = time.monotonic()
    run_job(client, job, set())
    elapsed = time.monotonic() - t0

    assert elapsed < 6.0, f"expected ~3s with grace=2, got {elapsed:.1f}s"
    completes = [p for p in client.posts if p[0].endswith("/complete")]
    assert completes[0][1]["final_state"] == "preempted"


def test_run_job_exports_checkpoint_grace_env_for_workload(tmp_path, monkeypatch):
    """#38: worker exposes the resolved per-job checkpoint_grace_s to the
    workload via JOBD_CHECKPOINT_GRACE_S so install_preemption_handler
    can compute time_remaining()."""
    import jobd.worker.job_worker as job_worker

    monkeypatch.setattr(job_worker.shutil, "which", lambda _name: None)

    out_path = tmp_path / "env.txt"
    job = {
        "id": 9501,
        "cmd": [
            "bash",
            "-c",
            f"echo $JOBD_CHECKPOINT_GRACE_S > {out_path}",
        ],
        "cwd": str(tmp_path),
        "checkpoint_grace_s": 90,
    }
    run_job(_FakeClient(cancel_after_s=999.0), job, set())
    assert out_path.read_text().strip() == "90"


def test_run_job_exports_default_checkpoint_grace_env_when_unset(tmp_path, monkeypatch):
    """No checkpoint_grace_s on the job → worker exports the default 60s
    so install_preemption_handler always has a sane value to read."""
    import jobd.worker.job_worker as job_worker

    monkeypatch.setattr(job_worker.shutil, "which", lambda _name: None)

    out_path = tmp_path / "env.txt"
    job = {
        "id": 9502,
        "cmd": [
            "bash",
            "-c",
            f"echo $JOBD_CHECKPOINT_GRACE_S > {out_path}",
        ],
        "cwd": str(tmp_path),
    }
    run_job(_FakeClient(cancel_after_s=999.0), job, set())
    assert out_path.read_text().strip() == "60"


# ---- #42 multi-tenant per-host co-routing -------------------------------


def _reset_in_flight():
    with job_worker._in_flight_lock:
        job_worker._in_flight.clear()


def test_max_concurrent_jobs_default_is_1(monkeypatch):
    """Default preserves single-slot behavior — opt-in via env."""
    monkeypatch.delenv("JOBD_WORKER_MAX_CONCURRENT_JOBS", raising=False)
    assert job_worker._max_concurrent_jobs() == 1


def test_max_concurrent_jobs_reads_env(monkeypatch):
    monkeypatch.setenv("JOBD_WORKER_MAX_CONCURRENT_JOBS", "4")
    assert job_worker._max_concurrent_jobs() == 4


def test_max_concurrent_jobs_floor_is_1(monkeypatch):
    """Garbage / negative env values fall back to 1, never 0."""
    monkeypatch.setenv("JOBD_WORKER_MAX_CONCURRENT_JOBS", "0")
    assert job_worker._max_concurrent_jobs() == 1
    monkeypatch.setenv("JOBD_WORKER_MAX_CONCURRENT_JOBS", "-3")
    assert job_worker._max_concurrent_jobs() == 1
    monkeypatch.setenv("JOBD_WORKER_MAX_CONCURRENT_JOBS", "not-a-number")
    assert job_worker._max_concurrent_jobs() == 1


def test_register_in_flight_records_ad():
    _reset_in_flight()
    try:
        job_worker._register_in_flight({"id": 901, "vram_gb": 8.0, "ram_gb": 4.0, "cpus": 2})
        v, r, c = job_worker._allocations_total()
        assert v == 8.0
        assert r == 4.0
        assert c == 2
    finally:
        _reset_in_flight()


def test_register_in_flight_uses_effective_vram_for_tier_tag():
    """vram_gb=0 + cuda-32gb in needs → tier ladder fallback in the ad."""
    _reset_in_flight()
    try:
        job_worker._register_in_flight(
            {
                "id": 902,
                "vram_gb": 0,
                "requires": {"needs": ["cuda-32gb"]},
                "ram_gb": 0,
                "cpus": 0,
            }
        )
        v, _, _ = job_worker._allocations_total()
        assert v == 32.0
    finally:
        _reset_in_flight()


def test_unregister_in_flight_clears_ad():
    _reset_in_flight()
    try:
        job_worker._register_in_flight({"id": 903, "vram_gb": 8.0, "ram_gb": 4.0, "cpus": 2})
        job_worker._unregister_in_flight(903)
        assert job_worker._allocations_total() == (0.0, 0.0, 0)
    finally:
        _reset_in_flight()


def test_allocations_sum_across_multiple_in_flight():
    _reset_in_flight()
    try:
        job_worker._register_in_flight({"id": 910, "vram_gb": 8.0, "ram_gb": 4.0, "cpus": 2})
        job_worker._register_in_flight({"id": 911, "vram_gb": 12.0, "ram_gb": 6.0, "cpus": 3})
        v, r, c = job_worker._allocations_total()
        assert v == 20.0
        assert r == 10.0
        assert c == 5
    finally:
        _reset_in_flight()


def test_resource_snapshot_subtracts_in_flight_vram(monkeypatch):
    """Heartbeat must report adjusted (raw - in-flight) free, so the broker
    matcher only sees true-available capacity. Without this, a worker
    running one 24 GB job still advertises its full GPU as free."""
    _reset_in_flight()
    try:
        monkeypatch.setattr(job_worker, "nvidia_free_vram_gb", lambda: 32.0)
        monkeypatch.setattr(job_worker, "nvidia_processes", lambda: [])
        job_worker._register_in_flight({"id": 920, "vram_gb": 24.0, "ram_gb": 0, "cpus": 0})
        snap = job_worker.resource_snapshot(set())
        assert snap["free_vram_gb"] == 8.0
    finally:
        _reset_in_flight()


def test_resource_snapshot_floor_at_zero_when_overcommitted(monkeypatch):
    """In-flight ad larger than current raw free (e.g., the workload
    crashed and freed VRAM but the ad still reflects the matcher's
    reservation) — clamp at 0 rather than going negative."""
    _reset_in_flight()
    try:
        monkeypatch.setattr(job_worker, "nvidia_free_vram_gb", lambda: 4.0)
        monkeypatch.setattr(job_worker, "nvidia_processes", lambda: [])
        job_worker._register_in_flight({"id": 930, "vram_gb": 24.0, "ram_gb": 0, "cpus": 0})
        snap = job_worker.resource_snapshot(set())
        assert snap["free_vram_gb"] == 0.0
    finally:
        _reset_in_flight()


def test_resource_snapshot_no_in_flight_unchanged(monkeypatch):
    """No allocations → snapshot reports raw NVML readings unchanged."""
    _reset_in_flight()
    monkeypatch.setattr(job_worker, "nvidia_free_vram_gb", lambda: 30.0)
    monkeypatch.setattr(job_worker, "nvidia_processes", lambda: [])
    snap = job_worker.resource_snapshot(set())
    assert snap["free_vram_gb"] == 30.0


def test_resource_snapshot_does_not_double_count_allocated_vram(monkeypatch):
    """M6: once an in-flight job has actually allocated its VRAM, NVML free
    already reflects it — the ad reservation must NOT subtract it again, or a
    steady multi-slot worker idles capacity the GPU actually has. Here a 24 GB
    job on a 32 GB card (NVML free = 8) must still report 8 GB free, not
    8 - 24 clamped to 0."""
    _reset_in_flight()
    try:
        monkeypatch.setattr(job_worker, "nvidia_free_vram_gb", lambda: 8.0)
        # The job's OWN GPU pid holds the full 24 GB (24 * 1024 MiB).
        monkeypatch.setattr(job_worker, "nvidia_processes", lambda: [(4242, 24 * 1024)])
        job_worker._register_in_flight({"id": 921, "vram_gb": 24.0, "ram_gb": 0, "cpus": 0})
        snap = job_worker.resource_snapshot({4242})  # tracked_pids owns 4242
        assert snap["free_vram_gb"] == 8.0
    finally:
        _reset_in_flight()


def test_resource_snapshot_reserves_only_unallocated_vram(monkeypatch):
    """A job mid-startup that has allocated only part of its ad reserves the
    remainder: raw_free - max(0, ad - allocated). Here NVML free = 20, the job
    has allocated 12 of its 24 GB ad, so reserve 12 -> report 8."""
    _reset_in_flight()
    try:
        monkeypatch.setattr(job_worker, "nvidia_free_vram_gb", lambda: 20.0)
        monkeypatch.setattr(job_worker, "nvidia_processes", lambda: [(4243, 12 * 1024)])
        job_worker._register_in_flight({"id": 922, "vram_gb": 24.0, "ram_gb": 0, "cpus": 0})
        snap = job_worker.resource_snapshot({4243})
        assert snap["free_vram_gb"] == 8.0
    finally:
        _reset_in_flight()


def test_compute_owned_vram_sums_owned_pids_only():
    """compute_owned_vram is the mirror of compute_unregistered_vram: it sums
    VRAM held by pids IN the owned set (GB, 2dp)."""
    procs = [(100, 8 * 1024), (200, 4 * 1024), (300, 2 * 1024)]
    assert job_worker.compute_owned_vram(procs, {100, 300}) == 10.0
    assert job_worker.compute_owned_vram(procs, set()) == 0.0
    assert job_worker.compute_owned_vram([], {100}) == 0.0


def test_resource_snapshot_reports_slot_usage(monkeypatch):
    """Heartbeat carries max_concurrent (the env knob) + running (live
    in-flight count) so the broker can surface slot usage in `job workers`."""
    _reset_in_flight()
    try:
        monkeypatch.setattr(job_worker, "nvidia_free_vram_gb", lambda: 30.0)
        monkeypatch.setattr(job_worker, "nvidia_processes", lambda: [])
        monkeypatch.setenv("JOBD_WORKER_MAX_CONCURRENT_JOBS", "3")
        job_worker._register_in_flight({"id": 1, "vram_gb": 0, "ram_gb": 0, "cpus": 1})
        job_worker._register_in_flight({"id": 2, "vram_gb": 0, "ram_gb": 0, "cpus": 1})
        snap = job_worker.resource_snapshot(set())
        assert snap["max_concurrent"] == 3
        assert snap["running"] == 2
    finally:
        _reset_in_flight()


def test_resource_snapshot_reports_in_flight_job_ids(monkeypatch):
    """SIGTERM-drain Phase 2: every heartbeat carries the worker's in-flight
    job ids so the broker can reconcile claims a restarted worker no longer
    knows about (docs/runbook.md, "Drain / restart a worker"). Sorted for determinism."""
    _reset_in_flight()
    try:
        monkeypatch.setattr(job_worker, "nvidia_free_vram_gb", lambda: 30.0)
        monkeypatch.setattr(job_worker, "nvidia_processes", lambda: [])
        snap = job_worker.resource_snapshot(set())
        assert snap["in_flight_job_ids"] == []
        job_worker._register_in_flight({"id": 972, "vram_gb": 0, "ram_gb": 0, "cpus": 1})
        job_worker._register_in_flight({"id": 971, "vram_gb": 0, "ram_gb": 0, "cpus": 1})
        snap = job_worker.resource_snapshot(set())
        assert snap["in_flight_job_ids"] == [971, 972]
    finally:
        _reset_in_flight()


def test_effective_owned_pids_unions_scope_cgroup_pids(tmp_path, monkeypatch):
    """A scope-wrapped job's real CUDA pids live in its cgroup, not in
    tracked_pids (which holds the systemd-run client pid). The owned set must
    union both — read from a real cgroup.procs file."""
    _reset_in_flight()
    try:
        scope_dir = tmp_path / job_worker.scope_unit_name(9300)
        scope_dir.mkdir()
        (scope_dir / "cgroup.procs").write_text("4242\n4243\n")
        monkeypatch.setattr(job_worker, "_REAPER_OK", True)
        monkeypatch.setattr(
            job_worker._cgroup_walk,
            "resolve_user_scope_path",
            lambda unit: scope_dir if unit == job_worker.scope_unit_name(9300) else None,
        )
        job_worker._register_in_flight({"id": 9300, "vram_gb": 8, "ram_gb": 0, "cpus": 0})
        # 999 = the systemd-run client pid we Popen'd; 4242/4243 = real workload.
        assert job_worker._effective_owned_pids({999}) == {999, 4242, 4243}
    finally:
        _reset_in_flight()


def test_effective_owned_pids_no_scope_is_just_tracked(monkeypatch):
    """Fast-path / unwrapped jobs have no scope cgroup (resolve → None); the
    owned set is then exactly tracked_pids (the real child is already there)."""
    _reset_in_flight()
    try:
        monkeypatch.setattr(job_worker, "_REAPER_OK", True)
        monkeypatch.setattr(job_worker._cgroup_walk, "resolve_user_scope_path", lambda unit: None)
        job_worker._register_in_flight({"id": 9301, "vram_gb": 0, "ram_gb": 0, "cpus": 0})
        assert job_worker._effective_owned_pids({777}) == {777}
    finally:
        _reset_in_flight()


def test_own_scope_job_not_counted_as_foreign_vram(tmp_path, monkeypatch):
    """The P3.3 bug: NVML reports a forked-child pid (inside the scope cgroup)
    holding the VRAM, but tracked_pids only has the job's top-level pid — so the
    worker's OWN child was mis-counted as foreign/unregistered. With the
    effective-owned-pids union (cgroup is the ownership boundary) it reads as 0
    unregistered. RTX 5090 real-exec confirmed NVML reports the child pid."""
    _reset_in_flight()
    try:
        scope_dir = tmp_path / job_worker.scope_unit_name(9302)
        scope_dir.mkdir()
        (scope_dir / "cgroup.procs").write_text("5000\n")  # the real CUDA pid
        monkeypatch.setattr(job_worker, "_REAPER_OK", True)
        monkeypatch.setattr(
            job_worker._cgroup_walk,
            "resolve_user_scope_path",
            lambda unit: scope_dir if unit == job_worker.scope_unit_name(9302) else None,
        )
        monkeypatch.setattr(job_worker, "nvidia_processes", lambda: [(5000, 8192)])
        monkeypatch.setattr(job_worker, "nvidia_free_vram_gb", lambda: 24.0)
        job_worker._register_in_flight({"id": 9302, "vram_gb": 8, "ram_gb": 0, "cpus": 0})
        # tracked_pids holds only the top-level pid 999; the forked CUDA child
        # (5000) is owned via the cgroup, so it is NOT foreign.
        snap = job_worker.resource_snapshot({999})
        assert snap["unregistered_vram_gb"] == 0.0
    finally:
        _reset_in_flight()


def test_the_reparented_orphan_sweep_runs_only_for_a_solo_job(monkeypatch):
    """P3.5: the reparented-orphan /proc sweep must only run when this is the
    sole in-flight job. With a concurrent job registered, the global sweep
    could kill the other job's reparented fast-path descendant. The gate is
    now atomic with the scan (audit 2026-09-02 L-4; see
    tests/unit/test_orphan_sweep_solo_gate_is_atomic.py), so this drives the
    helper and observes whether the scan ran."""
    scans: list[set[int]] = []
    monkeypatch.setattr(
        job_worker._subreaper,
        "sweep_and_kill_reparented_orphans",
        lambda known: scans.append(set(known)) or [],
    )
    _reset_in_flight()
    try:
        job_worker._sweep_reparented_orphans_if_solo(960, {1})
        assert len(scans) == 1  # nothing in flight
        job_worker._register_in_flight({"id": 960, "vram_gb": 0, "ram_gb": 0, "cpus": 0})
        job_worker._sweep_reparented_orphans_if_solo(960, {1})
        assert len(scans) == 2  # just me
        job_worker._register_in_flight({"id": 961, "vram_gb": 0, "ram_gb": 0, "cpus": 0})
        job_worker._sweep_reparented_orphans_if_solo(960, {1})
        assert len(scans) == 2  # a concurrent job exists: no scan
        job_worker._unregister_in_flight(961)
        job_worker._sweep_reparented_orphans_if_solo(960, {1})
        assert len(scans) == 3  # back to solo
    finally:
        _reset_in_flight()


def test_reserve_and_dispatch_registers_before_returning_threaded():
    """Over-subscribe race regression (P3.4): the in-flight reservation must be
    taken SYNCHRONOUSLY — visible the instant _reserve_and_dispatch returns,
    before the worker thread has even run. If registration were deferred into
    the thread (the pre-fix behavior), the poll loop's capacity gate could
    re-poll against a stale count and oversubscribe past max_concurrent."""
    _reset_in_flight()
    started = threading.Event()
    release = threading.Event()

    def fake_run(job):
        started.set()
        release.wait(5)  # keep the job "in flight" so we can observe it
        job_worker._unregister_in_flight(int(job["id"]))

    threads: list = []
    try:
        job_worker._reserve_and_dispatch(
            {"id": 940, "vram_gb": 8.0, "ram_gb": 4.0, "cpus": 2},
            job_threads=threads,
            run_in_thread=fake_run,
        )
        # Reservation visible immediately — the gate would see it next poll.
        with job_worker._in_flight_lock:
            assert 940 in job_worker._in_flight
        assert len(threads) == 1
        assert started.wait(5), "worker thread never started"
        release.set()
        threads[0].join(5)
        # Thread's finally unregisters once the job finishes.
        with job_worker._in_flight_lock:
            assert 940 not in job_worker._in_flight
    finally:
        release.set()
        _reset_in_flight()


def test_reserve_and_dispatch_single_slot_also_threads():
    """SIGTERM-drain prerequisite: even at max_concurrent == 1 the job must run
    in a worker thread, never inline in the poll loop. Inline execution parks
    the main thread inside proc.stdout.read() for the whole job, so a drain
    can't start until the job ends naturally (docs/runbook.md, "Drain / restart a worker")."""
    _reset_in_flight()
    started = threading.Event()
    release = threading.Event()
    ran: list = []

    def fake_run(job):
        started.set()
        release.wait(5)  # if this ran inline, _reserve_and_dispatch would hang
        ran.append(int(job["id"]))
        job_worker._unregister_in_flight(int(job["id"]))

    threads: list = []
    try:
        job_worker._reserve_and_dispatch(
            {"id": 941, "vram_gb": 8.0, "ram_gb": 4.0, "cpus": 2},
            job_threads=threads,
            run_in_thread=fake_run,
        )
        # Returned while the job is still running — therefore not inline.
        assert len(threads) == 1
        with job_worker._in_flight_lock:
            assert 941 in job_worker._in_flight  # reservation already visible
        assert started.wait(5), "worker thread never started"
        release.set()
        threads[0].join(5)
        assert ran == [941]
        with job_worker._in_flight_lock:
            assert 941 not in job_worker._in_flight
    finally:
        release.set()
        _reset_in_flight()


def test_reserve_and_dispatch_caps_concurrent_reservations():
    """Driving N claims through _reserve_and_dispatch with a gate check between
    each (mirroring the poll loop) never exceeds max_concurrent live
    reservations — the synchronous registration closes the race window."""
    _reset_in_flight()
    release = threading.Event()
    max_concurrent = 3
    threads: list = []

    def fake_run(job):
        release.wait(5)
        job_worker._unregister_in_flight(int(job["id"]))

    try:
        admitted = 0
        for jid in range(950, 960):  # 10 candidate jobs, cap is 3
            with job_worker._in_flight_lock:
                live = len(job_worker._in_flight)
            if live >= max_concurrent:
                continue  # gate refuses, exactly as the poll loop does
            job_worker._reserve_and_dispatch(
                {"id": jid, "vram_gb": 8.0, "ram_gb": 4.0, "cpus": 2},
                job_threads=threads,
                run_in_thread=fake_run,
            )
            admitted += 1
            with job_worker._in_flight_lock:
                assert len(job_worker._in_flight) <= max_concurrent
        assert admitted == max_concurrent
        release.set()
        for t in threads:
            t.join(5)
    finally:
        release.set()
        _reset_in_flight()


def test_run_job_exposes_checkpoint_dir(tmp_path, monkeypatch):
    """run_job must set JOBD_CHECKPOINT_DIR in the spawned env, pointing at a
    pre-created directory under the default root (~/.local/share/jobd/checkpoints).
    Workloads use this to write durable checkpoints during a preempt."""
    import subprocess

    import jobd.worker.job_worker as job_worker

    monkeypatch.setattr(
        job_worker.shutil,
        "which",
        lambda name: f"/fake/bin/{name}" if name == "systemd-run" else None,
    )
    # Force the default root: clear override, redirect HOME to tmp so we don't
    # touch the real ~/.local/share.
    monkeypatch.delenv("JOBD_WORKER_CHECKPOINT_ROOT", raising=False)
    monkeypatch.delenv("XDG_DATA_HOME", raising=False)
    monkeypatch.setenv("HOME", str(tmp_path))

    captured: dict[str, dict] = {}

    class _StubProc:
        def __init__(self, cmd, **kw):
            captured["env"] = kw.get("env") or {}
            self.pid = 12345
            self.stdout = type("F", (), {"read": lambda _s, _n: b"", "close": lambda _s: None})()

        def send_signal(self, _sig):
            pass

        def wait(self, timeout=None):
            return 0

        def kill(self):
            pass

    monkeypatch.setattr(subprocess, "Popen", _StubProc)

    job = {
        "id": 9200,
        "cmd": ["echo", "hi"],
        "cwd": str(tmp_path),
        "fast_path": True,
    }
    run_job(_FakeClient(cancel_after_s=999.0), job, set())

    ckpt_dir = captured["env"].get("JOBD_CHECKPOINT_DIR")
    assert ckpt_dir, f"JOBD_CHECKPOINT_DIR not set; env keys: {sorted(captured['env'].keys())}"
    expected_root = tmp_path / ".local" / "share" / "jobd" / "checkpoints"
    assert ckpt_dir == str(expected_root / "9200"), (
        f"expected {expected_root / '9200'}, got {ckpt_dir}"
    )
    assert (expected_root / "9200").is_dir(), f"directory not created at {ckpt_dir}"


def test_run_job_propagates_submitted_env(tmp_path, monkeypatch):
    """P0.3 (CRIT M1): caller-submitted env vars (`job["env"]`) reach the
    workload subprocess. They layer over the worker's inherited environment,
    and jobd-internal JOBD_* vars still take precedence (a caller cannot
    clobber JOBD_CHECKPOINT_DIR)."""
    import subprocess

    import jobd.worker.job_worker as job_worker

    monkeypatch.setattr(
        job_worker.shutil,
        "which",
        lambda name: f"/fake/bin/{name}" if name == "systemd-run" else None,
    )
    monkeypatch.delenv("JOBD_WORKER_CHECKPOINT_ROOT", raising=False)
    monkeypatch.delenv("XDG_DATA_HOME", raising=False)
    monkeypatch.setenv("HOME", str(tmp_path))

    captured: dict[str, dict] = {}

    class _StubProc:
        def __init__(self, cmd, **kw):
            captured["env"] = kw.get("env") or {}
            self.pid = 12346
            self.stdout = type("F", (), {"read": lambda _s, _n: b"", "close": lambda _s: None})()

        def send_signal(self, _sig):
            pass

        def wait(self, timeout=None):
            return 0

        def kill(self):
            pass

    monkeypatch.setattr(subprocess, "Popen", _StubProc)

    job = {
        "id": 9300,
        "cmd": ["printenv"],
        "cwd": str(tmp_path),
        "fast_path": True,
        "env": {
            "MY_TOKEN": "abc123",
            # A caller MUST NOT be able to clobber jobd-internal vars.
            "JOBD_CHECKPOINT_DIR": "/tmp/evil",
        },
    }
    run_job(_FakeClient(cancel_after_s=999.0), job, set())

    assert captured["env"].get("MY_TOKEN") == "abc123"
    # jobd-internal var wins over a caller attempting to override it.
    assert captured["env"].get("JOBD_CHECKPOINT_DIR") != "/tmp/evil"
    assert captured["env"]["JOBD_CHECKPOINT_DIR"].endswith("/9300")


def test_run_job_scrubs_broker_token_from_workload_env(tmp_path, monkeypatch):
    """The worker's JOBD_API_TOKEN must not be inherited by workloads — any
    script or framework inside the job can dump its environment, and the
    shared broker credential would ride along. A job that genuinely needs it
    opts in by passing it in its own submitted `env`, which layers over the
    scrub. Ordinary worker-side env vars are still inherited."""
    import subprocess

    monkeypatch.setenv("JOBD_API_TOKEN", "worker-secret-token")
    monkeypatch.setenv("SOME_WORKER_VAR", "inherited-ok")
    monkeypatch.setenv("HOME", str(tmp_path))

    captured: dict[str, dict] = {}

    class _StubProc:
        def __init__(self, cmd, **kw):
            captured["env"] = kw.get("env") or {}
            self.pid = 12347
            self.stdout = type("F", (), {"read": lambda _s, _n: b"", "close": lambda _s: None})()

        def send_signal(self, _sig):
            pass

        def wait(self, timeout=None):
            return 0

        def kill(self):
            pass

    monkeypatch.setattr(subprocess, "Popen", _StubProc)

    job = {"id": 9400, "cmd": ["printenv"], "cwd": str(tmp_path), "fast_path": True}
    run_job(_FakeClient(cancel_after_s=999.0), job, set())
    assert "JOBD_API_TOKEN" not in captured["env"], (
        "the broker credential leaked into the workload environment"
    )
    assert captured["env"].get("SOME_WORKER_VAR") == "inherited-ok"

    # Explicit opt-in: a submitted env carrying the token IS delivered.
    job = {
        "id": 9401,
        "cmd": ["printenv"],
        "cwd": str(tmp_path),
        "fast_path": True,
        "env": {"JOBD_API_TOKEN": "job-scoped-token"},
    }
    run_job(_FakeClient(cancel_after_s=999.0), job, set())
    assert captured["env"].get("JOBD_API_TOKEN") == "job-scoped-token"


def test_run_job_checkpoint_dir_root_override(tmp_path, monkeypatch):
    """JOBD_WORKER_CHECKPOINT_ROOT overrides the default root. Used when an
    operator wants checkpoints on a different filesystem (e.g., a fast NVMe
    instead of HDD-backed home)."""
    import subprocess

    import jobd.worker.job_worker as job_worker

    monkeypatch.setattr(
        job_worker.shutil,
        "which",
        lambda name: f"/fake/bin/{name}" if name == "systemd-run" else None,
    )
    override_root = tmp_path / "fast-disk" / "jobd-ckpts"
    monkeypatch.setenv("JOBD_WORKER_CHECKPOINT_ROOT", str(override_root))

    captured: dict[str, dict] = {}

    class _StubProc:
        def __init__(self, cmd, **kw):
            captured["env"] = kw.get("env") or {}
            self.pid = 12346
            self.stdout = type("F", (), {"read": lambda _s, _n: b"", "close": lambda _s: None})()

        def send_signal(self, _sig):
            pass

        def wait(self, timeout=None):
            return 0

        def kill(self):
            pass

    monkeypatch.setattr(subprocess, "Popen", _StubProc)

    job = {
        "id": 9201,
        "cmd": ["echo", "hi"],
        "cwd": str(tmp_path),
        "fast_path": True,
    }
    run_job(_FakeClient(cancel_after_s=999.0), job, set())

    ckpt_dir = captured["env"]["JOBD_CHECKPOINT_DIR"]
    assert ckpt_dir == str(override_root / "9201"), (
        f"override not honored: expected {override_root / '9201'}, got {ckpt_dir}"
    )
    assert (override_root / "9201").is_dir()


def test_run_job_checkpoint_dir_override_expands_tilde(tmp_path, monkeypatch):
    """JOBD_WORKER_CHECKPOINT_ROOT supports ~-prefixed paths via expanduser,
    matching the default branch. Operators setting the env var via systemd
    Environment= (where shell expansion doesn't happen) should not get a
    literal ~/ckpts directory."""
    import subprocess

    import jobd.worker.job_worker as job_worker

    monkeypatch.setattr(
        job_worker.shutil,
        "which",
        lambda name: f"/fake/bin/{name}" if name == "systemd-run" else None,
    )
    fake_home = tmp_path / "home"
    fake_home.mkdir()
    monkeypatch.setenv("HOME", str(fake_home))
    monkeypatch.setenv("JOBD_WORKER_CHECKPOINT_ROOT", "~/ckpts-override")

    captured: dict[str, dict] = {}

    class _StubProc:
        def __init__(self, cmd, **kw):
            captured["env"] = kw.get("env") or {}
            self.pid = 12347
            self.stdout = type("F", (), {"read": lambda _s, _n: b"", "close": lambda _s: None})()

        def send_signal(self, _sig):
            pass

        def wait(self, timeout=None):
            return 0

        def kill(self):
            pass

    monkeypatch.setattr(subprocess, "Popen", _StubProc)

    job = {
        "id": 9202,
        "cmd": ["echo", "hi"],
        "cwd": str(tmp_path),
        "fast_path": True,
    }
    run_job(_FakeClient(cancel_after_s=999.0), job, set())

    ckpt_dir = captured["env"]["JOBD_CHECKPOINT_DIR"]
    expected = fake_home / "ckpts-override" / "9202"
    assert ckpt_dir == str(expected), (
        f"~ not expanded in override: expected {expected}, got {ckpt_dir}"
    )
    # Sanity: literal-~ path should NOT exist
    assert not (tmp_path / "~" / "ckpts-override").exists()
    assert expected.is_dir()


def test_run_job_checkpoint_dir_xdg_data_home_branch(tmp_path, monkeypatch):
    """XDG_DATA_HOME (when set and non-empty) wins over the ~/.local/share
    fallback. Closes the contract→test gap on resolution rule #2."""
    import subprocess

    import jobd.worker.job_worker as job_worker

    monkeypatch.setattr(
        job_worker.shutil,
        "which",
        lambda name: f"/fake/bin/{name}" if name == "systemd-run" else None,
    )
    monkeypatch.delenv("JOBD_WORKER_CHECKPOINT_ROOT", raising=False)
    xdg_root = tmp_path / "xdg-data"
    monkeypatch.setenv("XDG_DATA_HOME", str(xdg_root))
    monkeypatch.setenv("HOME", str(tmp_path / "home-should-not-be-used"))

    captured: dict[str, dict] = {}

    class _StubProc:
        def __init__(self, cmd, **kw):
            captured["env"] = kw.get("env") or {}
            self.pid = 12348
            self.stdout = type("F", (), {"read": lambda _s, _n: b"", "close": lambda _s: None})()

        def send_signal(self, _sig):
            pass

        def wait(self, timeout=None):
            return 0

        def kill(self):
            pass

    monkeypatch.setattr(subprocess, "Popen", _StubProc)

    job = {
        "id": 9203,
        "cmd": ["echo", "hi"],
        "cwd": str(tmp_path),
        "fast_path": True,
    }
    run_job(_FakeClient(cancel_after_s=999.0), job, set())

    ckpt_dir = captured["env"]["JOBD_CHECKPOINT_DIR"]
    expected = xdg_root / "jobd" / "checkpoints" / "9203"
    assert ckpt_dir == str(expected), (
        f"XDG_DATA_HOME branch not taken: expected {expected}, got {ckpt_dir}"
    )
    assert expected.is_dir()


def test_missing_launcher_path_absolute_missing(tmp_path):
    missing = tmp_path / "nonexistent.sh"
    assert _missing_launcher_path([str(missing), "arg"], cwd=str(tmp_path)) == str(missing)


def test_missing_launcher_path_relative_missing(tmp_path):
    out = _missing_launcher_path(["./nope.sh"], cwd=str(tmp_path))
    assert out == str(tmp_path / "nope.sh")


def test_missing_launcher_path_existing_executable_is_none(tmp_path):
    script = tmp_path / "ok.sh"
    script.write_text("#!/bin/sh\necho hi\n")
    script.chmod(0o755)
    assert _missing_launcher_path([str(script)], cwd=str(tmp_path)) is None


def test_missing_launcher_path_existing_non_executable_caught(tmp_path):
    script = tmp_path / "noexec.sh"
    script.write_text("hi\n")
    script.chmod(0o644)
    assert _missing_launcher_path([str(script)], cwd=str(tmp_path)) == str(script)


def test_missing_launcher_path_path_resolved_name_skipped(tmp_path):
    # `bash`, `python`, `ctest` etc. — Popen resolves these via PATH; we
    # intentionally don't pre-check them. Returns None even though no such
    # absolute file exists.
    assert _missing_launcher_path(["bash", "./inner.sh"], cwd=str(tmp_path)) is None
    assert _missing_launcher_path(["python", "-c", "print(1)"], cwd=str(tmp_path)) is None


def test_missing_launcher_path_empty_cmd():
    assert _missing_launcher_path([], cwd="/tmp") is None


def test_run_job_refuses_dispatch_when_launcher_missing(tmp_path, monkeypatch):
    """Regression: jobs 261/262 (2026-04-30) failed exit 127 because the
    launcher wasn't committed. Worker must surface this at dispatch with
    termination_reason=launcher_missing rather than exec-ing into a silent
    failure inside the systemd-run scope."""
    import subprocess

    monkeypatch.setattr(job_worker.shutil, "which", lambda _name: None)

    popen_calls: list[list[str]] = []

    def _no_popen(cmd, **kw):
        popen_calls.append(cmd)
        raise AssertionError(f"Popen must not be called when launcher is missing; got {cmd}")

    monkeypatch.setattr(subprocess, "Popen", _no_popen)

    missing_path = str(tmp_path / "never_committed.sh")
    job = {"id": 9999, "cmd": [missing_path, "--arg"], "cwd": str(tmp_path)}
    client = _FakeClient(cancel_after_s=999.0)

    run_job(client, job, set())

    assert popen_calls == [], "Popen should not be reached for a missing launcher"
    completes = [p for p in client.posts if p[0].endswith("/complete")]
    assert len(completes) == 1, f"expected one /complete, got posts={client.posts}"
    body = completes[0][1]
    assert body["final_state"] == "failed"
    assert body["exit_code"] == 127
    assert body["termination_reason"] == "launcher_missing"
    # The offending path should also be logged via /log for diagnostics.
    # (_FakeClient stores content-POSTs as {"_raw": byte_length}; we just
    # check that a non-empty log line landed.)
    log_posts = [p for p in client.posts if p[0].endswith("/log")]
    assert len(log_posts) == 1
    assert log_posts[0][1].get("_raw", 0) > 0


def test_run_job_first_output_timeout_kills_silent_start(tmp_path, monkeypatch):
    """A job that produces zero output within
    JOBD_WORKER_FIRST_OUTPUT_TIMEOUT_S of dispatch must be killed via
    the scope cancel path and reported as failed with
    termination_reason='first_output_timeout'. Distinct from idle_timeout
    in that it has a typically-shorter, fires-once-only deadline."""
    import subprocess

    monkeypatch.setenv("JOBD_WORKER_FIRST_OUTPUT_TIMEOUT_S", "1")
    monkeypatch.setattr(job_worker, "SIGNAL_POLL_INTERVAL_S", 0.05)
    monkeypatch.setattr(
        job_worker.shutil,
        "which",
        lambda name: f"/fake/bin/{name}" if name == "systemd-run" else None,
    )
    StubProc, release = _quiet_stub_proc_pair()
    monkeypatch.setattr(subprocess, "Popen", StubProc)
    systemctl_calls: list[list[str]] = []

    def _fake_run(args, **_kw):
        if args and args[0] == "systemctl":
            systemctl_calls.append(args)
            release.set()

            class R:
                returncode = 0

            return R()

        class R:
            returncode = 0

        return R()

    monkeypatch.setattr(subprocess, "run", _fake_run)

    job = {"id": 9401, "cmd": ["bash", "-c", "sleep 60"], "cwd": str(tmp_path)}
    client = _FakeClient(cancel_after_s=999.0)
    t0 = time.monotonic()
    run_job(client, job, set())
    elapsed = time.monotonic() - t0

    assert elapsed < 4.0, f"first-output watchdog should fire promptly, took {elapsed:.1f}s"
    assert any(
        "kill" in c and c[-1] == job_worker.scope_unit_name(9401) for c in systemctl_calls
    ), f"expected systemctl kill on scope unit, got {systemctl_calls}"
    completes = [p for p in client.posts if p[0].endswith("/complete")]
    assert len(completes) == 1
    body = completes[0][1]
    assert body["final_state"] == "failed"
    assert body["termination_reason"] == "first_output_timeout"


def test_run_job_first_output_timeout_disarms_after_first_byte(tmp_path, monkeypatch):
    """Once the workload produces a single byte, the first-output watchdog
    must disarm permanently — subsequent silence is the idle_timeout's job.
    Without this disarming, the watchdog would double-fire and kill jobs
    that legitimately quiet down after a startup banner."""
    import subprocess

    monkeypatch.setenv("JOBD_WORKER_FIRST_OUTPUT_TIMEOUT_S", "1")
    monkeypatch.setattr(job_worker, "SIGNAL_POLL_INTERVAL_S", 0.05)
    monkeypatch.setattr(job_worker.shutil, "which", lambda _name: None)  # fast-path

    # Stub stdout: one chunk immediately, then EOF after a short wait.
    yielded = threading.Event()
    finished = threading.Event()

    class _StubStdout:
        def __init__(self):
            self._yielded = False

        def read(self, _n):
            if not self._yielded:
                self._yielded = True
                yielded.set()
                return b"startup banner\n"
            finished.wait(timeout=3.0)
            return b""

        def close(self):
            pass

    class _StubProc:
        def __init__(self, _cmd, **_kw):
            self.pid = 44444
            self.stdout = _StubStdout()

        def send_signal(self, _sig):
            finished.set()

        def wait(self, timeout=None):
            finished.wait(timeout=3.0)
            return 0

        def kill(self):
            finished.set()

    monkeypatch.setattr(subprocess, "Popen", _StubProc)

    job = {"id": 9402, "cmd": ["echo", "x"], "cwd": str(tmp_path), "fast_path": True}
    client = _FakeClient(cancel_after_s=999.0)

    # Let the first-output watchdog window elapse — but the byte from
    # _StubStdout lands first, so disarm should fire and the workload runs
    # to clean EOF.
    def _release_after_disarm():
        yielded.wait(timeout=3.0)
        time.sleep(2.0)  # well past first_output_timeout_s=1
        finished.set()

    t = threading.Thread(target=_release_after_disarm, daemon=True)
    t.start()
    run_job(client, job, set())

    completes = [p for p in client.posts if p[0].endswith("/complete")]
    assert len(completes) == 1
    body = completes[0][1]
    # No watchdog fired → either no termination_reason, or definitely not
    # first_output_timeout.
    assert body.get("termination_reason") != "first_output_timeout"
    assert body["final_state"] in ("completed", "failed")  # depends on rc


def test_run_job_refuses_admission_when_cwd_missing():
    """B: a cwd absent on this host -> POST refuse-admission(cwd_missing), no
    subprocess, no exit-127 /complete."""
    posts = []

    class _RecordingClient:
        def post(self, path, json=None, content=None, **kw):
            posts.append((path, json))

            class _R:
                status_code = 200

                def json(self_inner):
                    return {}

            return _R()

    job = {"id": 7, "cmd": ["true"], "cwd": "/no/such/dir/xyz_nonexistent", "env": {}}
    run_job(_RecordingClient(), job, set())
    paths = [p for p, _ in posts]
    assert any(p == "/jobs/7/refuse-admission" for p in paths), paths
    body = next(j for p, j in posts if p == "/jobs/7/refuse-admission")
    assert body["reason"] == "cwd_missing"
    assert body["cwd"] == "/no/such/dir/xyz_nonexistent"
    # must NOT have started the workload / posted a 127 completion
    assert not any(p == "/jobs/7/complete" for p in paths), paths