feat(ios): P1-B — W7 UI wave: deep links, timeline, quick-reply, diff, projects, session switcher, thumbnails, lock-screen push

T-iOS-22: DeepLinkRouter (full-field whitelist, cold-start stash, route(from:) for push-tap reuse), 21 tests
T-iOS-24: TimelineSheet mirroring web render() order; disabled→empty-state; reuses AwayDigest onExpand
T-iOS-25: QuickReply chips (built-ins mirror quick-reply.ts via KeyByteMap; visible iff live gate && !readOnly)
T-iOS-27: read-only DiffScreen + App-layer DiffFetcher (RO no-Origin; APIClient fold-in noted for T-iOS-38 owner)
T-iOS-26: Projects list/detail — grouping byte-identical to web group keys (prefs-shared collapse state),
prefs-clobber defenses (no blind PUT on empty base; adopt server echo), claude\r bootstrap
T-iOS-23: UnreadLedger + TitleSanitizer (SessionCore, +15 tests), lastOutputAt decode, list-boundary re-sanitize
T-iOS-29: new-in-cwd (untrusted cwd, no bootstrap re-injection) + exited-session reopen; fixes stale-controller
SwiftTerm view bug via .id(controller.id)
T-iOS-28: offscreen SwiftTerm thumbnail pipeline (LRU 32, concurrency gate 2, 256KiB cap, grid clamp)
T-iOS-21: PushRegistrar (WEBTERM_GATE category: Allow=.authenticationRequired, no .foreground) +
NotificationActionHandler (whitelisted payload, token never persisted, bg-task-wrapped POST, 403 fallback)
CRITICAL fix (verify-found boot crash): @Sendable literals on UN completion closures — MainActor-inherited
closures trapped Swift 6 executor check on UN's background queue; boot re-verified (no new crash reports,
permission prompt reached, privacy shade correctly covering during system alert)
Verified: 261 pkg + 247 app + 10 integration tests green; 7/7 semantics checks; Owns audit clean
This commit is contained in:
Yaojia Wang
2026-07-05 16:15:57 +02:00
parent 4871e8ac3d
commit f40b8f9400
52 changed files with 8645 additions and 28 deletions

View File

@@ -0,0 +1,90 @@
import APIClient
import Foundation
import HostRegistry
import SessionCore
import TestSupport
import Testing
import WireProtocol
@testable import WebTerm
/// T-iOS-26 · "" 线`TerminalSessionController`
/// `spawnCwd` + `bootstrapInput` 线 = `attach(null, cwd)`
/// `input("claude\r")`engine attach-first bootstrap
/// attach 线 sessionId cwd bootstrap
///
/// `SessionEngine` over `FakeTransport`
/// `openTask` + `waitUntilProcessed` open+send attach
///
@MainActor
@Suite("ProjectOpenWiring")
struct ProjectOpenWiringTests {
private struct Fixture {
let transport: FakeTransport
let host: HostRegistry.Host
let environment: AppEnvironment
}
private func makeFixture() throws -> Fixture {
let baseURL = try #require(URL(string: "http://192.168.1.5:3000"))
let endpoint = try #require(HostEndpoint(baseURL: baseURL))
let transport = FakeTransport()
let defaults = try #require(UserDefaults(suiteName: "ProjectOpenWiringTests"))
return Fixture(
transport: transport,
host: HostRegistry.Host(id: UUID(), name: "书房 Mac", endpoint: endpoint),
environment: AppEnvironment(
hostStore: InMemoryHostStore(),
lastSessionStore: UserDefaultsLastSessionStore(defaults: defaults),
http: FakeHTTPTransport(),
termTransport: transport,
probe: { _ in .failure(.timeout) }
)
)
}
/// controller open+bootstrap Task send /
/// attach .connecting/.connected VM
private func awaitAttachSettled(_ controller: TerminalSessionController) async {
await controller.openTask?.value
await controller.terminalViewModel.waitUntilProcessed(eventCount: 2)
}
@Test("spawn 变体:帧序 = attach(null, cwd) → input(claude\\r)")
func spawnSendsAttachWithCwdThenBootstrapInput() async throws {
let fixture = try makeFixture()
let controller = TerminalSessionController(
host: fixture.host, sessionId: nil, environment: fixture.environment,
onPendingChanged: { _, _ in },
spawnCwd: "/repos/api",
bootstrapInput: ProjectLaunch.claudeBootstrapInput
)
controller.start()
await awaitAttachSettled(controller)
let frames = await fixture.transport.sentFrames
#expect(frames == [
MessageCodec.encode(.attach(sessionId: nil, cwd: "/repos/api")),
MessageCodec.encode(.input(data: ProjectLaunch.claudeBootstrapInput)),
])
controller.teardown()
}
@Test("常规打开(带 sessionId只有 attach无 cwd、无 bootstrap")
func plainOpenSendsBareAttach() async throws {
let fixture = try makeFixture()
let sessionId = UUID()
let controller = TerminalSessionController(
host: fixture.host, sessionId: sessionId,
environment: fixture.environment,
onPendingChanged: { _, _ in }
)
controller.start()
await awaitAttachSettled(controller)
let frames = await fixture.transport.sentFrames
#expect(frames == [MessageCodec.encode(.attach(sessionId: sessionId, cwd: nil))])
controller.teardown()
}
}