From 22a1c4c2df5efe5e6286c8ee8251d03ee946e6fd Mon Sep 17 00:00:00 2001 From: Jason Walker <913443@dadeschools.net> Date: Tue, 7 Jul 2026 15:53:10 -0400 Subject: [PATCH] fix(webui): suppress empty queue copy on fetch failure (#458) When Gitea credentials are missing or the queue fetch fails closed, show "Not loaded" instead of "No open items." and keep pagination marked unavailable. Successful empty inventories still show the empty state with complete pagination proof. Closes #458 --- tests/test_webui_queue_dashboard.py | 74 +++++++++++++++++++++++++++++ webui/queue_views.py | 9 ++++ 2 files changed, 83 insertions(+) diff --git a/tests/test_webui_queue_dashboard.py b/tests/test_webui_queue_dashboard.py index cc9ea97..cbdcc16 100644 --- a/tests/test_webui_queue_dashboard.py +++ b/tests/test_webui_queue_dashboard.py @@ -12,12 +12,14 @@ from starlette.testclient import TestClient from webui.app import create_app from webui.queue_loader import ( PaginationMeta, + QueueSnapshot, _classify_issue, _classify_pr, _extract_linked_issue, load_queue_snapshot, snapshot_to_dict, ) +from webui.queue_views import render_queue_page _RECENT = datetime.now(timezone.utc).isoformat() _STALE = (datetime.now(timezone.utc) - timedelta(days=30)).isoformat() @@ -210,5 +212,77 @@ class TestQueueRoutes(unittest.TestCase): self.assertEqual(len(snapshot.prs), 0) +def _empty_pagination() -> PaginationMeta: + return PaginationMeta( + page=1, + per_page=50, + returned_count=0, + has_more=False, + is_final_page=True, + inventory_complete=True, + pages_fetched=1, + ) + + +def _empty_fetch(*_args, **_kwargs): + return [], _empty_pagination() + + +class TestQueueFailClosedUx(unittest.TestCase): + """Regression tests for #458 fail-closed empty-state copy.""" + + def test_fail_closed_view_suppresses_empty_queue_copy(self): + snapshot = QueueSnapshot( + project_id="gitea-tools", + repo_label="Scaled-Tech-Consulting/Gitea-Tools", + prs=(), + issues=(), + pr_pagination=None, + issue_pagination=None, + fetch_error="Gitea credentials unavailable for gitea.prgs.cc", + ) + html = render_queue_page(snapshot) + self.assertIn("Queue unavailable", html) + self.assertIn("Not loaded", html) + self.assertNotIn("No open items.", html) + self.assertIn("pagination: unavailable", html) + + def test_fail_closed_route_does_not_show_empty_queue_copy(self): + client = TestClient(create_app()) + snapshot = load_queue_snapshot( + fetch_prs=_empty_fetch, + fetch_issues=_empty_fetch, + ) + snapshot = QueueSnapshot( + project_id=snapshot.project_id, + repo_label=snapshot.repo_label, + prs=(), + issues=(), + pr_pagination=None, + issue_pagination=None, + fetch_error="Gitea credentials unavailable for gitea.prgs.cc", + ) + with mock.patch("webui.app.load_queue_snapshot", return_value=snapshot): + response = client.get("/queue") + self.assertEqual(response.status_code, 200) + self.assertIn("Queue unavailable", response.text) + self.assertNotIn("No open items.", response.text) + + def test_successful_empty_inventory_shows_empty_copy(self): + snapshot = load_queue_snapshot( + fetch_prs=_empty_fetch, + fetch_issues=_empty_fetch, + ) + self.assertIsNone(snapshot.fetch_error) + self.assertEqual(len(snapshot.prs), 0) + self.assertEqual(len(snapshot.issues), 0) + self.assertTrue(snapshot.pr_pagination.inventory_complete) + + html = render_queue_page(snapshot) + self.assertNotIn("Queue unavailable", html) + self.assertEqual(html.count("No open items."), 2) + self.assertIn("pagination (complete)", html) + + if __name__ == "__main__": unittest.main() \ No newline at end of file diff --git a/webui/queue_views.py b/webui/queue_views.py index 8294b8e..e5e6dd7 100644 --- a/webui/queue_views.py +++ b/webui/queue_views.py @@ -38,7 +38,13 @@ def _queue_table( title: str, items: tuple[QueueItem, ...], columns: tuple[tuple[str, str], ...], + fetch_failed: bool = False, ) -> str: + if fetch_failed: + return ( + f"

{html.escape(title)}

" + "

Not loaded — queue fetch did not complete.

" + ) if not items: return f"

{html.escape(title)}

No open items.

" @@ -73,9 +79,11 @@ def render_queue_page(snapshot: QueueSnapshot) -> str: f"{html.escape(snapshot.fetch_error)}

" ) + fetch_failed = bool(snapshot.fetch_error) pr_section = _queue_table( title="Open pull requests", items=snapshot.prs, + fetch_failed=fetch_failed, columns=( ("number", "#"), ("title", "Title"), @@ -88,6 +96,7 @@ def render_queue_page(snapshot: QueueSnapshot) -> str: issue_section = _queue_table( title="Open issues", items=snapshot.issues, + fetch_failed=fetch_failed, columns=( ("number", "#"), ("title", "Title"),