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

192 lines
9.0 KiB
Python

import copy
import json
import time
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.run_queue') as run:
run.return_value = 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_oversized_report_is_journaled_and_does_not_block_replay_or_summary(self):
with patch('bridge.run_queue') as run:
run.return_value = 0
oversized = dict(self.event, content='x' * 60001)
bridge.ingest(self.db, self.route, oversized)
bridge.deliver(self.db, self.cfg)
run.assert_not_called()
self.assertEqual(self.db.execute('SELECT id FROM rejected').fetchone()[0], oversized['id'])
reopened = bridge.open_db(self.path, self.cfg)
try:
bridge.ingest(reopened, self.route, oversized)
summary = dict(self.event, id='c' * 64, content='short summary')
bridge.ingest(reopened, self.route, summary)
bridge.deliver(reopened, self.cfg)
self.assertEqual(run.call_count, 1)
self.assertIn('short summary', run.call_args.args[0][-1])
self.assertEqual(reopened.execute('SELECT count(*) FROM rejected').fetchone()[0], 1)
finally:
reopened.close()
def test_ambiguous_submission_is_not_retried(self):
bridge.ingest(self.db, self.route, self.event)
with patch('bridge.run_queue', 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 process_tree_command(self):
child = self.root / 'child.py'
child.write_text('import os,signal,time\nfrom pathlib import Path\n'
'signal.signal(signal.SIGTERM, signal.SIG_IGN)\n'
'Path(' + repr(str(self.root / 'child.pid')) + ').write_text(str(os.getpid()))\n'
'time.sleep(60)\n')
wrapper = self.root / 'wrapper.py'
wrapper.write_text('import subprocess,sys,time\n'
'subprocess.Popen([sys.executable, ' + repr(str(child)) + '])\n'
'time.sleep(60)\n')
return [sys.executable, str(wrapper)]
def assert_child_stopped(self):
pid = int((self.root / 'child.pid').read_text())
deadline = time.monotonic() + 3
while time.monotonic() < deadline:
status = subprocess.run(['ps', '-o', 'stat=', '-p', str(pid)], capture_output=True, text=True)
if status.returncode != 0 or status.stdout.strip().startswith('Z'):
return
time.sleep(0.02)
self.fail('wrapper descendant is still running')
def test_stop_kills_wrapper_descendant_that_ignores_termination(self):
process = subprocess.Popen(self.process_tree_command(), stdout=subprocess.PIPE, start_new_session=True)
try:
deadline = time.monotonic() + 5
while not (self.root / 'child.pid').exists() and time.monotonic() < deadline:
time.sleep(0.02)
self.assertTrue((self.root / 'child.pid').exists())
bridge.stop(process)
self.assert_child_stopped()
finally:
if process.poll() is None:
bridge.stop(process)
def test_queue_timeout_kills_descendants(self):
with self.assertRaises(subprocess.TimeoutExpired):
bridge.run_queue(self.process_tree_command(), timeout=2)
self.assert_child_stopped()
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()