Repository navigation
fix(heating): refuse WHO 4 DIMENSION 22 only without an actuator token in WHERE (oracle thermo-dim22) - #151
Conversation
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
GreenGrassBlueOcean
left a comment
There was a problem hiding this comment.
Summary
Nice separation between keeping is_who4_dimension_22() as the general atoi()-tolerant detector and routing the three send paths (OWNCommandSession.send_to_gateway, OWNCommandSession._locked_send, AsyncSerialTransport.send) through is_who4_dimension_22_crash_form(), while preserving pre-connect() fast-fail, single-serialization, reason="destructive_frame", and \Z anchoring on *22.
Requesting changes for a few safety/accuracy items before undrafting and merging:
- Unbounded write tail
\*#22\*in_WHO4_DIMENSION_22_SENDABLE_REGEX(OWNd/message/heating.pyL119–121):.match(chunk)stops immediately after*#22*with no payload pattern and no\Zanchor. Combined withOWNMessage._DIMENSION_WRITING((?:\*\d*)+, compiled withoutre.ASCII), short/empty/over-wide writes (*#4*3#2#1*#22*##,*#4*3#2#1*#22*1##,*#4*3#2#1*#22*1*2*3*4*5*6##) and non-ASCII Unicode digits (*#4*3#2#1*#22*\uff11*0215*2*0##) pass both the guard andOWNMessage.parseand get written to the wire, and the publicis_who4_dimension_22_crash_form()predicate returnsFalseon#-concatenated crash frames (*#4*3#2#1*#22*1#*#4*2*22##) and trailing-newline/whitespace writes (*#4*2#1*#22*1*0215*2*0\n##). - Unreplayed
Z = 0(0#N/3#0#N) (OWNd/message/heating.pyL120,SPLIT_UNIT_FRAMES):thermo-dim22only replayedZ = 2andZ = 45. Inbt_termo,Z = 0is the crashing general-zone sentinel (*#4*0*22##SIGSEGVs on all 9 Linux gateways), andOWN-DEV-0136(IR Emitter Object196) definesZAZB = 01..99. Either restrictZto[1-9][0-9]?(1..99) or replay3#0#1/0#1inown-firmware-oraclefirst. - Oracle wording on prefix-less
*#4*Z#N*22##(CHANGELOG.md,heating.py,test_who4_dimension22_guard.py, commit message): 10 of 12 gateways forward only3#Z#N; prefix-lessZ#N(*#4*2#1*22##,*#4*45#1*#22*3**1*1##, etc.) isnack, silenton 11 of 12 gateways (everybt_termogateway drops it) and forwarded only byMH202 010024(coso). Worth also confirming whetherOWNdwants(?:3#)?optional or3#Z#Nonly, and noting thefindings/thermo-dim22.mdemulation-only caveat. - Rebase onto
master(#137,#138,#142):heating.pyandCHANGELOG.mdcurrently conflict withmaster, andWHERE_HASH_4_BUILDERSintests/test_who4_dimension22_guard.pyneedsmaster's"#0#4"central-unit fixtures from#137("#4"now raisesValueErroronset_holiday_end_date/set_holiday_end_time/set_timed_manual_end_time).
| # published split forms and the actuator form ``*#4*Z#N*22##`` did not crash | ||
| # any of the twelve emulated gateways: ten forward them to the bus as a | ||
| # telegram to zone Z, actuator N (``findings/thermo-dim22.md``). |
There was a problem hiding this comment.
findings/thermo-dim22.md shows that 10 of 12 gateways forward only the 3#Z#N forms (*#4*3#2#1*22## and *#4*3#2#1*#22*1*0215*2*0##). The prefix-less Z#N forms (*#4*2#1*22##, *#4*2#1*#22*1*0215*2*0##, *#4*45#1*#22*3**1*1##, *#4*45#1*#22*4**1*1##) are nack, silent on 11 of 12 gateways (dropped by every bt_termo gateway) and forwarded only by MH202 010024 (coso), while *#4*45#1*#22*0***## is nack, silent on all 12.
Please separate 3#Z#N (10/12 out) from Z#N (11/12 nack, silent, MH202 out only) here and in the is_who4_dimension_22_crash_form docstring below — and consider whether prefix-less Z#N even needs to be opened unless MH202 / legacy touchscreen replay is an explicit goal. Worth also keeping a brief note that thermo-dim22 is emulated-firmware evidence (harness=full) and has not yet been replayed on physical hardware (findings/thermo-dim22.md).
| _WHO4_DIMENSION_22_SENDABLE_REGEX = re.compile( | ||
| r"^\*#4\*(?:3#)?(?:0|[1-9][0-9]?)#[1-9](?:\*22\Z|\*#22\*)", re.ASCII | ||
| ) |
There was a problem hiding this comment.
Two safety issues in _WHO4_DIMENSION_22_SENDABLE_REGEX:
-
Unbounded write tail
\*#22\*: While the read arm anchors\*22\Z, the write arm stops right after*#22*under.match(chunk)with no payload pattern and no\Zanchor:- Untested arity /
strtokrisk on the wire:OWNMessage._DIMENSION_WRITING(OWNd/message/base.py) accepts(?:\*\d*)+without checking field count, andOWNHeatingCommandhas nodimension == 22validation. As a result,"*#4*3#2#1*#22*##","*#4*3#2#1*#22*1##","*#4*3#2#1*#22*1*0215##", and"*#4*3#2#1*#22*1*2*3*4*5*6##"all returnis_who4_dimension_22_crash_form(...) == False, parse withis_valid == True, and are written to the gateway. Every write replayed inthermo-dim22had the 4-fieldMOD*SP*VEL*SWINGlayout; short writes riskatoi(NULL)SIGSEGVinbt_termo. - Non-ASCII
\dbypass: BecauseOWNMessage._DIMENSION_WRITINGis compiled withoutre.ASCIIand.match(chunk)stops at*#22*,"*#4*3#2#1*#22*\uff11*0215*2*0##"passes both checks (is_valid == True) and writes UTF-8\xef\xbc\x91to the socket/serial port. - Public
is_who4_dimension_22_crash_form()contract: ReturnsFalseon#-concatenated crash frames smuggled in a write payload ("*#4*3#2#1*#22*1#*#4*2*22##") and on write tails with trailing newlines, whitespace, or+("*#4*2#1*#22*1*0215*2*0\n##","*#4*3#2#1*#22* +1*0215*2*0##").
- Untested arity /
-
Unreplayed
Z = 0(0#1/3#0#1):thermo-dim22only replayedZ = 2andZ = 45. Onbt_termo,Z = 0is the general-zone sentinel (*#4*0*22##SIGSEGVs on all 9 Linux gateways), andOWN-DEV-0136(Object196) restrictsZAZBto01..99. Without an oracle row showingbt_termoroutes3#0#1/0#1down the0x80 + Zactuator branch rather than theZ == 0branch,Zshould stay[1-9][0-9]?(1..99) until0#1/3#0#1are replayed.
| _WHO4_DIMENSION_22_SENDABLE_REGEX = re.compile( | |
| r"^\*#4\*(?:3#)?(?:0|[1-9][0-9]?)#[1-9](?:\*22\Z|\*#22\*)", re.ASCII | |
| ) | |
| _WHO4_DIMENSION_22_SENDABLE_REGEX = re.compile( | |
| r"^\*#4\*(?:3#)?[1-9][0-9]?#[1-9]" | |
| r"(?:\*22|\*#22\*[0-5]?\*(?:[0-9]{3,4})?\*[0-4]?\*[01]?)\Z", | |
| re.ASCII, | |
| ) |
| - `OWNTransport.send()` takes the same `raise_on_error` keyword as `OWNCommandSession.send()` on `AsyncTcpTransport` and `AsyncSerialTransport`, and `OWNTransport.last_error` holds the error recorded by the last `send()`: a local refusal on both transports, and on the TCP transport everything its command session records (NACK, timeout, connection errors). Both transports now share one contract: `RuntimeError` when not connected, then a frame refused locally is logged at ERROR, kept in `last_error` and answered with `None`, or raised as `OWNProtocolError` with `raise_on_error=True`. | ||
| ### Fixed | ||
| - **WHO 4 Dimension 22 is never sent**: Dimension 22 is the published split-unit IR control (`*#4*3#Z#N*22##` / `*#4*3#Z#N*#22*MOD*SP*VEL*SWING##`, WHO 4 specification pp. 66-68); in any spelling (`*#4*ZONE*22##`, `*#4*#0*22##`, leading zeros, whitespace or `+` tokens) it crashes the thermoregulation daemon (`bt_termo`) on Linux IP gateways and reboots an F461 2.0.11 about three minutes later (own-firmware-oracle `tools/expected_divergences.yaml` crash frames, live F461 2.0.11). `OWNCommandSession.send` (and so `AsyncTcpTransport.send` and `keepalive`), and `AsyncSerialTransport.send` refuse it before anything is written, with the contract above and `reason="destructive_frame"`; `OWNCommandSession.send_to_gateway` refuses it before `connect()` (no socket, no handshake), logs it at ERROR and answers `None`, or raises `OWNProtocolError` with `raise_on_error=True` (the session it would have used is discarded, so no `last_error` is observable on that path). The detector `is_who4_dimension_22()` is public, accepts `bytes`, and is anchored to the first `*` of each frame, so a mid-frame WHERE `#4` / DIMENSION `#4` such as `*#2*#4*#11#001*22##` or `*#1*14*#4*1*22##` stays sendable. Parsing is unchanged: the frame can still be read from the bus. | ||
| - **WHO 4 Dimension 22 without an actuator token is never sent**: Dimension 22 is the published split-unit IR control (`*#4*3#Z#N*22##` / `*#4*3#Z#N*#22*MOD*SP*VEL*SWING##`, WHO 4 specification pp. 66-68). Sent to a zone (`*#4*Z*22##`, `*#4*0ZZ*22##`), to every zone (`*#4*0*22##`) or to the central unit (`*#4*#0*22##`), as a read or a write, it crashes the thermoregulation daemon (`bt_termo`) on nine of the twelve emulated Linux IP gateways and reboots an F461 2.0.11 about three minutes later (own-firmware-oracle `thermo-dim22` suite and `tools/expected_divergences.yaml` crash frames, live F461 2.0.11). The published split forms and the actuator form `*#4*Z#N*22##` crashed none of the twelve gateways (ten forward them to the bus as a telegram to zone Z, actuator N; own-firmware-oracle `findings/thermo-dim22.md`), so their canonical spellings (`Z` 0..99, `N` 1..9) are sent; every other spelling (leading zeros, whitespace or `+` tokens, `#Z`, `#0#C`, `Z#4#IF`, `N` 0 or two digits, a dimension parameter) stays refused. `OWNCommandSession.send` (and so `AsyncTcpTransport.send` and `keepalive`), and `AsyncSerialTransport.send` refuse a crash form before anything is written, with the contract above and `reason="destructive_frame"`; `OWNCommandSession.send_to_gateway` refuses it before `connect()` (no socket, no handshake), logs it at ERROR and answers `None`, or raises `OWNProtocolError` with `raise_on_error=True` (the session it would have used is discarded, so no `last_error` is observable on that path). The detector `is_who4_dimension_22()` recognises every Dimension 22 spelling; the send paths refuse `is_who4_dimension_22_crash_form()`, both public, accepting `bytes`, and anchored to the first `*` of each frame, so a mid-frame WHERE `#4` / DIMENSION `#4` such as `*#2*#4*#11#001*22##` or `*#1*14*#4*1*22##` stays sendable. Parsing is unchanged: the frame can still be read from the bus. |
There was a problem hiding this comment.
Same factual clarification as OWNd/message/heating.py: 10 of 12 gateways forward the 3#Z#N split forms, whereas prefix-less *#4*Z#N*22## / *#4*Z#N*#22*...## is nack, silent on 11 of 12 gateways and forwarded only by MH202 010024 (and Z / write payload wording should match whatever scope is settled in _WHO4_DIMENSION_22_SENDABLE_REGEX).
| # through, and the string written is the string given. | ||
| session, writer = _command_session() | ||
|
|
||
| assert await session.send(frame, is_status_request=True) is True |
There was a problem hiding this comment.
SPLIT_UNIT_FRAMES includes *#22*... write commands, which callers send with is_status_request=False rather than True. Passing is_status_request="*#22*" not in frame exercises the command-ACK branch of OWNCommandSession._locked_send for writes as well as the status-request branch for reads.
Please also add regression cases to DIMENSION_22_FRAMES for malformed/unbounded *#22* writes:
"*#4*3#2#1*#22*##","*#4*3#2#1*#22*1##","*#4*3#2#1*#22*1*0215##","*#4*3#2#1*#22*1*0215*2*0*9##""*#4*3#2#1*#22*1*0215*2*0\n##","*#4*3#2#1*#22* +1*0215*2*0##""*#4*3#2#1*#22*\uff11*0215*2*0##""*#4*3#2#1*#22*1#*#4*2*22##"
| assert serial_writer.written == [] | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("frame", SPLIT_UNIT_FRAMES[:2]) |
There was a problem hiding this comment.
Minor test gaps here:
- Why slice
SPLIT_UNIT_FRAMES[:2]instead of parametrizing over allSPLIT_UNIT_FRAMES? - Adding a terminatorless split-unit case (e.g.
"*#4*3#2#1*22") would directly verify thatAsyncSerialTransport.sendappends"##"before checkingis_who4_dimension_22_crash_form(text)and writesb"*#4*3#2#1*22##". - Adding one
AsyncTcpTransport.send("*#4*3#2#1*22##")positive test would mirror the refusal tests across all four send entry points (OWNCommandSession.send,send_to_gateway,AsyncTcpTransport.send,AsyncSerialTransport.send).
…m 3#Z#N The firmware oracle's thermo-dim22 suite (12 emulated gateways, two byte-identical runs) crashes bt_termo only on the forms whose WHERE has no actuator token: *michnovka#4*Z*22##, *michnovka#4*0*22##, *michnovka#4*#0*22## and the zone write. The published split forms *michnovka#4*3#Z#N*22## / *michnovka#4*3#Z#N*OpenWebNet-HA#22*MOD*SP*VEL*SWING## crashed none of the twelve and ten forward them to the bus as a telegram to zone Z, actuator N; the prefix-less Z#N forms are nack, silent on eleven and forwarded by the MH202 only, so they stay refused. is_who4_dimension_22() keeps recognising every DIMENSION 22 spelling; the send paths now refuse is_who4_dimension_22_crash_form(), which lets through only the canonical 3#Z#N spelling (Z 1..99, N 1..9) as a request or as a write with exactly the four published payload fields, anchored with \Z and ASCII-only, and keeps every untested spelling refused. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
b73e723 to
54b5944
Compare
|
Revision
Tests: |
GreenGrassBlueOcean
left a comment
There was a problem hiding this comment.
LGTM on 54b5944 — all four review items are cleanly resolved (\Z-anchored re.ASCII 4-field write pattern + malformed/Unicode/smuggled regression cases, Z restricted to 1..99, 3#Z#N vs Z#N oracle breakdown with the harness=full emulation caveat, and clean rebase onto afedabf with full 4-entry-point test coverage).
Confirming on scope: 3#Z#N only is what OWNd wants. It matches the published WHO 4 specification (pp. 66–68), is the form forwarded by 10 of 12 gateways, and loses nothing on MH202 since MH202 emits byte-identical D1 telegrams for 3#Z#N and Z#N.
(Optional cosmetic nit if you touch the branch, totally fine as-is otherwise: DESTRUCTIVE_FRAME_REASON in OWNd/connection/command_session.py, OWNd/transport/base.py, the module docstring of tests/test_who4_dimension22_guard.py, and the PR title still carry v1's "without an actuator token in WHERE" wording rather than the narrowed 3#Z#N split-form scope.)
Feel free to mark ready for review whenever you'd like this merged!
OWNd/__init__.py: __version__ = "2.0.0b12". CHANGELOG.md: [Unreleased] promoted to [2.0.0b12] - 2026-11-01 (every PR merged since v2.0.0-b11: #134, #135, #136, #137, #138, #139, #140, #142, #145, #147, #148, #150, #151) with a fresh empty [Unreleased] header. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Requested by A
Summary
Before: since #136 every WHO 4 DIMENSION 22 frame is refused by every send path, in any spelling, the published split-unit forms
*#4*3#Z#N*22##/*#4*3#Z#N*#22*MOD*SP*VEL*SWING##included. The evidence behind it was two F461020011crash frames of the same shape (*#4*2*22##,*#4*#0*22##) and the live F461 reboot, and the CHANGELOG / docstrings generalised that to "in any spelling".After: the send paths refuse
is_who4_dimension_22_crash_form(): every DIMENSION 22 spelling except the canonical published split form3#Z#N(Z1..99,N1..9) as a request*22or as a write with exactly the four payload fieldsMOD*SP*VEL*SWING.is_who4_dimension_22()keeps its "every spelling" meaning for callers that want the detector.Context and how it was found. A questioned whether all DIMENSION 22 forms crash the daemon or only the zone / central one, and asked for the oracle to settle it. New suite
thermo-dim22(OpenWebNet-HA/own-firmware-oracle#68, findingfindings/thermo-dim22.md): 11 forms on the twelve emulated gateways, two byte-identical runs. On the nine gateways that crash at all (F450, F453, F453AV, F454, F459, F460, F461, MH200N, MyHomeServer1) the crash set is exactly the four forms without an actuator token (*#4*2*22##,*#4*2*#22*1*0215*2*0##,*#4*#0*22##,*#4*0*22##;bt_termoSIGSEGV where attributed). No form with an actuator token crashed anywhere:*#4*3#2#1*22##and its write go out on ten of twelve gateways (F461 included) as aD1telegram to zone 2, actuator 1, dimension 22; the prefix-less*#4*2#1*22##and the touchscreen's*#4*45#1*#22*3**1*1##arenack, silenton eleven and forwarded by the MH202 only, so this revision keeps them refused. Emulation only (harness=full), not yet replayed on hardware: no DIMENSION 22 frame was sent to a physical gateway, and the live F461 supervisor's reboot is not modelled, so this PR narrows the refusal to the evidence and nothing more.Follows up #136 and #148 (the "in any spelling ... Linux IP gateways" wording).
Changes
OWNd/message/heating.py:_WHO4_DIMENSION_22_REGEXunchanged (every spelling, atoi-tolerant, anchored per##chunk); new_WHO4_DIMENSION_22_SENDABLE_REGEX = ^\*#4\*3#[1-9][0-9]?#[1-9](?:\*22|\*#22\*[0-5]?\*(?:[0-9]{3,4})?\*[0-4]?\*[01]?)\Z(re.ASCII) andis_who4_dimension_22_crash_form(frame): a chunk the detector matches that the sendable pattern does not. Both arms end in\Z(review item 1: the write arm used to stop after*#22*, so short, long, whitespace, newline, smuggled-frame and Unicode-digit payloads passed).Zis 1..99 (item 2:Z0 is bt_termo's general-zone sentinel and OWN-DEV-0136 givesZAZB01..99;3#0#1/0#1were not replayed). The prefix-lessZ#Nforms are refused (item 3:nack, silenton eleven gateways, MH202 only forwards them; nothing is lost on a bt_termo gateway). Allowed:3#Z#Nas*22, or*#22*with exactly four fields,MOD0..5,SPthree or four digits,VEL0..4,SWING0 or 1, each optionally empty as the touchscreen writes*#22*3**1*1and*#22*0***. The oracle replayedZ2 and 45 withN1 and the write1*0215*2*0; the published ranges are allowed on the grammar, and the comment says so.OWNd/connection/command_session.py,OWNd/transport/serial.py,OWNd/transport/base.py: the three send paths call the crash-form detector;DESTRUCTIVE_FRAME_REASONnames the WHERE condition. Contract,reason="destructive_frame", logging andsend_to_gateway's refusal beforeconnect()unchanged.OWNd/message/__init__.py: exportsis_who4_dimension_22_crash_form.tests/test_who4_dimension22_guard.py:DIMENSION_22_FRAMESis the refused set (58 spellings: the four oracle crash forms, the untested ones, the prefix-lessZ#Nforms,3#0#1, and the review's malformed writes:*#22*##,*#22*1##,*#22*1*0215##, five fields, out-of-rangeMOD/SP/VEL/SWING, the smuggled*#22*1#*#4*2*22##,+1, a trailing newline,\uff11);SPLIT_UNIT_FRAMES(10 canonical3#Z#Nforms incl. the touchscreen payloads and the range ends3#1#1,3#99#9,5*040*4*1) asserted detectorTrue/ crash formFalseand written unchanged byOWNCommandSession.send(reads withis_status_request=True, writes withFalse),send_to_gateway(connects),AsyncTcpTransport.send(new positive case) andAsyncSerialTransport.send(all split forms plus a terminatorless*#4*3#2#1*22, written as*#4*3#2#1*22##);WHERE_HASH_4_BUILDERSuses master's#0#4central-unit fixtures (fix(heating): set_central_mode requires a central-unit address #137).CHANGELOG.md: the[Unreleased]/Fixedbullet rewritten to what is refused and what is sent, with the3#Z#NvsZ#Noracle split and the emulation caveat.State Diagram
stateDiagram-v2 direction TB state "send paths: OWNCommandSession.send / send_to_gateway, AsyncSerialTransport.send" as Guard { [*] --> Chunk Chunk --> NotDim22 : _WHO4_DIMENSION_22_REGEX no match\n*#4*22##, *#13**22##, *#2*#4*#11#001*22## Chunk --> Sendable : canonical *#4*3#Z#N*22## or *#4*3#Z#N*#22*MOD*SP*VEL*SWING##\nZ 1..99, N 1..9, exactly four payload fields Chunk --> Refused : every other DIMENSION 22 spelling\n*#4*2*22##, *#4*0*22##, *#4*#0*22##, *#4*2*#22*...##, *#4*2#1*22##, *#4*3#0#1*22##, *#4*3#2#1*#22*1##, *#04*2#1*022## Refused --> [*] : OWNProtocolError reason destructive_frame, never written Sendable --> Validate : OWNMessage.parse / is_valid as any frame NotDim22 --> Validate } state "oracle thermo-dim22, 12 gateways x 2 runs" as Oracle { [*] --> NoToken NoToken --> Crash : *#4*2*22##, *#4*2*#22*1*0215*2*0##, *#4*#0*22##, *#4*0*22##\nbt_termo SIGSEGV on 9 of 12 (F461, MyHomeServer1 included) [*] --> Token Token --> Out : *#4*3#2#1*22##, *#4*3#2#1*#22*...## out on 10 of 12 (sent)\n*#4*2#1*22##, *#4*45#1*#22*3**1*1## silent on 11, out on MH202 (kept refused) }Where are the traces
results/<P>/<V>/oracle/full/thermo-dim22.tsv(OpenWebNet-HA/own-firmware-oracle#68), emulated, two runs*#4*2*22##What does the Oracle say
crashoutsilent*#4*2*22##,*#4*2*#22*1*0215*2*0##,*#4*#0*22##,*#4*0*22##*#4*3#2#1*22##,*#4*3#2#1*#22*1*0215*2*0##*#4*2#1*22##,*#4*2#1*#22*1*0215*2*0##,*#4*45#1*#22*3**1*1##,*#4*45#1*#22*4**1*1##*#4*45#1*#22*0***##tools/expected_divergences.yamlgains the zone write and the general read for F461 in the oracle PR. Not settled by the rows, and so still refused here:0ZZ,#Z,#0#C,Z#4#IF,N0, other payloads' effect on a real split unit.Testing
pytest testsagainst this head (54b5944 on master afedabf): 2499 passed (-o asyncio_default_fixture_loop_scope=functionlocally, as CI sets since ci(pytest): set asyncio_default_fixture_loop_scope so pytest-asyncio 1.4.1 does not abort the suite #150).ruff check OWNd testsclean;mypy OWNdclean._WHO4_DIMENSION_22regex inservices.py/websocket.py, which still blocks every form from the service and the websocket; it does not call OWNd's detector, so the conformance suite is unaffected. Narrowing it is MyHOME's call once this lands.Revision 54b5944 (review 5724753021)
Rebased onto master
afedabf(#137, #138, #142) as one re-authored commit. Item 1: both regex arms now end in\Zand the write arm spells the four published fields with their ranges, so the listed short / long / whitespace / newline / smuggled / Unicode payloads are refused (tests added). Item 2:Zrestricted to 1..99 (3#0#1refused and tested). Item 3: the prefix-lessZ#Nforms are refused again, and the code comments, docstring, tests and CHANGELOG separate3#Z#N(10/12out) fromZ#N(11/12nack, silent, MH202 only) and carry the emulation-only caveat. Item 4:WHERE_HASH_4_BUILDERSon master's#0#4fixtures. Test notes:is_status_requestfollows the frame kind, the serial test runs every split form plus a terminatorless one, and a TCP-transport positive case mirrors the four entry points.Companion
🤖 Generated with Claude Code