New router intelligence modules (26 files): alert_ingest/store, audit_store, architecture_pressure, backlog_generator/store, cost_analyzer, data_governance, dependency_scanner, drift_analyzer, incident_* (5 files), llm_enrichment, platform_priority_digest, provider_budget, release_check_runner, risk_* (6 files), signature_state_store, sofiia_auto_router, tool_governance New services: - sofiia-console: Dockerfile, adapters/, monitor/nodes/ops/voice modules, launchd, react static - memory-service: integration_endpoints, integrations, voice_endpoints, static UI - aurora-service: full app suite (analysis, job_store, orchestrator, reporting, schemas, subagents) - sofiia-supervisor: new supervisor service - aistalk-bridge-lite: Telegram bridge lite - calendar-service: CalDAV calendar service with reminders - mlx-stt-service / mlx-tts-service: Apple Silicon speech services - binance-bot-monitor: market monitor service - node-worker: STT/TTS memory providers New tools (9): agent_email, browser_tool, contract_tool, observability_tool, oncall_tool, pr_reviewer_tool, repo_tool, safe_code_executor, secure_vault New crews: agromatrix_crew (10 modules: depth_classifier, doc_facts, doc_focus, farm_state, light_reply, llm_factory, memory_manager, proactivity, reflection_engine, session_context, style_adapter, telemetry) Tests: 85+ test files for all new modules Made-with: Cursor
176 lines
6.1 KiB
Python
176 lines
6.1 KiB
Python
"""
|
|
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)
|