From 005dd28d63da097ef3aaf6bf18e15b2ff71774a6 Mon Sep 17 00:00:00 2001 From: wangzhizhou <824219521@qq.com> Date: Sat, 24 Oct 2026 23:50:32 +0800 Subject: [PATCH] =?UTF-8?q?fix(status):=20=E8=A1=A5=E4=B8=8A=20TRANSFERRIN?= =?UTF-8?q?G(9)=20=E5=B9=B6=E8=AE=A9=E6=9C=AA=E7=9F=A5=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E7=A0=81=E9=80=80=E5=8C=96=E8=80=8C=E4=B8=8D=E6=98=AF=E6=95=B4?= =?UTF-8?q?=E6=9D=A1=E6=B6=88=E6=81=AF=E8=A7=A3=E7=A0=81=E5=A4=B1=E8=B4=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 官方状态码有 0..10(含 9 = TRANSFERRING),但 `ServerStatus` 枚举漏了 9。 后果不是"显示错一个值",而是: - WebSocket 的 `status` 消息 `data` 解码成 `Server` 时,枚举初始化失败 → 整条消息抛错 → 上层收不到这次状态更新(服务器启动/停止时的迁移阶段会卡住)。 实测(真实账号,HTTP 轮询与 WS 推送对照)确认服务器会依次经过 10 PREPARING → 6 LOADING → 2 STARTING → 1 ONLINE → 3 STOPPING → 5 SAVING → 0 OFFLINE, 其中迁移阶段的状态码就是 9。 改动: - `ServerStatus` 补 `case TRANSFERRING = 9`; - 新增 `case UNKNOWN = -1` 与宽容的 `init(from:)`:将来新增状态码时退化成 `.UNKNOWN`, 不再让整条 `Server` 解码失败; - 新增 `ServerStatusDecodingTests`:11 个官方取值逐一断言 + 未知码退化用例。 上层(OrzMCApp)需要相应把 `.UNKNOWN` 当成过渡态展示。 --- Sources/ExarotonWebSocket/Models.swift | 21 +++++++++ .../ServerStatusDecodingTests.swift | 45 +++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 Tests/ExarotonWebSocketTests/ServerStatusDecodingTests.swift diff --git a/Sources/ExarotonWebSocket/Models.swift b/Sources/ExarotonWebSocket/Models.swift index 0613055..3c02b5b 100644 --- a/Sources/ExarotonWebSocket/Models.swift +++ b/Sources/ExarotonWebSocket/Models.swift @@ -7,6 +7,18 @@ import Foundation +/// exaroton 服务器状态码(https://developers.exaroton.com/ → "Server status")。 +/// +/// ``` +/// 0 = OFFLINE 1 = ONLINE 2 = STARTING 3 = STOPPING 4 = RESTARTING +/// 5 = SAVING 6 = LOADING 7 = CRASHED 8 = PENDING 9 = TRANSFERRING +/// 10 = PREPARING +/// ``` +/// +/// ⚠️ **必须覆盖全部官方取值**:这个枚举参与 WebSocket `status` 消息的解码, +/// 少一个值就意味着"整个状态消息解码失败 → 状态更新被丢掉"(实测漏了 9 = TRANSFERRING, +/// 服务器启动/停止期间的迁移阶段状态会卡住不动)。 +/// 另外解码是**宽容**的:将来 exaroton 新增状态码时退化成 `.UNKNOWN`,而不是让整条消息失败。 public enum ServerStatus: Int, CaseIterable, Codable { case OFFLINE = 0 case ONLINE = 1 @@ -17,7 +29,16 @@ public enum ServerStatus: Int, CaseIterable, Codable { case LOADING = 6 case CRASHED = 7 case PENDING = 8 + case TRANSFERRING = 9 case PREPARING = 10 + + /// 未知/未来新增的状态码。调用方应把它当成"过渡态"处理(不允许动作,等状态稳定)。 + case UNKNOWN = -1 + + public init(from decoder: any Decoder) throws { + let raw = try decoder.singleValueContainer().decode(Int.self) + self = ServerStatus(rawValue: raw) ?? .UNKNOWN + } } public struct Server: Codable, Identifiable { diff --git a/Tests/ExarotonWebSocketTests/ServerStatusDecodingTests.swift b/Tests/ExarotonWebSocketTests/ServerStatusDecodingTests.swift new file mode 100644 index 0000000..0df042a --- /dev/null +++ b/Tests/ExarotonWebSocketTests/ServerStatusDecodingTests.swift @@ -0,0 +1,45 @@ +import Foundation +import Testing +@testable import ExarotonWebSocket + +/// 状态码解码:文档里的每个取值都必须能解出来。 +/// +/// 回归背景:枚举曾漏掉 `9 = TRANSFERRING`,导致服务器启动/停止迁移期间的 +/// `status` 消息**整条解码失败**,上层看到的就是"状态卡住不动"。 +@Suite(.tags(.unit, .websocket, .codec)) +final class ServerStatusDecodingTests { + + private func serverJSON(status: Int) -> Data { + Data(""" + {"id":"X","name":"demo","address":"demo.exaroton.me","motd":"hi","status":\(status), + "players":{"max":20,"count":0,"list":[]}, + "software":{"id":"1","name":"Paper","version":"1.0"},"shared":false} + """.utf8) + } + + @Test(arguments: [ + (0, ServerStatus.OFFLINE), + (1, ServerStatus.ONLINE), + (2, ServerStatus.STARTING), + (3, ServerStatus.STOPPING), + (4, ServerStatus.RESTARTING), + (5, ServerStatus.SAVING), + (6, ServerStatus.LOADING), + (7, ServerStatus.CRASHED), + (8, ServerStatus.PENDING), + (9, ServerStatus.TRANSFERRING), + (10, ServerStatus.PREPARING), + ]) + func decodesEveryDocumentedCode(raw: Int, expected: ServerStatus) throws { + let server = try JSONDecoder().decode(Server.self, from: serverJSON(status: raw)) + #expect(server.status == expected) + #expect(server.status.rawValue == raw) + } + + /// 将来 exaroton 新增状态码时不能把整条消息炸掉,退化成 `.UNKNOWN`。 + @Test + func unknownCodeDegradesInsteadOfThrowing() throws { + let server = try JSONDecoder().decode(Server.self, from: serverJSON(status: 99)) + #expect(server.status == .UNKNOWN) + } +}