diff --git a/ROADMAP.md b/ROADMAP.md index afab51d..602f4b0 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -11,7 +11,7 @@ and ideas that are not ready for scheduling are recorded in ## Active milestone -### Milestone 24.8 — MSI direct-UDP retrieval foundation +### Milestone 24.8 — MSI UDP correlation/completion hardening The first four Milestone 24.8 slices established lossless bounded MSI XML modeling/parsing, exact serialized `MSI\r` retrieval for CR-line/replay @@ -27,6 +27,15 @@ assembled; numbered `Footer`/`Foot` fragments use the existing bounded reassembly path; retryable sequence failures resend the exact original `MSI` wire; and successful completion clears one-shot MSI retry/expectation state. +This sixth narrow hardening slice adds no production protocol behavior. It adds +direct behavioral regression coverage for two invariants already provided by +the shared UDP machinery: an exact one-shot MSI expectation survives unrelated +bare XML roots until matching MSI XML arrives, and successful MSI completion +after an automatic sequence-gap retry removes the one-shot retry authority so +later stray MSI fragment gaps cannot resend a stale command. The tests use only +deterministic fake datagrams and synchronize on a later ordinary response before +asserting that no additional UDP write occurred. + Direct `UdpTransport` and its capture wrapper are covered only with deterministic fake datagrams. This is software transport/framing evidence, not a physical scanner or firmware-support claim. Fallback transports remain blocked because @@ -35,7 +44,7 @@ has been established. Custom transports that merely advertise an `udp://` endpoint remain fail-closed unless they are the repository's direct `UdpTransport` path. -This slice does not add `MSV`/`MSB` execution, unindexed `MNU`, menu +This hardening slice does not add `MSV`/`MSB` execution, unindexed `MNU`, menu lifecycle/state ownership, renderer behavior, model/firmware applicability, or physical validation. The serialized `[RSV]` field in `MSV`/`MSB` remains unresolved, and Quick Search (`QSH`) remains blocked pending exact `FRQ` diff --git a/docs/advanced-protocol-research.md b/docs/advanced-protocol-research.md index 6784db1..a2b11b2 100644 --- a/docs/advanced-protocol-research.md +++ b/docs/advanced-protocol-research.md @@ -588,7 +588,18 @@ completion clears MSI one-shot retry state. `SDSScanner.get_msi()` is therefore allowed on the repository's direct `UdpTransport`, including when wrapped by capture recording. -All UDP evidence in this slice is deterministic fake-datagram software evidence. +The sixth narrow slice adds no production protocol behavior. It hardens the +existing direct-UDP contract with explicit behavioral regressions for root +correlation and one-shot retry cleanup: while `MSI` is expected, unrelated bare +`ScannerInfo`, `GLT`, or `AST` XML remains uncorrelated and does not consume the +MSI expectation; after a retryable MSI fragment gap and a subsequent successful +MSI completion, another stray MSI fragment gap cannot reuse the completed +request's automatic-retry authority. The latter assertion waits for a later +ordinary decoded response before checking the transmitted datagrams, avoiding a +negative-write race. + +All UDP evidence in the fifth and sixth slices is deterministic fake-datagram +software evidence. It does not establish physical SDS200 behavior, firmware availability, or a broader transport guarantee. Fallback transports remain fail-closed because their active transport can change and no fallback-wide MSI framing contract is diff --git a/tests/test_network.py b/tests/test_network.py index fdf25e4..b436c29 100644 --- a/tests/test_network.py +++ b/tests/test_network.py @@ -284,6 +284,29 @@ def test_udp_decoder_does_not_expect_nonexact_msi_command(command: str) -> None: assert decoder.feed(bare_xml) == (bare_xml.decode(),) +@pytest.mark.parametrize( + "other_xml", + [ + ( + b'' + b"" + ), + b'', + b'', + ], +) +def test_udp_decoder_keeps_msi_expectation_across_other_xml_roots( + other_xml: bytes, +) -> None: + decoder = UdpDatagramDecoder() + decoder.expect_command("MSI") + msi = b'' + + assert decoder.feed(other_xml) == (other_xml.decode(),) + assert decoder.feed(msi) == ("MSI,,", msi.decode()) + assert decoder.feed(msi) == (msi.decode(),) + + def test_decoder_malformed_bare_glt_does_not_complete_expectation() -> None: completed: list[str] = [] decoder = UdpDatagramDecoder(completion_handler=completed.append) @@ -504,6 +527,61 @@ def test_udp_transport_retries_msi_with_exact_original_wire_command() -> None: assert transport.statistics["xml_fragments_dropped"] == 1 +def test_udp_transport_completed_msi_clears_one_shot_retry_state() -> None: + fake = FakeDatagramSocket() + transport = UdpTransport( + "192.0.2.25", + socket_factory=FakeDatagramSocketFactory(fake), + reconnect=False, + max_xml_retries=2, + ) + diagnostics: list[TransportDiagnostic] = [] + received: list[str] = [] + transport.set_diagnostic_handler(diagnostics.append) + transport.start(received.append) + try: + transport.write_command("MSI") + fake.feed( + b'MSI,,' + b'