iPhone app: test pairing and power against a Holo Core stand-in

Valve publishes no Steam Frame OS image, so tests/frame-container builds the
Frame's SSH surface on Valve and Collabora's Holo Core aarch64 base: a
steamos user with a password and sudo, OpenSSH with keys and passwords,
Python, and a systemctl that only records requests.

Against it from the Simulator: password pairing, the host-key pin, the power
password check. Found and fixed: a changed host key or a refused login said
"Can't reach the Frame" and retried forever; they now say "Pair with the
Frame again" and offer that. The server's key rejection now says the header
may be wrong, not only missing.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
saphidandClaude Opus 5.5 committed 2026-09-27 15:44:42 +10:00
1 parent db75d1ca71
commit aafd2dbda8
8 files changed
+78 -14

No files matched your search

+11 -3
View File
@@ -70,7 +70,9 @@ final class AppModel: ObservableObject {
settings = target
} catch {
guard mine == attempt else { return }
fail((error as? FrameFailure)?.message ?? FrameLink.describe(error, host: target.host), retry: false)
let failure = error as? FrameFailure
fail(failure?.message ?? FrameLink.describe(error, host: target.host), retry: false,
needsPairing: failure?.needsPairing ?? false)
return
}
await connect()
@@ -197,11 +199,17 @@ final class AppModel: ObservableObject {
forwarder?.stop()
if let link { await link.close() } // ends its server too
guard current(), !(error is CancellationError) else { return }
fail((error as? FrameFailure)?.message ?? FrameLink.describe(error, host: settings.host), retry: true)
let failure = error as? FrameFailure
fail(failure?.message ?? FrameLink.describe(error, host: settings.host), retry: !(failure?.needsPairing ?? false),
needsPairing: failure?.needsPairing ?? false)
}
}
private func fail(_ message: String, retry: Bool) {
/// Whether the last failure needs the user to pair again rather than wait.
@Published private(set) var needsPairing = false
private func fail(_ message: String, retry: Bool, needsPairing: Bool = false) {
self.needsPairing = needsPairing
phase = .failed(message)
retrying = retry && settings != nil
guard retrying else { return }
@@ -14,6 +14,11 @@ struct FrameControlApp: App {
// and connect to FRAME_TEST_HOST with it (`simctl launch` passes
// SIMCTL_CHILD_FRAME_TEST_HOST through as FRAME_TEST_HOST).
print("FRAME_CONTROL_KEY: \(model.authorizedKeysLine)")
// FRAME_TEST_PAIR="host|user|password" runs the real password pairing.
if model.settings == nil, let pair = ProcessInfo.processInfo.environment["FRAME_TEST_PAIR"] {
let f = pair.components(separatedBy: "|")
if f.count == 3 { await model.pair(host: f[0], user: f[1], password: f[2]); return }
}
if model.settings == nil, let host = ProcessInfo.processInfo.environment["FRAME_TEST_HOST"] {
await model.useKey(host: host, user: "steamos")
return