"""Tests for canonical issue workflow label policy (#513).""" from __future__ import annotations import sys import unittest from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) import issue_workflow_labels as labels # noqa: E402 class TestIssueWorkflowLabelValidation(unittest.TestCase): def test_valid_labeled_issue(self): result = labels.assess_issue_labels(["type:feature", "status:ready"]) self.assertTrue(result["valid"]) self.assertEqual(result["type_labels"], ["type:feature"]) self.assertEqual(result["status_labels"], ["status:ready"]) def test_issue_missing_type_label(self): result = labels.assess_issue_labels(["status:ready"]) self.assertFalse(result["valid"]) self.assertIn("issue is missing a type:* label", result["errors"]) def test_issue_missing_status_label(self): result = labels.assess_issue_labels(["type:bug"]) self.assertFalse(result["valid"]) self.assertIn("issue is missing a status:* label", result["errors"]) def test_discussion_issue_missing_type_discussion(self): result = labels.assess_issue_labels( ["type:feature", "status:triage"], discussion=True, ) self.assertFalse(result["valid"]) self.assertIn("discussion issue is missing type:discussion", result["errors"]) def test_multiple_conflicting_status_labels(self): result = labels.assess_issue_labels( ["type:feature", "status:ready", "status:blocked"] ) self.assertFalse(result["valid"]) self.assertTrue( any("multiple active status:* labels" in err for err in result["errors"]) ) class TestIssueWorkflowStatusTransitions(unittest.TestCase): def test_status_transition_removes_old_status_label(self): result = labels.transition_status_labels( ["type:feature", "status:ready", "workflow"], "in-progress", ) self.assertEqual(result, ["type:feature", "workflow", "status:in-progress"]) def test_pr_open_transition_applies_status_pr_open(self): result = labels.transition_status_labels( ["type:feature", "status:in-progress"], "pr-open", ) self.assertEqual(result, ["type:feature", "status:pr-open"]) def test_duplicate_transition_applies_status_duplicate(self): result = labels.transition_status_labels( ["type:process", "status:triage"], "duplicate", ) self.assertEqual(result, ["type:process", "status:duplicate"]) class TestLifecycleRoleLabels(unittest.TestCase): def test_role_transition_author_to_reviewer_to_merger_single_active(self): after_author = labels.transition_role_labels( ["type:feature", "status:pr-open"], "author" ) self.assertEqual(after_author, ["type:feature", "status:pr-open", "role:author"]) after_reviewer = labels.transition_role_labels(after_author, "reviewer") self.assertEqual( after_reviewer, ["type:feature", "status:pr-open", "role:reviewer"] ) self.assertNotIn("role:author", after_reviewer) after_merger = labels.transition_role_labels(after_reviewer, "merger") self.assertEqual(labels.role_labels(after_merger), ["role:merger"]) def test_role_transition_accepts_canonical_label(self): result = labels.transition_role_labels(["type:bug"], "role:reviewer") self.assertEqual(result, ["type:bug", "role:reviewer"]) def test_unknown_role_raises(self): with self.assertRaises(ValueError): labels.canonical_role_label("wizard") def test_assess_reports_multiple_active_role_labels(self): result = labels.assess_issue_labels( ["type:feature", "status:pr-open", "role:author", "role:reviewer"] ) self.assertFalse(result["valid"]) self.assertTrue( any("multiple active role:* labels" in err for err in result["errors"]) ) self.assertEqual( sorted(result["role_labels"]), ["role:author", "role:reviewer"] ) class TestLifecycleHazardLabels(unittest.TestCase): def test_hazards_are_additive_and_multiple(self): one = labels.add_hazard_label(["type:bug", "status:blocked"], "conflicted") two = labels.add_hazard_label(one, "stale-lease") self.assertIn("hazard:conflicted", two) self.assertIn("hazard:stale-lease", two) # status/type untouched self.assertIn("status:blocked", two) self.assertIn("type:bug", two) self.assertEqual(len(labels.hazard_labels(two)), 2) def test_add_hazard_is_idempotent(self): once = labels.add_hazard_label(["type:bug", "status:ready"], "root-mutation") twice = labels.add_hazard_label(once, "root_mutation") self.assertEqual(twice.count("hazard:root-mutation"), 1) def test_clear_hazard_leaves_others_intact(self): start = ["type:bug", "status:blocked", "hazard:conflicted", "hazard:stale-lease"] cleared = labels.clear_hazard_label(start, "conflicted") self.assertNotIn("hazard:conflicted", cleared) self.assertIn("hazard:stale-lease", cleared) self.assertIn("status:blocked", cleared) def test_terminal_blocker_hazard_normalizes(self): self.assertEqual( labels.canonical_hazard_label("terminal-blocker"), "hazard:terminal-blocker", ) def test_assess_surfaces_hazard_labels(self): result = labels.assess_issue_labels( ["type:bug", "status:blocked", "hazard:conflicted"] ) self.assertEqual(result["hazard_labels"], ["hazard:conflicted"]) class TestDiscussionExclusion(unittest.TestCase): def test_discussion_issue_excluded_from_implementation_queue(self): self.assertTrue(labels.is_discussion(["type:discussion", "status:ready"])) self.assertFalse( labels.is_implementation_candidate(["type:discussion", "status:ready"]) ) def test_non_discussion_issue_is_candidate(self): self.assertTrue( labels.is_implementation_candidate(["type:feature", "status:ready"]) ) class TestBlockingReasonRequirement(unittest.TestCase): def test_blocked_requires_reason(self): self.assertTrue( labels.requires_blocking_reason(["type:bug", "status:blocked"]) ) def test_hazard_requires_reason(self): self.assertTrue( labels.requires_blocking_reason( ["type:bug", "status:ready", "hazard:stale-lease"] ) ) def test_clean_ready_issue_needs_no_reason(self): self.assertFalse( labels.requires_blocking_reason(["type:feature", "status:ready"]) ) class TestState603SynonymTransitions(unittest.TestCase): def test_authoring_maps_to_in_progress(self): self.assertEqual( labels.canonical_status_label("authoring"), "status:in-progress" ) def test_changes_requested_transition(self): result = labels.transition_status_labels( ["type:feature", "status:needs-review"], "changes-requested" ) self.assertEqual(result, ["type:feature", "status:changes-requested"]) def test_merge_ready_maps_to_approved(self): self.assertEqual(labels.canonical_status_label("merge-ready"), "status:approved") def test_abandoned_maps_to_wontfix(self): self.assertEqual(labels.canonical_status_label("abandoned"), "status:wontfix") def test_blocked_and_merged_transitions(self): blocked = labels.transition_status_labels( ["type:bug", "status:in-progress"], "blocked" ) self.assertEqual(blocked, ["type:bug", "status:blocked"]) merged = labels.transition_status_labels( ["type:feature", "status:approved"], "merged" ) self.assertEqual(merged, ["type:feature", "status:reconcile"]) if __name__ == "__main__": unittest.main()