#!/usr/bin/env python3 """Read-only / gating self-test for the screenconnect skill. Runs each CLI command as a subprocess and checks exit code + output markers. NO state-changing API calls (writes are tested only in their --confirm-absent refusal path). build-installer is a pure URL build (no network). Prints a PASS/FAIL report. """ from __future__ import annotations import json import os import subprocess import sys HERE = os.path.dirname(os.path.abspath(__file__)) SC = os.path.join(HERE, "sc.py") results = [] def run(args): env = dict(os.environ) env.setdefault("CLAUDETOOLS_ROOT", "C:/claudetools") env["PYTHONIOENCODING"] = "utf-8" env["SC_SUPPRESS_ERRORLOG"] = "1" # never let the self-test touch errorlog.md p = subprocess.run([sys.executable, SC] + args, capture_output=True, text=True, env=env, timeout=120) return p.returncode, p.stdout, p.stderr def check(name, args, *, want_rc=None, out_has=None, out_json_ok=False): rc, out, err = run(args) problems = [] if want_rc is not None and rc != want_rc: problems.append(f"rc={rc} want {want_rc}") if out_has and out_has not in out: problems.append(f"stdout missing {out_has!r}") if out_json_ok: try: json.loads(out) except Exception as e: problems.append(f"stdout not JSON: {e}") results.append(("PASS" if not problems else "FAIL", name, "; ".join(problems))) # --- reads: succeed (rc 0) [hit the live instance] --- check("status", ["status"], want_rc=0, out_has="Authenticated") check("sessions", ["sessions"], want_rc=0, out_has="Sessions:") check("sessions json", ["sessions", "--json"], want_rc=0, out_json_ok=True) # --- build-installer: pure URL build (no network) --- check("build-installer", ["build-installer", "--name", "HOST", "--company", "AZ Computer Guru", "--site", "Howard-VM", "--tag", "T1"], want_rc=0, out_has="e=Access") check("build-installer encodes spaces", ["build-installer", "--company", "A B C"], want_rc=0, out_has="A%20B%20C") check("build-installer json", ["build-installer", "--json", "--tag", "X"], want_rc=0, out_json_ok=True) # --- gating: writes refuse without --confirm (rc 3, no API call) --- check("send-command no confirm -> rc3", ["send-command", "--session", "x", "--command", "whoami"], want_rc=3, out_has="Would") check("send-message no confirm -> rc3", ["send-message", "--session", "x", "--message", "hi"], want_rc=3) check("set-properties no confirm -> rc3", ["set-properties", "--session", "x", "--props-json", '["A","B","C"]'], want_rc=3) check("set-properties bad json -> rc2", ["set-properties", "--session", "x", "--props-json", "{bad", "--confirm"], want_rc=2) # --- raw: read ok; destructive method refused without --confirm --- check("raw read ok", ["raw", "--method", "GetSessionsByName", "--body", '{"sessionName":""}'], want_rc=0) check("raw SendCommandToSession no confirm -> rc3", ["raw", "--method", "SendCommandToSession", "--body", "[]"], want_rc=3) # --- report --- print("\n==== screenconnect skill self-test ====") npass = sum(1 for r in results if r[0] == "PASS") for status, name, prob in results: print(f"[{status}] {name}" + (f" -> {prob}" if prob else "")) print(f"\n{npass}/{len(results)} passed, {len(results)-npass} failed") sys.exit(0 if npass == len(results) else 1)