test: keep webui CI hermetic offline
This commit is contained in:
@@ -66,6 +66,10 @@ modules when present on the branch.
|
|||||||
WEBUI_CI_FORCE=1 ./scripts/ci-webui-check
|
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
|
Wire `scripts/ci-webui-check` into Jenkins (or equivalent) for PRs that touch
|
||||||
`webui/`, `tests/test_webui_*`, or `docs/webui*`.
|
`webui/`, `tests/test_webui_*`, or `docs/webui*`.
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
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:
|
Or invoke unittest directly:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 -m unittest discover -s tests -p 'test_webui_*.py' -q
|
python3 -m unittest discover -s tests -p 'test_webui_*.py' -q
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 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
|
||||||
|
(`<!-- mcp-review-lease:v1 -->`, #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
|
||||||
|
```
|
||||||
|
|||||||
@@ -10,4 +10,5 @@ if [[ -x "$repo_root/venv/bin/python" ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
pattern="${WEBUI_TEST_PATTERN:-test_webui_*.py}"
|
pattern="${WEBUI_TEST_PATTERN:-test_webui_*.py}"
|
||||||
|
export WEBUI_TEST_OFFLINE="${WEBUI_TEST_OFFLINE:-1}"
|
||||||
exec "$python_bin" -m unittest discover -s tests -p "$pattern" "$@"
|
exec "$python_bin" -m unittest discover -s tests -p "$pattern" "$@"
|
||||||
@@ -4,10 +4,14 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
import unittest
|
import unittest
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||||
|
|
||||||
from webui.ci_paths import should_run_webui_ci, touches_webui_surface
|
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
|
_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)
|
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__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
@@ -206,6 +206,7 @@ class TestQueueRoutes(unittest.TestCase):
|
|||||||
self.assertEqual(data["pagination"]["issues"]["returned_count"], 3)
|
self.assertEqual(data["pagination"]["issues"]["returned_count"], 3)
|
||||||
|
|
||||||
def test_queue_fail_closed_without_credentials(self):
|
def test_queue_fail_closed_without_credentials(self):
|
||||||
|
with mock.patch.dict("os.environ", {"WEBUI_TEST_OFFLINE": "0"}):
|
||||||
with mock.patch("webui.queue_loader.get_auth_header", return_value=None):
|
with mock.patch("webui.queue_loader.get_auth_header", return_value=None):
|
||||||
snapshot = load_queue_snapshot()
|
snapshot = load_queue_snapshot()
|
||||||
self.assertIsNotNone(snapshot.fetch_error)
|
self.assertIsNotNone(snapshot.fetch_error)
|
||||||
|
|||||||
+23
-4
@@ -228,10 +228,29 @@ def load_lease_snapshot(
|
|||||||
)
|
)
|
||||||
|
|
||||||
host = _host_from_url(project.remote_host)
|
host = _host_from_url(project.remote_host)
|
||||||
pr_fetch = fetch_prs or _fetch_prs
|
offline_test = (os.environ.get("WEBUI_TEST_OFFLINE") or "").strip() in {
|
||||||
issue_fetch = fetch_issues or _fetch_issues
|
"1",
|
||||||
comment_fetch = fetch_comments or _fetch_comments
|
"true",
|
||||||
using_live = fetch_prs is None or fetch_issues is None
|
"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"
|
auth = get_auth_header(host) if using_live else "test-auth"
|
||||||
|
|
||||||
if using_live and not auth:
|
if using_live and not auth:
|
||||||
|
|||||||
+18
-3
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
@@ -268,9 +269,23 @@ def load_queue_snapshot(
|
|||||||
)
|
)
|
||||||
|
|
||||||
host = _host_from_url(project.remote_host)
|
host = _host_from_url(project.remote_host)
|
||||||
pr_fetch = fetch_prs or _fetch_prs
|
offline_test = (os.environ.get("WEBUI_TEST_OFFLINE") or "").strip() in {
|
||||||
issue_fetch = fetch_issues or _fetch_issues
|
"1",
|
||||||
using_live_fetch = fetch_prs is None or fetch_issues is None
|
"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"
|
auth = get_auth_header(host) if using_live_fetch else "test-auth"
|
||||||
if using_live_fetch and not auth:
|
if using_live_fetch and not auth:
|
||||||
return QueueSnapshot(
|
return QueueSnapshot(
|
||||||
|
|||||||
+17
-2
@@ -217,6 +217,11 @@ def load_runtime_snapshot(
|
|||||||
repo = _repo_root()
|
repo = _repo_root()
|
||||||
host = _host_from_url(project.remote_host)
|
host = _host_from_url(project.remote_host)
|
||||||
remote = _remote_name_for_host(host)
|
remote = _remote_name_for_host(host)
|
||||||
|
offline_test = (os.environ.get("WEBUI_TEST_OFFLINE") or "").strip() in {
|
||||||
|
"1",
|
||||||
|
"true",
|
||||||
|
"yes",
|
||||||
|
}
|
||||||
profile = get_profile()
|
profile = get_profile()
|
||||||
allowed = profile.get("allowed_operations") or []
|
allowed = profile.get("allowed_operations") or []
|
||||||
forbidden = profile.get("forbidden_operations") or []
|
forbidden = profile.get("forbidden_operations") or []
|
||||||
@@ -247,10 +252,20 @@ def load_runtime_snapshot(
|
|||||||
fetch_error=str(exc),
|
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)
|
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"
|
remote_ref = "prgs" if remote == "prgs" else "origin"
|
||||||
local_sha, remote_sha, behind = git_fn(repo, remote_ref, project.default_branch)
|
local_sha, remote_sha, behind = git_fn(repo, remote_ref, project.default_branch)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user