"""Unit tests for the FakeRuntime adapter.""" import contextlib from nix_builder_autoscaler.runtime.base import RuntimeError as RuntimeAdapterError from nix_builder_autoscaler.runtime.fake import FakeRuntime class TestLaunchSpot: def test_returns_synthetic_instance_id(self): rt = FakeRuntime() iid = rt.launch_spot("slot001", "#!/bin/bash\necho hello") assert iid.startswith("i-fake-") assert len(iid) > 10 def test_instance_starts_pending(self): rt = FakeRuntime() iid = rt.launch_spot("slot001", "") info = rt.describe_instance(iid) assert info["state"] == "pending" assert info["tailscale_ip"] is None class TestTickProgression: def test_transitions_to_running_after_configured_ticks(self): rt = FakeRuntime(launch_latency_ticks=3, ip_delay_ticks=1) iid = rt.launch_spot("slot001", "") for _ in range(2): rt.tick() assert rt.describe_instance(iid)["state"] == "pending" rt.tick() # tick 3 assert rt.describe_instance(iid)["state"] == "running" def test_tailscale_ip_appears_after_configured_delay(self): rt = FakeRuntime(launch_latency_ticks=2, ip_delay_ticks=2) iid = rt.launch_spot("slot001", "") for _ in range(2): rt.tick() assert rt.describe_instance(iid)["state"] == "running" assert rt.describe_instance(iid)["tailscale_ip"] is None rt.tick() # tick 3 — still no IP (need tick 4) assert rt.describe_instance(iid)["tailscale_ip"] is None rt.tick() # tick 4 info = rt.describe_instance(iid) assert info["tailscale_ip"] is not None assert info["tailscale_ip"].startswith("100.64.0.") class TestInjectedFailure: def test_launch_failure_raises(self): rt = FakeRuntime() rt.inject_launch_failure("slot001") try: rt.launch_spot("slot001", "") raise AssertionError("Should have raised") except RuntimeAdapterError as e: assert e.category == "capacity_unavailable" def test_failure_is_one_shot(self): rt = FakeRuntime() rt.inject_launch_failure("slot001") with contextlib.suppress(RuntimeAdapterError): rt.launch_spot("slot001", "") # Second call should succeed iid = rt.launch_spot("slot001", "") assert iid.startswith("i-fake-") class TestInjectedInterruption: def test_interruption_returns_terminated(self): rt = FakeRuntime(launch_latency_ticks=1) iid = rt.launch_spot("slot001", "") rt.tick() assert rt.describe_instance(iid)["state"] == "running" rt.inject_interruption(iid) info = rt.describe_instance(iid) assert info["state"] == "terminated" def test_interruption_is_one_shot(self): """After the interruption fires, subsequent describes stay terminated.""" rt = FakeRuntime(launch_latency_ticks=1) iid = rt.launch_spot("slot001", "") rt.tick() rt.inject_interruption(iid) rt.describe_instance(iid) # consumes the injection info = rt.describe_instance(iid) assert info["state"] == "terminated" class TestTerminate: def test_terminate_marks_instance(self): rt = FakeRuntime(launch_latency_ticks=1) iid = rt.launch_spot("slot001", "") rt.tick() rt.terminate_instance(iid) assert rt.describe_instance(iid)["state"] == "terminated" class TestListManaged: def test_lists_non_terminated(self): rt = FakeRuntime(launch_latency_ticks=1) iid1 = rt.launch_spot("slot001", "") iid2 = rt.launch_spot("slot002", "") rt.tick() rt.terminate_instance(iid1) managed = rt.list_managed_instances() ids = [m["instance_id"] for m in managed] assert iid2 in ids assert iid1 not in ids