fix(admin-config-tool): unwrap Request reply envelope before decoding as RequestReply

config push crashed on every real controller with a capnp KjException --
xce_reply() wraps every reply in an outer Request{body.replymsg} envelope,
but discovery.py/push.py decoded raw wire bytes directly as a bare
RequestReply. Adds a shared decode_reply() unwrap point used by both call
sites, and fixes the same wrong-shape assumption baked into task 5.6's
and test_cli.py's mock-server fixtures.
This commit is contained in:
Rongsong Shen 2026-07-30 09:43:56 +00:00
parent 46bbdaa9a6
commit 915e6fdf64
7 changed files with 231 additions and 26 deletions

View file

@ -99,7 +99,7 @@ def cluster_bin(tmp_path):
# ---------------------------------------------------------------------------
def test_push_config_successful_commit(mock_controller, cluster_bin):
def test_push_config_successful_commit(mock_controller, cluster_bin, wrap_reply):
mod = load_schema()
received = {}
@ -117,7 +117,7 @@ def test_push_config_successful_commit(mock_controller, cluster_bin):
upgrade_reply.versionTag = 7
upgrade_reply.ackedCount = 3
upgrade_reply.error = ""
wire.send_frame(conn, reply.to_bytes())
wire.send_frame(conn, wrap_reply(reply))
srv = mock_controller(_handler)
@ -133,7 +133,7 @@ def test_push_config_successful_commit(mock_controller, cluster_bin):
assert received["sha256"] == hashlib.sha256(Path(cluster_bin).read_bytes()).digest()
def test_push_config_follows_one_redirect_to_primary(mock_controller, cluster_bin):
def test_push_config_follows_one_redirect_to_primary(mock_controller, cluster_bin, wrap_reply):
mod = load_schema()
def _standby_handler(conn):
@ -144,7 +144,7 @@ def test_push_config_follows_one_redirect_to_primary(mock_controller, cluster_bi
# port filled in once the primary server exists, see below
reply.redirectPort = _standby_handler.primary_port
reply.data.nodata = None
wire.send_frame(conn, reply.to_bytes())
wire.send_frame(conn, wrap_reply(reply))
def _primary_handler(conn):
wire.recv_frame(conn)
@ -154,7 +154,7 @@ def test_push_config_follows_one_redirect_to_primary(mock_controller, cluster_bi
upgrade_reply.versionTag = 9
upgrade_reply.ackedCount = 2
upgrade_reply.error = ""
wire.send_frame(conn, reply.to_bytes())
wire.send_frame(conn, wrap_reply(reply))
primary_srv = mock_controller(_primary_handler)
_standby_handler.primary_port = primary_srv.port
@ -166,7 +166,7 @@ def test_push_config_follows_one_redirect_to_primary(mock_controller, cluster_bi
assert result.acked_count == 2
def test_push_config_non_zero_status_reports_error_not_exception(mock_controller, cluster_bin):
def test_push_config_non_zero_status_reports_error_not_exception(mock_controller, cluster_bin, wrap_reply):
mod = load_schema()
def _handler(conn):
@ -174,7 +174,7 @@ def test_push_config_non_zero_status_reports_error_not_exception(mock_controller
reply = mod.RequestReply.new_message()
reply.status = 1
reply.data.debugmsg = "simulated failure"
wire.send_frame(conn, reply.to_bytes())
wire.send_frame(conn, wrap_reply(reply))
srv = mock_controller(_handler)
@ -184,7 +184,7 @@ def test_push_config_non_zero_status_reports_error_not_exception(mock_controller
assert "status 1" in result.error
def test_push_config_wrong_reply_variant_raises_reply_decode_error(mock_controller, cluster_bin):
def test_push_config_wrong_reply_variant_raises_reply_decode_error(mock_controller, cluster_bin, wrap_reply):
mod = load_schema()
def _handler(conn):
@ -192,7 +192,34 @@ def test_push_config_wrong_reply_variant_raises_reply_decode_error(mock_controll
reply = mod.RequestReply.new_message()
reply.status = 0
reply.data.nodata = None # not upgradeconfigreplymsg
wire.send_frame(conn, reply.to_bytes())
wire.send_frame(conn, wrap_reply(reply))
srv = mock_controller(_handler)
with pytest.raises(ReplyDecodeError):
push.push_config(cluster_bin, f"127.0.0.1:{srv.port}")
def test_push_config_rejects_bare_requestreply_bytes_old_wire_shape_regression(
mock_controller, cluster_bin
):
"""Regression test (task 6.1): a mock server that still sends the OLD,
wrong shape -- a bare RequestReply with no outer Request envelope --
must be cleanly rejected as ReplyDecodeError, not silently accepted
and not an unhandled capnp.KjException. This locks in that the fixed
client only accepts the real controller wire shape.
"""
mod = load_schema()
def _handler(conn):
wire.recv_frame(conn)
reply = mod.RequestReply.new_message()
reply.status = 0
upgrade_reply = reply.data.init("upgradeconfigreplymsg")
upgrade_reply.versionTag = 1
upgrade_reply.ackedCount = 1
upgrade_reply.error = ""
wire.send_frame(conn, reply.to_bytes()) # NOT wrapped -- old shape
srv = mock_controller(_handler)
@ -301,7 +328,7 @@ def test_probe_config_has_no_wire_level_rejection_signal_by_design(
# ---------------------------------------------------------------------------
def test_push_config_propagates_too_many_redirects_end_to_end(mock_controller, cluster_bin):
def test_push_config_propagates_too_many_redirects_end_to_end(mock_controller, cluster_bin, wrap_reply):
"""A real chain of 4 redirecting mock servers -- push_config's own
composition with xceadmin.discovery must surface TooManyRedirectsError,
not silently follow forever or return a bogus result.
@ -317,7 +344,7 @@ def test_push_config_propagates_too_many_redirects_end_to_end(mock_controller, c
reply.redirectHost = "127.0.0.1"
reply.redirectPort = next_port_getter()
reply.data.nodata = None
wire.send_frame(conn, reply.to_bytes())
wire.send_frame(conn, wrap_reply(reply))
return _handler