diff --git a/clients/macos/vellum-assistantTests/Network/HostAppControlTypesTests.swift b/clients/macos/vellum-assistantTests/Network/HostAppControlTypesTests.swift index f69c2e8a17e..432246ee35c 100644 --- a/clients/macos/vellum-assistantTests/Network/HostAppControlTypesTests.swift +++ b/clients/macos/vellum-assistantTests/Network/HostAppControlTypesTests.swift @@ -99,13 +99,16 @@ final class HostAppControlTypesTests: XCTestCase { // MARK: - HostAppControlInput wire shape func test_input_decodes_from_tool_discriminator() throws { + // Wire format uses snake_case (matches TOOLS.json input schema and the + // TypeScript HostAppControlPressInput shape). Swift maps to camelCase + // via explicit CodingKey raw values. let json = #""" { "tool": "press", "app": "com.apple.Safari", "key": "Return", "modifiers": ["cmd"], - "durationMs": 100 + "duration_ms": 100 } """# let decoded = try JSONDecoder().decode(HostAppControlInput.self, from: Data(json.utf8)) @@ -118,6 +121,34 @@ final class HostAppControlTypesTests: XCTestCase { XCTAssertEqual(durationMs, 100) } + func test_input_drag_decodes_snake_case_coordinates() throws { + // Regression guard for the pre-existing CodingKey bug where the + // snake_case `from_x`/`from_y`/`to_x`/`to_y` wire keys silently + // failed to decode and drag coordinates fell through to undefined + // behavior. + let json = #""" + { + "tool": "drag", + "app": "com.apple.Safari", + "from_x": 10, + "from_y": 20, + "to_x": 100, + "to_y": 200, + "button": "left" + } + """# + let decoded = try JSONDecoder().decode(HostAppControlInput.self, from: Data(json.utf8)) + guard case .drag(let app, let fromX, let fromY, let toX, let toY, let button) = decoded else { + return XCTFail("Expected .drag variant, got \(decoded)") + } + XCTAssertEqual(app, "com.apple.Safari") + XCTAssertEqual(fromX, 10) + XCTAssertEqual(fromY, 20) + XCTAssertEqual(toX, 100) + XCTAssertEqual(toY, 200) + XCTAssertEqual(button, "left") + } + func test_input_unknown_tool_throws() { let json = #"{"tool": "teleport", "app": "x"}"# XCTAssertThrowsError( diff --git a/clients/shared/Network/MessageTypes.swift b/clients/shared/Network/MessageTypes.swift index 7ad36243005..13a4bc4b266 100644 --- a/clients/shared/Network/MessageTypes.swift +++ b/clients/shared/Network/MessageTypes.swift @@ -1757,17 +1757,21 @@ public enum HostAppControlInput: Codable, Equatable, Sendable { case key case keys case modifiers - case durationMs + // Wire format uses snake_case for multi-word fields (driven by + // TOOLS.json schema property names). Map explicitly — without these + // raw values, decode silently misses `duration_ms` / `from_x` / etc. + // and hold-durations and drag coordinates fall through to defaults. + case durationMs = "duration_ms" case steps case text case x case y case button case double - case fromX - case fromY - case toX - case toY + case fromX = "from_x" + case fromY = "from_y" + case toX = "to_x" + case toY = "to_y" case reason }