mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-06 04:04:21 +02:00
Frame Control: Mac app, web UI, Android and Steam tooling
Package the Frame Control web UI as an installable Electron Mac app and bring in the tooling built alongside it. - app/: Electron wrapper that starts ui/server.py on a free loopback port, hardened window (sandbox, no navigation, runAsNode fuse off), login-shell PATH so Homebrew tools work from Finder, first-run offer to run connect.sh, ad-hoc signed DMG/zip via electron-builder. - ui/: headset view (OpenVR screenshots), device status, library, Steam "Get games" (owned games, install, store search), Android apps as persistent Lepton instances with a rated F-Droid catalogue and a private compatibility database, Android display controls over ADB, file and clipboard transfer, Flatpaks, remote and power actions. - apk-catalog/, compat-db/, frame/: catalogue build pipeline, Lakebed capsule for compatibility reports, Frame-side launchers. - tests/ and CI: server guard and validation tests plus Steam helper tests, run on Python 3.9 with script and app syntax checks. - Docs: README leads with the Mac app; new Android, panels, Steam games and field-notes docs; security notes on LAN-exposed ADB ports. Screenshot values for the headset's IP and Wi-Fi name are placeholders. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
6ccf562756
commit
d4486a7681
56 files changed
+19298
-11
No files matched your search
@@ -0,0 +1,105 @@
|
||||
"""Get games checks that need no headset or network.
|
||||
|
||||
Run: python3 -m unittest discover -s tests
|
||||
"""
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT / "ui"))
|
||||
|
||||
import frame_store # noqa: E402
|
||||
import test_server # noqa: E402 (not `from … import`, or unittest runs ServerGuards twice)
|
||||
|
||||
|
||||
class SteamRoutes(test_server.ServerGuards):
|
||||
"""Reuses ServerGuards' server (unresolvable SSH alias); validation runs before any SSH."""
|
||||
|
||||
def test_steam_input_validation(self):
|
||||
for body in ({"action": "install", "appid": "620; reboot"}, {"action": "install", "appid": ""},
|
||||
{"action": "uninstall", "appid": "620"}, {"appid": "620"}):
|
||||
status, payload = self.post("/api/steam", body)
|
||||
self.assertEqual(status, 400, f"{body} -> {payload}")
|
||||
|
||||
def test_search_needs_country(self):
|
||||
for query in ("q=portal", "q=portal&cc=A", "q=portal&cc=AU1", "q=portal&cc=%27x"):
|
||||
status, _, _ = self.request("GET", f"/api/steam/search?{query}", headers={"X-Frame-UI": "1"})
|
||||
self.assertEqual(status, 400, query)
|
||||
|
||||
def test_steam_routes_need_custom_header(self):
|
||||
self.assertEqual(self.request("GET", "/api/steam/owned")[0], 403)
|
||||
self.assertEqual(self.request("GET", "/api/steam/search?q=x&cc=AU")[0], 403)
|
||||
self.assertEqual(self.request("POST", "/api/steam", {"action": "install", "appid": "620"})[0], 403)
|
||||
|
||||
|
||||
# Don't rerun the inherited ServerGuards tests under this module.
|
||||
for name in [n for n in dir(test_server.ServerGuards) if n.startswith("test_")]:
|
||||
setattr(SteamRoutes, name, None)
|
||||
|
||||
|
||||
class FrameSteamHelper(unittest.TestCase):
|
||||
def test_bad_usage_prints_json_error(self):
|
||||
for args in ([], ["install"], ["install", "12x"], ["remove", "620"]):
|
||||
out = subprocess.run([sys.executable, str(ROOT / "ui" / "frame_steam.py"), *args],
|
||||
capture_output=True, text=True, timeout=30)
|
||||
self.assertEqual(out.returncode, 1, args)
|
||||
self.assertIn("error", json.loads(out.stdout), args)
|
||||
|
||||
|
||||
class HelperErrors(unittest.TestCase):
|
||||
def test_json_error_found_despite_ssh_stderr(self):
|
||||
import server
|
||||
noise = server.Failure("Warning: Permanently added 'frame' to the list of known hosts.")
|
||||
noise.stdout = '{"error": "Steam\'s UI isn\'t answering"}\n'
|
||||
with mock.patch.object(server, "ssh", side_effect=noise):
|
||||
with self.assertRaises(server.Failure) as cm:
|
||||
server.steam_frame("owned")
|
||||
self.assertEqual(str(cm.exception), "Steam's UI isn't answering")
|
||||
|
||||
def test_other_failures_pass_through(self):
|
||||
import server
|
||||
with mock.patch.object(server, "ssh", side_effect=server.Failure("Timed out talking to frame")):
|
||||
with self.assertRaises(server.Failure) as cm:
|
||||
server.steam_frame("owned")
|
||||
self.assertEqual(str(cm.exception), "Timed out talking to frame")
|
||||
|
||||
|
||||
class StoreSearch(unittest.TestCase):
|
||||
def setUp(self):
|
||||
frame_store._compat.clear()
|
||||
|
||||
def test_keeps_apps_and_attaches_frame_rating(self):
|
||||
def fake_get(path, params, timeout=10):
|
||||
if path == "api/storesearch":
|
||||
self.assertEqual(params["cc"], "AU")
|
||||
return {"items": [{"type": "app", "id": 620, "name": "Portal 2", "price": {"final": 1450}},
|
||||
{"type": "sub", "id": 7, "name": "Bundle"}]}
|
||||
return {"results": {"frame_resolved_category": 3}}
|
||||
with mock.patch.object(frame_store, "_get", side_effect=fake_get):
|
||||
results = frame_store.search("portal", "AU")
|
||||
self.assertEqual([(r["id"], r["frame"]) for r in results], [(620, 3)])
|
||||
|
||||
def test_rating_failure_is_unknown_and_not_cached(self):
|
||||
with mock.patch.object(frame_store, "_get", side_effect=OSError("offline")):
|
||||
self.assertEqual(frame_store.frame_rating(620), 0)
|
||||
self.assertNotIn(620, frame_store._compat)
|
||||
|
||||
def test_malformed_rating_is_unknown(self):
|
||||
for payload in ({"results": []}, {"results": {"frame_resolved_category": {}}},
|
||||
{"results": {"frame_resolved_category": 9}}):
|
||||
frame_store._compat.clear()
|
||||
with mock.patch.object(frame_store, "_get", return_value=payload):
|
||||
self.assertEqual(frame_store.frame_rating(620), 0, payload)
|
||||
|
||||
def test_blank_query_makes_no_request(self):
|
||||
with mock.patch.object(frame_store, "_get") as get:
|
||||
self.assertEqual(frame_store.search(" ", "AU"), [])
|
||||
get.assert_not_called()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user