feat: add views.py with mutual exclusion invariant and name validation

This commit is contained in:
Brian Krabach
2026-04-15 11:10:34 -07:00
parent eb2c02b477
commit 353ea5fd76
2 changed files with 198 additions and 0 deletions
+129
View File
@@ -0,0 +1,129 @@
"""
Tests for muxplex/views.py — views invariant enforcement.
"""
from muxplex.views import (
enforce_mutual_exclusion,
validate_view_name,
)
# ---------------------------------------------------------------------------
# enforce_mutual_exclusion
# ---------------------------------------------------------------------------
def test_enforce_removes_from_hidden_when_in_view():
"""If a session is in both hidden_sessions and a view, remove from hidden (favor visibility)."""
settings = {
"hidden_sessions": ["abc:dev", "def:build"],
"views": [
{"name": "Work", "sessions": ["abc:dev", "abc:web"]},
],
}
result = enforce_mutual_exclusion(settings)
assert "abc:dev" not in result["hidden_sessions"]
assert "def:build" in result["hidden_sessions"]
assert "abc:dev" in result["views"][0]["sessions"]
def test_enforce_no_change_when_no_overlap():
"""No changes when there is no overlap between hidden and views."""
settings = {
"hidden_sessions": ["abc:old"],
"views": [
{"name": "Work", "sessions": ["abc:dev"]},
],
}
result = enforce_mutual_exclusion(settings)
assert result["hidden_sessions"] == ["abc:old"]
assert result["views"][0]["sessions"] == ["abc:dev"]
def test_enforce_handles_empty_views():
"""Works when views is an empty list."""
settings = {
"hidden_sessions": ["abc:dev"],
"views": [],
}
result = enforce_mutual_exclusion(settings)
assert result["hidden_sessions"] == ["abc:dev"]
def test_enforce_handles_empty_hidden():
"""Works when hidden_sessions is empty."""
settings = {
"hidden_sessions": [],
"views": [{"name": "Work", "sessions": ["abc:dev"]}],
}
result = enforce_mutual_exclusion(settings)
assert result["hidden_sessions"] == []
def test_enforce_deduplicates_view_sessions():
"""Duplicate session keys within a view are deduplicated."""
settings = {
"hidden_sessions": [],
"views": [
{"name": "Work", "sessions": ["abc:dev", "abc:dev", "abc:web"]},
],
}
result = enforce_mutual_exclusion(settings)
assert result["views"][0]["sessions"] == ["abc:dev", "abc:web"]
def test_enforce_overlap_across_multiple_views():
"""A hidden session appearing in multiple views is removed from hidden."""
settings = {
"hidden_sessions": ["abc:dev"],
"views": [
{"name": "Work", "sessions": ["abc:dev"]},
{"name": "Hobby", "sessions": ["abc:dev", "abc:printer"]},
],
}
result = enforce_mutual_exclusion(settings)
assert "abc:dev" not in result["hidden_sessions"]
# ---------------------------------------------------------------------------
# validate_view_name
# ---------------------------------------------------------------------------
def test_validate_rejects_empty_name():
assert validate_view_name("", []) is not None
def test_validate_rejects_whitespace_only():
assert validate_view_name(" ", []) is not None
def test_validate_rejects_too_long():
assert validate_view_name("a" * 31, []) is not None
def test_validate_rejects_reserved_all():
assert validate_view_name("all", []) is not None
def test_validate_rejects_reserved_hidden():
assert validate_view_name("Hidden", []) is not None
def test_validate_rejects_duplicate():
existing = [{"name": "Work", "sessions": []}]
assert validate_view_name("Work", existing) is not None
def test_validate_accepts_valid_name():
assert validate_view_name("My Project", []) is None
def test_validate_trims_whitespace():
"""A name that is valid after trimming should pass."""
assert validate_view_name(" My Project ", []) is None
def test_validate_accepts_at_max_length():
assert validate_view_name("a" * 30, []) is None
+69
View File
@@ -0,0 +1,69 @@
"""
Views invariant enforcement and validation for muxplex.
Core invariants:
- hidden_sessions and any views[].sessions never share a session key.
- View names are non-empty, max 30 chars, trimmed, unique, not reserved.
- Duplicate session keys within a view are deduplicated.
"""
RESERVED_VIEW_NAMES = frozenset({"all", "hidden"})
MAX_VIEW_NAME_LENGTH = 30
def enforce_mutual_exclusion(settings: dict) -> dict:
"""Enforce that hidden_sessions and view sessions are disjoint.
If a session key appears in both hidden_sessions and any view,
it is removed from hidden_sessions (favor visibility over hiding).
Also deduplicates session keys within each view.
Mutates and returns the settings dict.
"""
views = settings.get("views", [])
hidden = settings.get("hidden_sessions", [])
# Collect all session keys across all views
all_view_sessions: set[str] = set()
for view in views:
all_view_sessions.update(view.get("sessions", []))
# Remove overlap from hidden (favor visibility)
if all_view_sessions and hidden:
settings["hidden_sessions"] = [s for s in hidden if s not in all_view_sessions]
# Deduplicate session keys within each view (preserve order)
for view in views:
sessions = view.get("sessions", [])
seen: set[str] = set()
deduped: list[str] = []
for s in sessions:
if s not in seen:
seen.add(s)
deduped.append(s)
view["sessions"] = deduped
return settings
def validate_view_name(name: str, existing_views: list[dict]) -> str | None:
"""Validate a view name. Returns an error message string, or None if valid.
Rules:
- Non-empty after trimming
- Max 30 characters after trimming
- Not a reserved name ("all", "hidden") case-insensitive
- Unique among existing views (case-sensitive match)
"""
trimmed = name.strip()
if not trimmed:
return "View name cannot be empty"
if len(trimmed) > MAX_VIEW_NAME_LENGTH:
return f"View name must be {MAX_VIEW_NAME_LENGTH} characters or fewer"
if trimmed.lower() in RESERVED_VIEW_NAMES:
return f"'{trimmed}' is a reserved name"
existing_names = {v.get("name", "") for v in existing_views}
if trimmed in existing_names:
return f"A view named '{trimmed}' already exists"
return None