nousresearch--hermes-agent
b4fbd6fe9f
Deploy Site / deploy-vercel (push) Has been skipped
Deploy Site / deploy-docs (push) Has been skipped
Build Skills Index / build-index (push) Has been skipped
CI / Deny unrelated histories (push) Has been skipped
CI / Detect affected areas (push) Successful in 27m35s
CI / OSV scan (push) Failing after 4s
CI / Build&Test Docker image (push) Successful in 9s
CI / Supply-chain scan (push) Has been skipped
CI / Lint Docker scripts (push) Failing after 5m13s
CI / Check contributors (push) Failing after 12m8s
CI / Docs Site (push) Failing after 12m8s
CI / TypeScript (push) Failing after 12m8s
CI / Python lints (push) Failing after 12m9s
CI / Python tests (push) Failing after 12m9s
CI / Check uv.lock (push) Failing after 23m22s
CI / CI timing report (push) Has been cancelled
Build Skills Index / trigger-deploy (push) Has been cancelled
CI / All required checks pass (push) Has been cancelled
85 行
3.2 KiB
Python
85 行
3.2 KiB
Python
"""Tests for the store-level CAS fire claim (Phase 4C).
|
|
|
|
`claim_job_for_fire` gives multi-machine at-most-once semantics when an external
|
|
scheduler (Chronos) fires a job: across N gateway replicas, exactly ONE wins the
|
|
claim for a given fire. Single-machine deployments always win (unaffected).
|
|
|
|
These exercise the real store against a temp HERMES_HOME (no mocks) per the
|
|
E2E-over-mocks discipline for file-touching code.
|
|
"""
|
|
import pytest
|
|
|
|
|
|
@pytest.fixture
|
|
def temp_home(tmp_path, monkeypatch):
|
|
"""Isolated HERMES_HOME so jobs.json doesn't touch the real store."""
|
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
|
# cron.jobs caches no home at import; get_hermes_home() reads the env live.
|
|
yield tmp_path
|
|
|
|
|
|
def test_claim_succeeds_once_then_blocks(temp_home):
|
|
"""First claim for a fire wins; a second claim for the same fire loses, and
|
|
next_run_at is advanced (a re-delivery for the old time can't re-fire)."""
|
|
from cron.jobs import create_job, claim_job_for_fire, get_job
|
|
|
|
job = create_job(prompt="x", schedule="every 5m", name="t")
|
|
jid = job["id"]
|
|
before = get_job(jid)["next_run_at"]
|
|
|
|
assert claim_job_for_fire(jid) is True
|
|
assert claim_job_for_fire(jid) is False
|
|
assert get_job(jid)["next_run_at"] != before
|
|
|
|
|
|
def test_claim_oneshot_cannot_be_double_claimed(temp_home):
|
|
"""A one-shot can't be double-claimed (the fresh claim blocks the retry)."""
|
|
from cron.jobs import create_job, claim_job_for_fire
|
|
|
|
job = create_job(prompt="x", schedule="30m", name="o")
|
|
assert claim_job_for_fire(job["id"]) is True
|
|
assert claim_job_for_fire(job["id"]) is False
|
|
|
|
|
|
def test_claim_unknown_job_returns_false(temp_home):
|
|
from cron.jobs import claim_job_for_fire
|
|
|
|
assert claim_job_for_fire("nope-does-not-exist") is False
|
|
|
|
|
|
def test_claim_paused_job_returns_false(temp_home):
|
|
"""A paused job can't be claimed."""
|
|
from cron.jobs import create_job, claim_job_for_fire, pause_job
|
|
|
|
job = create_job(prompt="x", schedule="every 5m", name="p")
|
|
pause_job(job["id"])
|
|
assert claim_job_for_fire(job["id"]) is False
|
|
|
|
|
|
def test_stale_claim_is_reclaimable(temp_home, monkeypatch):
|
|
"""A claim older than the TTL is overwritten — the fire isn't stuck forever
|
|
if the winning machine crashed before mark_job_run cleared the claim."""
|
|
from cron.jobs import create_job, claim_job_for_fire
|
|
|
|
job = create_job(prompt="x", schedule="every 5m", name="s")
|
|
jid = job["id"]
|
|
assert claim_job_for_fire(jid) is True
|
|
# With a 0s TTL, the existing claim is always considered stale.
|
|
assert claim_job_for_fire(jid, claim_ttl_seconds=0) is True
|
|
|
|
|
|
def test_mark_job_run_clears_claim(temp_home):
|
|
"""After a recurring job completes, its claim is cleared so the next fire
|
|
can be claimed again."""
|
|
from cron.jobs import create_job, claim_job_for_fire, mark_job_run, get_job
|
|
|
|
job = create_job(prompt="x", schedule="every 5m", name="c")
|
|
jid = job["id"]
|
|
assert claim_job_for_fire(jid) is True
|
|
assert get_job(jid).get("fire_claim") is not None
|
|
|
|
mark_job_run(jid, success=True)
|
|
assert get_job(jid).get("fire_claim") is None
|
|
# …and the re-armed recurring job is claimable again.
|
|
assert claim_job_for_fire(jid) is True
|