Files
buzz/scripts/buzz-codex-bridge/test_bridge.py
2026-10-15 18:14:15 -07:00

131 lines
6.0 KiB
Python

import copy
import json
from pathlib import Path
import sys
import selectors
import signal
import subprocess
import tempfile
import unittest
from unittest.mock import patch
import uuid
import bridge
class BridgeTest(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.root = Path(self.temp.name)
self.cfg = {'relay': 'wss://example.test', 'buzz_command': ['buzz'],
'codex_command': ['codex'], 'routes': [{
'channel': str(uuid.uuid4()), 'thread': str(uuid.uuid4()),
'authors': ['a' * 64], 'worktrees': [str(self.root)]}]}
self.route = self.cfg['routes'][0]
self.path = self.root / 'state.sqlite'
self.event = {'id': 'b' * 64, 'pubkey': 'a' * 64, 'kind': 9,
'tags': [['h', self.route['channel']]], 'content': 'complete'}
self.db = bridge.open_db(self.path, self.cfg)
self.addCleanup(self.db.close)
def test_replay_after_restart_delivers_only_once(self):
bridge.ingest(self.db, self.route, self.event)
with patch('bridge.subprocess.run') as run:
run.return_value.returncode = 0
bridge.deliver(self.db, self.cfg)
self.assertEqual(run.call_count, 1)
args = run.call_args.args[0]
self.assertEqual(args[:4], ['codex', 'queue', '--thread', self.route['thread']])
self.assertIn('external data', args[-1])
self.assertIn(self.event['id'], args[-1])
reopened = bridge.open_db(self.path, self.cfg)
try:
bridge.ingest(reopened, self.route, self.event)
bridge.deliver(reopened, self.cfg)
self.assertEqual(run.call_count, 1)
finally:
reopened.close()
def test_ambiguous_submission_is_not_retried(self):
bridge.ingest(self.db, self.route, self.event)
with patch('bridge.subprocess.run', side_effect=TimeoutError('queue timed out')):
with self.assertRaises(TimeoutError):
bridge.deliver(self.db, self.cfg)
with self.assertRaisesRegex(RuntimeError, 'ambiguous'):
bridge.open_db(self.path, self.cfg)
def test_rejects_unregistered_author_and_channel(self):
for changed in [{'pubkey': 'c' * 64}, {'tags': [['h', str(uuid.uuid4())]]}, {'kind': 20001}]:
with self.assertRaises(ValueError):
bridge.ingest(self.db, self.route, dict(self.event, **changed))
self.assertEqual(self.db.execute('SELECT count(*) FROM deliveries').fetchone()[0], 0)
def test_changed_config_cannot_redirect_pending_reports(self):
cfg = copy.deepcopy(self.cfg)
cfg['routes'][0]['thread'] = str(uuid.uuid4())
with self.assertRaisesRegex(ValueError, 'configuration changed'):
bridge.open_db(self.path, cfg)
def test_multiple_worktrees_per_task_but_no_shared_worktree(self):
second = self.root / 'other-repo'
second.mkdir()
self.route['worktrees'].append(str(second))
path = self.root / 'routes.json'
path.write_text(json.dumps(self.cfg))
self.assertEqual(len(bridge.load_config(path)['routes'][0]['worktrees']), 2)
duplicate = dict(self.route, channel=str(uuid.uuid4()), thread=str(uuid.uuid4()))
self.cfg['routes'].append(duplicate)
path.write_text(json.dumps(self.cfg))
with self.assertRaisesRegex(ValueError, 'must not share'):
bridge.load_config(path)
def test_real_queue_subprocess_gets_literal_message_without_shell(self):
capture = self.root / 'argv.json'
script = self.root / 'queue.py'
script.write_text('import json,sys\nfrom pathlib import Path\nPath(sys.argv[1]).write_text(json.dumps(sys.argv[2:]))\n')
cfg = copy.deepcopy(self.cfg)
cfg['codex_command'] = [sys.executable, str(script), str(capture)]
self.event['content'] = '$(touch should-not-exist); `echo nope`'
bridge.ingest(self.db, self.route, self.event)
bridge.deliver(self.db, cfg)
args = json.loads(capture.read_text())
self.assertEqual(args[0], 'queue')
self.assertIn(self.event['content'], args[-1])
self.assertEqual(self.db.execute('SELECT state FROM deliveries').fetchone()[0], 'sent')
def test_foreground_bridge_streams_and_stops_its_watcher(self):
watcher = self.root / 'watcher.py'
watcher.write_text('import json,time\n' +
'event = ' + repr(self.event) + '\n' +
'print(json.dumps({"type":"event","event":event}), flush=True)\n' +
'print(json.dumps({"type":"ready"}), flush=True)\n' +
'time.sleep(30)\n')
cfg = copy.deepcopy(self.cfg)
cfg['buzz_command'] = [sys.executable, str(watcher)]
cfg['codex_command'] = [sys.executable, '-c', 'pass']
config = self.root / 'integration.json'
config.write_text(json.dumps(cfg))
process = subprocess.Popen([sys.executable, str(Path(bridge.__file__)),
'--config', str(config), '--state', str(self.root / 'integration.sqlite')],
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
try:
with selectors.DefaultSelector() as selector:
selector.register(process.stdout, selectors.EVENT_READ)
self.assertTrue(selector.select(timeout=10), 'report was not delivered')
record = json.loads(process.stdout.readline())
self.assertEqual(record['delivered'], self.event['id'])
process.send_signal(signal.SIGINT)
_, errors = process.communicate(timeout=10)
self.assertEqual(process.returncode, 0, errors.decode())
finally:
if process.poll() is None:
process.kill()
process.wait()
process.stdout.close()
process.stderr.close()
if __name__ == '__main__':
unittest.main()