mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-25 15:00:25 +01:00
Merge pull request #238 from saphid/feat/framey-official-fixtures
Test official Decky plugin archives in fake Frame
This commit is contained in:
7 files changed
+132
-15
No files matched your search
@@ -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
@@ -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']))
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
+18
@@ -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.
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user