#!/usr/bin/env python3 """Smoke-test an OpenCode ACP binary without sending a model prompt.""" from __future__ import annotations import argparse import json import selectors import subprocess import sys import time from pathlib import Path def process_metrics(pid: int) -> dict[str, int]: metrics: dict[str, int] = {} try: stat_tail = Path(f"/proc/{pid}/stat").read_text().rsplit(") ", 1)[1].split() metrics["minor_faults"] = int(stat_tail[7]) metrics["major_faults"] = int(stat_tail[9]) except (OSError, IndexError, ValueError): pass try: for line in Path(f"/proc/{pid}/io").read_text().splitlines(): key, value = line.split(":", 1) if key in {"rchar", "read_bytes"}: metrics[key] = int(value.strip()) except (OSError, ValueError): pass try: for line in Path(f"/proc/{pid}/smaps_rollup").read_text().splitlines(): key, _, value = line.partition(":") if key in {"Rss", "Pss_File", "Shared_Clean", "Private_Clean"}: metrics[f"{key.lower()}_kb"] = int(value.strip().split()[0]) except (OSError, ValueError): pass return metrics def request( process: subprocess.Popen[str], selector: selectors.BaseSelector, *, request_id: int, method: str, params: dict, timeout_s: float, ) -> tuple[dict, float]: started_at = time.monotonic() assert process.stdin is not None process.stdin.write( json.dumps( { "jsonrpc": "2.0", "id": request_id, "method": method, "params": params, }, separators=(",", ":"), ) + "\n" ) process.stdin.flush() deadline = started_at + timeout_s while True: remaining = deadline - time.monotonic() if remaining <= 0: raise TimeoutError(f"Timed out waiting for ACP {method}") if process.poll() is not None: raise RuntimeError( f"OpenCode exited during ACP {method}: rc={process.returncode}" ) if not selector.select(remaining): continue assert process.stdout is not None line = process.stdout.readline() if not line: continue message = json.loads(line) if message.get("id") != request_id: continue if "error" in message: raise RuntimeError(f"ACP {method} failed: {message['error']}") return message["result"], (time.monotonic() - started_at) * 1000 def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("--binary", default="opencode") parser.add_argument( "--acp-arg", action="append", default=[], help="additional argument passed after 'opencode acp' (repeatable)", ) parser.add_argument("--cwd", default="/workspace") parser.add_argument("--timeout", type=float, default=30.0) args = parser.parse_args() cwd = Path(args.cwd) process = subprocess.Popen( [args.binary, "acp", *args.acp_arg], cwd=cwd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1, ) selector = selectors.DefaultSelector() assert process.stdout is not None selector.register(process.stdout, selectors.EVENT_READ) try: initialized, initialize_ms = request( process, selector, request_id=1, method="initialize", params={ "protocolVersion": 1, "clientCapabilities": {}, "clientInfo": { "name": "investibook-run9-smoke", "version": "1", }, }, timeout_s=args.timeout, ) session, new_session_ms = request( process, selector, request_id=2, method="session/new", params={"cwd": str(cwd), "mcpServers": []}, timeout_s=args.timeout, ) print( json.dumps( { "agent_info": initialized.get("agentInfo"), "initialize_ms": round(initialize_ms), "new_session_ms": round(new_session_ms), "session_id": session.get("sessionId"), "process_metrics": process_metrics(process.pid), }, sort_keys=True, ) ) finally: selector.close() process.terminate() try: process.wait(timeout=5) except subprocess.TimeoutExpired: process.kill() process.wait(timeout=5) assert process.stderr is not None stderr = process.stderr.read() if stderr: sys.stderr.write(stderr) if __name__ == "__main__": main()