import unittest from scripts.lab_loop import Gitea, LoopError, issue_label_names, transition, validate_owner class RecordingGitea(Gitea): def __init__(self): super().__init__("https://example.invalid/api/v1", "owner/repo", "secret") self.calls = [] def labels(self): return {"agent:timmy": 1, "state:claimed": 2, "priority:P0": 3} def request(self, method, path, payload=None): self.calls.append((method, path, payload)) class LoopProtocolTests(unittest.TestCase): def test_set_labels_uses_gitea_replace_labels_endpoint(self): api = RecordingGitea() api.set_labels(7, {"agent:timmy", "state:claimed", "priority:P0"}) self.assertEqual( api.calls, [ ( "PUT", "/repos/owner/repo/issues/7/labels", {"labels": [1, 3, 2]}, ) ], ) def test_transition_replaces_agent_and_state_but_preserves_kind_and_priority(self): labels = {"agent:timmy", "state:claimed", "priority:P1", "kind:build"} self.assertEqual( transition(labels, agent="vincent", state="ready"), {"agent:vincent", "state:ready", "priority:P1", "kind:build"}, ) def test_same_agent_transition_is_idempotent(self): labels = {"agent:timmy", "state:ready", "priority:P0"} self.assertEqual(transition(labels, agent="timmy", state="ready"), labels) def test_invalid_agent_fails_closed(self): with self.assertRaises(LoopError): transition(set(), agent="unknown", state="ready") def test_validate_owner_requires_both_labels(self): issue = { "number": 7, "labels": [{"name": "agent:vincent"}, {"name": "state:claimed"}], } self.assertEqual(validate_owner(issue, "vincent", "claimed"), {"agent:vincent", "state:claimed"}) with self.assertRaises(LoopError): validate_owner(issue, "timmy", "claimed") with self.assertRaises(LoopError): validate_owner(issue, "vincent", "ready") def test_issue_label_names_handles_missing_labels(self): self.assertEqual(issue_label_names({}), set()) if __name__ == "__main__": unittest.main()