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:
90
ios/App/WebTermTests/ProjectOpenWiringTests.swift
Normal file
90
ios/App/WebTermTests/ProjectOpenWiringTests.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user