diff --git a/ventures/up-to-date/install_schedule.py b/ventures/up-to-date/install_schedule.py new file mode 100644 index 0000000..89f4d4c --- /dev/null +++ b/ventures/up-to-date/install_schedule.py @@ -0,0 +1,21 @@ +"""Install only this pilot's user LaunchAgent, after reviewed files are synced.""" +import os +from pathlib import Path +import plistlib +import subprocess + +home = Path.home() +root = home / 'claude/up-to-date' +logs = home / 'claude/logs/up-to-date' +site = home / 'sites/notplanned/v/up-to-date' +assert (root / 'monitor.py').is_file() and (root / 'registry.json').is_file() +logs.mkdir(parents=True, exist_ok=True) +target = home / 'Library/LaunchAgents/xyz.teahub.up-to-date.plist' +assert not target.exists(), 'Reconcile existing schedule before replacing it' +config = {'Label': 'xyz.teahub.up-to-date', 'StartInterval': 900, 'RunAtLoad': True, + 'ProgramArguments': ['/usr/bin/python3', str(root / 'monitor.py'), '--registry', str(root / 'registry.json'), + '--state-dir', str(logs), '--public', str(site / 'monitor-status.json')], + 'StandardOutPath': str(logs / 'scan.log'), 'StandardErrorPath': str(logs / 'scan.log')} +target.parent.mkdir(parents=True, exist_ok=True) +target.write_bytes(plistlib.dumps(config)) +subprocess.run(['launchctl', 'bootstrap', 'gui/' + str(os.getuid()), str(target)], check=True) diff --git a/ventures/up-to-date/test_monitor.py b/ventures/up-to-date/test_monitor.py index f3118ff..5b45dfc 100644 --- a/ventures/up-to-date/test_monitor.py +++ b/ventures/up-to-date/test_monitor.py @@ -2,10 +2,24 @@ import json from pathlib import Path import tempfile import unittest -from monitor import APIError, connect, tick, validate +from unittest.mock import patch +import urllib.error +from monitor import APIError, Client, connect, tick, validate class MonitorTest(unittest.TestCase): + def test_transport_rate_limit_and_budget(self): + c = Client() + error = urllib.error.HTTPError('https://api.github.com', 429, 'limited', {'Retry-After': '7200'}, None) + with patch('urllib.request.urlopen', side_effect=error) as transport: + with self.assertRaises(APIError) as result: + c.get('https://api.github.com/repos/expressjs/express') + self.assertEqual(result.exception.retry, 7200) + c.calls = 24 + with self.assertRaisesRegex(APIError, 'budget'): + c.get('https://api.github.com/repos/expressjs/express') + self.assertEqual(transport.call_count, 1) + def test_durable_delta_errors_and_optout(self): with tempfile.TemporaryDirectory() as tmp: path = Path(tmp) @@ -51,6 +65,9 @@ class MonitorTest(unittest.TestCase): self.assertEqual(calls, []) self.assertFalse(out['repos'][0]['alerts']) self.assertIsNone(out['repos'][0]['next_due']) + reg['repos'][0]['state'] = 'requested' + tick(reg, db, path/'public.json', get, 400001) + self.assertEqual(calls, []) reg['repos'][0]['state'] = 'active' reg['repos'][0]['notification'] = 'none' fail[0] = False