""" tests/test_backlog_workflow.py — Workflow state machine tests. """ import os import sys import pytest sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "services", "router")) from backlog_store import ( MemoryBacklogStore, BacklogItem, BacklogEvent, validate_transition, _builtin_workflow, _new_id, _now_iso, ) def _make_item(status: str = "open", **kw) -> BacklogItem: base = dict( id=_new_id("bl"), created_at=_now_iso(), updated_at=_now_iso(), env="prod", service="gateway", category="arch_review", title="Test item", description="", priority="P1", status=status, owner="oncall", due_date="2026-06-01", source="manual", dedupe_key=_new_id("dk"), evidence_refs={}, tags=[], meta={}, ) base.update(kw) return BacklogItem.from_dict(base) WORKFLOW = _builtin_workflow() class TestValidateTransition: def test_open_to_in_progress(self): assert validate_transition("open", "in_progress") is True def test_open_to_blocked(self): assert validate_transition("open", "blocked") is True def test_open_to_canceled(self): assert validate_transition("open", "canceled") is True def test_open_to_done_rejected(self): assert validate_transition("open", "done") is False def test_in_progress_to_done(self): assert validate_transition("in_progress", "done") is True def test_in_progress_to_blocked(self): assert validate_transition("in_progress", "blocked") is True def test_in_progress_to_canceled(self): assert validate_transition("in_progress", "canceled") is True def test_in_progress_to_open_rejected(self): assert validate_transition("in_progress", "open") is False def test_blocked_to_open(self): assert validate_transition("blocked", "open") is True def test_blocked_to_in_progress(self): assert validate_transition("blocked", "in_progress") is True def test_blocked_to_canceled(self): assert validate_transition("blocked", "canceled") is True def test_blocked_to_done_rejected(self): assert validate_transition("blocked", "done") is False def test_done_to_anything_rejected(self): for target in ("open", "in_progress", "blocked", "canceled"): assert validate_transition("done", target) is False def test_canceled_to_anything_rejected(self): for target in ("open", "in_progress", "blocked", "done"): assert validate_transition("canceled", target) is False class TestWorkflowInStore: def _store(self): return MemoryBacklogStore() def test_set_status_valid_transition(self): store = self._store() item = _make_item(status="open") store.create(item) item.status = "in_progress" store.update(item) fetched = store.get(item.id) assert fetched.status == "in_progress" def test_set_status_records_event(self): store = self._store() item = _make_item(status="open") store.create(item) ev = BacklogEvent( id=_new_id("ev"), item_id=item.id, ts=_now_iso(), type="status_change", message="open → in_progress", actor="oncall", meta={"old_status": "open", "new_status": "in_progress"}, ) store.add_event(ev) events = store.get_events(item.id) assert any(e.type == "status_change" for e in events) def test_full_lifecycle_open_to_done(self): store = self._store() item = _make_item(status="open") store.create(item) for new_status in ("in_progress", "done"): assert validate_transition(item.status, new_status) is True item.status = new_status store.update(item) fetched = store.get(item.id) assert fetched.status == "done" def test_done_item_cannot_reopen(self): item = _make_item(status="done") assert validate_transition(item.status, "open") is False assert validate_transition(item.status, "in_progress") is False def test_canceled_item_is_terminal(self): item = _make_item(status="canceled") for t in ("open", "in_progress", "blocked", "done"): assert validate_transition(item.status, t) is False def test_policy_overrides_builtin(self): custom_policy = { "workflow": { "allowed_transitions": { "open": ["done"], # Only allow direct done (custom) } } } assert validate_transition("open", "done", custom_policy) is True assert validate_transition("open", "in_progress", custom_policy) is False class TestCommentEvents: def test_add_comment_creates_event(self): store = MemoryBacklogStore() item = _make_item() store.create(item) ev = BacklogEvent( id=_new_id("ev"), item_id=item.id, ts=_now_iso(), type="comment", message="Investigated — deprioritizing", actor="cto", ) store.add_event(ev) events = store.get_events(item.id) comments = [e for e in events if e.type == "comment"] assert len(comments) == 1 assert "Investigated" in comments[0].message def test_multiple_events_preserved_in_order(self): store = MemoryBacklogStore() item = _make_item() store.create(item) for i in range(5): store.add_event(BacklogEvent( id=_new_id("ev"), item_id=item.id, ts=_now_iso(), type="comment", message=f"Comment {i}", actor="agent", )) events = store.get_events(item.id, limit=10) assert len(events) == 5 def test_auto_update_event_type(self): store = MemoryBacklogStore() item = _make_item() store.create(item) ev = BacklogEvent( id=_new_id("ev"), item_id=item.id, ts=_now_iso(), type="auto_update", message="Updated by weekly digest", actor="backlog_generator", ) store.add_event(ev) events = store.get_events(item.id) assert any(e.type == "auto_update" for e in events)