Ctrl K
ComunidadGuías…

tests/test_worker_payload_compat.py

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

Archivo original ↗Preguntar a KiBot
"""The broker must keep accepting the payload an OLDER worker sends.

jobd runs mixed versions BY DESIGN, not by accident: install-worker.sh pins a
new worker to the broker's version so it is never ahead, update-worker.sh
DEFERS an upgrade while the worker is busy (a laptop sat on 0.5.25 against a
0.5.28 broker for days), and the `version_drift` event exists precisely because
a prolonged mismatch is expected. So "older worker, newer broker" is a supported
state, and the wire contract has to survive it.

Nothing enforced that. `NextJobQuery` and `WorkerHeartbeat` declare required
fields with no defaults, so ADDING one silently locks out every worker that
predates it — and the worker cannot tell, because its poll loop treats any
non-200 from /next-job as "no job" (audit 2026-07-25 V-1; the logging half of
that fix lives in job_worker.py).

These payloads are FROZEN. Do not add keys to make a new field pass — that
defeats the entire point. If a genuinely new field is needed, give it a default
so old workers keep working, and only then decide whether the golden payload
should learn about it.
"""

from __future__ import annotations

import pytest

# The minimum a worker has ever had to send to claim work. Frozen deliberately.
_OLDEST_NEXT_JOB = {
    "host": "old-worker",
    "free_vram_gb": 0.0,
    "unregistered_vram_gb": 0.0,
    "free_ram_gb": 8.0,
    "idle_cpus": 2,
}

# The heartbeat is what keeps a worker VISIBLE. If it starts 422ing, the worker
# vanishes from `job workers` entirely; if only /next-job 422s, the worker looks
# perfectly healthy while never claiming anything. Both shapes are pinned.
_OLDEST_HEARTBEAT = {
    "host": "old-worker",
    "free_vram_gb": 0.0,
    "unregistered_vram_gb": 0.0,
    "free_ram_gb": 8.0,
    "idle_cpus": 2,
    "arch": "x86_64",
    "os": "linux",
    "gpu": False,
    "tags": [],
}

_FAILURE_HINT = (
    "A required field was added to a worker-facing request model. Every worker "
    "older than that change now gets 422 and can no longer {what} — silently, "
    "because the poll loop cannot distinguish a refusal from an idle queue. "
    "Give the new field a default instead of making it required."
)


def test_an_older_workers_claim_is_still_accepted(client):
    r = client.post("/next-job", json=dict(_OLDEST_NEXT_JOB))
    assert r.status_code == 200, f"{_FAILURE_HINT.format(what='claim jobs')}\n{r.text}"


def test_an_older_workers_heartbeat_is_still_accepted(client):
    r = client.post("/heartbeat", json=dict(_OLDEST_HEARTBEAT))
    assert r.status_code == 200, f"{_FAILURE_HINT.format(what='register')}\n{r.text}"


def test_an_older_worker_can_actually_claim_a_queued_job(client):
    """End-to-end, not just schema acceptance: register and claim with the
    frozen payloads and confirm a real job comes back."""
    submitted = client.post("/submit", json={"project": "p", "cmd": ["echo", "hi"], "cwd": "/tmp"})
    assert submitted.status_code == 200, submitted.text

    client.post("/heartbeat", json=dict(_OLDEST_HEARTBEAT))
    claim = client.post("/next-job", json=dict(_OLDEST_NEXT_JOB))

    assert claim.status_code == 200, claim.text
    assert claim.json() is not None, (
        "an older worker registered and polled but was handed nothing — the "
        "contract accepts its payload yet the job never reaches it"
    )
    assert claim.json()["id"] == submitted.json()["id"]


@pytest.mark.parametrize("dropped", sorted(_OLDEST_NEXT_JOB))
def test_the_frozen_payload_is_minimal(client, dropped):
    """Guards the guard: every key in the frozen payload must be load-bearing.

    If dropping a key still yields 200, that key is optional and does not belong
    in a compatibility floor — a bloated golden payload would keep passing after
    a required field was added, which is exactly the failure it exists to catch.
    """
    payload = {k: v for k, v in _OLDEST_NEXT_JOB.items() if k != dropped}
    r = client.post("/next-job", json=payload)
    assert r.status_code == 422, (
        f"{dropped!r} is optional, so it is dead weight in the compatibility "
        "floor — drop it from _OLDEST_NEXT_JOB."
    )