Merge pull request #238 from saphid/feat/framey-official-fixtures

Test official Decky plugin archives in fake Frame
This commit is contained in:
Alex Southwell authored and GitHub committed 2026-10-25 10:22:58 +00:00
commit 229f17696e
7 files changed
+132 -15

No files matched your search

+4
View File
@@ -53,6 +53,10 @@ live Decky settings persistence, or badges on Steam game pages. The last item
has no supported injection target in the Frame Control panel.
The [browser and fake Frame evidence](evidence/framey-browser-2026-10-25.md)
records exactly what was exercised.
Fake Frame CI also installs and removes pinned official ProtonDB Badges 1.3.4
and Steam Status 1.2.3 archives, checks their API v2 manifests and frontend
bytes, and verifies they can be packed as Framey panels. It does not execute
their actual backend code or prove their frontend controls work in VR.
The fake Frame integration test now writes a Decky setting through Framey's
scoped backend endpoint, reads it in a new CEF request, updates the plugin,
+50 -15
View File
@@ -10,6 +10,7 @@ import threading
import zipfile
from harness import FrameTestCase, ROOT, Server, api, finished, ok, require, ssh
from framey_official import PLUGINS as OFFICIAL_PLUGINS, archive as official_archive
require() # Skip the module outside the disposable fake Frame, like its peers.
@@ -26,6 +27,10 @@ def archive(version):
ARTIFACTS = {hashlib.sha256(a).hexdigest(): a for a in (archive('1.0.0'), archive('1.1.0'))}
HASHES = list(ARTIFACTS)
for plugin in OFFICIAL_PLUGINS:
data = official_archive(plugin)
assert hashlib.sha256(data).hexdigest() == plugin['sha256']
ARTIFACTS[plugin['sha256']] = data
class StoreFixture(BaseHTTPRequestHandler):
@@ -35,10 +40,15 @@ class StoreFixture(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == '/plugins':
body = json.dumps([{'id':123,'name':'Sample Panel','author':'Fixture',
records = [{'id':999,'name':'Sample Panel','author':'Fixture',
'description':'A harmless panel','tags':[],
'versions':[{'name':['1.0.0','1.1.0'][self.release],
'hash':HASHES[self.release]}]}]).encode()
'hash':HASHES[self.release]}]}]
records.extend({'id':plugin['id'], 'name':plugin['name'], 'author':'Official Decky store',
'description':'Pinned official archive', 'tags':[],
'versions':[{'name':plugin['version'], 'hash':plugin['sha256']}]}
for plugin in OFFICIAL_PLUGINS)
body = json.dumps(records).encode()
content_type = 'application/json'
elif self.path.startswith('/versions/') and self.path.endswith('.zip'):
body = ARTIFACTS.get(self.path.split('/')[-1][:-4])
@@ -93,41 +103,66 @@ u=h/'.config/systemd/user/frame-control-decky.service'; u.parent.mkdir(parents=T
self.assertEqual(status, 403)
catalogue = ok('GET', '/api/decky/store')['plugins']
self.assertEqual((catalogue[0]['version'], catalogue[0]['sha256']), ('1.0.0', HASHES[0]))
status, _, _ = api('POST', '/api/decky/plugin', {'action':'install','id':123})
status, _, _ = api('POST', '/api/decky/plugin', {'action':'install','id':999})
self.assertEqual(status, 400) # Third-party code needs explicit acknowledgement.
job = finished(ok('POST', '/api/decky/plugin',
{'action':'install','id':123,'acknowledged':True}))
{'action':'install','id':999,'acknowledged':True}))
self.assertFalse(job.get('error'), job)
self.assertEqual(ok('GET', '/api/decky')['plugins'][0]['version'], '1.0.0')
self.assertIn('Sample 1.0.0', ssh('cat ~/.local/share/frame-control/decky/plugins/sample-panel/dist/index.js'))
status, _, _ = api('POST', '/api/framey/method', {'id':123,'method':'get_setting','args':[]},
status, _, _ = api('POST', '/api/framey/method', {'id':999,'method':'get_setting','args':[]},
headers={'X-Frame-UI':''})
self.assertEqual(status, 403)
status, _, _ = api('POST', '/api/framey/method', {'id':123,'method':'x;alert(1)','args':[]})
status, _, _ = api('POST', '/api/framey/method', {'id':999,'method':'x;alert(1)','args':[]})
self.assertEqual(status, 400)
self.assertEqual(ok('POST', '/api/framey/method',
{'id':123,'method':'get_setting','args':['theme','dark']})['result'], 'dark')
{'id':999,'method':'get_setting','args':['theme','dark']})['result'], 'dark')
self.assertTrue(ok('POST', '/api/framey/method',
{'id':123,'method':'set_setting','args':['theme','light']})['result'])
{'id':999,'method':'set_setting','args':['theme','light']})['result'])
self.assertEqual(ok('POST', '/api/framey/method',
{'id':123,'method':'get_setting','args':['theme','dark']})['result'], 'light')
{'id':999,'method':'get_setting','args':['theme','dark']})['result'], 'light')
StoreFixture.release = 1
job = finished(ok('POST', '/api/decky/plugin',
{'action':'update','id':123,'acknowledged':True}))
{'action':'update','id':999,'acknowledged':True}))
self.assertFalse(job.get('error'), job)
self.assertEqual(ok('GET', '/api/decky')['plugins'][0]['version'], '1.1.0')
self.assertEqual(ok('POST', '/api/framey/method',
{'id':123,'method':'get_setting','args':['theme','dark']})['result'], 'light')
{'id':999,'method':'get_setting','args':['theme','dark']})['result'], 'light')
job = finished(ok('POST', '/api/decky/plugin',
{'action':'remove','id':123,'acknowledged':True}))
{'action':'remove','id':999,'acknowledged':True}))
self.assertFalse(job.get('error'), job)
self.assertEqual(ok('GET', '/api/decky')['plugins'], [])
self.assertEqual(ssh('test -e ~/.local/share/frame-control/decky/plugins/sample-panel && echo yes || echo no').strip(), 'no')
status, _, _ = api('POST', '/api/framey/method',
{'id':123,'method':'get_setting','args':['theme','dark']})
{'id':999,'method':'get_setting','args':['theme','dark']})
self.assertEqual(status, 400) # No method calls while the plugin is removed.
job = finished(ok('POST', '/api/decky/plugin',
{'action':'install','id':123,'acknowledged':True}))
{'action':'install','id':999,'acknowledged':True}))
self.assertFalse(job.get('error'), job)
self.assertEqual(ok('POST', '/api/framey/method',
{'id':123,'method':'get_setting','args':['theme','dark']})['result'], 'light')
{'id':999,'method':'get_setting','args':['theme','dark']})['result'], 'light')
def test_two_pinned_official_api_v2_plugins_install_and_remove(self):
self.backend()
catalog = {plugin['id']:plugin for plugin in ok('GET', '/api/decky/store')['plugins']}
for plugin in OFFICIAL_PLUGINS:
with self.subTest(plugin=plugin['name']):
self.assertEqual(catalog[plugin['id']]['sha256'], plugin['sha256'])
job = finished(ok('POST', '/api/decky/plugin',
{'action':'install','id':plugin['id'],'acknowledged':True}))
self.assertFalse(job.get('error'), job)
found = [item for item in ok('GET', '/api/decky')['plugins']
if item['storeId'] == plugin['id']]
self.assertEqual(len(found), 1)
self.assertEqual(found[0]['version'], plugin['version'])
installed = f"~/.local/share/frame-control/decky/plugins/{plugin['folder']}"
with zipfile.ZipFile(io.BytesIO(official_archive(plugin))) as bundle:
expected = hashlib.sha256(bundle.read(plugin['folder']+'/dist/index.js')).hexdigest()
self.assertEqual(ssh(f'sha256sum {installed}/dist/index.js').split()[0], expected)
self.assertEqual(json.loads(ssh(f'cat {installed}/plugin.json'))['api_version'], 2)
job = finished(ok('POST', '/api/decky/plugin',
{'action':'remove','id':plugin['id'],'acknowledged':True}))
self.assertFalse(job.get('error'), job)
self.assertFalse(any(item['storeId'] == plugin['id']
for item in ok('GET', '/api/decky')['plugins']))
Binary file not shown.
Binary file not shown.
+18
View File
@@ -0,0 +1,18 @@
# Pinned Decky store test archives
These are unmodified ZIPs downloaded from the official Decky store CDN on
2026-10-25. 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.
| File | Official plugin | Version | Store SHA-256 |
| --- | --- | --- | --- |
| `14.zip` | ProtonDB Badges | 1.3.4 | `90a3453ac02709cad64096e7539fd8141fa56f99ae3441fc24a4990dc9684358` |
| `123.zip` | Steam Status | 1.2.3 | `9053308e04a74f44745b45b9f4f50af3673348d367e7b14bacc5677de707ac4e` |
The matching source projects are
[ProtonDB Badges](https://github.com/bschelst/protondb-decky) and
[Steam Status](https://github.com/bschelst/steamstatus-decky).
The test verifies the ZIP digest, archive policy, API v2 manifest, installed
frontend bytes, managed store receipt, and removal. It does **not** run their
Python backends or prove their JavaScript panels render in the VR shell.
+15
View File
@@ -0,0 +1,15 @@
"""Pinned official Decky store archives used only in disposable test fixtures."""
from pathlib import Path
ROOT = Path(__file__).parent / 'fixtures/framey-official'
PLUGINS = (
{'id': 14, 'name': 'ProtonDB Badges', 'version': '1.3.4',
'folder': 'protondb-decky', 'sha256': '90a3453ac02709cad64096e7539fd8141fa56f99ae3441fc24a4990dc9684358'},
{'id': 123, 'name': 'Steam Status', 'version': '1.2.3',
'folder': 'steamstatus-decky', 'sha256': '9053308e04a74f44745b45b9f4f50af3673348d367e7b14bacc5677de707ac4e'},
)
def archive(plugin):
return (ROOT / f"{plugin['id']}.zip").read_bytes()
+45
View File
@@ -0,0 +1,45 @@
"""The pinned store bundles remain valid API v2 Framey panel inputs."""
import sandbox # noqa: F401
import base64
import hashlib
import json
from pathlib import Path
import re
import sys
import tempfile
import unittest
from unittest.mock import patch
import zipfile
import io
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'ui'))
import frame_decky_store
import framey_panel
from framey_official import PLUGINS, archive
class OfficialBundles(unittest.TestCase):
def test_pinned_store_archives_install_and_pack_into_framey_panel(self):
with tempfile.TemporaryDirectory() as root:
plugins = Path(root)
for plugin in PLUGINS:
with self.subTest(plugin=plugin['name']):
data = archive(plugin)
self.assertEqual(hashlib.sha256(data).hexdigest(), plugin['sha256'])
self.assertEqual(frame_decky_store.archive_info(data, plugin['sha256'], plugin['name']),
plugin['folder'])
with zipfile.ZipFile(io.BytesIO(data)) as z:
manifest = json.loads(z.read(plugin['folder'] + '/plugin.json'))
self.assertEqual(manifest['api_version'], 2)
z.extractall(plugins)
folder = plugins / plugin['folder']
(folder / '.frame-control-store.json').write_text(json.dumps({'storeId': plugin['id']}))
with patch.object(framey_panel, 'PLUGINS', plugins):
page = framey_panel.panel(plugin['id']).decode()
encoded = re.search(r'<script id="framey-bundle" type="application/json">([^<]+)</script>', page)
self.assertIsNotNone(encoded)
self.assertEqual(base64.b64decode(encoded.group(1)), (folder / 'dist/index.js').read_bytes())
if __name__ == '__main__':
unittest.main()