fix(client): address PR #5 review — race condition, buffer corruption, input error
Critical fixes from Hoshe review: 1. Race condition: defer TCP connect to _process() with retry logic (MAX_CONNECT_RETRIES=20, 100ms interval) so server has time to bind. 2. Buffer corruption: disconnect on oversized message instead of clearing valid buffered data (_corrupt flag, fail-safe). 3. Silent input drop: send_input() returns Error so callers can detect encode/validation failures. Warnings addressed: - ServerProcess validates server_path exists before spawning - SIGKILL and health check TODOs documented for future work - Diagonal keybindings documented as intentional deferral - send_message uses single put_data() call (no partial write risk) - Static frame helpers documented as D-030 Layer 2 test-only New tests (36 total, up from 33): - Partial read scenario (chunked TCP delivery) - Multi-message sequential decode (exercises buffer corruption fix) - send_input error return on invalid action Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -157,3 +157,72 @@ func test_action_enum_to_wire_open_menu_returns_empty() -> void:
|
||||
func test_action_enum_to_wire_unknown_returns_empty() -> void:
|
||||
var wire_name := SimBridge._action_enum_to_wire(9999)
|
||||
assert_that(wire_name).is_equal("")
|
||||
|
||||
|
||||
# -- Partial read and state machine tests --------------------------------------
|
||||
|
||||
func test_frame_decode_partial_then_complete() -> void:
|
||||
# Simulate chunked TCP delivery: header arrives first, payload arrives later
|
||||
var payload := PackedByteArray([0xCA, 0xFE, 0xBA, 0xBE])
|
||||
var framed := LocalBridge.frame_encode(payload)
|
||||
|
||||
# Split at byte 6 (header + 2 bytes of payload)
|
||||
var chunk1 := framed.slice(0, 6)
|
||||
var chunk2 := framed.slice(6)
|
||||
|
||||
# First chunk: incomplete message
|
||||
var decoded: Variant = LocalBridge.frame_decode(chunk1)
|
||||
assert_that(decoded).is_null()
|
||||
|
||||
# Reassemble and decode
|
||||
var full := PackedByteArray()
|
||||
full.append_array(chunk1)
|
||||
full.append_array(chunk2)
|
||||
decoded = LocalBridge.frame_decode(full)
|
||||
assert_that(decoded).is_not_null()
|
||||
assert_that(decoded.payload).is_equal(payload)
|
||||
|
||||
|
||||
func test_frame_decode_multiple_messages_sequential() -> void:
|
||||
# Three messages concatenated — decode all sequentially via remainder
|
||||
var p1 := PackedByteArray([0x01])
|
||||
var p2 := PackedByteArray([0x02, 0x03])
|
||||
var p3 := PackedByteArray([0x04, 0x05, 0x06])
|
||||
|
||||
var buffer := PackedByteArray()
|
||||
buffer.append_array(LocalBridge.frame_encode(p1))
|
||||
buffer.append_array(LocalBridge.frame_encode(p2))
|
||||
buffer.append_array(LocalBridge.frame_encode(p3))
|
||||
|
||||
# Decode message 1
|
||||
var d1: Variant = LocalBridge.frame_decode(buffer)
|
||||
assert_that(d1).is_not_null()
|
||||
assert_that(d1.payload).is_equal(p1)
|
||||
|
||||
# Decode message 2 from remainder
|
||||
var d2: Variant = LocalBridge.frame_decode(d1.remainder)
|
||||
assert_that(d2).is_not_null()
|
||||
assert_that(d2.payload).is_equal(p2)
|
||||
|
||||
# Decode message 3 from remainder
|
||||
var d3: Variant = LocalBridge.frame_decode(d2.remainder)
|
||||
assert_that(d3).is_not_null()
|
||||
assert_that(d3.payload).is_equal(p3)
|
||||
assert_that(d3.remainder.size()).is_equal(0)
|
||||
|
||||
|
||||
func test_send_input_returns_error_on_invalid_action() -> void:
|
||||
# send_input returns ERR_INVALID_PARAMETER for unknown actions
|
||||
# SimBridge is in test_mode=true and CONNECTED, so we need to temporarily
|
||||
# disable test_mode to exercise the encode path
|
||||
var original_test_mode: bool = SimBridge.test_mode
|
||||
var original_state: SimBridge.ConnectionState = SimBridge.state
|
||||
SimBridge.test_mode = false
|
||||
SimBridge.state = SimBridge.ConnectionState.CONNECTED
|
||||
|
||||
var err := SimBridge.send_input({"action": 9999, "timestamp_msec": 0})
|
||||
assert_that(err).is_equal(ERR_INVALID_PARAMETER)
|
||||
|
||||
# Restore
|
||||
SimBridge.test_mode = original_test_mode
|
||||
SimBridge.state = original_state
|
||||
|
||||
Reference in New Issue
Block a user