From 23535e30bc4aa44d321a82c2bdcca209fafb6da0 Mon Sep 17 00:00:00 2001 From: Jason Walker <913443@dadeschools.net> Date: Tue, 7 Jul 2026 15:46:05 -0400 Subject: [PATCH 1/2] feat(webui): test coverage and CI gate (#436) Add consolidated MVP route/read-only tests, path-filter helpers, and scripts/test-webui plus scripts/ci-webui-check for Jenkins path-filtered runs on PRs touching web UI surfaces. Closes #436 --- docs/developer-testing-guidelines.md | 15 +++ docs/webui-local-dev.md | 21 ++++ scripts/ci-webui-check | 53 +++++++++ scripts/test-webui | 13 +++ tests/test_webui_ci_gate.py | 54 ++++++++++ tests/test_webui_mvp_suite.py | 156 +++++++++++++++++++++++++++ webui/ci_paths.py | 20 ++++ 7 files changed, 332 insertions(+) create mode 100755 scripts/ci-webui-check create mode 100755 scripts/test-webui create mode 100644 tests/test_webui_ci_gate.py create mode 100644 tests/test_webui_mvp_suite.py create mode 100644 webui/ci_paths.py diff --git a/docs/developer-testing-guidelines.md b/docs/developer-testing-guidelines.md index d85c670..61abd75 100644 --- a/docs/developer-testing-guidelines.md +++ b/docs/developer-testing-guidelines.md @@ -54,6 +54,21 @@ Use `-q` for a compact summary and `-v` to see individual test names. ./venv/bin/python -m pytest tests/ -q ``` +### Web UI suite (#436) + +Hermetic unittest modules matching `test_webui_*.py` cover route rendering, +read-only guards, registry/prompt/queue loaders, and optional child-issue +modules when present on the branch. + +```bash +./scripts/test-webui +./scripts/ci-webui-check # skip unless the diff touches web UI paths +WEBUI_CI_FORCE=1 ./scripts/ci-webui-check +``` + +Wire `scripts/ci-webui-check` into Jenkins (or equivalent) for PRs that touch +`webui/`, `tests/test_webui_*`, or `docs/webui*`. + ### Run targeted tests ```bash diff --git a/docs/webui-local-dev.md b/docs/webui-local-dev.md index 73c4f47..1ee929b 100644 --- a/docs/webui-local-dev.md +++ b/docs/webui-local-dev.md @@ -155,4 +155,25 @@ pytest tests/test_webui_skeleton.py tests/test_webui_project_registry.py tests/t pytest tests/test_webui_skeleton.py tests/test_webui_project_registry.py tests/test_webui_prompt_library.py tests/test_webui_queue_dashboard.py tests/test_webui_lease_visibility.py tests/test_webui_runtime_health.py -q pytest tests/test_webui_skeleton.py tests/test_webui_project_registry.py tests/test_webui_prompt_library.py tests/test_webui_queue_dashboard.py tests/test_webui_deployment_boundary.py -q + +## Tests (#436) + +Run the full hermetic web UI suite (all `test_webui_*.py` modules): + +```bash +./scripts/test-webui +``` + +CI / Jenkins multibranch can call the path-filtered gate (runs only when the +diff touches `webui/`, `tests/test_webui_*`, or web UI docs/scripts): + +```bash +./scripts/ci-webui-check +WEBUI_CI_FORCE=1 ./scripts/ci-webui-check # always run +``` + +Or invoke unittest directly: + +```bash +python3 -m unittest discover -s tests -p 'test_webui_*.py' -q ``` \ No newline at end of file diff --git a/scripts/ci-webui-check b/scripts/ci-webui-check new file mode 100755 index 0000000..a679993 --- /dev/null +++ b/scripts/ci-webui-check @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# Path-filtered CI gate for the internal web UI (#436). +# Runs the hermetic web UI unittest suite when a change touches UI surfaces. +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd "$script_dir/.." && pwd)" +cd "$repo_root" + +if [[ "${WEBUI_CI_FORCE:-}" == "1" ]]; then + exec "$script_dir/test-webui" +fi + +base_ref="${WEBUI_CI_BASE_REF:-}" +if [[ -z "$base_ref" ]]; then + if git rev-parse --verify prgs/master >/dev/null 2>&1; then + base_ref="$(git merge-base HEAD prgs/master 2>/dev/null || true)" + fi + if [[ -z "$base_ref" ]]; then + base_ref="${GITHUB_BASE_REF:-${CHANGE_TARGET:-master}}" + if git rev-parse --verify "origin/$base_ref" >/dev/null 2>&1; then + base_ref="$(git merge-base HEAD "origin/$base_ref")" + elif git rev-parse --verify "$base_ref" >/dev/null 2>&1; then + base_ref="$(git merge-base HEAD "$base_ref")" + else + base_ref="HEAD~1" + fi + fi +fi + +changed="$(git diff --name-only "$base_ref" HEAD 2>/dev/null || true)" +if [[ -z "$changed" ]]; then + echo "ci-webui-check: no changed files vs $base_ref; skipping web UI suite" + exit 0 +fi + +python_bin="${WEBUI_TEST_PYTHON:-python3}" +if [[ -x "$repo_root/venv/bin/python" ]]; then + python_bin="$repo_root/venv/bin/python" +fi + +if printf '%s\n' "$changed" | "$python_bin" -c " +import sys +from webui.ci_paths import should_run_webui_ci +paths = [line.strip() for line in sys.stdin if line.strip()] +raise SystemExit(0 if should_run_webui_ci(paths) else 1) +"; then + echo "ci-webui-check: web UI surface changed; running suite" + exec "$script_dir/test-webui" +fi + +echo "ci-webui-check: no web UI paths in diff; skipping suite" +exit 0 \ No newline at end of file diff --git a/scripts/test-webui b/scripts/test-webui new file mode 100755 index 0000000..d7d4e31 --- /dev/null +++ b/scripts/test-webui @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -euo pipefail +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd "$script_dir/.." && pwd)" +cd "$repo_root" + +python_bin="${WEBUI_TEST_PYTHON:-python3}" +if [[ -x "$repo_root/venv/bin/python" ]]; then + python_bin="$repo_root/venv/bin/python" +fi + +pattern="${WEBUI_TEST_PATTERN:-test_webui_*.py}" +exec "$python_bin" -m unittest discover -s tests -p "$pattern" "$@" \ No newline at end of file diff --git a/tests/test_webui_ci_gate.py b/tests/test_webui_ci_gate.py new file mode 100644 index 0000000..2f4a577 --- /dev/null +++ b/tests/test_webui_ci_gate.py @@ -0,0 +1,54 @@ +"""Tests for web UI CI path-filter gate (#436).""" +import os +import subprocess +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from webui.ci_paths import should_run_webui_ci, touches_webui_surface + +_REPO_ROOT = Path(__file__).resolve().parent.parent + + +class TestCiPathMatching(unittest.TestCase): + def test_positive_paths(self): + for path in ( + "webui/app.py", + "tests/test_webui_skeleton.py", + "docs/webui-local-dev.md", + "scripts/test-webui", + ): + with self.subTest(path=path): + self.assertTrue(touches_webui_surface(path)) + + def test_negative_paths(self): + for path in ("mcp_server.py", "tests/test_mcp_server.py", "README.md"): + with self.subTest(path=path): + self.assertFalse(touches_webui_surface(path)) + + def test_should_run_aggregate(self): + self.assertTrue(should_run_webui_ci(["README.md", "webui/layout.py"])) + self.assertFalse(should_run_webui_ci(["mcp_server.py", "gitea_auth.py"])) + + +class TestCiRunnerScript(unittest.TestCase): + def test_test_webui_smoke(self): + script = _REPO_ROOT / "scripts" / "test-webui" + result = subprocess.run( + ["bash", str(script), "-q"], + cwd=_REPO_ROOT, + capture_output=True, + text=True, + timeout=60, + env={ + **os.environ, + "WEBUI_TEST_PATTERN": "test_webui_skeleton.py", + }, + ) + self.assertEqual(result.returncode, 0, result.stderr or result.stdout) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/tests/test_webui_mvp_suite.py b/tests/test_webui_mvp_suite.py new file mode 100644 index 0000000..2d5bd54 --- /dev/null +++ b/tests/test_webui_mvp_suite.py @@ -0,0 +1,156 @@ +"""Consolidated MVP coverage for the internal web UI (#436).""" +from __future__ import annotations + +import importlib +import os +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from starlette.testclient import TestClient +from starlette.routing import Route + +from webui.app import create_app + +_REPO_ROOT = Path(__file__).resolve().parent.parent + +# HTML pages and JSON exports registered on master MVP. +MVP_GET_ROUTES: tuple[tuple[str, str | tuple[str, ...]], ...] = ( + ("/", "Operator console"), + ("/health", "mcp-control-plane-webui"), + ("/queue", "Live queue"), + ("/api/queue", ("prs", "issues")), + ("/projects", "Gitea-Tools"), + ("/api/projects", "projects"), + ("/prompts", "Prompt library"), + ("/api/prompts", "prompts"), + ("/runtime", "Runtime"), + ("/audit", "Audit"), + ("/worktrees", "Worktrees"), + ("/leases", "Leases"), +) + +MUTATION_METHODS = ("POST", "PUT", "PATCH", "DELETE") + + +def _import_optional(module_name: str): + try: + return importlib.import_module(module_name) + except ImportError: + return None + + +class TestWebuiServerStartup(unittest.TestCase): + def test_create_app_imports_cleanly(self): + app = create_app() + self.assertIsNotNone(app) + + def test_main_module_imports(self): + import webui.__main__ as main_mod + + self.assertTrue(callable(main_mod.main)) + + +class TestWebuiRouteRendering(unittest.TestCase): + def setUp(self): + self.client = TestClient(create_app()) + + def test_all_mvp_get_routes_render(self): + for path, needle in MVP_GET_ROUTES: + with self.subTest(path=path): + response = self.client.get(path) + self.assertEqual(response.status_code, 200, path) + if path == "/health": + assert isinstance(needle, str) + self.assertEqual(response.json()["service"], needle) + elif path.startswith("/api/"): + data = response.json() + if isinstance(needle, tuple): + for key in needle: + self.assertIn(key, data) + else: + self.assertIn(needle, data) + else: + assert isinstance(needle, str) + self.assertIn(needle, response.text) + + def test_registered_routes_include_mvp_paths(self): + app = create_app() + get_paths = { + route.path + for route in app.routes + if isinstance(route, Route) and "GET" in route.methods + } + for path, _ in MVP_GET_ROUTES: + with self.subTest(path=path): + self.assertIn(path, get_paths) + + +class TestWebuiReadOnlyGuards(unittest.TestCase): + def setUp(self): + self.client = TestClient(create_app()) + + def test_mutation_methods_rejected_on_core_paths(self): + paths = ("/health", "/queue", "/projects", "/prompts", "/api/queue") + for path in paths: + for method in MUTATION_METHODS: + with self.subTest(path=path, method=method): + response = self.client.request(method, path) + self.assertEqual(response.status_code, 405) + self.assertEqual(response.json()["error"], "read-only-mvp") + + +class TestWebuiOptionalModules(unittest.TestCase): + """Exercise child-issue modules when present on the branch under test.""" + + def test_audit_validator_basics_when_present(self): + mod = _import_optional("webui.audit_validator") + if mod is None: + self.skipTest("webui.audit_validator not merged on this branch") + report = "## Controller Handoff\n- Task: review PR #1\n" + self.assertEqual(mod.infer_task_kind(report), "review_pr") + result = mod.audit_report(report) + self.assertIn("grade", result) + + def test_gated_actions_fail_closed_when_present(self): + mod = _import_optional("webui.gated_actions") + if mod is None: + self.skipTest("webui.gated_actions not merged on this branch") + registry = mod.build_action_registry() + for action in registry.actions: + with self.subTest(action=action.action_id): + self.assertFalse(action.enabled) + result = mod.attempt_action("merge_pr", pr_number=1) + self.assertFalse(result["success"]) + + def test_deployment_boundary_when_present(self): + mod = _import_optional("webui.deployment_boundary") + if mod is None: + self.skipTest("webui.deployment_boundary not merged on this branch") + assessment = mod.assess_bind_host("0.0.0.0") + self.assertEqual(assessment.disposition, "refuse") + + +class TestWebuiTestInventory(unittest.TestCase): + def test_webui_test_modules_exist(self): + modules = sorted(_REPO_ROOT.glob("tests/test_webui_*.py")) + names = [path.name for path in modules] + self.assertIn("test_webui_skeleton.py", names) + self.assertIn("test_webui_project_registry.py", names) + self.assertIn("test_webui_prompt_library.py", names) + self.assertIn("test_webui_queue_dashboard.py", names) + self.assertGreaterEqual(len(names), 5) + + +class TestWebuiCiScripts(unittest.TestCase): + def test_runner_scripts_exist(self): + for name in ("test-webui", "ci-webui-check"): + script = _REPO_ROOT / "scripts" / name + self.assertTrue(script.is_file(), name) + self.assertTrue(os.access(script, os.X_OK), name) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/webui/ci_paths.py b/webui/ci_paths.py new file mode 100644 index 0000000..779acf4 --- /dev/null +++ b/webui/ci_paths.py @@ -0,0 +1,20 @@ +"""Path filters for web UI CI gating (#436).""" + +from __future__ import annotations + +import re +from collections.abc import Iterable + +_WEBUI_TOUCH_RE = re.compile( + r"^(?:webui/|tests/test_webui_|docs/webui|scripts/(?:run-webui|test-webui|ci-webui-check)$)" +) + + +def touches_webui_surface(path: str) -> bool: + """Return True when *path* is a web UI surface file.""" + return bool(_WEBUI_TOUCH_RE.match(path.strip())) + + +def should_run_webui_ci(changed_paths: Iterable[str]) -> bool: + """Return True when any changed path should trigger the web UI test suite.""" + return any(touches_webui_surface(path) for path in changed_paths) \ No newline at end of file -- 2.43.7 From 64b83cd1fbca546c7e665734a52be35c19825472 Mon Sep 17 00:00:00 2001 From: Jason Walker <913443@dadeschools.net> Date: Thu, 9 Jul 2026 11:34:33 -0400 Subject: [PATCH 2/2] test: keep webui CI hermetic offline --- docs/developer-testing-guidelines.md | 4 +++ docs/webui-local-dev.md | 31 ++++++++++++++++++++++- scripts/test-webui | 3 ++- tests/test_webui_ci_gate.py | 38 +++++++++++++++++++++++++++- tests/test_webui_queue_dashboard.py | 7 ++--- webui/lease_loader.py | 29 +++++++++++++++++---- webui/queue_loader.py | 23 ++++++++++++++--- webui/runtime_health.py | 21 ++++++++++++--- 8 files changed, 138 insertions(+), 18 deletions(-) diff --git a/docs/developer-testing-guidelines.md b/docs/developer-testing-guidelines.md index 61abd75..3e180fc 100644 --- a/docs/developer-testing-guidelines.md +++ b/docs/developer-testing-guidelines.md @@ -66,6 +66,10 @@ modules when present on the branch. WEBUI_CI_FORCE=1 ./scripts/ci-webui-check ``` +`scripts/test-webui` defaults `WEBUI_TEST_OFFLINE=1` so route coverage never +needs Gitea credentials or MCP daemon credential access. Set +`WEBUI_TEST_OFFLINE=0` only for explicit operator live-fetch checks. + Wire `scripts/ci-webui-check` into Jenkins (or equivalent) for PRs that touch `webui/`, `tests/test_webui_*`, or `docs/webui*`. diff --git a/docs/webui-local-dev.md b/docs/webui-local-dev.md index 1ee929b..f1e9145 100644 --- a/docs/webui-local-dev.md +++ b/docs/webui-local-dev.md @@ -172,8 +172,37 @@ diff touches `webui/`, `tests/test_webui_*`, or web UI docs/scripts): WEBUI_CI_FORCE=1 ./scripts/ci-webui-check # always run ``` +`scripts/test-webui` sets `WEBUI_TEST_OFFLINE=1` by default. In that mode the +queue, lease, and runtime routes use empty offline snapshots instead of Gitea +credentials, so CI can run without MCP daemon credential access. Set +`WEBUI_TEST_OFFLINE=0` only when deliberately validating live fetch behavior. + Or invoke unittest directly: ```bash python3 -m unittest discover -s tests -p 'test_webui_*.py' -q -``` \ No newline at end of file +``` + +## Lease visibility (#433) + +`/leases` surfaces read-only lease and collision state: local issue lock file, +in-progress claim inventory (#268), reviewer PR lease comments when present +(``, #407), duplicate open PRs per issue (#400), +and duplicate local branches per issue. Links to collision-history backend +issues (#267, #268, #400, #407) are included. No lease acquire/release from UI. + +## Runtime health (#430) + +`/runtime` surfaces read-only MCP/runtime diagnostics for the default registry +project: active profile and role kind, authenticated identity (when credentials +are available), config model/mode, local vs remote `master` SHA sync, shell +health, workflow/schema SHA-256 hashes, and stale-runtime warnings when the +checkout is behind merged safety-gate changes. Restart guidance links to #420; +no tokens or MCP restart actions are exposed. + +## Tests + +```bash +pytest tests/test_webui_skeleton.py tests/test_webui_project_registry.py tests/test_webui_prompt_library.py tests/test_webui_queue_dashboard.py tests/test_webui_audit.py tests/test_webui_worktree_hygiene.py -q +pytest tests/test_webui_skeleton.py tests/test_webui_project_registry.py tests/test_webui_prompt_library.py tests/test_webui_queue_dashboard.py tests/test_webui_lease_visibility.py tests/test_webui_runtime_health.py -q +``` diff --git a/scripts/test-webui b/scripts/test-webui index d7d4e31..a5c49b7 100755 --- a/scripts/test-webui +++ b/scripts/test-webui @@ -10,4 +10,5 @@ if [[ -x "$repo_root/venv/bin/python" ]]; then fi pattern="${WEBUI_TEST_PATTERN:-test_webui_*.py}" -exec "$python_bin" -m unittest discover -s tests -p "$pattern" "$@" \ No newline at end of file +export WEBUI_TEST_OFFLINE="${WEBUI_TEST_OFFLINE:-1}" +exec "$python_bin" -m unittest discover -s tests -p "$pattern" "$@" diff --git a/tests/test_webui_ci_gate.py b/tests/test_webui_ci_gate.py index 2f4a577..3d2c602 100644 --- a/tests/test_webui_ci_gate.py +++ b/tests/test_webui_ci_gate.py @@ -4,10 +4,14 @@ import subprocess import sys import unittest from pathlib import Path +from unittest import mock sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from webui.ci_paths import should_run_webui_ci, touches_webui_surface +from webui.lease_loader import load_lease_snapshot +from webui.queue_loader import load_queue_snapshot +from webui.runtime_health import load_runtime_snapshot _REPO_ROOT = Path(__file__).resolve().parent.parent @@ -50,5 +54,37 @@ class TestCiRunnerScript(unittest.TestCase): self.assertEqual(result.returncode, 0, result.stderr or result.stdout) +class TestOfflineWebuiCiMode(unittest.TestCase): + def test_offline_mode_avoids_live_queue_auth(self): + with mock.patch.dict(os.environ, {"WEBUI_TEST_OFFLINE": "1"}): + with mock.patch( + "webui.queue_loader.get_auth_header", + side_effect=AssertionError("live auth should not run"), + ): + snapshot = load_queue_snapshot() + self.assertIsNone(snapshot.fetch_error) + self.assertEqual(snapshot.prs, ()) + self.assertEqual(snapshot.issues, ()) + + def test_offline_mode_avoids_live_lease_auth(self): + with mock.patch.dict(os.environ, {"WEBUI_TEST_OFFLINE": "1"}): + with mock.patch( + "webui.lease_loader.get_auth_header", + side_effect=AssertionError("live auth should not run"), + ): + snapshot = load_lease_snapshot() + self.assertIsNone(snapshot.fetch_error) + self.assertEqual(snapshot.reviewer_leases, ()) + + def test_offline_mode_avoids_live_runtime_identity(self): + with mock.patch.dict(os.environ, {"WEBUI_TEST_OFFLINE": "1"}): + with mock.patch( + "webui.runtime_health.get_auth_header", + side_effect=AssertionError("live auth should not run"), + ): + snapshot = load_runtime_snapshot() + self.assertEqual(snapshot.identity_error, "offline web UI test mode") + + if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/test_webui_queue_dashboard.py b/tests/test_webui_queue_dashboard.py index cbdcc16..90ff8e7 100644 --- a/tests/test_webui_queue_dashboard.py +++ b/tests/test_webui_queue_dashboard.py @@ -206,8 +206,9 @@ class TestQueueRoutes(unittest.TestCase): self.assertEqual(data["pagination"]["issues"]["returned_count"], 3) def test_queue_fail_closed_without_credentials(self): - with mock.patch("webui.queue_loader.get_auth_header", return_value=None): - snapshot = load_queue_snapshot() + with mock.patch.dict("os.environ", {"WEBUI_TEST_OFFLINE": "0"}): + with mock.patch("webui.queue_loader.get_auth_header", return_value=None): + snapshot = load_queue_snapshot() self.assertIsNotNone(snapshot.fetch_error) self.assertEqual(len(snapshot.prs), 0) @@ -285,4 +286,4 @@ class TestQueueFailClosedUx(unittest.TestCase): if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/webui/lease_loader.py b/webui/lease_loader.py index 318a46f..cd05d61 100644 --- a/webui/lease_loader.py +++ b/webui/lease_loader.py @@ -228,10 +228,29 @@ def load_lease_snapshot( ) host = _host_from_url(project.remote_host) - pr_fetch = fetch_prs or _fetch_prs - issue_fetch = fetch_issues or _fetch_issues - comment_fetch = fetch_comments or _fetch_comments - using_live = fetch_prs is None or fetch_issues is None + offline_test = (os.environ.get("WEBUI_TEST_OFFLINE") or "").strip() in { + "1", + "true", + "yes", + } + + def _empty_pr_fetch(*_args, **_kwargs): + return [], None + + def _empty_issue_fetch(*_args, **_kwargs): + return [], None + + def _empty_comment_fetch(*_args, **_kwargs): + return [] + + pr_fetch = fetch_prs or (_empty_pr_fetch if offline_test else _fetch_prs) + issue_fetch = fetch_issues or ( + _empty_issue_fetch if offline_test else _fetch_issues + ) + comment_fetch = fetch_comments or ( + _empty_comment_fetch if offline_test else _fetch_comments + ) + using_live = not offline_test and (fetch_prs is None or fetch_issues is None) auth = get_auth_header(host) if using_live else "test-auth" if using_live and not auth: @@ -328,4 +347,4 @@ def snapshot_to_dict(snapshot: LeaseSnapshot) -> dict[str, Any]: ], "collision_history": list(snapshot.collision_history), "fetch_error": snapshot.fetch_error, - } \ No newline at end of file + } diff --git a/webui/queue_loader.py b/webui/queue_loader.py index a70e58d..a6d35c7 100644 --- a/webui/queue_loader.py +++ b/webui/queue_loader.py @@ -2,6 +2,7 @@ from __future__ import annotations +import os import re from dataclasses import dataclass from datetime import datetime, timezone @@ -268,9 +269,23 @@ def load_queue_snapshot( ) host = _host_from_url(project.remote_host) - pr_fetch = fetch_prs or _fetch_prs - issue_fetch = fetch_issues or _fetch_issues - using_live_fetch = fetch_prs is None or fetch_issues is None + offline_test = (os.environ.get("WEBUI_TEST_OFFLINE") or "").strip() in { + "1", + "true", + "yes", + } + + def _empty_fetch(*_args, **_kwargs): + return [], _pagination_from_pages( + per_page=50, + pages_fetched=1, + returned_count=0, + is_final_page=True, + ) + + pr_fetch = fetch_prs or (_empty_fetch if offline_test else _fetch_prs) + issue_fetch = fetch_issues or (_empty_fetch if offline_test else _fetch_issues) + using_live_fetch = not offline_test and (fetch_prs is None or fetch_issues is None) auth = get_auth_header(host) if using_live_fetch else "test-auth" if using_live_fetch and not auth: return QueueSnapshot( @@ -382,4 +397,4 @@ def snapshot_to_dict(snapshot: QueueSnapshot) -> dict[str, Any]: "prs": _page(snapshot.pr_pagination), "issues": _page(snapshot.issue_pagination), }, - } \ No newline at end of file + } diff --git a/webui/runtime_health.py b/webui/runtime_health.py index 8f3b5d6..ee44187 100644 --- a/webui/runtime_health.py +++ b/webui/runtime_health.py @@ -217,6 +217,11 @@ def load_runtime_snapshot( repo = _repo_root() host = _host_from_url(project.remote_host) remote = _remote_name_for_host(host) + offline_test = (os.environ.get("WEBUI_TEST_OFFLINE") or "").strip() in { + "1", + "true", + "yes", + } profile = get_profile() allowed = profile.get("allowed_operations") or [] forbidden = profile.get("forbidden_operations") or [] @@ -247,10 +252,20 @@ def load_runtime_snapshot( fetch_error=str(exc), ) - identity_fn = resolve_username or _resolve_username + identity_fn = resolve_username + if identity_fn is None: + if offline_test: + identity_fn = lambda _host: (None, "offline web UI test mode") + else: + identity_fn = _resolve_username username, identity_error = identity_fn(host) - git_fn = git_sync or _git_sync_status + git_fn = git_sync + if git_fn is None: + if offline_test: + git_fn = lambda _repo, _remote, _branch: (None, None, None) + else: + git_fn = _git_sync_status remote_ref = "prgs" if remote == "prgs" else "origin" local_sha, remote_sha, behind = git_fn(repo, remote_ref, project.default_branch) @@ -317,4 +332,4 @@ def snapshot_to_dict(snapshot: RuntimeSnapshot) -> dict[str, Any]: "schema_hashes": [_hash_row(item) for item in snapshot.schema_hashes], "restart_guidance": snapshot.restart_guidance, "fetch_error": snapshot.fetch_error, - } \ No newline at end of file + } -- 2.43.7