Tools → Backup & recovery copies game saves (Proton prefixes' AppData,
Documents and Saved Games, Unity and Godot saves), Steam controller layouts,
desktop settings and Android app data (the existing backup-data helper) to
~/Documents/Frame Control Backups/<time>/ with a manifest of every file's size
and SHA-256 and the headset's BUILD_ID. Backing up only reads the headset;
sign-in files, keys and keyrings are refused by name wherever they are.
Restore checks each part against the headset first and skips, rather than
fails, games not installed yet, prefixes Proton hasn't made, a missing Steam
account and closed or missing Android apps. It asks before replacing anything,
writes only inside each part's folders and keeps every replaced file under
~/.local/share/frame-control/restore-previous/.
Return to Game Mode reads the SDDM session config and runs the switch Valve's
devkit tools use (steamos-session-select / holo-session-select gamescope),
only when the headset isn't already in Game Mode and only after confirmation.
Not tested on a headset.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Running now, at the top of Games, lists what's running on the Frame (Steam
games, sideloaded titles, non-Steam shortcuts, Android apps), with Force quit
on each, on a game's library tile and in a sideloaded title's menu. The
Control Center gets a Force quit a game card. Each asks first: unsaved
progress will be lost. Nothing running says so.
ui/frame_running.py finds games by Steam's `reaper SteamLaunch AppId=<id>`
process and its tree, never by name. It asks Steam to quit
(SteamClient.Apps.TerminateApp), then sends SIGTERM and SIGKILL to the tree,
skipping init, the SSH session, other users and Steam, gamescope, SteamVR,
sshd and the session. Android apps also get the Apps tab's container stop.
The fake Frame runs games under a stand-in reaper and lists them in
SteamUIStore.RunningApps, so tests/e2e/test_running.py force quits real
process trees. Not tested on a headset.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Home, Tools and the Control Center now call the keyboard "Type on the
headset", and T opens it from anywhere. The clipboard moves from Send to
Frame to sit beside the keyboard, with a new Copy the headset's clipboard
button: the server reads Klipper's getClipboardContents on the headset
desktop's bus (base64 back, 1 MB limit, empty and image clipboards say so),
and the desktop app, the iPhone app or frame_host.set_clipboard_text puts
it on the clipboard here. Clipboard text is never logged.
Not tested on a headset.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test_agents had no harness.require() and FrameTestCase started ui/server.py
unconditionally, so plain discovery over tests/e2e started a server on this
computer's real app data, ~/.ssh and cache. FRAME_E2E=1 alone outside the
container did the same with the real cache.
require() now also needs FRAME_E2E_CONTAINER=1 (set in compose.yaml) and the
container runtime's marker file, and says why it refuses; FrameTestCase and
Server.start call it before starting anything; test_agents calls it at
module level. test_sandbox checks discovery over tests/e2e, with and
without FRAME_E2E=1, skips everything and starts no server.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tests/e2e/test_android imported its APK helpers from the sandboxed
test_frame_apk, so the HOME move reached the whole e2e process: frame_connect
wrote keys and config into the temp home while OpenSSH, which finds ~/.ssh
from the password database, kept reading the container user's. The helpers
now live in the sandbox-free tests/apk_fixtures.py, and the e2e harness sets
the data dir, telemetry and compat DB guards itself (it used to get them
through that import).
sandbox.py also points LOCALAPPDATA, APPDATA and the XDG folders into the
throwaway home, so test servers on Windows and Linux no longer warm indexes
or sweep staging folders in the real app cache. test_sandbox checks both.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Preserve the complete store, artwork, telemetry, input, media and agent route table alongside the newly landed VR utilities and performance HUD.
Co-Authored-By: GPT-6 Astra <noreply@openai.com>
Keep the union of server routes, desktop resources and responsive controls. Preserve OpenXR install defaults and telemetry hooks alongside library artwork. Adapt the resource test to single-file entries and avoid a completed-refresh race in the F-Droid test.
Co-Authored-By: GPT-6 Astra <noreply@openai.com>
Also from review: a SteamVR build without the timing exports can't break status
(AttributeError), and the device test class runs when the file is run directly.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Title removal ran the Steam shortcut tidy-up before steamos-delete, which
finds the Proton prefix through that shortcut, so compatdata was left behind
(e2e caught it). steamos-delete runs first again; art/collection tidy-up after.
- Test fixtures are byte-exact: never convert line endings (a text-looking
fixture APK got CRLF on Windows and failed its SHA-256).
- Read index.html/artwork-settings.js as UTF-8 in tests; app-data backup and
OBB shell tests run only on POSIX (they exercise the Frame-side scripts).
- e2e expects the icon under artwork/ now.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The tabs UI (#4) runs Flatpak installs as server.start_job jobs, so a failed
install answers 200 with a job id and reports the error on /api/job. The test
now waits for the job instead of expecting an immediate 502.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
On the Frame, compat_log.txt has binary bytes in it, so plain grep only said
"binary file matches" and the x86-64 launch check missed Steam's "is not
installed" line. The fake now writes that line to a compat_log.txt that starts
with a NUL, and the end-to-end test finds it the way the smoke test does.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Headset smoke test: drop the paired key from authorized_keys with a
same-mode copy swapped in, so a failed write can't truncate it; check the
throwaway key with no ssh_config or agent; clean up idempotently (tracked
and leftover titles, their json files, Steam's shortcuts via steamos-delete,
and ~/devkit-utils if it wasn't there before), with a failed cleanup a
failed step; count a launch only with fresh evidence (the process, Steam's
log, or the known missing-runtime line), matching with [d]evkit-game so
pgrep doesn't find its own shell. The test programs sleep 10 s.
Fake Frame: log a launch before its reaper can look for it. e2e: kill a
pairing client's process group when a test ends; accept an aarch64 program
running under QEMU on x86 hosts.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
tests/fakeframe: a container that stands in for the Frame (Arch Linux, or
Arch Linux ARM on arm64) with sshd, rsync, Valve's steamos-devkit-service
and hooks (vendored unmodified), a fake Steam client for the devkit pipe and
the DevTools port (the app's JavaScript runs in Node against stand-in
SteamClient/appStore objects), stubs for steam, wpctl, flatpak, podman,
Lepton and friends, battery and thermal files under /sys, and fault
switches (fakeframe-ctl): pairing mode, approve/deny/timeout, Steam not
running, headset asleep, sshd off, disk full, runtimes missing. A second
container is the computer running Frame Control.
tests/e2e: 29 tests driving the real ui/server.py, frame_connect.py and
frame_titles.py against it; skipped unless FRAME_E2E=1. scripts/e2e.sh
builds, runs and tears down; CI runs it on ubuntu-24.04-arm.
tests/smoke + scripts/frame-smoke.sh: the core cases against a real Frame,
recorded with its BUILD_ID, cleaning up after itself; --pair to pair a
throwaway key. docs/testing.md describes the layers.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>