refactor(macos): drop duplicate AnyCodable

This commit is contained in:
Peter Steinberger
2026-01-07 19:30:01 +00:00
parent e4f62c5b0c
commit 8913bfbcd5
24 changed files with 64 additions and 90 deletions

View File

@@ -1,3 +1,4 @@
import ClawdbotProtocol
import SwiftUI
@MainActor

View File

@@ -1,54 +0,0 @@
import Foundation
/// Lightweight `Codable` wrapper that round-trips heterogeneous JSON payloads.
/// Marked `@unchecked Sendable` because it can hold reference types.
struct AnyCodable: Codable, @unchecked Sendable {
let value: Any
init(_ value: Any) { self.value = value }
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let intVal = try? container.decode(Int.self) { self.value = intVal; return }
if let doubleVal = try? container.decode(Double.self) { self.value = doubleVal; return }
if let boolVal = try? container.decode(Bool.self) { self.value = boolVal; return }
if let stringVal = try? container.decode(String.self) { self.value = stringVal; return }
if container.decodeNil() { self.value = NSNull(); return }
if let dict = try? container.decode([String: AnyCodable].self) { self.value = dict; return }
if let array = try? container.decode([AnyCodable].self) { self.value = array; return }
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: "Unsupported type")
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self.value {
case let intVal as Int: try container.encode(intVal)
case let doubleVal as Double: try container.encode(doubleVal)
case let boolVal as Bool: try container.encode(boolVal)
case let stringVal as String: try container.encode(stringVal)
case is NSNull: try container.encodeNil()
case let dict as [String: AnyCodable]: try container.encode(dict)
case let array as [AnyCodable]: try container.encode(array)
case let dict as [String: Any]:
try container.encode(dict.mapValues { AnyCodable($0) })
case let array as [Any]:
try container.encode(array.map { AnyCodable($0) })
case let dict as NSDictionary:
var converted: [String: AnyCodable] = [:]
for (k, v) in dict {
guard let key = k as? String else { continue }
converted[key] = AnyCodable(v)
}
try container.encode(converted)
case let array as NSArray:
try container.encode(array.map { AnyCodable($0) })
default:
let context = EncodingError.Context(
codingPath: encoder.codingPath,
debugDescription: "Unsupported type")
throw EncodingError.invalidValue(self.value, context)
}
}
}

View File

