"""Unit: forge sidecar dispatch, op log, queue relay, socket server.""" from __future__ import annotations import json import socket import tempfile import threading import unittest from pathlib import Path from bot_bottle.orchestrator.sidecar import ( ForgeSidecar, OpLog, drain_done_events, serve, write_done_event, ) from ._fakes import FakeForge class SidecarDispatchTest(unittest.TestCase): def setUp(self): self.tmp = Path(self.enterContext(tempfile.TemporaryDirectory())) # pylint: disable=consider-using-with self.forge = FakeForge() self.log = OpLog(self.tmp / "ops.jsonl", now=lambda: "T") self.queue = self.tmp / "queue" self.sc = ForgeSidecar( forge=self.forge, op_log=self.log, queue_dir=self.queue, run_key=("o", "r", 17), ) def test_read_pr_ok_and_logged(self): resp = self.sc.dispatch("read_pr", {"number": 5}) self.assertTrue(resp["ok"]) self.assertEqual(5, resp["result"]["number"]) self.assertEqual([("read_pr", 5, "ok")], [(o["op"], o["target"], o["detail"]) for o in self.log.read()]) def test_post_comment_writes_and_logs(self): resp = self.sc.dispatch("post_comment", {"number": 17, "body": "done"}) self.assertTrue(resp["ok"]) self.assertEqual([(17, "done")], self.forge.comments) def test_scope_denied_write_returns_error_and_audits_rejection(self): self.forge.scope_denied.add(999) resp = self.sc.dispatch("post_comment", {"number": 999, "body": "x"}) self.assertFalse(resp["ok"]) self.assertIn("denied", resp["error"]) # The rejection is recorded in the op log, not just the allows. self.assertIn("error", self.log.read()[-1]["detail"]) self.assertEqual([], self.forge.comments) def test_signal_done_queues_event(self): resp = self.sc.dispatch("signal_done", {"status": "success", "summary": "ok"}) self.assertTrue(resp["ok"]) events = drain_done_events(self.queue) self.assertEqual(1, len(events)) self.assertEqual(("o", "r", 17, "success"), (events[0]["owner"], events[0]["repo"], events[0]["issue_number"], events[0]["status"])) def test_unknown_method(self): resp = self.sc.dispatch("delete_repo", {}) self.assertFalse(resp["ok"]) class QueueTest(unittest.TestCase): def test_drain_removes_events(self): tmp = Path(self.enterContext(tempfile.TemporaryDirectory())) # pylint: disable=consider-using-with write_done_event(tmp, {"owner": "o", "repo": "r", "issue_number": 1}) self.assertEqual(1, len(drain_done_events(tmp))) self.assertEqual([], drain_done_events(tmp)) # drained def test_drain_missing_dir(self): self.assertEqual([], drain_done_events(Path("/nonexistent/queue"))) class SocketServerTest(unittest.TestCase): def test_round_trip_over_unix_socket(self): tmp = tempfile.mkdtemp() sock = Path(tmp) / "s.sock" if len(str(sock)) > 100: # AF_UNIX path limit; skip on long tmp paths self.skipTest("temp socket path too long for AF_UNIX") sidecar = ForgeSidecar( forge=FakeForge(), op_log=OpLog(Path(tmp) / "ops.jsonl"), queue_dir=Path(tmp) / "q", run_key=("o", "r", 1), ) srv = serve(sidecar, sock) t = threading.Thread(target=srv.handle_request, daemon=True) t.start() try: client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) client.connect(str(sock)) client.sendall(b'{"method": "read_issue", "params": {"number": 3}}\n') line = client.makefile().readline() client.close() finally: t.join(timeout=5) srv.server_close() resp = json.loads(line) self.assertTrue(resp["ok"]) self.assertEqual(3, resp["result"]["number"]) if __name__ == "__main__": unittest.main()