feat(ios,android): P2 wave, git panel, token UX, per-host WS token, docs
App layer, four sequential slices (a shared .xcodeproj means adding files regenerates it, so these could not run in parallel): - token UX end to end: pairing prompts for a token when a host 401s, POST /auth validates it, and 204-without-Set-Cookie is correctly read as "this server has auth disabled" rather than "authenticated". A host paired before the token was turned on recovers by re-pairing in place. Remove-host now exists and finally gives PushRegistrar.handleHostRemoved a caller. - project git panel + worktree lifecycle (T-iOS-32) + claude --resume history — the parity gap with Android and the web front end. - terminal search (T-iOS-33) and voice PTT (T-iOS-31) with an epoch guard so a session switch between dictation and confirm cannot inject into the wrong session. - theme + Dynamic Type (T-iOS-34) and web ?join= interop (T-iOS-35). RootView no longer hard-locks .preferredColorScheme(.dark). Also unpins SwiftTerm to 1.15.0 by dropping the local hasActiveSelection that collided with the upstream one, verified green from a fresh derivedDataPath. Includes the two HIGH fixes the security review found: - iOS resolved the WS token host-independently, so a token-gated host sitting next to an open one could never open a terminal and no on-screen remedy could fix it. Now one transport per host; cross-host leakage is structurally impossible since both read paths return only that host's own value. - Android reported the host's own git-credential 401 (git-ops.ts:108, "Push authentication required on the host.") as "your access token is wrong", because a blanket 401 mapping ran ahead of the per-route one. Git-write routes are now ROUTE_DEFINED and keep the server's message. And the doc sync: README/ios README no longer claim the client is unmerged on feat/ios-client, the Clients section finally lists Android, and the plan checkboxes reflect what is actually built. iOS 534 app tests + 452 package tests; Android 687 tests.
This commit is contained in:
@@ -42,7 +42,14 @@ struct PairingViewModelTests {
|
||||
store: any HostStore = InMemoryHostStore(),
|
||||
script: ProbeScript
|
||||
) -> PairingViewModel {
|
||||
PairingViewModel(store: store, probe: { await script.invoke($0) })
|
||||
// C1 · `Probe` is now a value carrying the three capabilities; the
|
||||
// token/push ones keep their zero-config defaults for these tests.
|
||||
PairingViewModel(
|
||||
store: store,
|
||||
probe: PairingViewModel.Probe(verifyHost: { endpoint, _ in
|
||||
await script.invoke(endpoint)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
private struct StoreFailure: Error {}
|
||||
@@ -67,7 +74,11 @@ struct PairingViewModelTests {
|
||||
let store = InMemoryHostStore()
|
||||
let viewModel = PairingViewModel(
|
||||
store: store,
|
||||
probe: { await runPairingProbe(endpoint: $0, http: http, ws: ws) }
|
||||
probe: PairingViewModel.Probe(verifyHost: { endpoint, token in
|
||||
await runPairingProbe(
|
||||
endpoint: endpoint, http: http, ws: ws, accessToken: token?.rawValue
|
||||
)
|
||||
})
|
||||
)
|
||||
let base = "http://192.168.1.5:3000"
|
||||
let probeSessionId = "1b671a64-40d5-491e-99b0-da01ff1f3341"
|
||||
@@ -270,8 +281,10 @@ struct PairingViewModelTests {
|
||||
(PairingError.httpOkButNotWebTerminal,
|
||||
PairingViewModel.RecoveryAction.retry,
|
||||
["端口"]),
|
||||
// C1 · 401 is ambiguous (Origin OR access token, same status), so the
|
||||
// primary offered remedy is now the token prompt; retry stays on screen.
|
||||
(PairingError.originRejected(hint: "在主机加 ALLOWED_ORIGINS=http://192.168.1.5:3000"),
|
||||
PairingViewModel.RecoveryAction.retry,
|
||||
PairingViewModel.RecoveryAction.enterAccessToken,
|
||||
["ALLOWED_ORIGINS=http://192.168.1.5:3000"]),
|
||||
(PairingError.atsBlocked(host: "198.18.0.1"),
|
||||
PairingViewModel.RecoveryAction.retry,
|
||||
|
||||
Reference in New Issue
Block a user