mirror of
https://github.com/block/buzz.git
synced 2026-10-28 13:01:10 +00:00
131 lines
6.0 KiB
Python
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()
|