"""Startup must survive Postgres arriving late.
On prod, 2026-10-07 15:20 the service came up before docker did (the agent had
just rebooted the host itself) and a single ConnectError from
create_container() aborted the whole lifespan — the table-DDL retry loop sits
*below* that call and never got a turn.
"""
import pytest
import navi.main as main_mod
def stub_create_container(monkeypatch, errors):
"""`errors` — exceptions to raise, in order; then a sentinel container."""
container = object()
calls = {"n": 0}
async def fake():
calls["n"] += 1
if calls["n"] <= len(errors):
raise errors[calls["n"] - 1]
return container
monkeypatch.setattr(main_mod, "create_container", fake)
return container, calls
class TestTransientClassification:
def test_connection_failures_are_transient(self):
assert main_mod._is_transient_db_error(ConnectionRefusedError()) is True
assert main_mod._is_transient_db_error(OSError("no route to host")) is True
# what asyncpg raises when the pool gives up mid-handshake; the code
# spells it asyncio.TimeoutError, which is the same class on 3.11+
assert main_mod._is_transient_db_error(TimeoutError()) is True
def test_misconfiguration_is_not(self):
assert main_mod._is_transient_db_error(RuntimeError("DATABASE_URL is not set")) is False
assert main_mod._is_transient_db_error(ValueError("bad port")) is False
class TestContainerRetry:
async def test_a_refused_connection_is_retried(self, monkeypatch):
container, calls = stub_create_container(monkeypatch, [ConnectionRefusedError()])
result = await main_mod._create_container_with_retry(attempts=5, delay=0)
assert result is container
assert calls["n"] == 2
async def test_a_late_postgres_is_waited_out(self, monkeypatch):
_, calls = stub_create_container(
monkeypatch, [ConnectionRefusedError(), OSError("timeout")] * 3
)
await main_mod._create_container_with_retry(attempts=8, delay=0)
assert calls["n"] == 7
async def test_exhausting_the_attempts_still_raises(self, monkeypatch):
_, calls = stub_create_container(monkeypatch, [ConnectionRefusedError()] * 10)
with pytest.raises(ConnectionRefusedError):
await main_mod._create_container_with_retry(attempts=3, delay=0)
assert calls["n"] == 3
async def test_misconfiguration_fails_fast(self, monkeypatch):
"""Retrying a missing DATABASE_URL just delays an error that will never
clear itself."""
_, calls = stub_create_container(
monkeypatch, [RuntimeError("DATABASE_URL is not set")] * 10
)
with pytest.raises(RuntimeError):
await main_mod._create_container_with_retry(attempts=5, delay=0)
assert calls["n"] == 1