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:
2026-02-11 21:14:52 +01:00
co-authored by Claude Opus 4.6
parent c4e1b07346
commit 3befd2309c
5 changed files with 175 additions and 33 deletions
+69
View File
@@ -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