mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-29 03:02:01 +01:00
Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a18f2bda7 | ||
|
|
f7ff94aae4 | ||
|
|
f4c8308f8a | ||
|
|
a2e578f6c3 | ||
|
|
e8fb154107 | ||
|
|
35b59e62da | ||
|
|
be07dbeeeb | ||
|
|
b123f5ff34 | ||
|
|
08c99c2d7d | ||
|
|
b3e3e21f46 | ||
|
|
84bb7fa0b1 | ||
|
|
01e5d8b2db |
No files matched your search
@@ -0,0 +1,20 @@
|
||||
# Frame smoke sensor validation, 29 October 2026
|
||||
|
||||
The locked real Frame smoke reads SteamVR activity, IIO proximity and battery
|
||||
before installing a test title. The proximity reader converts sensor text to a
|
||||
Python float. Python accepts `NaN` and infinity as floats, while range
|
||||
comparisons with `NaN` do not reject it. Before this change, a proximity array
|
||||
containing `NaN` could pass the standby preflight and the saved-result release
|
||||
verifier.
|
||||
|
||||
Focused local tests supplied `NaN`, positive and negative infinity to both
|
||||
the live preflight parser and the release-result verifier. They also supplied
|
||||
boolean, nonfinite, below-threshold and above-100 battery values. Every invalid
|
||||
sample was rejected. Valid standby, finite proximity and 99–100% integer
|
||||
battery samples continued to pass. The 25 smoke/cleanup tests, six result-gate
|
||||
tests and 16 publisher tests passed locally.
|
||||
|
||||
This evidence uses simulated sensor responses. It does not record a real Frame
|
||||
smoke, a worn-headset check, or controller input. Release publication still
|
||||
requires a complete locked smoke from the exact clean release commit, including
|
||||
the x86 launch that was skipped in the held v0.4.54 candidate.
|
||||
@@ -0,0 +1,24 @@
|
||||
# Game Sync 1.0.4 isolated backend check, 29 October 2026
|
||||
|
||||
The official Decky archive for Game Sync (ID 104) is 2,228,806 bytes with
|
||||
SHA-256 `65e021ebb37d2ebfbfbe3dbc8af95aef9556cf484730362c6b26c4ecc3e62ccd`.
|
||||
Its unmodified ZIP is pinned as `tests/fixtures/framey-official/104.zip`.
|
||||
|
||||
`python3 scripts/framey-test-decky-runtime.py --case 104 --output /tmp/framey-gamesync-native-104.json`
|
||||
passed with the published Decky 3.3.0 x86_64 loader, SHA-256
|
||||
`e80d03e641d1766a33a817cb27ab8d2e0a4633685e5d27e4de948a6687526181`.
|
||||
The disposable container had no external network or host devices, dropped
|
||||
capabilities and used a private home. The real loader discovered the exact
|
||||
plugin. Its `get_config` returned defaults with automatic game sync and
|
||||
capture upload disabled. A private `advanced_mode` setting changed from false
|
||||
to true and remained true after the loader restarted. The runner removed its
|
||||
container and image; a separate `docker ps` and image-list audit found no
|
||||
owned resources.
|
||||
|
||||
The original opaque-iframe browser audit blocked `get_config` before panel
|
||||
initialization completed, so it did not prove a usable frontend. This native
|
||||
test confirms only a configuration backend path. No cloud authentication,
|
||||
rclone download or sync, screenshot deletion, process signal, game event,
|
||||
real Frame backend, VR visibility or physical controller was exercised.
|
||||
The published `rclone_manager.py` names an amd64 rclone download, so this
|
||||
result does not establish ARM64 sync support. Game Sync remains **partial**.
|
||||
@@ -0,0 +1,20 @@
|
||||
# Release audit pagination check, 29 October 2026
|
||||
|
||||
The independent installer audit previously returned on the first GitHub
|
||||
release-list page containing a matching tag or draft title. A second matching
|
||||
draft on a later page was never inspected. The audit could therefore report
|
||||
one release while the complete list was ambiguous.
|
||||
|
||||
Local tests mocked two GitHub pages with the same version draft. The old code
|
||||
accepted the first draft; the corrected lookup rejects the duplicate after
|
||||
reading both pages. A second test finds a release on page two, and a full
|
||||
20-page response fails closed because uniqueness cannot be established. All
|
||||
13 focused `test_audit_release_assets.py` tests passed locally.
|
||||
|
||||
The corrected lookup also read GitHub's live release list without downloading
|
||||
assets or changing a release. It found the published `v0.4.53` release as ID
|
||||
`427006429`.
|
||||
|
||||
The duplicate and pagination cases use mocked API responses. No release asset
|
||||
was downloaded, no release was changed, and these results are not a full release
|
||||
verification or real Frame smoke.
|
||||
@@ -118,7 +118,7 @@ The API column shows manifest / actual bundled frontend API. Many manifests say
|
||||
| 101: XIVOmega 1.0.2 | 1 / 2 | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
|
||||
| 102: DeckSP 1.1.3 | 1 / 2 | broken | Optional UI-audio lookup succeeds, but the published frontend raises "Cannot access PluginManager before initialization" when Steam MobX reaction lookup is unavailable. JamesDSP installation/audio routing and ARM64 remain unverified. | static archive, local browser, fake-Frame |
|
||||
| 103: Decky Ludusavi 1.2.2 | 1 / 2 | partial | Initializer succeeds; Flatpak Ludusavi discovery and backup/restore unverified. | static archive, local browser, fake-Frame |
|
||||
| 104: Game Sync 1.0.4 | 1 / 2 | partial | Backend get_config is intentionally rejected by the audit before initialization completes; actual backend compatibility is unknown. | static archive, local browser, fake-Frame |
|
||||
| 104: Game Sync 1.0.4 | 1 / 2 | partial | Exact official backend discovers under Decky 3.3.0 amd64; default get_config and a private setting read/write survive a loader restart. The isolated browser baseline still stops at its denied get_config call. Cloud login, rclone download/sync (including ARM64), process signals, real Frame and VR are unverified. The published updater names an amd64 rclone download, so no ARM64 sync claim follows. | static archive, local browser, fake-Frame, native Decky amd64 configuration |
|
||||
| 105: WiFi Locker 1.0.2 | 1 / 2 | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
|
||||
| 106: LetMeReShade 1.8 | 1 / 2 | partial | Initializer succeeds; Deck-model assumptions, shader downloads and game modifications unverified. | static archive, local browser, fake-Frame |
|
||||
| 107: Decky Sunshine 2025.10.27-dddf365 | legacy / legacy | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
|
||||
|
||||
@@ -51,16 +51,16 @@ def find_release(tag):
|
||||
if not isinstance(batch, list):
|
||||
raise AuditError("GitHub release list is not an array")
|
||||
releases.extend(batch)
|
||||
try:
|
||||
return CHECKSUMS["find_release"](
|
||||
(json.dumps(item) for item in releases), tag
|
||||
)
|
||||
except ValueError as error:
|
||||
if "found 0" not in str(error):
|
||||
raise AuditError(str(error)) from error
|
||||
if len(batch) < 100:
|
||||
break
|
||||
raise AuditError(f"no unique release found for {tag}")
|
||||
else:
|
||||
raise AuditError("release list exceeded 20 pages; cannot verify uniqueness")
|
||||
try:
|
||||
return CHECKSUMS["find_release"](
|
||||
(json.dumps(item) for item in releases), tag
|
||||
)
|
||||
except ValueError as error:
|
||||
raise AuditError(str(error)) from error
|
||||
|
||||
|
||||
def validate_assets(release):
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"""Check a locked Frame smoke result against the exact release commit."""
|
||||
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
@@ -40,7 +41,8 @@ def verify(result, commit):
|
||||
state = result.get('preflight') or {}
|
||||
if (not isinstance(state, dict) or state.get('activity') != 3 or
|
||||
not isinstance(state.get('proximity'), list) or not state['proximity'] or
|
||||
any(type(value) not in (int, float) or value < 0 or value > 10
|
||||
any(type(value) not in (int, float) or not math.isfinite(value) or
|
||||
value < 0 or value > 10
|
||||
for value in state['proximity']) or
|
||||
not isinstance(state.get('battery'), list) or not state['battery'] or
|
||||
any(type(value) is not int or value < 20 or value > 100
|
||||
|
||||
@@ -19,6 +19,11 @@
|
||||
"shim": null,
|
||||
"probe": "radio-local"
|
||||
},
|
||||
{
|
||||
"id": 104,
|
||||
"shim": null,
|
||||
"probe": "game-sync-config"
|
||||
},
|
||||
{
|
||||
"id": 63,
|
||||
"shim": null,
|
||||
|
||||
@@ -267,6 +267,27 @@ def main():
|
||||
assert current('read_deals') == {}
|
||||
report['result'] = ('discovery, default settings, empty local deals, '
|
||||
'private setting write/read and restart persistence passed; feed, notifications and VR unverified')
|
||||
elif case['probe'] == 'game-sync-config':
|
||||
# Exercise only private settings. This plugin also exposes
|
||||
# rclone cloud sync and arbitrary process signals; neither
|
||||
# belongs in a passive compatibility probe.
|
||||
settings = current('get_config')
|
||||
assert settings['sync_on_game_start'] is False, settings
|
||||
assert settings['sync_on_game_stop'] is False, settings
|
||||
assert settings['capture_upload'] is False, settings
|
||||
assert settings['advanced_mode'] is False, settings
|
||||
assert current('set_config', 'advanced_mode', True) is None
|
||||
assert current('get_config')['advanced_mode'] is True
|
||||
os.killpg(backend.pid, signal.SIGTERM)
|
||||
backend.wait(timeout=8)
|
||||
backend = subprocess.Popen([str(loader)], env=env, stdout=log, stderr=log,
|
||||
start_new_session=True)
|
||||
processes.append(backend)
|
||||
until_ready(lambda: rpc('loader/get_plugins'), 'restarted Decky discovery')
|
||||
assert until_ready(lambda: current('get_config')['advanced_mode'],
|
||||
'private Game Sync setting after restart') is True
|
||||
report['result'] = ('discovery, default config and private setting restart persistence passed; '
|
||||
'rclone, cloud authentication, sync, process signals, real Frame and VR unverified')
|
||||
elif case['probe'] == 'radio-local':
|
||||
# The published backend accepts arbitrary URLs and disables
|
||||
# HTTPS verification. Exercise only a loopback fixture inside
|
||||
|
||||
BIN
Binary file not shown.
+6
-1
@@ -3,7 +3,7 @@
|
||||
These are unmodified ZIPs downloaded from the official Decky store CDN on
|
||||
2026-10-25, except `96.zip` and `111.zip` downloaded on 2026-10-26 and `60.zip` on
|
||||
2026-10-27; `13.zip` and `125.zip` were added from the digest-verified audit cache on 2026-10-27,
|
||||
and `67.zip` and `114.zip` on 2026-10-28. `24.zip` was added on 2026-10-29.
|
||||
and `67.zip` and `114.zip` on 2026-10-28. `24.zip` and `104.zip` were added on 2026-10-29.
|
||||
Each ZIP includes its upstream `LICENSE`. They are test data, not plugins shipped or installed by
|
||||
Frame Control. The fake Frame tests serve them
|
||||
from a local fixture so CI does not depend on the store being online.
|
||||
@@ -12,6 +12,7 @@ from a local fixture so CI does not depend on the store being online.
|
||||
| --- | --- | --- | --- |
|
||||
| `13.zip` | Pause Games | 1.0.3 | `4aefdfb0ef25dea9c6d9fd037731c8b99ee8a1be966c2f7d5b0602112ef29f62` |
|
||||
| `24.zip` | RadiYo! | 1.3 | `647111578aa7a5ecd16af8d7fe6c068a6d7ea8737c6dce5ac2d52e84a549ec43` |
|
||||
| `104.zip` | Game Sync | 1.0.4 | `65e021ebb37d2ebfbfbe3dbc8af95aef9556cf484730362c6b26c4ecc3e62ccd` |
|
||||
| `60.zip` | DeckyFileServer | 1.2.0 | `7dc2ecdcd05aa415d1ac90afaff28ecb17f77809a2bc7a77aee8e92505b976f0` |
|
||||
| `125.zip` | Decky Localsend | 0.4.5 | `e271ccb55eb23f918b99a69f79aec905df4c9b2ef7b23bbf4ec1c2d1a6f0c6b7` |
|
||||
| `114.zip` | Network Sentinel | 1.0.4 | `d3e07f40a0c800e72d15594e03c8c61e9bf3092524a578a0260520e735b971a5` |
|
||||
@@ -64,6 +65,10 @@ checks saved true/false state and matching settings callbacks; effects are inert
|
||||
RadiYo! runs through actual published Decky 3.3.0 legacy RPC in a network-isolated
|
||||
container. Only loopback station fetch, custom-station fetch and redirect are
|
||||
tested. External radio, HTTPS and audio playback remain unverified.
|
||||
Game Sync runs through actual published Decky 3.3.0 instance RPC in the same
|
||||
isolation. Only default configuration and a private setting surviving restart
|
||||
are checked. No rclone, cloud sync, process signal, Frame backend or VR action
|
||||
is tested.
|
||||
Network Sentinel runs through the same isolated native Decky gate. Its exact
|
||||
archive gets plugin-local psutil and a user-service manifest; the probe checks
|
||||
discovery, private settings and one monitoring-loop sample without external
|
||||
|
||||
@@ -12,6 +12,8 @@ PLUGINS = (
|
||||
'folder': 'SDH-PauseGames', 'sha256': '4aefdfb0ef25dea9c6d9fd037731c8b99ee8a1be966c2f7d5b0602112ef29f62'},
|
||||
{'id': 24, 'name': 'RadiYo!', 'version': '1.3', 'api_version': 'legacy',
|
||||
'folder': 'radiyo-steam-deck', 'sha256': '647111578aa7a5ecd16af8d7fe6c068a6d7ea8737c6dce5ac2d52e84a549ec43'},
|
||||
{'id': 104, 'name': 'Game Sync', 'version': '1.0.4', 'api_version': 1,
|
||||
'folder': 'SDH-GameSync', 'sha256': '65e021ebb37d2ebfbfbe3dbc8af95aef9556cf484730362c6b26c4ecc3e62ccd'},
|
||||
{'id': 60, 'name': 'DeckyFileServer', 'version': '1.2.0', 'api_version': 1,
|
||||
'folder': 'DeckyFileServer', 'sha256': '7dc2ecdcd05aa415d1ac90afaff28ecb17f77809a2bc7a77aee8e92505b976f0'},
|
||||
{'id': 125, 'name': 'Decky Localsend', 'version': '0.4.5', 'api_version': 1,
|
||||
|
||||
@@ -33,6 +33,7 @@ headset isn't reachable. A skipped launch is not a release smoke pass.
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
@@ -126,10 +127,13 @@ def preflight():
|
||||
proximity = state.get('proximity')
|
||||
if (not isinstance(proximity, list) or not proximity or
|
||||
any(not isinstance(value, (int, float)) or isinstance(value, bool) or
|
||||
value < 0 or value > 10 for value in proximity)):
|
||||
not math.isfinite(value) or value < 0 or value > 10
|
||||
for value in proximity)):
|
||||
raise RuntimeError('Frame proximity is active or unknown; active session untouched')
|
||||
battery = state.get('battery')
|
||||
if not battery or min(battery) < 20:
|
||||
if (not isinstance(battery, list) or not battery or
|
||||
any(type(value) is not int or value < 20 or value > 100
|
||||
for value in battery)):
|
||||
raise RuntimeError('Frame battery is unavailable or below the smoke threshold')
|
||||
return state
|
||||
|
||||
|
||||
@@ -34,6 +34,38 @@ def release_assets():
|
||||
|
||||
|
||||
class AssetAudit(unittest.TestCase):
|
||||
def test_release_lookup_rejects_matching_draft_on_later_page(self):
|
||||
first = {"id": 42, "tag_name": "untagged-first", "draft": True,
|
||||
"name": "Frame Control 9.8.7"}
|
||||
second = {"id": 43, "tag_name": "untagged-second", "draft": True,
|
||||
"name": "Frame Control 9.8.7"}
|
||||
first_page = [first] + [
|
||||
{"id": number, "tag_name": f"v0.0.{number}", "draft": False}
|
||||
for number in range(1, 100)
|
||||
]
|
||||
with mock.patch.object(audit, "github_json", side_effect=[first_page, [second]]) as github:
|
||||
with self.assertRaisesRegex(audit.AuditError, "expected one release"):
|
||||
audit.find_release("v9.8.7")
|
||||
self.assertEqual(github.call_count, 2)
|
||||
|
||||
def test_release_lookup_finds_match_after_full_first_page(self):
|
||||
first_page = [
|
||||
{"id": number, "tag_name": f"v0.0.{number}", "draft": False}
|
||||
for number in range(1, 101)
|
||||
]
|
||||
matching = {"id": 42, "tag_name": "v9.8.7", "draft": True}
|
||||
with mock.patch.object(audit, "github_json", side_effect=[first_page, [matching]]) as github:
|
||||
self.assertEqual(audit.find_release("v9.8.7"), matching)
|
||||
self.assertEqual(github.call_count, 2)
|
||||
|
||||
def test_release_lookup_refuses_unfinished_pagination(self):
|
||||
page = [{"id": number, "tag_name": "v9.8.7" if number == 1 else f"v0.0.{number}"}
|
||||
for number in range(1, 101)]
|
||||
with mock.patch.object(audit, "github_json", return_value=page) as github:
|
||||
with self.assertRaisesRegex(audit.AuditError, "cannot verify uniqueness"):
|
||||
audit.find_release("v9.8.7")
|
||||
self.assertEqual(github.call_count, 20)
|
||||
|
||||
def test_published_manifest_matches_release_bytes(self):
|
||||
assets, _ = release_assets()
|
||||
release = {"tag_name": "v9.8.7", "body": "Release notes", "assets": assets}
|
||||
|
||||
@@ -21,7 +21,7 @@ class RuntimeCoverage(unittest.TestCase):
|
||||
self.assertIn(case['id'], official)
|
||||
self.assertIn(case['probe'], ('mpris', 'screenshots', 'system-monitor', 'timer-lifecycle',
|
||||
'toggle-settings', 'fileserver', 'localsend', 'network-sentinel',
|
||||
'battery-tracker', 'free-loader', 'radio-local'))
|
||||
'battery-tracker', 'free-loader', 'radio-local', 'game-sync-config'))
|
||||
|
||||
def test_new_shim_without_runtime_case_fails_coverage(self):
|
||||
import frame_decky_store
|
||||
|
||||
@@ -108,7 +108,8 @@ class Safety(unittest.TestCase):
|
||||
self.assertEqual(frame_smoke.source_revision(), {'commit': None, 'clean': False})
|
||||
|
||||
def test_locked_preflight_rejects_active_or_unknown_proximity_before_titles(self):
|
||||
for proximity in ([], [88.0625], [4.6875, 88.0625], [None]):
|
||||
for proximity in ([], [88.0625], [4.6875, 88.0625], [None],
|
||||
[float('nan')], [float('inf')], [float('-inf')]):
|
||||
with self.subTest(proximity=proximity), mock.patch.object(
|
||||
frame_smoke, 'remote', return_value=mock.Mock(
|
||||
returncode=0, stdout=json.dumps({
|
||||
@@ -261,7 +262,10 @@ class SharedSmokeLock(unittest.TestCase):
|
||||
for state in ({'activity':1,'proximity':[1],'battery':[100]},
|
||||
{'activity':None,'proximity':[],'battery':[100]},
|
||||
{'activity':3,'proximity':[],'battery':[19]},
|
||||
{'activity':3,'proximity':[],'battery':[]}):
|
||||
{'activity':3,'proximity':[],'battery':[]},
|
||||
{'activity':3,'proximity':[0],'battery':[True]},
|
||||
{'activity':3,'proximity':[0],'battery':[float('nan')]},
|
||||
{'activity':3,'proximity':[0],'battery':[101]}):
|
||||
with self.subTest(state=state), mock.patch.object(frame_smoke, 'remote',
|
||||
return_value=mock.Mock(returncode=0, stdout=json.dumps(state))):
|
||||
with self.assertRaises(RuntimeError):
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""The release smoke gate requires a complete result from its exact checkout."""
|
||||
|
||||
import importlib.util
|
||||
import math
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
@@ -51,6 +52,20 @@ class VerifyFrameSmokeResult(unittest.TestCase):
|
||||
with self.assertRaisesRegex(ValueError, 'preflight'):
|
||||
gate.verify(data, SHA)
|
||||
|
||||
def test_rejects_nonfinite_proximity_and_invalid_battery(self):
|
||||
for proximity in (math.nan, math.inf, -math.inf):
|
||||
with self.subTest(proximity=proximity):
|
||||
data = result()
|
||||
data['preflight']['proximity'] = [proximity]
|
||||
with self.assertRaisesRegex(ValueError, 'preflight'):
|
||||
gate.verify(data, SHA)
|
||||
for battery in ([True], [math.nan], [101], [19], ['99']):
|
||||
with self.subTest(battery=battery):
|
||||
data = result()
|
||||
data['preflight']['battery'] = battery
|
||||
with self.assertRaisesRegex(ValueError, 'preflight'):
|
||||
gate.verify(data, SHA)
|
||||
|
||||
def test_rejects_missing_lock_release_record(self):
|
||||
data = result()
|
||||
data['lock_released'] = False
|
||||
|
||||
@@ -8880,12 +8880,13 @@
|
||||
"https://github.com/GedasFX/decky-cloud-save"
|
||||
],
|
||||
"status": "partial",
|
||||
"reason": "Backend get_config is intentionally rejected by the audit before initialization completes; actual backend compatibility is unknown.",
|
||||
"reason": "Exact official backend discovers under Decky 3.3.0 amd64; default get_config and a private setting read/write survive a loader restart. The isolated browser baseline still stops at its denied get_config call. Cloud login, rclone download/sync (including ARM64), process signals, real Frame and VR are unverified. The published updater names an amd64 rclone download, so no ARM64 sync claim follows.",
|
||||
"blockInstall": false,
|
||||
"verification": [
|
||||
"static archive",
|
||||
"local browser",
|
||||
"fake-Frame"
|
||||
"fake-Frame",
|
||||
"native Decky amd64 configuration"
|
||||
],
|
||||
"browser": {
|
||||
"id": 104,
|
||||
|
||||
Reference in new issue
Block a user