import pytest from app.runtime.docker import DockerError, Light, demux_logs, light, parse_stats, parse_time from tests.runtime.fake_docker import FakeDocker, MiB, frame, now_minus @pytest.fixture def fake() -> FakeDocker: fake = FakeDocker() fake.add( "tgit-dev-bot-1", service="bot", labels={"tgit.sha": "526eb56aa", "tgit.deployed_at": "2026-10-17T07:52:00Z"} ) fake.add("tgit-dev-postgres-1", service="postgres") fake.add("vobot-api-1", managed=False) # someone else's project on the same host return fake async def test_lists_only_managed_containers(fake: FakeDocker) -> None: async with fake.client() as docker: names = [c.name for c in await docker.containers()] assert names == ["tgit-dev-bot-1", "tgit-dev-postgres-1"] assert all(r.method == "GET" for r in fake.requests) async def test_foreign_container_is_refused_without_a_request(fake: FakeDocker) -> None: foreign = next(c for c in fake.containers.values() if not c.managed) async with fake.client() as docker: await docker.containers() fake.requests.clear() with pytest.raises(DockerError): await docker.inspect(foreign.id) with pytest.raises(DockerError): await docker.logs(foreign.id) with pytest.raises(DockerError): await docker.resolve(foreign.id[:12]) assert all("/containers/json" in str(r.url) for r in fake.requests) # only re-listings, never the foreign id async def test_inspect_and_stats(fake: FakeDocker) -> None: async with fake.client() as docker: bot = next(c for c in await docker.containers() if c.service == "bot") details = await docker.inspect(await docker.resolve(bot.short_id)) stats = await docker.stats(bot.id) assert details.light is Light.GREEN assert details.sha == "526eb56aa" assert details.deployed_at is not None and details.deployed_at.hour == 7 assert details.started_at is not None and details.started_at.microsecond == 123456 # nanoseconds trimmed assert stats.cpu_pct == 5.0 assert stats.mem_bytes == 64 * MiB # page cache subtracted assert stats.mem_pct == 25.0 async def test_logs_are_demultiplexed_and_since_filtered(fake: FakeDocker) -> None: bot = next(c for c in fake.containers.values() if c.service == "bot") bot.logs = [(now_minus(10), "old line"), (now_minus(1), "fresh line"), (now_minus(0.5), "ERR on stderr")] async with fake.client() as docker: await docker.containers() text = await docker.logs(bot.id, since=now_minus(5)) assert text == "fresh line\nERR on stderr\n" async def test_unreachable_docker_is_a_docker_error(fake: FakeDocker) -> None: fake.down = True async with fake.client() as docker: assert not await docker.ping() with pytest.raises(DockerError, match="unreachable"): await docker.containers() async def test_ping(fake: FakeDocker) -> None: async with fake.client() as docker: assert await docker.ping() def test_demux_plain_tty_stream_passes_through() -> None: assert demux_logs(b"plain text\n") == "plain text\n" assert demux_logs(frame("a\n") + frame("b\n", 2)) == "a\nb\n" @pytest.mark.parametrize( ("state", "health", "code", "expected"), [ ("running", "healthy", 0, Light.GREEN), ("running", None, 0, Light.GREEN), ("running", "starting", 0, Light.YELLOW), ("running", "unhealthy", 0, Light.RED), ("restarting", None, 0, Light.RED), ("exited", None, 137, Light.RED), ("exited", None, 0, Light.WHITE), ("created", None, 0, Light.YELLOW), ], ) def test_light(state: str, health: str | None, code: int, expected: Light) -> None: assert light(state, health, code) is expected def test_parse_time_edge_cases() -> None: assert parse_time(None) is None assert parse_time("0001-01-01T00:00:00Z") is None assert parse_time("garbage") is None def test_parse_stats_without_previous_sample() -> None: stats = parse_stats({"cpu_stats": {"cpu_usage": {"total_usage": 5}}, "memory_stats": {}}) assert stats.cpu_pct == 0.0 assert stats.mem_pct == 0.0