@@ -229,7 +229,7 @@ actor BridgeServer {
error: BridgeRPCError(code: "FORBIDDEN", message: "Method not allowed"))
}
let params: [String: AnyCodable]?
let params: [String: ClawdbotProtocol.AnyCodable]?
if let json = req.paramsJSON?.trimmingCharacters(in: .whitespacesAndNewlines), !json.isEmpty {
guard let data = json.data(using: .utf8) else {
return BridgeRPCResponse(
@@ -238,7 +238,7 @@ actor BridgeServer {
error: BridgeRPCError(code: "INVALID_REQUEST", message: "paramsJSON not UTF-8"))
}
do {
params = try JSONDecoder().decode([String: AnyCodable].self, from: data)
params = try JSONDecoder().decode([String: ClawdbotProtocol.AnyCodable].self, from: data)
} catch {
return BridgeRPCResponse(
id: req.id,
@@ -360,16 +360,16 @@ actor BridgeServer {
"reason \(reason)",
].compactMap(\.self).joined(separator: " · ")
var params: [String: AnyCodable] = [
"text": AnyCodable(summary),
"instanceId": AnyCodable(nodeId),
"host": AnyCodable(host),
"mode": AnyCodable("node"),
"reason": AnyCodable(reason),
"tags": AnyCodable(tags),
var params: [String: ClawdbotProtocol.AnyCodable] = [
"text": ClawdbotProtocol.AnyCodable(summary),
"instanceId": ClawdbotProtocol.AnyCodable(nodeId),
"host": ClawdbotProtocol.AnyCodable(host),
"mode": ClawdbotProtocol.AnyCodable("node"),
"reason": ClawdbotProtocol.AnyCodable(reason),
"tags": ClawdbotProtocol.AnyCodable(tags),
]
if let ip { params["ip"] = AnyCodable(ip) }
if let version { params["version"] = AnyCodable(version) }
if let ip { params["ip"] = ClawdbotProtocol.AnyCodable(ip) }
if let version { params["version"] = ClawdbotProtocol.AnyCodable(version) }
await GatewayConnection.shared.sendSystemEvent(params)
}

View File

@@ -1,3 +1,4 @@
import ClawdbotProtocol
import Foundation
enum ClawdbotConfigFile {

View File

@@ -3,9 +3,9 @@ import Foundation
enum ClawdbotEnv {
static func path(_ key: String) -> String? {
// Normalize env overrides once so UI + file IO stay consistent.
guard let value = ProcessInfo.processInfo.environment[key]?
.trimmingCharacters(in: .whitespacesAndNewlines),
!value.isEmpty
guard let raw = getenv(key) else { return nil }
let value = String(cString: raw).trimmingCharacters(in: .whitespacesAndNewlines)
guard !value.isEmpty
else {
return nil
}

View File

@@ -1,3 +1,4 @@
import ClawdbotProtocol
import Foundation
enum ConfigStore {

View File

@@ -1,3 +1,4 @@
import ClawdbotProtocol
import Foundation
import SwiftUI

View File

@@ -1,3 +1,4 @@
import ClawdbotProtocol
import SwiftUI
struct CronJobEditor: View {

View File

@@ -1,3 +1,4 @@
import ClawdbotProtocol
import Foundation
extension CronSettings {

View File

@@ -1,4 +1,5 @@
import ClawdbotKit
import ClawdbotProtocol
import Foundation
import Observation
import SwiftUI
@@ -53,7 +54,7 @@ final class WorkActivityStore {
phase: String,
name: String?,
meta: String?,
args: [String: AnyCodable]?)
args: [String: ClawdbotProtocol.AnyCodable]?)
{
let toolKind = Self.mapToolKind(name)
let label = Self.buildLabel(name: name, meta: meta, args: args)
@@ -211,7 +212,7 @@ final class WorkActivityStore {
private static func buildLabel(
name: String?,
meta: String?,
args: [String: AnyCodable]?) -> String
args: [String: ClawdbotProtocol.AnyCodable]?) -> String
{
let wrappedArgs = self.wrapToolArgs(args)
let display = ToolDisplayRegistry.resolve(name: name ?? "tool", args: wrappedArgs, meta: meta)
@@ -221,17 +222,17 @@ final class WorkActivityStore {
return display.label
}
private static func wrapToolArgs(_ args: [String: AnyCodable]?) -> ClawdbotKit.AnyCodable? {
private static func wrapToolArgs(_ args: [String: ClawdbotProtocol.AnyCodable]?) -> ClawdbotKit.AnyCodable? {
guard let args else { return nil }
let converted: [String: Any] = args.mapValues { self.unwrapJSONValue($0.value) }
return ClawdbotKit.AnyCodable(converted)
}
private static func unwrapJSONValue(_ value: Any) -> Any {
if let dict = value as? [String: AnyCodable] {
if let dict = value as? [String: ClawdbotProtocol.AnyCodable] {
return dict.mapValues { self.unwrapJSONValue($0.value) }
}
if let array = value as? [AnyCodable] {
if let array = value as? [ClawdbotProtocol.AnyCodable] {
return array.map { self.unwrapJSONValue($0.value) }
}
if let dict = value as? [String: Any] {

View File

@@ -1,5 +1,6 @@
import Foundation
import Testing
import ClawdbotProtocol
@testable import Clawdbot
@Suite
@@ -15,7 +16,7 @@ struct AgentEventStoreTests {
seq: 1,
stream: "test",
ts: 0,
data: [:] as [String: AnyCodable],
data: [:] as [String: ClawdbotProtocol.AnyCodable],
summary: nil))
#expect(store.events.count == 1)
@@ -32,7 +33,7 @@ struct AgentEventStoreTests {
seq: i,
stream: "test",
ts: Double(i),
data: [:] as [String: AnyCodable],
data: [:] as [String: ClawdbotProtocol.AnyCodable],
summary: nil))
}

View File

@@ -12,7 +12,7 @@ import Testing
"null": NSNull(),
]
let data = try JSONEncoder().encode(Clawdbot.AnyCodable(payload))
let data = try JSONEncoder().encode(ClawdbotProtocol.AnyCodable(payload))
let obj = try #require(JSONSerialization.jsonObject(with: data) as? [String: Any])
#expect(obj["tags"] as? [String] == ["node", "ios"])

View File

@@ -35,7 +35,7 @@ struct CronJobEditorSmokeTests {
thinking: "low",
timeoutSeconds: 120,
deliver: true,
channel: "whatsapp",
provider: "whatsapp",
to: "+15551234567",
bestEffortDeliver: true),
isolation: CronIsolation(postToMainPrefix: "Cron"),

View File

@@ -31,7 +31,7 @@ struct CronModelsTests {
thinking: "low",
timeoutSeconds: 15,
deliver: true,
channel: "whatsapp",
provider: "whatsapp",
to: "+15551234567",
bestEffortDeliver: false)
let data = try JSONEncoder().encode(payload)

View File

@@ -170,7 +170,7 @@ import Testing
let url = URL(string: "ws://example.invalid")!
let cfg = ConfigSource(token: nil)
let conn = GatewayConnection(
configProvider: { (url, cfg.snapshotToken()) },
configProvider: { (url: url, token: cfg.snapshotToken(), password: nil) },
sessionBox: WebSocketSessionBox(session: session))
_ = try await conn.request(method: "status", params: nil)
@@ -186,7 +186,7 @@ import Testing
let url = URL(string: "ws://example.invalid")!
let cfg = ConfigSource(token: "a")
let conn = GatewayConnection(
configProvider: { (url, cfg.snapshotToken()) },
configProvider: { (url: url, token: cfg.snapshotToken(), password: nil) },
sessionBox: WebSocketSessionBox(session: session))
_ = try await conn.request(method: "status", params: nil)
@@ -203,7 +203,7 @@ import Testing
let url = URL(string: "ws://example.invalid")!
let cfg = ConfigSource(token: nil)
let conn = GatewayConnection(
configProvider: { (url, cfg.snapshotToken()) },
configProvider: { (url: url, token: cfg.snapshotToken(), password: nil) },
sessionBox: WebSocketSessionBox(session: session))
async let r1: Data = conn.request(method: "status", params: nil)
@@ -218,7 +218,7 @@ import Testing
let url = URL(string: "ws://example.invalid")!
let cfg = ConfigSource(token: nil)
let conn = GatewayConnection(
configProvider: { (url, cfg.snapshotToken()) },
configProvider: { (url: url, token: cfg.snapshotToken(), password: nil) },
sessionBox: WebSocketSessionBox(session: session))
_ = try await conn.request(method: "status", params: nil)
@@ -239,7 +239,7 @@ import Testing
let url = URL(string: "ws://example.invalid")!
let cfg = ConfigSource(token: nil)
let conn = GatewayConnection(
configProvider: { (url, cfg.snapshotToken()) },
configProvider: { (url: url, token: cfg.snapshotToken(), password: nil) },
sessionBox: WebSocketSessionBox(session: session))
let stream = await conn.subscribe(bufferingNewest: 10)

View File

@@ -20,11 +20,27 @@ import Testing
}
@Test func gatewayPortDefaultsAndRespectsOverride() {
let envKey = "CLAWDBOT_CONFIG_PATH"
let previousEnv = getenv(envKey).map { String(cString: $0) }
let configPath = FileManager.default.temporaryDirectory
.appendingPathComponent("clawdbot-test-config-\(UUID().uuidString).json")
.path
setenv(envKey, configPath, 1)
defer {
if let previousEnv {
setenv(envKey, previousEnv, 1)
} else {
unsetenv(envKey)
}
}
UserDefaults.standard.removeObject(forKey: "gatewayPort")
defer { UserDefaults.standard.removeObject(forKey: "gatewayPort") }
let defaultPort = GatewayEnvironment.gatewayPort()
#expect(defaultPort == 18789)
UserDefaults.standard.set(19999, forKey: "gatewayPort")
defer { UserDefaults.standard.removeObject(forKey: "gatewayPort") }
#expect(GatewayEnvironment.gatewayPort() == 19999)
}

View File

@@ -1,6 +1,7 @@
import AppKit
import Foundation
import Testing
import ClawdbotProtocol
@testable import Clawdbot
@@ -23,7 +24,7 @@ struct LowCoverageHelperTests {
#expect(dict["list"]?.arrayValue?.count == 2)
let foundation = any.foundationValue as? [String: Any]
#expect(foundation?["title"] as? String == "Hello")
#expect((foundation?["title"] as? String) == "Hello")
}
@Test func attributedStringStripsForegroundColor() {

View File

@@ -1,6 +1,7 @@
import AppKit
import SwiftUI
import Testing
import ClawdbotProtocol
@testable import Clawdbot

View File

@@ -30,7 +30,7 @@ struct MenuSessionsInjectorTests {
key: "main",
kind: .direct,
displayName: nil,
surface: nil,
provider: nil,
subject: nil,
room: nil,
space: nil,
@@ -47,7 +47,7 @@ struct MenuSessionsInjectorTests {
key: "discord:group:alpha",
kind: .group,
displayName: nil,
surface: nil,
provider: nil,
subject: nil,
room: nil,
space: nil,

View File

@@ -28,7 +28,7 @@ struct SessionDataTests {
key: "user@example.com",
kind: .direct,
displayName: nil,
surface: nil,
provider: nil,
subject: nil,
room: nil,
space: nil,

View File

@@ -45,7 +45,7 @@ struct SettingsViewSmokeTests {
thinking: "low",
timeoutSeconds: 30,
deliver: true,
channel: "sms",
provider: "sms",
to: "+15551234567",
bestEffortDeliver: true),
isolation: CronIsolation(postToMainPrefix: "[cron] "),

View File

@@ -1,4 +1,5 @@
import Testing
import ClawdbotProtocol
@testable import Clawdbot
@Suite(.serialized)

View File

@@ -17,6 +17,6 @@ import Testing
#expect(opts.thinking == "low")
#expect(opts.deliver == true)
#expect(opts.to == nil)
#expect(opts.channel == .last)
#expect(opts.provider == .last)
}
}

View File

@@ -1,5 +1,6 @@
import Foundation
import Testing
import ClawdbotProtocol
@testable import Clawdbot
@Suite