初始提交:terminalX 可运行态(M0/M1/M1.5 已真机验证)

- M0: libghostty SSH 终端(渲染/输入/连接)+ 白屏修复(OutputGate) + 会话状态机/自动重连
- M1: tsnet 用户态组网 + SSH-over-tsnet(fd 桥),shell 级真机验证;R5(Go+gvisor+C+++Swift 同进程) retire
- M1.5: tmux -CC 原生 tab(MVP)
- 结构: packages/(TXCore·TXTransport), apps/TerminalX, vendor/(libghostty-spm/libssh2/mbedtls/tsnet-bridge), artifacts/
- 文档: CLAUDE.md + docs/HANDOFF.md(新会话入口)
- 环境: 认证代理→依赖 vendor 本地化;Go 在 ~/.local/go;仅模拟器/未签名
- 待续: M2 mosh, tmux 多 pane, M4 安全(host key/SE)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
kid
2026-07-24 10:20:46 +08:00
commit aa92d0e676
2761 changed files with 803505 additions and 0 deletions

View File

@@ -0,0 +1,128 @@
import Testing
@testable import TXCore
@Suite("会话生命周期状态机")
struct SessionMachineTests {
@Test("正常连接流程")
func happyPath() {
var m = SessionMachine()
#expect(m.reduce(.connectRequested) == [.startConnect])
#expect(m.phase == .connecting)
#expect(m.reduce(.authenticating) == [])
#expect(m.phase == .authenticating)
#expect(m.reduce(.established) == [.notify("已连接")])
#expect(m.phase == .connected)
}
@Test("认证失败为终态,不自动重连")
func authFailedTerminal() {
var m = SessionMachine()
_ = m.reduce(.connectRequested)
let effects = m.reduce(.authFailed(reason: "密码错误"))
#expect(m.phase == .failed(reason: "密码错误"))
#expect(effects.contains(.teardownTransport))
// failed
#expect(m.reduce(.transportClosed(reason: nil)) == [])
#expect(m.phase == .failed(reason: "密码错误"))
}
@Test("断线进入退避重连")
func disconnectSchedulesReconnect() {
var m = SessionMachine()
_ = m.reduce(.connectRequested)
_ = m.reduce(.established)
let effects = m.reduce(.transportClosed(reason: "eof"))
#expect(m.phase == .waitingToReconnect(attempt: 1))
#expect(effects.contains(.teardownTransport))
#expect(effects.contains(.scheduleReconnect(attempt: 1, delayMS: 500)))
}
@Test("重连计时→重连→成功")
func reconnectSuccess() {
var m = SessionMachine()
_ = m.reduce(.connectRequested)
_ = m.reduce(.established)
_ = m.reduce(.transportClosed(reason: nil))
#expect(m.reduce(.reconnectTimerFired) == [.startConnect])
#expect(m.phase == .reconnecting(attempt: 1))
#expect(m.reduce(.established) == [.notify("已重连")])
#expect(m.phase == .connected)
}
@Test("重连反复失败,退避递增,达上限转 failed")
func reconnectExhaustion() {
var m = SessionMachine(maxReconnectAttempts: 3, baseDelayMS: 500, maxDelayMS: 16000)
_ = m.reduce(.connectRequested)
_ = m.reduce(.established)
// 1
_ = m.reduce(.transportClosed(reason: nil))
#expect(m.phase == .waitingToReconnect(attempt: 1))
_ = m.reduce(.reconnectTimerFired) // reconnecting(1)
// 2attempt 2, delay 1000
let e2 = m.reduce(.transportClosed(reason: nil))
#expect(m.phase == .waitingToReconnect(attempt: 2))
#expect(e2.contains(.scheduleReconnect(attempt: 2, delayMS: 1000)))
_ = m.reduce(.reconnectTimerFired) // reconnecting(2)
// 3attempt 3, delay 2000
let e3 = m.reduce(.transportClosed(reason: nil))
#expect(m.phase == .waitingToReconnect(attempt: 3))
#expect(e3.contains(.scheduleReconnect(attempt: 3, delayMS: 2000)))
_ = m.reduce(.reconnectTimerFired) // reconnecting(3)
// 4 maxReconnectAttempts(3) failed
let e4 = m.reduce(.transportClosed(reason: "gone"))
if case .failed = m.phase {} else { Issue.record("期望 failed实际 \(m.phase)") }
#expect(e4.contains(.notify("重连失败,请手动重试")))
}
@Test("退避封顶")
func backoffCap() {
let m = SessionMachine(baseDelayMS: 500, maxDelayMS: 16000)
#expect(m.backoffDelayMS(attempt: 1) == 500)
#expect(m.backoffDelayMS(attempt: 2) == 1000)
#expect(m.backoffDelayMS(attempt: 5) == 8000)
#expect(m.backoffDelayMS(attempt: 6) == 16000)
#expect(m.backoffDelayMS(attempt: 10) == 16000) //
}
@Test("后台冻结与前台恢复")
func backgroundResume() {
var m = SessionMachine()
_ = m.reduce(.connectRequested)
_ = m.reduce(.established)
#expect(m.reduce(.enteredBackground) == [])
#expect(m.phase == .backgroundParked)
let effects = m.reduce(.enteredForeground)
#expect(m.phase == .reconnecting(attempt: 1))
#expect(effects.contains(.startConnect))
}
@Test("等待重连时进入后台取消计时")
func backgroundDuringWait() {
var m = SessionMachine()
_ = m.reduce(.connectRequested)
_ = m.reduce(.established)
_ = m.reduce(.transportClosed(reason: nil))
#expect(m.phase == .waitingToReconnect(attempt: 1))
#expect(m.reduce(.enteredBackground) == [.cancelReconnectTimer])
#expect(m.phase == .backgroundParked)
}
@Test("主动关闭与手动重试复位")
func closeAndRetry() {
var m = SessionMachine()
_ = m.reduce(.connectRequested)
_ = m.reduce(.established)
let closeEffects = m.reduce(.closeRequested)
#expect(m.phase == .closed)
#expect(closeEffects.contains(.teardownTransport))
// failed
var m2 = SessionMachine()
_ = m2.reduce(.connectRequested)
_ = m2.reduce(.authFailed(reason: "x"))
#expect(m2.reduce(.retryRequested) == [.startConnect])
#expect(m2.phase == .connecting)
}
}