diff --git a/docs/framey-loader.md b/docs/framey-loader.md index ce9878b..76c1119 100644 --- a/docs/framey-loader.md +++ b/docs/framey-loader.md @@ -82,7 +82,10 @@ The in-headset **Open official store** view loads the full official catalogue. Search matches names, authors, and descriptions. Filters show non-root plugins or installed plugins, and Previous/Next buttons browse 20 results per page. The root label is a compatibility warning; other APIs are checked only when a -plugin is opened. The store still needs the managed Decky user service running +plugin is opened. Both `root` and `_root` +store tags mark a plugin as needing root, and duplicate store IDs are omitted +because an ID alone could select a different archive than the visible row. +The store still needs the managed Decky user service running before its Install or Update buttons are enabled. If a plugin prevents Decky from starting, Remove remains available for Frame Control-owned plugins while the managed service is stopped; it does not restart the service. diff --git a/tests/test_frame_decky_store.py b/tests/test_frame_decky_store.py index 7108835..7824bac 100644 --- a/tests/test_frame_decky_store.py +++ b/tests/test_frame_decky_store.py @@ -41,6 +41,15 @@ class ArchiveTests(unittest.TestCase): with patch.object(frame_decky_store.urllib.request, 'urlopen', return_value=io.BytesIO(json.dumps(records).encode())): self.assertEqual([(p['id'], p['author']) for p in frame_decky_store.catalog()], [(42, 'Panel Author')]) + def test_catalog_marks_underscore_root_and_omits_ambiguous_store_ids(self): + records = [{'id': 42, 'name': 'Needs root', 'tags': ['_root'], + 'versions': [{'name': '1.0', 'hash': 'a' * 64}]}, + {'id': 43, 'name': 'First', 'versions': [{'name': '1.0', 'hash': 'b' * 64}]}, + {'id': 43, 'name': 'Second', 'versions': [{'name': '2.0', 'hash': 'c' * 64}]}] + with patch.object(frame_decky_store.urllib.request, 'urlopen', + return_value=io.BytesIO(json.dumps(records).encode())): + self.assertEqual([(p['id'], p['root']) for p in frame_decky_store.catalog()], [(42, True)]) + def test_accepts_hashed_non_root_archive(self): data, digest = archive() self.assertEqual(frame_decky_store.archive_info(data, digest, 'Example'), 'Example') diff --git a/ui/frame_decky_store.py b/ui/frame_decky_store.py index 2abf75a..437bb4d 100644 --- a/ui/frame_decky_store.py +++ b/ui/frame_decky_store.py @@ -59,8 +59,13 @@ def catalog(): 'description': str(item.get('description') or '')[:300], 'author': author[:100], 'version': latest['name'][:40], 'sha256': latest['hash'], - 'root': 'root' in tags}) - return result + 'root': 'root' in tags or '_root' in tags}) + # A repeated store ID makes a clicked row ambiguous: change() resolves by ID + # and could install a different archive than the one the user reviewed. + counts = {} + for item in result: + counts[item['id']] = counts.get(item['id'], 0) + 1 + return [item for item in result if counts[item['id']] == 1] def archive_info(data, expected_hash, expected_name):