Adds read-only /worktrees and /api/worktrees routes that scan branches/ directories, classify worktree state, flag missing preserved worktrees from the issue lock, and surface the canonical cleanup prompt without deletion. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
114 lines
4.0 KiB
Python
114 lines
4.0 KiB
Python
"""Tests for web UI worktree hygiene dashboard (#432)."""
|
|
import json
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
|
|
from starlette.testclient import TestClient
|
|
|
|
from webui.app import create_app
|
|
from webui.worktree_scanner import (
|
|
classify_entry,
|
|
load_hygiene_snapshot,
|
|
parse_worktree_list_porcelain,
|
|
snapshot_to_dict,
|
|
)
|
|
|
|
|
|
class TestWorktreeScanner(unittest.TestCase):
|
|
def test_parse_worktree_porcelain(self):
|
|
porcelain = (
|
|
"worktree /repo/branches/feat-issue-1-foo\n"
|
|
"HEAD abcdef0123456789\n"
|
|
"branch refs/heads/feat/issue-1-foo\n"
|
|
"\n"
|
|
)
|
|
entries = parse_worktree_list_porcelain(porcelain)
|
|
self.assertEqual(len(entries), 1)
|
|
self.assertEqual(entries[0]["branch"], "feat/issue-1-foo")
|
|
|
|
def test_classify_active_pr(self):
|
|
classification, _ = classify_entry(
|
|
rel_path="branches/feat-issue-99-demo",
|
|
worktree_record={"branch": "feat/issue-99-demo"},
|
|
state={"exists": True, "clean": True, "dirty_files": []},
|
|
open_pr_branches={"feat/issue-99-demo"},
|
|
active_lock_branch=None,
|
|
)
|
|
self.assertEqual(classification, "active-pr")
|
|
|
|
def test_classify_dirty(self):
|
|
classification, _ = classify_entry(
|
|
rel_path="branches/feat-issue-2-bar",
|
|
worktree_record={"branch": "feat/issue-2-bar"},
|
|
state={"exists": True, "clean": False, "dirty_files": ["webui/app.py"]},
|
|
open_pr_branches=set(),
|
|
active_lock_branch=None,
|
|
)
|
|
self.assertEqual(classification, "dirty")
|
|
|
|
def test_missing_lock_worktree_anomaly(self):
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
branches = Path(tmp) / "branches" / "other-worktree"
|
|
branches.mkdir(parents=True)
|
|
lock_path = Path(tmp) / "lock.json"
|
|
lock_path.write_text(
|
|
json.dumps({
|
|
"issue_number": 432,
|
|
"branch_name": "feat/issue-432-worktree-hygiene",
|
|
"worktree_path": str(Path(tmp) / "branches" / "missing-tree"),
|
|
}),
|
|
encoding="utf-8",
|
|
)
|
|
snapshot = load_hygiene_snapshot(
|
|
project_root=tmp,
|
|
worktree_porcelain="",
|
|
open_pr_branches=set(),
|
|
issue_lock_path=str(lock_path),
|
|
)
|
|
self.assertTrue(snapshot.anomalies)
|
|
self.assertIn("Missing preserved worktree", snapshot.anomalies[0])
|
|
|
|
def test_snapshot_to_dict(self):
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
(Path(tmp) / "branches").mkdir()
|
|
snapshot = load_hygiene_snapshot(
|
|
project_root=tmp,
|
|
worktree_porcelain="",
|
|
open_pr_branches=set(),
|
|
issue_lock_path=str(Path(tmp) / "no-lock.json"),
|
|
)
|
|
data = snapshot_to_dict(snapshot)
|
|
self.assertIn("entries", data)
|
|
self.assertIn("cleanup_prompt", data)
|
|
|
|
|
|
class TestWorktreeRoutes(unittest.TestCase):
|
|
def setUp(self):
|
|
self.client = TestClient(create_app())
|
|
|
|
def test_worktrees_page_renders(self):
|
|
response = self.client.get("/worktrees")
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertIn("Worktree hygiene", response.text)
|
|
self.assertIn("Canonical cleanup prompt", response.text)
|
|
self.assertIn("Copy cleanup prompt", response.text)
|
|
self.assertNotIn("child issue", response.text.lower())
|
|
|
|
def test_api_worktrees_json(self):
|
|
response = self.client.get("/api/worktrees")
|
|
self.assertEqual(response.status_code, 200)
|
|
data = response.json()
|
|
self.assertIn("entries", data)
|
|
self.assertIn("cleanup_prompt", data)
|
|
self.assertIn("anomalies", data)
|
|
|
|
def test_nav_includes_worktrees(self):
|
|
self.assertIn('href="/worktrees"', self.client.get("/").text)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main() |