From 391f50da7a085bec75155c0eb9b47910266058cc Mon Sep 17 00:00:00 2001 From: historia Date: Tue, 1 Sep 2026 16:42:15 -0400 Subject: fix: server status for already-running server --- app/tests/test_runview.py | 100 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 97 insertions(+), 3 deletions(-) (limited to 'app/tests/test_runview.py') diff --git a/app/tests/test_runview.py b/app/tests/test_runview.py index 6add83f..0d99a6a 100644 --- a/app/tests/test_runview.py +++ b/app/tests/test_runview.py @@ -90,6 +90,25 @@ class StateTransitionTests(_FakeTui, unittest.TestCase): "url": "http://x"}) self.assertEqual(view.server, "ready") + def test_running_event_escapes_starting_without_a_boot(self): + # Regression for the stuck "Status: starting" run view: when this + # run did not boot the server (already running locally, or remote) + # the worker's probe reports "running" — and the book events that + # follow must still move the panel to "processing". + view, _ = self.make_view() + view.handle_event({"kind": "running", "name": "audiocpp", + "url": "http://x"}) + self.assertEqual(view.server, "ready") + self.assertFalse(view.started_server) + view.handle_event({"kind": "book", "index": 1, "total": 1, + "name": "b"}) + self.assertEqual(view.phase, "convert") + self.assertEqual(view.server, "processing") + view.handle_event({"kind": "book_done", "name": "b", "ok": True}) + view.handle_event({"kind": "done", "ok": 1, "total": 1}) + self.assertEqual(view.phase, "done") + self.assertNotEqual(view.server, "starting") + def test_chunk_progress_updates(self): view, _ = self.make_view() view.handle_event({"kind": "book", "index": 1, "total": 2, @@ -225,6 +244,16 @@ class StateTransitionTests(_FakeTui, unittest.TestCase): view.handle_event({"kind": "server_down"}) self.assertEqual(view.server, "down") + def test_server_down_during_boot_marks_not_responding(self): + # The worker's boot-time probe of a server this run did not start: + # a target that never answers is reported "down" (not the eternal + # "starting"), and never offered for a stop (not started here). + view, _ = self.make_view() + view.handle_event({"kind": "server_down"}) + self.assertEqual(view.server, "down") + self.assertEqual(view.phase, "boot") + self.assertFalse(view.started_server) + def test_server_stopped_event_marks_server_stopped(self): view, _ = self.make_view() view.server = "stopping" @@ -308,7 +337,9 @@ class LogAppenderTests(_FakeTui, unittest.TestCase): tmp = tempfile.TemporaryDirectory() self.addCleanup(tmp.cleanup) log_path = os.path.join(tmp.name, "audiobook_20260828.log") - view, _ = self.make_view(log_path=log_path) + # server_url=None: no boot and no probe, so the run log test sees + # only the worker's own events. + view, _ = self.make_view(log_path=log_path, server_url=None) fake = types.ModuleType("audiobook") @@ -348,6 +379,24 @@ class RenderTests(_FakeTui, unittest.TestCase): self.assertIn("starting", text) self.assertIn("Esc or q: cancel", text) + def test_ready_without_autostart_shows_the_not_started_tag(self): + # A run that talks to a server it did not start (already running + # locally, or remote) tags the ready state so the panel does not + # imply this run booted it. + view, screen = self.make_view() + view.handle_event({"kind": "running", "name": "audiocpp", + "url": "http://x"}) + view.render() + self.assertIn("ready (not started by this run)", self._strings(screen)) + + def test_not_responding_message_shown_while_down(self): + view, screen = self.make_view() + view.handle_event({"kind": "server_down"}) + view.render() + text = self._strings(screen) + self.assertIn("not responding", text) + self.assertIn("the server is not responding", text) + def test_summary_screen_after_done(self): view, screen = self.make_view() view.handle_event({"kind": "book", "index": 1, "total": 1, @@ -620,7 +669,9 @@ class WorkerTests(_FakeTui, unittest.TestCase): kwargs={"voice": "alloy", "book_files": ["b.txt"], "planned": ["b.txt"]}, book_files=["b.txt"], planned=["b.txt"]) - with patch("audiobook.convert") as mk_convert: + with patch("audiobook.convert") as mk_convert, \ + patch.object(runview.common, "server_running", + return_value=True): view._worker_main() _, kw = mk_convert.call_args self.assertEqual(kw["book_files"], ["b.txt"]) @@ -635,7 +686,9 @@ class WorkerTests(_FakeTui, unittest.TestCase): log_path = os.path.join(tmp, "audiobook_20260825.log") view = self.make_view(log_path=log_path) with patch("audiobook.convert", - side_effect=TypeError("boom")): + side_effect=TypeError("boom")), \ + patch.object(runview.common, "server_running", + return_value=False): view._worker_main() with open(log_path, encoding="utf-8") as handle: text = handle.read() @@ -681,6 +734,47 @@ class WorkerTests(_FakeTui, unittest.TestCase): view._worker_main() mk_stop.assert_not_called() + def test_worker_probes_the_server_it_does_not_boot(self): + # No autostart (the server is already running locally, or remote): + # the worker still reports the target as "running" so the status + # leaves "starting", then converts against it. + with patch("audiobook.convert") as mk_convert, \ + patch.object(runview.common, "server_running", + return_value=True) as mk_running: + view = self.make_view() + view._worker_main() + mk_running.assert_called_once_with("http://127.0.0.1:8080") + mk_convert.assert_called_once() + events = self._drain(view) + self.assertEqual(events[0], + {"kind": "running", "name": "audiocpp", + "url": "http://127.0.0.1:8080"}) + self.assertEqual(events[-1]["kind"], "worker_exit") + + def test_worker_reports_down_when_the_probe_fails(self): + # A target that never answers (dead remote, server stopped between + # the form and the run) is reported "down"; the conversion still + # runs and fails through the normal chunk/error path. + with patch("audiobook.convert") as mk_convert, \ + patch.object(runview.common, "server_running", + return_value=False): + view = self.make_view() + view._worker_main() + mk_convert.assert_called_once() + self.assertEqual(self._drain(view)[0]["kind"], "server_down") + + def test_worker_skips_the_probe_without_a_url(self): + # No server URL at all (backend vanished between menu and run): + # there is nothing to probe, so no server events are queued. + with patch("audiobook.convert"), \ + patch.object(runview.common, "server_running") as mk_running: + view = self.make_view(server_url=None, server_name=None, + server_identity=None) + view._worker_main() + mk_running.assert_not_called() + self.assertEqual([e["kind"] for e in self._drain(view)], + ["worker_exit"]) + if __name__ == "__main__": unittest.main() -- cgit v1.2.3