#!/usr/bin/env python3 """Check the CI app's unauthenticated API without changing saved preferences.""" import json import os import signal 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 socket.socket() as listener: listener.bind(("127.0.0.1", 0)) port = listener.getsockname()[1] with tempfile.NamedTemporaryFile() as log: process = subprocess.Popen( ["open", "-n", "-W", "-g", "-a", str(staged), "--stdout", log.name, "--stderr", log.name, "--args", "-ApplePersistenceIgnoreState", "YES", "-muteDeckAPIEnabled", "YES", "-muteDeckAPIPort", str(port), "-hasCompletedOnboarding", "YES", "-hasAcceptedTerms", "YES", "-SUEnableAutomaticChecks", "NO"], stdout=log, stderr=subprocess.STDOUT, ) def app_pid(): # The unique staged path identifies only this test's app instance. listing = subprocess.check_output(["ps", "-axo", "pid=,command="], text=True) for line in listing.splitlines(): fields = line.strip().split(None, 1) if len(fields) == 2 and fields[1].startswith(str(app) + " "): return int(fields[0]) return None 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: pid = app_pid() if pid is not None: sample = subprocess.run(["sample", str(pid), "1", "1"], capture_output=True, text=True, timeout=10) print(sample.stdout[:14000], file=sys.stderr) log.seek(0) print(log.read().decode(errors="replace")[-8000:], file=sys.stderr) raise finally: pid = app_pid() if pid is not None: try: os.kill(pid, signal.SIGTERM) except ProcessLookupError: pass try: process.wait(timeout=10) except subprocess.TimeoutExpired: if app_pid() == pid and pid is not None: try: os.kill(pid, signal.SIGKILL) except ProcessLookupError: pass process.terminate() process.wait(timeout=10)