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) // 第2次断(attempt 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) // 第3次断(attempt 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) } }