feat(ios): add native settings and storage controls
This commit is contained in:
@@ -174,6 +174,10 @@ private final class MemoryLogStore: LogStore, @unchecked Sendable {
|
||||
func pruneConnections(before _: Date) async throws -> Int {
|
||||
0
|
||||
}
|
||||
|
||||
func clearRuntimeLogs() async throws -> Int {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
private struct LogCenterTestClient: SimAdminClientProtocol {
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
import XCTest
|
||||
|
||||
@testable import MultiSimAdmin
|
||||
|
||||
final class SettingsViewModelTests: XCTestCase {
|
||||
@MainActor
|
||||
func testLoadsRuntimeAppAndDisabledServerCapabilities() async throws {
|
||||
let store = SettingsMemoryLogStore()
|
||||
let model = SettingsViewModel(store: store)
|
||||
|
||||
await model.refresh()
|
||||
|
||||
XCTAssertEqual(model.runtime?.appName, "MultiSimAdmin")
|
||||
XCTAssertEqual(model.runtime?.marketingVersion, "0.1.0")
|
||||
XCTAssertEqual(model.runtime?.buildVersion, "1")
|
||||
XCTAssertEqual(model.runtime?.platform, "iOS")
|
||||
XCTAssertTrue(model.runtime?.deviceModel.isEmpty == false)
|
||||
|
||||
XCTAssertTrue(model.serverCapabilities.isEmpty)
|
||||
XCTAssertEqual(model.serverCapabilitiesNotice, "定时任务与后台刷新已在 iOS 版停用。")
|
||||
XCTAssertNil(model.errorMessage)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testClearsRuntimeLogsOnlyAndReportsRemovedCount() async throws {
|
||||
let store = SettingsMemoryLogStore()
|
||||
try await store.append(
|
||||
runtime: [Self.runtimeLog()],
|
||||
connections: [Self.connectionLog()]
|
||||
)
|
||||
let model = SettingsViewModel(store: store)
|
||||
|
||||
let removed = await model.clearRuntimeLogs()
|
||||
|
||||
XCTAssertEqual(removed, 1)
|
||||
XCTAssertTrue(store.runtimeStorage.isEmpty)
|
||||
XCTAssertEqual(store.connectionStorage.count, 1)
|
||||
XCTAssertEqual(model.notice, "已清除 1 条运行日志。")
|
||||
XCTAssertNil(model.errorMessage)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testClearFailureSetsErrorWithoutChangingStorage() async throws {
|
||||
let store = SettingsMemoryLogStore()
|
||||
try await store.append(
|
||||
runtime: [Self.runtimeLog()],
|
||||
connections: [Self.connectionLog()]
|
||||
)
|
||||
let failing = FailingLogStore(base: store)
|
||||
let model = SettingsViewModel(store: failing)
|
||||
|
||||
let removed = await model.clearRuntimeLogs()
|
||||
|
||||
XCTAssertEqual(removed, 0)
|
||||
XCTAssertEqual(store.runtimeStorage.count, 1)
|
||||
XCTAssertEqual(store.connectionStorage.count, 1)
|
||||
XCTAssertEqual(model.errorMessage, "清除运行日志失败。")
|
||||
XCTAssertNil(model.notice)
|
||||
}
|
||||
|
||||
private static func runtimeLog() -> RuntimeLogEntry {
|
||||
RuntimeLogEntry(
|
||||
id: "runtime-1",
|
||||
source: .delivery,
|
||||
level: .info,
|
||||
message: "测试日志",
|
||||
occurredAt: Date()
|
||||
)
|
||||
}
|
||||
|
||||
private static func connectionLog() -> ConnectionLogEntry {
|
||||
ConnectionLogEntry(
|
||||
id: "connection-1",
|
||||
instanceID: UUID(),
|
||||
instanceName: "Node",
|
||||
outcome: .success,
|
||||
state: "online",
|
||||
durationMs: 20,
|
||||
observedAt: Date()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private final class SettingsMemoryLogStore: LogStore, @unchecked Sendable {
|
||||
private let lock = NSLock()
|
||||
private var runtimeItems: [RuntimeLogEntry] = []
|
||||
private var connectionItems: [ConnectionLogEntry] = []
|
||||
|
||||
var runtimeStorage: [RuntimeLogEntry] {
|
||||
lock.withLock { runtimeItems }
|
||||
}
|
||||
|
||||
var connectionStorage: [ConnectionLogEntry] {
|
||||
lock.withLock { connectionItems }
|
||||
}
|
||||
|
||||
func runtimeLogs() async throws -> [RuntimeLogEntry] {
|
||||
runtimeStorage
|
||||
}
|
||||
|
||||
func connectionLogs() async throws -> [ConnectionLogEntry] {
|
||||
connectionStorage
|
||||
}
|
||||
|
||||
func append(
|
||||
runtime: [RuntimeLogEntry],
|
||||
connections: [ConnectionLogEntry]
|
||||
) async throws {
|
||||
lock.withLock {
|
||||
var mergedRuntime = runtime
|
||||
for existing in runtimeItems where !runtime.contains(where: { $0.id == existing.id }) {
|
||||
mergedRuntime.append(existing)
|
||||
}
|
||||
runtimeItems = mergedRuntime.sorted { $0.occurredAt > $1.occurredAt }
|
||||
|
||||
var mergedConnections = connections
|
||||
for existing in connectionItems
|
||||
where !connections.contains(where: { $0.id == existing.id }) {
|
||||
mergedConnections.append(existing)
|
||||
}
|
||||
connectionItems = mergedConnections.sorted { $0.observedAt > $1.observedAt }
|
||||
}
|
||||
}
|
||||
|
||||
func pruneConnections(before _: Date) async throws -> Int {
|
||||
0
|
||||
}
|
||||
|
||||
func clearRuntimeLogs() async throws -> Int {
|
||||
lock.withLock {
|
||||
let removed = runtimeItems.count
|
||||
runtimeItems = []
|
||||
return removed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct FailingLogStore: LogStore {
|
||||
let base: SettingsMemoryLogStore
|
||||
|
||||
func runtimeLogs() async throws -> [RuntimeLogEntry] {
|
||||
try await base.runtimeLogs()
|
||||
}
|
||||
|
||||
func connectionLogs() async throws -> [ConnectionLogEntry] {
|
||||
try await base.connectionLogs()
|
||||
}
|
||||
|
||||
func append(
|
||||
runtime: [RuntimeLogEntry],
|
||||
connections: [ConnectionLogEntry]
|
||||
) async throws {
|
||||
try await base.append(runtime: runtime, connections: connections)
|
||||
}
|
||||
|
||||
func pruneConnections(before cutoff: Date) async throws -> Int {
|
||||
try await base.pruneConnections(before: cutoff)
|
||||
}
|
||||
|
||||
func clearRuntimeLogs() async throws -> Int {
|
||||
throw RepositoryError.databaseFailed
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user