"""Allocator semantic container exclusion for vision/roadmap/umbrella (#854). #844 excluded epic / child-only containers (live #631) but product-vision (#652), phased-roadmap (#653), and umbrella (#655) coordination records still ranked as implementable work. This module is the live-equivalent canary: * #631 / #652 / #653 / #655-shaped records are all excluded in one inventory. * Independently executable children remain eligible and can be selected. * Ordinary issues that merely mention vision / roadmap / umbrella stay eligible. * Excluded containers never receive assignments or workflow leases. * Structured skip reason ``epic_or_child_only_container`` is reported. * Candidate-set fingerprint remains stable after exclusions. """ from __future__ import annotations import os import tempfile import unittest from allocator_service import ( OUTCOME_ASSIGNED, OUTCOME_PREVIEW, SKIP_EPIC_OR_CHILD_ONLY_CONTAINER, WorkCandidate, allocate_next_work, candidate_set_fingerprint, classify_epic_or_child_only_container, ) from control_plane_db import ControlPlaneDB REMOTE = "prgs" ORG = "Scaled-Tech-Consulting" REPO = "Gitea-Tools" # Minimal bodies mirroring live coordination records (not full issue text). _EPIC_631_BODY = """ ## Scope (umbrella) This epic owns the **product roadmap and linkage** for the Web Console. Implementation is delivered via child issues only. ## Explicit non-goals * Do not implement product features in this epic issue itself. * No product feature implementation is claimed complete solely on this epic. """ _VISION_652_BODY = """ ## Canonical product vision — enduring source of truth **This issue is the enduring source of truth for the MCP Control Plane Web Console product vision.** ## Implementation linkage * **Do not implement features on this issue.** * Sequencing: roadmap issue + #631 children. ## Canonical issue state ```text STATE: vision-active WHO_IS_NEXT: controller (triage/ordering) / author (implementation of linked children only) ``` """ _ROADMAP_653_BODY = """ ## Purpose This issue is the **phased delivery roadmap and epic sequencing** for the MCP Control Plane Web Console. ## Non-goals * Implementing features on this roadmap issue. * Deleting vision items by omitting them from phases without #652 change log. ## Canonical issue state ```text STATE: roadmap-active WHO_IS_NEXT: author ``` """ _UMBRELLA_655_BODY = """ ## Scope (umbrella) This issue owns the **canonical restart-governance program**. Implementation is via linked children only. ## Acceptance criteria (umbrella) 6. No product feature claimed complete on this issue alone. """ _CHILD_BODY = """ ## Problem Operators need a workflow-event timeline model for Phase 1. ## Acceptance criteria - [ ] Timeline model API exists """ def _issue( number: int, *, title: str = "", body: str = "", labels: tuple[str, ...] = ("status:ready", "type:feature"), priority: int = 20, ) -> WorkCandidate: return WorkCandidate( kind="issue", number=number, state="open", labels=labels, title=title or f"issue {number}", body=body, priority=priority, ) def _live_shaped_containers() -> list[WorkCandidate]: return [ _issue( 631, title="Epic: MCP Control Plane Web Console", body=_EPIC_631_BODY, ), _issue( 652, title="Product vision: MCP Control Plane Web Console (canonical)", body=_VISION_652_BODY, ), _issue( 653, title="Roadmap: MCP Control Plane Web Console (phased delivery)", body=_ROADMAP_653_BODY, ), _issue( 655, title="Umbrella: Governed MCP restart coordination and zero-disruption recovery", body=_UMBRELLA_655_BODY, ), ] class ClassifySemanticContainersTest(unittest.TestCase): def test_652_vision_is_container(self) -> None: c = _issue( 652, title="Product vision: MCP Control Plane Web Console (canonical)", body=_VISION_652_BODY, ) is_c, detail = classify_epic_or_child_only_container(c) self.assertTrue(is_c) self.assertIsNotNone(detail) self.assertIn("body_marker", detail or "") def test_653_roadmap_is_container(self) -> None: c = _issue( 653, title="Roadmap: MCP Control Plane Web Console (phased delivery)", body=_ROADMAP_653_BODY, ) is_c, detail = classify_epic_or_child_only_container(c) self.assertTrue(is_c) self.assertIn("body_marker", detail or "") def test_655_umbrella_is_container(self) -> None: c = _issue( 655, title="Umbrella: Governed MCP restart coordination and zero-disruption recovery", body=_UMBRELLA_655_BODY, ) is_c, detail = classify_epic_or_child_only_container(c) self.assertTrue(is_c) self.assertIn("body_marker", detail or "") def test_631_still_container_after_854(self) -> None: c = _issue( 631, title="Epic: MCP Control Plane Web Console", body=_EPIC_631_BODY, ) is_c, detail = classify_epic_or_child_only_container(c) self.assertTrue(is_c) self.assertIn("body_marker", detail or "") def test_incidental_vision_roadmap_umbrella_words_not_container(self) -> None: cases = ( ( "Document vision handoff conventions", "Update docs so implementable issues that mention a vision " "remain independently executable.", ), ( "Clarify roadmap sequencing notes", "Write a short note about how the roadmap issue relates to children.", ), ( "Umbrella recovery checklist for authors", "Authors should still implement the concrete recovery fix here.", ), ( "Product vision wording in the help text", "Fix a typo in the operator-facing help string that says product vision.", ), ) for title, body in cases: with self.subTest(title=title): c = _issue(900, title=title, body=body) is_c, detail = classify_epic_or_child_only_container(c) self.assertFalse(is_c) self.assertIsNone(detail) def test_title_prefix_alone_not_container(self) -> None: for title in ( "Epic: something mentioned only in title", "Roadmap: title only without body scope", "Product vision: title only without body scope", "Umbrella: title only without body scope", ): with self.subTest(title=title): c = _issue( 901, title=title, body="Implement a concrete fix for the allocator skip list.", ) is_c, detail = classify_epic_or_child_only_container(c) self.assertFalse(is_c) self.assertIsNone(detail) def test_roadmap_label_alone_is_container(self) -> None: c = _issue( 902, title="Console delivery sequencing", body="Track phased delivery only.", labels=("status:ready", "type:roadmap"), ) is_c, detail = classify_epic_or_child_only_container(c) self.assertTrue(is_c) self.assertIn("type:roadmap", detail or "") def test_child_referencing_parent_policy_stays_eligible(self) -> None: """Children may quote parent policy without becoming containers.""" c = _issue( 637, title="Web Console: Workflow-event timeline model (Phase 1)", body=( _CHILD_BODY + "\n\nParent #652 says do not implement on the vision issue; " "this child is the implementable unit." ), ) is_c, _ = classify_epic_or_child_only_container(c) self.assertFalse(is_c) class AllocateSemanticContainerExclusionTest(unittest.TestCase): def setUp(self) -> None: self._tmp = tempfile.TemporaryDirectory() self.addCleanup(self._tmp.cleanup) self.db = ControlPlaneDB(os.path.join(self._tmp.name, "cp.sqlite3")) def _alloc(self, candidates, **kwargs): defaults = dict( session_id="sess-854", role="author", remote=REMOTE, org=ORG, repo=REPO, profile_name="prgs-author", username="jcwalker3", claims={}, apply=False, ) defaults.update(kwargs) return allocate_next_work(self.db, candidates=candidates, **defaults) def test_live_equivalent_canary_excludes_all_containers_selects_child(self) -> None: containers = _live_shaped_containers() child = _issue( 637, title="Web Console: Workflow-event timeline model (Phase 1)", body=_CHILD_BODY, ) inventory = containers + [child] res = self._alloc(inventory, apply=False) self.assertTrue(res["success"], res) self.assertEqual(res["outcome"], OUTCOME_PREVIEW) self.assertEqual(res["selected"]["number"], 637) skipped = {s["number"]: s for s in res["skipped"]} for number in (631, 652, 653, 655): self.assertIn(number, skipped, res["skipped"]) self.assertEqual( skipped[number]["reason_code"], SKIP_EPIC_OR_CHILD_ONLY_CONTAINER, ) self.assertIn( SKIP_EPIC_OR_CHILD_ONLY_CONTAINER, skipped[number]["reason"] ) def test_containers_cannot_receive_assignment_or_lease(self) -> None: containers = _live_shaped_containers() res = self._alloc(containers, apply=True) self.assertTrue(res["success"], res) self.assertNotEqual(res["outcome"], OUTCOME_ASSIGNED) self.assertIsNone(res.get("assignment")) self.assertIsNone(res.get("selected")) skipped = {s["number"]: s for s in res["skipped"]} for number in (631, 652, 653, 655): self.assertEqual( skipped[number]["reason_code"], SKIP_EPIC_OR_CHILD_ONLY_CONTAINER, ) leases = [] if hasattr(self.db, "list_active_leases"): leases = self.db.list_active_leases( remote=REMOTE, org=ORG, repo=REPO ) if not leases and hasattr(self.db, "list_leases"): leases = self.db.list_leases(remote=REMOTE, org=ORG, repo=REPO) for lease in leases or []: work_number = ( lease.get("work_number") if isinstance(lease, dict) else None ) self.assertNotIn(work_number, {631, 652, 653, 655}) def test_apply_selects_child_not_container(self) -> None: containers = _live_shaped_containers() child = _issue( 637, title="Web Console: Workflow-event timeline model (Phase 1)", body=_CHILD_BODY, ) res = self._alloc(containers + [child], apply=True) self.assertTrue(res["success"], res) self.assertEqual(res["outcome"], OUTCOME_ASSIGNED) self.assertEqual(res["selected"]["number"], 637) self.assertEqual(res["assignment"]["work_number"], 637) def test_fingerprint_stable_with_containers_present(self) -> None: containers = _live_shaped_containers() child = _issue( 637, title="Web Console: Workflow-event timeline model (Phase 1)", body=_CHILD_BODY, ) inventory = containers + [child] fp_before = candidate_set_fingerprint(inventory) res = self._alloc(inventory, apply=False) self.assertTrue(res["success"], res) self.assertEqual(res["selected"]["number"], 637) # Allocator reports the same CAS fingerprint for the full candidate set. reported = res.get("candidate_set_fingerprint") self.assertEqual(reported, fp_before) # Re-fingerprint of the same inventory is byte-stable. self.assertEqual(candidate_set_fingerprint(inventory), fp_before) def test_incidental_mentions_remain_eligible(self) -> None: ordinary = _issue( 700, title="Document roadmap handoff conventions", body="Write runbook text about vision vs roadmap vs child issues.", ) res = self._alloc([ordinary], apply=False) self.assertTrue(res["success"], res) self.assertEqual(res["selected"]["number"], 700) self.assertEqual(res["skipped"], []) if __name__ == "__main__": unittest.main()