"""Fetching, caching and the boot-loop guard."""
from pathlib import Path
import pytest
from navi.overlay import fetch as fetch_mod
from navi.overlay.fetch import (
CachedState,
Fetched,
confirm_startup,
load_cached,
resolve_startup,
save_cached,
store_applied,
)
from navi.overlay.util import content_hash
from navi.settings_model import Settings
@pytest.fixture()
def base(tmp_path) -> Settings:
return Settings(
_env_file=None,
hive_url="http://hive.test:8087",
hive_config_cache_file=str(tmp_path / "cache.json"),
hive_config_timeout_sec=0.1,
)
def _doc(**settings) -> list[dict]:
return [{"name": "global", "config": {"settings": settings}}]
def _path(base) -> Path:
return Path(base.hive_config_cache_file)
def _patch_fetch(monkeypatch, result):
def fake(base_settings):
if isinstance(result, Exception):
raise result
return result
monkeypatch.setattr(fetch_mod, "fetch_layers", fake)
# ── cache file ──────────────────────────────────────────────────────────────
def test_cache_roundtrip(base):
save_cached(
_path(base),
CachedState(
good={"version": 1, "hash": "sha256:x", "layers": _doc()},
pending=None,
blocked_hash="",
),
)
loaded = load_cached(_path(base))
assert loaded.good["version"] == 1
assert loaded.pending is None
def test_cache_is_private(base):
save_cached(_path(base), CachedState(good={"version": 1, "hash": "", "layers": []}))
assert (_path(base).stat().st_mode & 0o777) == 0o600
def test_broken_cache_is_treated_as_empty(base):
_path(base).write_text("{not json")
loaded = load_cached(_path(base))
assert loaded.good is None
# ── startup resolution ──────────────────────────────────────────────────────
def test_no_hive_and_no_cache_runs_local(base, monkeypatch):
_patch_fetch(monkeypatch, RuntimeError("connection refused"))
result = resolve_startup(base)
assert result.layers == []
assert result.source == "local"
assert "connection refused" in (result.error or "")
def test_hive_url_without_a_key_is_reported_as_unreachable(tmp_path):
# No monkeypatching: the real fetch runs and stops at the missing key.
settings = Settings(
_env_file=None,
hive_url="http://hive.test:8087",
swarm_key_file=str(tmp_path / "absent-key"),
hive_config_cache_file=str(tmp_path / "cache.json"),
hive_config_timeout_sec=0.1,
)
result = resolve_startup(settings)
assert result.layers == []
assert result.source == "local"
assert "swarm key" in (result.error or "")
def test_fetched_document_is_used_and_marked_pending(base, monkeypatch):
_patch_fetch(
monkeypatch,
Fetched(version=3, layers=_doc(llm_complete_timeout=30), hash="sha256:a"),
)
result = resolve_startup(base)
assert result.source == "hive"
assert result.merged["settings"] == {"llm_complete_timeout": 30}
cached = load_cached(_path(base))
assert cached.pending is not None
assert cached.pending["attempts"] == 1
assert cached.good is None
def test_confirm_startup_promotes_pending(base, monkeypatch):
_patch_fetch(
monkeypatch,
Fetched(version=3, layers=_doc(llm_complete_timeout=30), hash="sha256:a"),
)
resolve_startup(base)
confirm_startup(base)
cached = load_cached(_path(base))
assert cached.good is not None
assert cached.pending is None
def test_same_config_retried_once_then_blocked(base, monkeypatch):
"""A config that was applied but never confirmed is retried once."""
_patch_fetch(
monkeypatch,
Fetched(version=3, layers=_doc(llm_complete_timeout=30), hash="sha256:a"),
)
resolve_startup(base) # attempt 1
resolve_startup(base) # attempt 2
result = resolve_startup(base) # guard trips, nothing is applied
assert result.layers == []
assert result.boot_guard is True
cached = load_cached(_path(base))
assert cached.blocked_hash == "sha256:a"
assert cached.pending is None
def test_a_new_document_clears_the_guard(base, monkeypatch):
_patch_fetch(
monkeypatch,
Fetched(version=3, layers=_doc(llm_complete_timeout=30), hash="sha256:a"),
)
for _ in range(3):
resolve_startup(base)
_patch_fetch(
monkeypatch,
Fetched(version=4, layers=_doc(llm_complete_timeout=45), hash="sha256:b"),
)
result = resolve_startup(base)
assert result.source == "hive"
assert result.merged["settings"] == {"llm_complete_timeout": 45}
def test_fetch_failure_falls_back_to_the_last_good_cache(base, monkeypatch):
good = _doc(llm_complete_timeout=30)
save_cached(
_path(base),
CachedState(good={"version": 2, "hash": "sha256:g", "layers": good}),
)
_patch_fetch(monkeypatch, RuntimeError("hive down"))
result = resolve_startup(base)
assert result.source == "cache"
assert result.merged["settings"] == {"llm_complete_timeout": 30}
assert result.error and "hive down" in result.error
def test_invalid_fetched_document_falls_back_to_good_cache(base, monkeypatch):
good = _doc(llm_complete_timeout=30)
save_cached(
_path(base),
CachedState(good={"version": 2, "hash": "sha256:g", "layers": good}),
)
_patch_fetch(
monkeypatch,
Fetched(version=3, layers=_doc(llm_complete_timeout="soon"), hash="sha256:b"),
)
result = resolve_startup(base)
assert result.source == "cache"
assert result.merged["settings"] == {"llm_complete_timeout": 30}
def test_invalid_document_with_no_cache_runs_local(base, monkeypatch):
_patch_fetch(
monkeypatch,
Fetched(version=3, layers=_doc(llm_complete_timeout="soon"), hash="sha256:b"),
)
result = resolve_startup(base)
assert result.layers == []
assert result.source == "local"
assert result.validation is not None and not result.validation.ok
def test_store_applied_refreshes_good_and_drops_pending(base):
save_cached(
_path(base),
CachedState(
good={"version": 1, "hash": "sha256:g", "layers": []},
pending={"version": 2, "hash": "sha256:p", "layers": [], "attempts": 1},
),
)
store_applied(base, Fetched(version=3, layers=_doc(llm_complete_timeout=30), hash="sha256:n"))
cached = load_cached(_path(base))
assert cached.good["version"] == 3
assert cached.pending is None
def test_empty_layer_answer_is_a_real_answer(base, monkeypatch):
"""The hive saying "no layers" must revert to local, not to the cache."""
save_cached(
_path(base),
CachedState(
good={
"version": 2,
"hash": "sha256:g",
"layers": _doc(llm_complete_timeout=30),
}
),
)
_patch_fetch(
monkeypatch, Fetched(version=5, layers=[], hash=content_hash([]))
)
result = resolve_startup(base)
assert result.layers == []
assert result.source == "hive"
assert result.merged == {}