From a68c5584a9247d0f7cd04485f7d74cdc74c794fc Mon Sep 17 00:00:00 2001 From: SAPHID Date: Mon, 26 Oct 2026 05:03:11 +0000 Subject: [PATCH 1/3] Document v0.4.41 tagged release verification --- docs/current-support-matrix.md | 6 +++--- docs/evidence/v0441-release-2026-10-26.md | 23 +++++++++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 docs/evidence/v0441-release-2026-10-26.md diff --git a/docs/current-support-matrix.md b/docs/current-support-matrix.md index 79c2994..a5043a1 100644 --- a/docs/current-support-matrix.md +++ b/docs/current-support-matrix.md @@ -1,11 +1,11 @@ # What we have tested -Release baseline: Frame Control **v0.4.40**, 26 October 2026. Rows also identify verified post-release main changes explicitly. “Real Frame + physical PC” means an actual headset and a non-virtual computer. “VM only” means a Windows virtual machine, sometimes connected to the real Frame. A CI test or a simulated device is **not** a physical hardware test. The date/version column is the last relevant check, not a promise that every new release was retested on that platform. See each link for the exact procedure and limits. +Release baseline: Frame Control **v0.4.41**, 26 October 2026. Rows also identify verified post-release main changes explicitly. “Real Frame + physical PC” means an actual headset and a non-virtual computer. “VM only” means a Windows virtual machine, sometimes connected to the real Frame. A CI test or a simulated device is **not** a physical hardware test. The date/version column is the last relevant check, not a promise that every new release was retested on that platform. See each link for the exact procedure and limits. | Feature | Real Frame + physical PC tested | VM only | Untested or limited | Last check and proof | |---|---|---|---|---| | Mac desktop release — tested on a real Mac | Physical MacBook Air (MacBookAir9,1), Intel Core i3, macOS 15.7.7 (24G720): official v0.4.39 x64 DMG verified, installed and launched. Native UI, preference persistence across restart (original file restored), bundled Python logs, update check, offline OBS Start/Stop and 116-entry Decky store checked. A 40-byte relay upload/readback/delete passed under coordinator/headset locks | No | No quarantined Safari/Chrome download or Gatekeeper override test; native Open at Login/menu automation unavailable. Direct frame.local had no route and the app refused a stale SSH identity. Relay transfer is not native Mac transfer acceptance. Real OBS/Framey VR unverified. Quit exposed a server cleanup race; fix #284 shipped in v0.4.40 with physical-Mac source regression coverage, but the tagged Intel installer was not launched on physical Intel hardware | 26 Oct, official v0.4.39 tag `fd0b0a9`: [Intel release evidence](evidence/mac-v0439-2026-10-26.md); [earlier ARM64 failure](evidence/mac-desktop-2026-10-26.md) | -| Core title smoke test | Linux lxso2 ran the exact v0.4.40 tagged source against the real Frame; all 13 reserved native/Proton title steps passed and cleanup completed | No | The headset was unworn; visible rendering and controller input were not checked. Other feature paths retain their separate limits below | 26 Oct, exact v0.4.40 tag `a4d8073`, Frame OS 0.5.6 build `20261022.6153909`: [tagged release evidence](evidence/v0440-release-2026-10-26.md); [earlier candidate smoke](evidence/v0440-candidate-frame-2026-10-26.md) | +| Core title smoke test | Linux lxso2 ran the exact v0.4.41 tagged source against the real Frame; 13/13 reserved title steps passed and cleanup completed | No | The headset was unworn; visible rendering and controller input were not checked. Optional Steam Linux Runtime 4 was absent, so the x86_64 test executable did not actually launch. Other feature paths retain their separate limits below | 26 Oct, exact v0.4.41 tag `b5d50a8`, Frame OS 0.5.6 build `20261022.6153909`: [tagged release evidence](evidence/v0441-release-2026-10-26.md); [earlier candidate smoke](evidence/v0441-candidate-frame-2026-10-26.md) | | Windows desktop in the headset | No physical Windows PC test | Windows 11 VM sent a whole desktop to a real Frame; this used an intermediate fix, not the final native DLL | Physical GPU, capture and controller input; individual windows unavailable in bundled DXGI build | 24 Oct, v0.4.10 installer + candidate fix: [VM log](evidence/windows-vm-2026-10-24.md), [PC guide](pc-in-headset.md) | | Linux desktop in the headset | No | No | Native Linux PC capture is disabled; lxso2 lacked a desktop portal and PipeWire session. The separate Steam `-pipewire` helper is not Frame Control's PC capture | 24 Oct, v0.4.17: [host audit](evidence/linux-pc-host-lxso2-2026-10-24.md), [Steam helper PR #163](https://github.com/saphid/frame-control/pull/163) | | Mac desktop in the headset | A physical Mac sent its screen/window to a real Frame | No | Current v0.4.26 Mac installer and worn-headset controller input have not been repeated | 28 Sep, pre-v0.4 / Frame OS 0.4.1: [Mac guide and measurement](streaming.md#b-show-the-macs-desktop-inside-the-frame) | @@ -24,7 +24,7 @@ Release baseline: Frame Control **v0.4.40**, 26 October 2026. Rows also identify | Open at login | No Frame needed; Linux autostart file toggled on a physical Linux host with an isolated config directory | No | A full login session and physical Windows/macOS host login have not been checked; OS API calls were unit tested | 25 Oct, v0.4.26: [PR #189](https://github.com/saphid/frame-control/pull/189) | | Local Frame Control extensions | Example panel displayed live Frame status in a browser preview; the real Frame served a read-only example in the experimental headset panel | No | No wearer-visible extension view; third-party extensions and physical Windows/macOS extension folders untested | 25 Oct, v0.4.29: [PR #194](https://github.com/saphid/frame-control/pull/194), [PR #201](https://github.com/saphid/frame-control/pull/201), [extension guide](extensions.md) | | Experimental Frame Control SteamVR panel | Frame OS 0.5.6 registered and served the opt-in overlay. A later unattended check safely woke SteamVR and found the panel in stereo compositor and mirror captures; it removed its temporary service and restored power settings | No | No person wore the headset for that check. Physical controller input, scrolling, VR keyboard, comfort and legibility remain unverified. The large status view has browser and packaging checks, not a wearer review | 26 Oct, v0.4.29–0.4.40: [panel PR #201](https://github.com/saphid/frame-control/pull/201), [unattended evidence](evidence/framey-vr-2026-10-25/README.md), [post-#292 unworn sample exercise](evidence/v0440-release-2026-10-26.md#post-tag-292-panel-regression), [earlier standby-gated exercise](evidence/framey-vr-standby-gate-2026-10-26.md), [worn checklist](framey-vr-check.md) | -| Experimental Decky Loader manager | Frame OS 0.5.6 ARM64 user-service install/status/update/remove and managed ProtonDB install/update/remove passed earlier. A private Decky 3.3.0 service returned read-only ProtonDB and Steam Status setting RPCs; a later locked check verified the official Notebook default drawing/save/reopen flow and Simple Timer backend events in local Chromium | No | The loader remains opt-in. Installs/updates reject an archive whose Decky name collides with another installed plugin before download and under the headset transaction lock; this safety path has host/fake Frame tests, not a new real-headset collision test. v0.4.40 classifies the 116 exact-version plugins as 1 works (Notebook default flow only), 49 partial, 55 broken and 11 not applicable. Post-tag PRs #289 and #293 give 3 works / 48 partial / 54 broken / 11 N/A on current main: TabMaster is partial, and Volume Mixer and Mixer work for tested per-app silent-stream volume control only. Deck Shelves moved to partial after its exact chunked frontend rendered settings in local Chromium; its backend and Steam Home shelves remain unverified. Steamback, PlayTime and Deck Settings became partial after detached read-only library/user snapshots and isolated amd64 backend checks; their official ARM64 backends, automatic behavior and VR panels remain unverified. A read-only Steam bridge returned the Frame country, install folders and system information; IsThereAnyDeal selected that country in local Chromium, but its deployed HTTP endpoint and deal lookup remain unverified. An isolated ARM64 LocalSend core answered its info endpoint, but the official x86-only plugin archive and file transfers remain blocked. Volume Mixer and Mixer controlled a named silent stream through real ARM64 Decky with independent Pulse readbacks; Volume Boost default-sink reads matched, but real writes were denied. Audible output, Bluetooth playback, internal-node controls and worn VR remain unverified. Most official backends and VR controls remain unverified. Physical controller use and wearer-visible official panels remain unverified | 26 Oct, v0.4.31–0.4.40: [managed store PR #208](https://github.com/saphid/frame-control/pull/208), [name-collision PR #269](https://github.com/saphid/frame-control/pull/269), [catalog PR #266](https://github.com/saphid/frame-control/pull/266), [backend event evidence](evidence/framey-events-backend-2026-10-26.json), [Steam read PR #274](https://github.com/saphid/frame-control/pull/274), [LocalSend evidence PR #275](https://github.com/saphid/frame-control/pull/275), [local audio evidence](evidence/framey-audio-backends-2026-10-26.md), [ARM64 audio evidence](evidence/framey-audio-arm64-2026-10-26.md), [post-release library evidence](evidence/framey-library-context-2026-10-26.md), [compatibility table](framey-compatibility.md) | +| Experimental Decky Loader manager | Frame OS 0.5.6 ARM64 user-service install/status/update/remove and managed ProtonDB install/update/remove passed earlier. A private Decky 3.3.0 service returned read-only ProtonDB and Steam Status setting RPCs; a later locked check verified the official Notebook default drawing/save/reopen flow and Simple Timer backend events in local Chromium | No | The loader remains opt-in. Installs/updates reject an archive whose Decky name collides with another installed plugin before download and under the headset transaction lock; this safety path has host/fake Frame tests, not a new real-headset collision test. v0.4.41 classifies the 116 exact-version plugins as 3 works, 48 partial, 54 broken and 11 not applicable. Notebook works for its default flow; Volume Mixer and Mixer work for tested per-app silent-stream volume control only. TabMaster is partial. Deck Shelves moved to partial after its exact chunked frontend rendered settings in local Chromium; its backend and Steam Home shelves remain unverified. Steamback, PlayTime and Deck Settings became partial after detached read-only library/user snapshots and isolated amd64 backend checks; their official ARM64 backends, automatic behavior and VR panels remain unverified. A read-only Steam bridge returned the Frame country, install folders and system information; IsThereAnyDeal selected that country in local Chromium, but its deployed HTTP endpoint and deal lookup remain unverified. An isolated ARM64 LocalSend core answered its info endpoint, but the official x86-only plugin archive and file transfers remain blocked. Volume Mixer and Mixer controlled a named silent stream through real ARM64 Decky with independent Pulse readbacks; Volume Boost default-sink reads matched, but real writes were denied. Audible output, Bluetooth playback, internal-node controls and worn VR remain unverified. Most official backends and VR controls remain unverified. Physical controller use and wearer-visible official panels remain unverified | 26 Oct, v0.4.31–0.4.41: [managed store PR #208](https://github.com/saphid/frame-control/pull/208), [name-collision PR #269](https://github.com/saphid/frame-control/pull/269), [catalog PR #266](https://github.com/saphid/frame-control/pull/266), [backend event evidence](evidence/framey-events-backend-2026-10-26.json), [Steam read PR #274](https://github.com/saphid/frame-control/pull/274), [LocalSend evidence PR #275](https://github.com/saphid/frame-control/pull/275), [local audio evidence](evidence/framey-audio-backends-2026-10-26.md), [ARM64 audio evidence](evidence/framey-audio-arm64-2026-10-26.md), [post-release library evidence](evidence/framey-library-context-2026-10-26.md), [compatibility table](framey-compatibility.md) | | Experimental Framey VR plugin panels and store | Unattended Frame OS 0.5.6 runs showed the Framey sample panel in stereo compositor and mirror captures. The latest standby-gated exercise restored the original power settings and released its shared lock. Synthetic OpenVR clicks changed a live sample setting, read its Decky backend file and confirmed it after a fresh iframe reopen | No | This was the repository sample, not an official store plugin. Physical controllers, native scrolling, VR keyboard, wearer comfort and legibility remain unverified. CSS Loader and SteamGridDB exact archives passed fake Frame install/remove and panel packing; this does not establish theme or artwork controls in VR. Panel-local backend events and React toasts have local/real-backend checks, but no wearer review. Pause Games and Tailscale Control settings render in local Chromium; their live actions are unverified. The 116-plugin catalog had 60 fake installs/removals and 56 expected blocks; v0.4.40 recorded 101 local Chromium initializers ready and 15 failed, up from 98/18 in v0.4.39. Notebook is marked works only for its default drawing/save/reopen flow | 26 Oct, v0.4.38–0.4.40: [post-#294 unworn sample and scroll evidence](evidence/framey-scroll-probe-2026-10-26.md#post-294-exact-main-sample-check), [backend event PR #270](https://github.com/saphid/frame-control/pull/270), [settings/cache PR #271](https://github.com/saphid/frame-control/pull/271), [post-release sample exercise](evidence/v0439-release-2026-10-26.md#post-release-framey-library-change-282), [worn checklist](framey-vr-check.md) | Release notes must preserve these limits until a newer recorded check changes them. When a feature is retested, add its date, exact app/Frame version, host type and proof here. The older [v0.3.1 matrix](support-matrix.html) is a historical snapshot and does not describe current coverage. diff --git a/docs/evidence/v0441-release-2026-10-26.md b/docs/evidence/v0441-release-2026-10-26.md new file mode 100644 index 0000000..e553b00 --- /dev/null +++ b/docs/evidence/v0441-release-2026-10-26.md @@ -0,0 +1,23 @@ +# v0.4.41 tagged release, 26 October 2026 + +The annotated `v0.4.41` tag dereferences to main merge commit `b5d50a8d896f790a94baf4e937db914eab475ed9` from PR #297. The [tagged release workflow](https://github.com/saphid/frame-control/actions/runs/40271174791) passed its draft, Linux, Windows, Apple Silicon Mac, Intel Mac and checksum jobs. The platform jobs launched the built Linux AppImage, checked the Windows installation and bundled PC Python libraries, and checked Mac bundle architectures. These CI results do not establish a physical Windows or Mac launch. + +## Real Frame, exact tag + +From an exact tag checkout, `scripts/frame-smoke.sh` acquired its unique owner in `/tmp/frame-test.lock` before reading the headset. Preflight found SteamVR activity 3 (standby) and battery 99%. On Frame OS 0.5.6 build `20261022.6153909`, all **13/13 reserved title steps passed**; the command exited 0, including test title and process cleanup and release of its owned lock. ARM64 and Proton test executables launched. The x86_64 launch step recorded that optional Steam Linux Runtime 4 was **not installed**, so that executable did not actually launch. The private detailed result is `tests/smoke/results/2026-10-26T045140Z-20261022.6153909.json` in the exact-tag checkout. + +No person wore the headset. This title smoke does not verify active passthrough pixels, wearer-visible panels, physical controller input, official Decky plugin panels, audio output, comfort or VR keyboard use. The separate [unworn Framey sample exercise](framey-scroll-probe-2026-10-26.md) established live sample setting persistence, stereo compositor changes and trusted synthetic OpenVR input; it did not establish a worn result. The hourly worn check remains enabled. + +## Downloaded installer and checksum checks + +The draft held ten installers plus `SHA256SUMS`, each with a GitHub SHA-256 digest. `scripts/publish-release.sh --dry-run v0.4.41` checked the exact tag and all ten installer digests, regenerated `SHA256SUMS` from those digests and matched its own GitHub digest. A fresh downloaded checksum list independently matched `e7555956aeb9ec3a464081aaafe1ed6762f65a17c04edaa6ea45286f10927109`. + +A fresh Linux x86_64 AppImage download matched `5d1f50dc8527faf299f2058c7b5ed19057ec7c81c41449f6ccd44d68e6b4d212`. `scripts/check-appimage-launch.py` launched that exact file under isolated Xvfb: the bundled server answered HTTP 200 with `FrameControl/1`, then stopped. The first local attempt failed at Chromium sandbox startup inside the command sandbox; rerunning the same downloaded file with normal host permissions passed. The release workflow's independent Linux launch also passed. + +Fresh Mac ZIP downloads matched GitHub digests: arm64 `84615ec8a0c62481a9f33bae1815d55ec902db9aa9d4bd767fcf9380c3d44d7a`, x64 `2800b749cda98d15207950eca4a6bd205d35f30551f16efd7720b41347edc7ca`. Both contain bundle version 0.4.41; the app executable Mach-O CPU types are arm64 and x86_64 respectively. The physical Mac was unreachable over SSH, so neither tagged ZIP was launched there. The physical Intel Air was also not tested with this tag. + +## Publication and updater readback + +The final dry run passed with the release notes and hardware limits in the draft. `scripts/publish-release.sh v0.4.41` published [v0.4.41](https://github.com/saphid/frame-control/releases/tag/v0.4.41) at 2026-10-26 05:01:47 UTC. GitHub's latest-release API read back `draft=false`, tag `v0.4.41`, ten installers, `SHA256SUMS` and `update.json`. A fresh public latest-download of `update.json` parsed as version 0.4.41 with the stable release page, ten SHA-256 digests and the unworn-test limits in its notes. The release contains the tagged `b5d50a8` commit; no post-tag changes are implied. + +The exact-version official Decky catalog in this release is **3 works / 48 partial / 54 broken / 11 N/A**. Volume Mixer and Mixer are works only for the tested per-app silent-stream volume scope on real ARM64 Decky; [audio evidence](framey-audio-arm64-2026-10-26.md) describes the independent Pulse readbacks. Volume Boost remains partial. Audible playback, Bluetooth, other mixer controls and official-plugin worn VR remain unverified. From 240288df65417fffa3973ff535a5c619ad09cbca Mon Sep 17 00:00:00 2001 From: SAPHID Date: Mon, 26 Oct 2026 05:06:43 +0000 Subject: [PATCH 2/3] Close stale Framey panels during plugin changes --- docs/framey-loader.md | 6 ++++ tests/page/framey_cache.mjs | 2 +- tests/page/framey_event_parent.mjs | 2 +- tests/page/framey_store.mjs | 48 ++++++++++++++++++++++++++++++ ui/headset.html | 29 ++++++++++++++---- 5 files changed, 80 insertions(+), 7 deletions(-) diff --git a/docs/framey-loader.md b/docs/framey-loader.md index ab0c19e..d872f24 100644 --- a/docs/framey-loader.md +++ b/docs/framey-loader.md @@ -177,6 +177,12 @@ the store and review the new version before retrying. If Frame Control restarts, job result. The panel clears that job ID and asks you to refresh the installed list before making another change. +An open plugin panel closes before its own Update or Remove starts, so its old +frontend cannot call the changing backend. Open panel is unavailable while a +store job is pending. A later installed-status refresh also closes an open panel +if Decky stopped, its store receipt changed, or the status read failed. Reopen +the plugin after a successful refresh to use its current frontend. + ## For plugin authors Start with the [Framey settings sample](../examples/framey-settings/README.md) diff --git a/tests/page/framey_cache.mjs b/tests/page/framey_cache.mjs index 838e2fa..c88a38b 100644 --- a/tests/page/framey_cache.mjs +++ b/tests/page/framey_cache.mjs @@ -19,7 +19,7 @@ const parent=vm.createContext({document:{getElementById:el,createElement:()=>({} window:{addEventListener:(type,fn)=>parentListeners.push({type,fn})}, setTimeout(){},setInterval(){},fetch(){throw Error('No network expected');},console}); vm.runInContext(parentSource,parent); -vm.runInContext('deckyState={plugins:[{storeId:14}]};openFrameyPanel(14,"Steam Status")',parent); +vm.runInContext('deckyState={running:true,plugins:[{storeId:14,sha256:"a",version:"1"}]};openFrameyPanel(14,"Steam Status")',parent); let session=vm.runInContext('frameySession',parent); const firstURL=new URL(el('frameyFrame').src,'http://127.0.0.1'); const onParent=parentListeners.find(x=>x.type==='message').fn; diff --git a/tests/page/framey_event_parent.mjs b/tests/page/framey_event_parent.mjs index f00d11f..42f7e73 100644 --- a/tests/page/framey_event_parent.mjs +++ b/tests/page/framey_event_parent.mjs @@ -15,7 +15,7 @@ const context=vm.createContext({document:{getElementById:el,createElement:()=>({ setTimeout(){},setInterval(){},console, fetch:(path,options)=>new Promise(resolve=>requests.push({path,body:JSON.parse(options.body),resolve}))}); vm.runInContext(source,context); -vm.runInContext('deckyState={plugins:[{storeId:96}]};openFrameyPanel(96,"Timer")',context); +vm.runInContext('deckyState={running:true,plugins:[{storeId:96,sha256:"a",version:"1"}]};openFrameyPanel(96,"Timer")',context); let session=vm.runInContext('frameySession',context); const onMessage=listeners.find(x=>x.type==='message').fn; const send=(data,origin=frame)=>onMessage({source:origin,data}); diff --git a/tests/page/framey_store.mjs b/tests/page/framey_store.mjs index 210e6b0..8a24c3f 100644 --- a/tests/page/framey_store.mjs +++ b/tests/page/framey_store.mjs @@ -68,6 +68,7 @@ ctx.mockApi=async path => path==='/api/decky' ? vm.runInContext('api=mockApi',ctx); await vm.runInContext('refreshDecky()',ctx); assert.equal(el('deckyInstalled').children[0].children.find(child=>child.textContent==='Remove').disabled,false); +assert.equal(el('deckyInstalled').children[0].children.find(child=>child.textContent==='Open panel').disabled,true); el('deckySearch').value='';el('deckyStoreFilter').value='all'; vm.runInContext('showDeckyStore()',ctx); assert.equal(el('deckyStoreList').children[1].children[1].disabled,true); @@ -97,6 +98,7 @@ await vm.runInContext('deckyChange("install",{id:2,name:"Plugin 2",sha256:"a".re assert.equal(changes,1); assert.equal(session.has('frameyPluginJob'),true); assert.equal(el('deckyJobCheck').hidden,false); +assert.equal(el('deckyInstalled').children[0].children.find(child=>child.textContent==='Open panel').disabled,true); assert.equal(el('deckyInstalled').children[0].children.find(child=>child.textContent==='Remove').disabled,true); assert.equal(el('deckyStoreList').children[1].children[1].disabled,true); await vm.runInContext('deckyChange("remove",{id:2,name:"Plugin 2"})',ctx); @@ -111,6 +113,7 @@ ctx.mockApi=async path => { vm.runInContext('api=mockApi',ctx); await vm.runInContext('checkDeckyJob()',ctx); assert.equal(session.has('frameyPluginJob'),false); +assert.equal(el('deckyInstalled').children[0].children.find(child=>child.textContent==='Open panel').disabled,false); assert.equal(el('deckyInstalled').children[0].children.find(child=>child.textContent==='Remove').disabled,false); assert.equal(el('deckyStoreList').children[1].children[1].disabled,false); @@ -196,3 +199,48 @@ await staleFailure; assert.match(el('deckyInstalled').children[0].children[0].textContent,/Current/); assert.doesNotMatch(el('deckyState').textContent,/offline/); assert.equal(el('deckyStoreList').children[0].children[1].disabled,false); + +// A panel must not keep an old frontend after the installed receipt changes. +let installedHash='a'.repeat(64); +ctx.mockApi=async path => { + assert.equal(path,'/api/decky'); + return {managed:true,running:true,plugins:[{storeId:2,name:'Plugin 2',version:'1.0',sha256:installedHash}]}; +}; +vm.runInContext('api=mockApi',ctx); +await vm.runInContext('refreshDecky()',ctx); +vm.runInContext('openFrameyPanel(2,"Plugin 2")',ctx); +assert.equal(el('frameyPanel').hidden,false); +await vm.runInContext('refreshDecky()',ctx); +assert.equal(el('frameyPanel').hidden,false,'unchanged receipt keeps the current panel'); +installedHash='b'.repeat(64); +await vm.runInContext('refreshDecky()',ctx); +assert.equal(el('frameyPanel').hidden,true); +assert.equal(el('frameyFrame').src,undefined); +assert.match(el('deckyState').textContent,/old plugin panel was closed/); + +// The panel closes before updating its own backend, and stale Open handlers +// cannot recreate it while a job is active. +vm.runInContext('openFrameyPanel(2,"Plugin 2")',ctx); +assert.equal(el('frameyPanel').hidden,false); +ctx.mockApi=async (path,body) => { + if(path==='/api/decky/plugin'){ + assert.equal(el('frameyPanel').hidden,true); + vm.runInContext('openFrameyPanel(2,"Plugin 2")',ctx); + assert.equal(el('frameyPanel').hidden,true); + return {job:'job-update'}; + } + if(path.startsWith('/api/job'))return {done:true,message:'Updated Plugin 2'}; + if(path==='/api/decky')return {managed:true,running:true, + plugins:[{storeId:2,name:'Plugin 2',version:'2.0',sha256:'c'.repeat(64)}]}; + throw Error(`unexpected ${path}`); +}; +vm.runInContext('api=mockApi',ctx); +await vm.runInContext('deckyChange("update",{id:2,name:"Plugin 2",sha256:"c".repeat(64)})',ctx); +assert.equal(el('frameyPanel').hidden,true); +assert.match(el('deckyJobStatus').textContent,/Old plugin panel closed; reopen it/); +vm.runInContext('openFrameyPanel(2,"Plugin 2")',ctx); +ctx.mockApi=async()=>{throw Error('offline');}; +vm.runInContext('api=mockApi',ctx); +await vm.runInContext('refreshDecky()',ctx); +assert.equal(el('frameyPanel').hidden,true); +assert.match(el('deckyState').textContent,/panel was closed until status can be refreshed/); diff --git a/ui/headset.html b/ui/headset.html index 01278f9..3350a25 100644 --- a/ui/headset.html +++ b/ui/headset.html @@ -108,25 +108,34 @@ function compatibilityText(plugin){const c=plugin.compatibility;return c ? `${c. function deckyJobPending(){return !!sessionStorage.getItem('frameyPluginJob');} let deckyState=null, deckyStore=[], deckyStoreIndex=0, deckyStoreRequest=0, deckyStatusRequest=0, - activeFrameyId=null, frameySession=null, deckyBusy=false; + activeFrameyId=null, activeFrameyReceipt=null, frameySession=null, deckyBusy=false; async function refreshDecky(){const request=++deckyStatusRequest;try{const state=await api('/api/decky'); if(request!==deckyStatusRequest)return; deckyState=state; + const active=(state.plugins||[]).find(p=>p.storeId===activeFrameyId); + const panelStale=activeFrameyId!==null&&(!state.running||!active|| + active.sha256!==activeFrameyReceipt?.sha256||active.version!==activeFrameyReceipt?.version); + if(panelStale)closeFrameyPanel(); byId('deckyState').textContent=deckyState.running?'Decky loader is running. Backend loading is not a VR menu check.': deckyState.managed?'Decky is stopped. You can remove a managed plugin to recover, then start Decky from Frame Control on your computer.': 'Decky is not running. Install it from Frame Control on your computer.'; + if(panelStale)byId('deckyState').textContent+=' The old plugin panel was closed; reopen it after refreshing.'; const list=byId('deckyInstalled');list.replaceChildren(); for(const plugin of deckyState.plugins||[]){const row=document.createElement('div');row.className='block'; const name=document.createElement('strong');name.textContent=`${plugin.name} ${plugin.version||''}`;row.append(name); const note=document.createElement('p');note.textContent=compatibilityText(plugin);row.append(note); if(plugin.storeId){const open=document.createElement('button');open.className='secondary';open.textContent='Open panel'; + open.disabled=deckyBusy||deckyJobPending()||!deckyState.running; open.onclick=()=>openFrameyPanel(plugin.storeId,plugin.name);row.append(open); const button=document.createElement('button');button.className='danger';button.textContent='Remove'; button.disabled=deckyBusy||deckyJobPending();button.onclick=()=>deckyChange('remove',{id:plugin.storeId,name:plugin.name});row.append(button);} list.append(row);}if(!deckyState.plugins?.length)list.textContent='No managed plugins installed.'; showDeckyStore();}catch(error){if(request!==deckyStatusRequest)return; + const panelClosed=activeFrameyId!==null; + if(panelClosed)closeFrameyPanel(); deckyState=null; byId('deckyState').textContent=`Installed plugins unavailable: ${error.message}`; + if(panelClosed)byId('deckyState').textContent+=' The plugin panel was closed until status can be refreshed.'; byId('deckyInstalled').replaceChildren(); byId('deckyInstalled').textContent='Refresh to check installed plugins.'; showDeckyStore();}} @@ -191,14 +200,20 @@ async function deckyChange(action,plugin){if(deckyBusy)return; } if(!confirm(`${action} ${plugin.name}? Store plugins run code as your Frame user.`))return; deckyBusy=true;showDeckyStore(); - deckyJobView(`${action} ${plugin.name}`,'Starting…',0); + const panelClosed=(action==='update'||action==='remove')&&activeFrameyId===plugin.id; + const panelNote=panelClosed?(action==='remove'?' Plugin panel closed after removal.': + ' Old plugin panel closed; reopen it after refreshing installed plugins.') : ''; + if(panelClosed)closeFrameyPanel(); + deckyJobView(`${action} ${plugin.name}`, + panelClosed?'Starting… Old plugin panel closed until the change finishes.':'Starting…',0); try{const started=await api('/api/decky/plugin',{action,id:plugin.id, sha256:action==='remove'?undefined:plugin.sha256,acknowledged:true}); if(!started.job)throw Error('The server did not return a job ID'); sessionStorage.setItem('frameyPluginJob',JSON.stringify({job:started.job,action,name:plugin.name})); await followDeckyJob(started.job,action,plugin.name); sessionStorage.removeItem('frameyPluginJob'); - }catch(error){deckyJobView(`${action} ${plugin.name}`,`Error: ${error.message}`,null,true);} + if(panelNote)byId('deckyJobStatus').textContent+=panelNote; + }catch(error){deckyJobView(`${action} ${plugin.name}`,`Error: ${error.message}${panelNote}`,null,true);} finally{deckyBusy=false;await refreshDecky();} } async function checkDeckyJob(){let saved;try{saved=JSON.parse(sessionStorage.getItem('frameyPluginJob')||'null');}catch(_){saved=null;} @@ -210,10 +225,12 @@ async function checkDeckyJob(){let saved;try{saved=JSON.parse(sessionStorage.get finally{deckyBusy=false;await refreshDecky();}} byId('deckyJobCheck').onclick=checkDeckyJob; function openFrameyPanel(id,name){ + if(deckyBusy||deckyJobPending()||!deckyState?.running)return; const installed=(deckyState?.plugins||[]).find(p=>p.storeId===id);if(!installed)return; byId('frameyCompatibility').textContent=compatibilityText(installed); closeFrameyEvents(); - activeFrameyId=id;frameySession=`${Date.now()}-${Math.random()}`;byId('frameyTitle').textContent=name; + activeFrameyId=id;activeFrameyReceipt={sha256:installed.sha256,version:installed.version}; + frameySession=`${Date.now()}-${Math.random()}`;byId('frameyTitle').textContent=name; byId('frameyPanelStatus').textContent='Loading plugin panel…'; byId('frameyFrame').src=`/framey/panel/${id}?session=${encodeURIComponent(frameySession)}#${frameySession}`; byId('frameyPanel').hidden=false;byId('frameyPanel').scrollIntoView(); @@ -226,7 +243,9 @@ function closeFrameyEvents(){ api('/api/framey/events',{id:activeFrameyId,session:frameySession,events:[],cursor:0}).catch(()=>{}); frameyEventsActive=false; } -byId('frameyClose').onclick=()=>{closeFrameyEvents();activeFrameyId=null;frameySession=null;byId('frameyFrame').removeAttribute('src');byId('frameyPanel').hidden=true;}; +function closeFrameyPanel(){closeFrameyEvents();activeFrameyId=null;activeFrameyReceipt=null;frameySession=null; + byId('frameyFrame').removeAttribute('src');byId('frameyPanel').hidden=true;} +byId('frameyClose').onclick=closeFrameyPanel; function frameyCacheKey(id){return `framey-plugin-cache-v1:${id}`;} function frameyCacheRead(id){ try{const data=JSON.parse(localStorage.getItem(frameyCacheKey(id))||'{}'); From a3e0ec01d4811b814b9f75f5952d364ffdeea6a9 Mon Sep 17 00:00:00 2001 From: SAPHID Date: Mon, 26 Oct 2026 05:09:01 +0000 Subject: [PATCH 3/3] Report unavailable x86 runtime as skipped in Frame smoke --- .../frame-smoke-runtime-skip-2026-10-26.md | 9 ++++ docs/testing.md | 28 +++++----- tests/smoke/frame_smoke.py | 36 +++++++++---- tests/test_smoke_cleanup.py | 54 +++++++++++++++++++ 4 files changed, 105 insertions(+), 22 deletions(-) create mode 100644 docs/evidence/frame-smoke-runtime-skip-2026-10-26.md diff --git a/docs/evidence/frame-smoke-runtime-skip-2026-10-26.md b/docs/evidence/frame-smoke-runtime-skip-2026-10-26.md new file mode 100644 index 0000000..4763678 --- /dev/null +++ b/docs/evidence/frame-smoke-runtime-skip-2026-10-26.md @@ -0,0 +1,9 @@ +# Headset smoke: unavailable x86 runtime is a skip + +The v0.4.41 tagged smoke result recorded 13/13 passed even though its x86_64 launch outcome was `runtime missing`: Steam had acknowledged the title but had not started it. This follow-up separates an unavailable optional runtime from a verified launch. It keeps the smoke usable on a Frame without Steam Linux Runtime 4 while making the missing coverage explicit in its console summary and saved JSON. + +Focused local `test_smoke_cleanup.py` ran **22 tests successfully**. The new regression starts an x86_64 title cycle with a fresh missing-runtime Steam log, verifies `skipped=true` and `outcome=runtime missing`, and verifies that the title is still removed. A result test checks separate passed/skipped/failed counts and a zero exit code when an optional runtime is absent but nothing fails. + +The revised `scripts/frame-smoke.sh` then ran on the real Frame from this branch. It acquired its unique owner in `/tmp/frame-test.lock` before any headset read. Locked preflight found SteamVR activity 3 (standby), proximity 6.25 and battery 99%. On Frame OS 0.5.6 build `20261022.6153909`, the result was **12 passed / 1 skipped / 0 failed**. ARM64 and Proton test executables ran; x86_64 was explicitly skipped because Steam Linux Runtime 4 was not installed. Both title cleanup steps passed. The command exited 0, including release of its owned shared lock. The detailed result remains private at `tests/smoke/results/2026-10-26T050711Z-20261022.6153909.json` in the test checkout. + +The headset was in standby and nobody wore it for this test. Title smoke does not prove wearer-visible pixels, physical controller input, official Decky panels, audio output or comfort. It also does not prove x86_64 runtime execution on this Frame; that path needs the optional runtime present. The hourly worn check remains enabled. diff --git a/docs/testing.md b/docs/testing.md index 569206b..49429db 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -176,19 +176,21 @@ switch while SSH is down: ## Headset smoke test -**Documented shared-device procedure:** before a test installs, launches or -stops an application, acquire `ssh frame 'mkdir /tmp/frame-test.lock'`. If it -fails, leave that lock alone and continue offline work. Only the thread that -acquired it releases it with `ssh frame 'rmdir /tmp/frame-test.lock'`, after -cleanup. Keep each device session to a few minutes. +**Documented shared-device procedure:** before any headset read or a test that +installs, launches or stops an application, atomically create +`/tmp/frame-test.lock` on the Frame and write a unique token to its `owner` +file. If creation fails, leave that lock alone and continue offline work. Only +the owner that verifies its token may remove its `owner` file and lock directory +after cleanup. Keep each device session to a few minutes. Check battery capacity and charging state under `/sys/class/power_supply` before and after; keep capacity above 20%. Stop only processes started by the test, remove temporary installs and profiles, and restore the prior dashboard state. Leave Steam and SteamVR running. Do not reboot or change global settings. Record the build, actual interaction results, cleanup and any unworn-headset -limits alongside screenshots or logs. These are caller responsibilities; the -smoke script below does not acquire this shared lock itself. +limits alongside screenshots or logs. The smoke script below acquires and +releases its own tokenized shared lock; other device tests follow this +procedure themselves. [Packaged Desktop Capture evidence, 2026-10-24](evidence/desktop-capture-packaged-2026-10-24.md) records v0.4.17 GUI button captures of a real mapped nested desktop on SteamOS @@ -220,17 +222,19 @@ removes three tiny titles built from bytes by `tests/smoke/tiny_programs.py` (an ARM64 and an x86-64 static Linux program that sleep for ten seconds, and an x86-64 `.exe` that exits at once). A launch passes only with fresh evidence: the ARM64 program running, the `.exe` started (its process or Steam's log), -and the x86-64 program running or Steam logging that its runtime isn't -installed, which is what the Frame does today. Steam's log lines about each -title are kept. +and the x86-64 program running. If Steam logs that the optional x86-64 runtime +is not installed, that launch is **skipped**, with the reason and log saved in +the result. It does not prove that the x86-64 title ran. Steam's log lines about +each title are kept. Everything it installs is removed again, also after a failure: the titles and their Steam shortcuts, a paired key, and `~/devkit-utils` if it wasn't there before (if it was, it stays, synced to this checkout as Frame Control always does). A cleanup that fails counts as a failed step. Results go to `tests/smoke/results/