#!/usr/bin/env python3 """Check the CI app's unauthenticated API without changing saved preferences.""" import json import plistlib import socket import subprocess import sys import tempfile import time import urllib.error import urllib.request from pathlib import Path # Launch from the Mac's internal volume; the runner checkout is on removable storage. cache = Path.home() / "Library/Caches/meetingnotes-ci" cache.mkdir(parents=True, exist_ok=True) with tempfile.TemporaryDirectory(prefix="smoke-", dir=cache) as directory: staged = Path(directory) / "Meetingnotes.app" subprocess.run(["ditto", sys.argv[1], str(staged)], check=True) app = staged / "Contents/MacOS/Meetingnotes" with (staged / "Contents/Info.plist").open("rb") as info_file: bundle_id = plistlib.load(info_file)["CFBundleIdentifier"] assert bundle_id == "net.jamesbone.meetingnotes.ci", "Smoke tests require the isolated CI app" def clear_test_token(): result = subprocess.run( ["security", "delete-generic-password", "-s", bundle_id, "-a", "muteDeckAPIToken"], capture_output=True, timeout=10, ) if result.returncode not in (0, 44): # 44: item does not exist raise RuntimeError("Could not clear the CI Keychain token") clear_test_token() with socket.socket() as listener: listener.bind(("127.0.0.1", 0)) port = listener.getsockname()[1] with tempfile.TemporaryFile() as log: process = subprocess.Popen( [str(app), "-muteDeckAPIEnabled", "YES", "-muteDeckAPIPort", str(port), "-hasCompletedOnboarding", "YES", "-hasAcceptedTerms", "YES", "-SUEnableAutomaticChecks", "NO"], stdout=log, stderr=subprocess.STDOUT, ) try: for attempt in range(30): if process.poll() is not None: raise RuntimeError("CI app exited before the API became ready") try: with urllib.request.urlopen(f"http://127.0.0.1:{port}/api/info", timeout=2) as response: info = json.load(response) break except (urllib.error.URLError, TimeoutError): time.sleep(1) else: raise RuntimeError("CI API did not become ready") assert info["name"] == "MeetingDebrief", "Unexpected API identity" try: urllib.request.urlopen(f"http://127.0.0.1:{port}/api/recording/status", timeout=2) except urllib.error.HTTPError as error: assert error.code == 401, f"Unexpected status: {error.code}" else: raise RuntimeError("Recording status allowed an unauthenticated request") print("Local API readiness and authentication checks passed") except Exception: if process.poll() is None: sample = subprocess.run(["sample", str(process.pid), "1", "1"], capture_output=True, text=True, timeout=10) print(sample.stdout.split("Binary Images:")[0], file=sys.stderr) log.seek(0) print(log.read().decode(errors="replace")[-8000:], file=sys.stderr) raise finally: if process.poll() is None: process.terminate() try: process.wait(timeout=10) except subprocess.TimeoutExpired: process.kill() process.wait() clear_test_token()