131 lines
3.9 KiB
Python
131 lines
3.9 KiB
Python
import json
|
|
import subprocess
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
|
|
REPO = Path(__file__).resolve().parents[1]
|
|
BUILD_SCRIPT = REPO / "scripts" / "build-win-helper.ps1"
|
|
HELPER_EXE = REPO / "runs" / "win-helper" / "ISphereWinHelper.exe"
|
|
|
|
|
|
def build_helper():
|
|
result = subprocess.run(
|
|
[
|
|
"powershell",
|
|
"-NoProfile",
|
|
"-ExecutionPolicy",
|
|
"Bypass",
|
|
"-File",
|
|
str(BUILD_SCRIPT),
|
|
],
|
|
cwd=REPO,
|
|
text=True,
|
|
encoding="utf-8",
|
|
errors="replace",
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
timeout=60,
|
|
)
|
|
if result.returncode != 0:
|
|
raise AssertionError(result.stdout + result.stderr)
|
|
|
|
|
|
def invoke_helper(payload):
|
|
if isinstance(payload, str):
|
|
stdin = payload
|
|
else:
|
|
stdin = json.dumps(payload, ensure_ascii=False)
|
|
result = subprocess.run(
|
|
[str(HELPER_EXE), "--json"],
|
|
cwd=REPO,
|
|
input=stdin,
|
|
text=True,
|
|
encoding="utf-8",
|
|
errors="replace",
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
timeout=30,
|
|
)
|
|
try:
|
|
decoded = json.loads(result.stdout)
|
|
except json.JSONDecodeError as exc:
|
|
raise AssertionError(f"stdout was not JSON: {result.stdout!r}; stderr={result.stderr!r}") from exc
|
|
return result, decoded
|
|
|
|
|
|
class WinHelperBuildTests(unittest.TestCase):
|
|
def test_build_script_creates_helper_exe(self):
|
|
if HELPER_EXE.exists():
|
|
HELPER_EXE.unlink()
|
|
|
|
build_helper()
|
|
|
|
self.assertTrue(HELPER_EXE.exists(), f"helper exe missing: {HELPER_EXE}")
|
|
|
|
|
|
class WinHelperProtocolTests(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
build_helper()
|
|
|
|
def request(self, op, args=None):
|
|
result, decoded = invoke_helper(
|
|
{
|
|
"protocol": "isphere.helper.v1",
|
|
"request_id": f"test-{op}",
|
|
"op": op,
|
|
"timeout_ms": 5000,
|
|
"args": args or {},
|
|
}
|
|
)
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
return decoded
|
|
|
|
def test_version_returns_contract_metadata(self):
|
|
decoded = self.request("version")
|
|
|
|
self.assertTrue(decoded["ok"])
|
|
self.assertEqual(decoded["protocol"], "isphere.helper.v1")
|
|
self.assertEqual(decoded["request_id"], "test-version")
|
|
self.assertEqual(decoded["op"], "version")
|
|
self.assertEqual(decoded["data"]["helper_name"], "ISphereWinHelper")
|
|
self.assertEqual(decoded["data"]["protocol"], "isphere.helper.v1")
|
|
|
|
def test_self_check_reports_desktop_and_uia_status(self):
|
|
decoded = self.request("self_check")
|
|
|
|
self.assertTrue(decoded["ok"])
|
|
self.assertIn("desktop_access", decoded["data"])
|
|
self.assertIn("uia_available", decoded["data"])
|
|
|
|
def test_scan_windows_returns_json_window_list(self):
|
|
decoded = self.request("scan_windows", {"include_all_visible": True})
|
|
|
|
self.assertTrue(decoded["ok"])
|
|
self.assertIsInstance(decoded["data"]["windows"], list)
|
|
|
|
def test_unsupported_op_returns_structured_error(self):
|
|
decoded = self.request("does_not_exist")
|
|
|
|
self.assertFalse(decoded["ok"])
|
|
self.assertEqual(decoded["error"]["code"], "UNSUPPORTED_OP")
|
|
|
|
def test_invalid_json_returns_structured_error(self):
|
|
result, decoded = invoke_helper("{not json")
|
|
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
self.assertFalse(decoded["ok"])
|
|
self.assertEqual(decoded["protocol"], "isphere.helper.v1")
|
|
self.assertEqual(decoded["error"]["code"], "INVALID_REQUEST")
|
|
|
|
def test_dump_uia_with_missing_window_returns_structured_error(self):
|
|
decoded = self.request("dump_uia", {"hwnd": "0x0"})
|
|
|
|
self.assertFalse(decoded["ok"])
|
|
self.assertIn(decoded["error"]["code"], {"WINDOW_NOT_FOUND", "UIA_DUMP_FAILED"})
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|