Skip to content

fix(tcp): resolve the connection flags before building the options (#587) - #597

Merged
JarryShaw merged 1 commit into
mainfrom
fix/587-mp-join-flag-ordering
Sep 21, 2026
Merged

JarryShaw merged 1 commit into
mainfrom
fix/587-mp-join-flag-ordering

Conversation

@JarryShaw

@JarryShaw JarryShaw commented Sep 21, 2026

Copy link
Copy Markdown
Owner

Fixes #587

The defect

Constructing any MP_JOIN option raised AttributeError: 'TCP' object has no attribute '_flags'.

TCP.make built the options first and assigned the flags afterwards. Line numbers re-located on origin/main (fa6d18e31) — the issue's own citations for _make_mptcp_join and _read_mptcp_join predate #585 and have since moved:

What origin/main In the issue
make builds options tcp.py:547 tcp.py:547
make assigns self._flags tcp.py:567 tcp.py:567
_make_mptcp_join branches tcp.py:2780-2786 tcp.py:2695-2699moved
read assigns self._flags tcp.py:485 tcp.py:485
read parses options tcp.py:494
_read_mptcp_join branches tcp.py:1526-1532 tcp.py:1521-1525moved

The read path was never affected: read assigns the flags at tcp.py:485, before it parses the options at tcp.py:494, so the identical branches at tcp.py:1526-1532 always had them. Only make was inverted.

Why the branch is right and the flags have to move

MP_JOIN is genuinely flag-dependent. RFC 8684 section 3.2 gives it three layouts, and they are three different lengths, so no single form can serve:

Segment Figure Length Payload after Kind/Length
SYN figure 5 12 subtype/rsv/B 1, Address ID 1, token 4, random number 4
SYN/ACK figure 6 16 subtype/rsv/B 1, Address ID 1, truncated HMAC 8, random number 4
ACK figure 7 24 subtype and 12 reserved bits 2, full HMAC 20

The fix

Hoist the flags dict and the _flag accumulation above the _make_tcp_options call. The offset computation stays below it, being the one statement in that block that genuinely needs the options' total_length.

#585's author called this "a two-line hoist". Checked, and it is not: _flag is accumulated from the flags dict, so the dict has to travel with it — a 16-line block move, plus one changed line. Hoisting only self._flags = _flag would not compile. Nothing between the old and new sites reads self._flags or depends on the option build, so the move is behaviour-preserving for every other option; TCPMakeOtherOptionsUnitTests is the witness.

It is deliberately not a zero initialisation

Protocol.pack is public and calls make, so an instance that has already parsed a segment can be asked to build a different one — and it already has _flags set, so no default value would ever come into play for it. Pre-fix it silently built the option for the segment it had read. Measured on origin/main: a parsed MP_JOIN-SYN instance asked to pack an MP_JOIN-ACK segment produced

c3500050 00000001 00000000 8010 2000 0000 0000  1e0c 10 00 00000000 00000000

— header flag octet 0x10 (ACK, as requested), option 1e0c…, figure 5's 12-octet SYN form, with the caller's 20-octet HMAC (the entire authentication payload of figure 7, and the only reason to send that form) replaced by an all-zero phantom token and nonce. Nothing raised. Post-fix the same call emits 1e18 10 00 + the 20 HMAC octets: figure 7, length 24.

So the AttributeError was the benign symptom. A zero default fixes only the fresh-instance crash, leaves the silent corruption exactly as it was, and converts the flagless case into a spurious error. TCPMPTCPJoinStaleFlagsUnitTests pins this.

A second, smaller fix in the same block

The hoist made one branch reachable for the first time: an MP_JOIN asked for with neither SYN nor ACK set. _make_mptcp_join ends in a ProtocolError for exactly that case, but it could not be reached — the accumulator was seeded with cast('Enum_Flags', 0), and typing.cast is a runtime no-op, so with no flag set self._flags stayed the plain int 0 and the first membership test raised instead:

TypeError: argument of type 'int' is not a container or iterable

Seeding with Enum_Flags(0) — a real flagless aenum.IntFlag member, which still compares equal to 0 and still ORs identically — lets the library's own documented error surface. A bare TypeError escaping the library would break the in-library exception contract, and it is newly reachable only because of this change, so it is fixed here rather than deferred.

The read path was checked for the same shape of problem and does not have it: mptcp_data_selector guards the flagless case at schema/transport/tcp.py:204-212 and raises FieldError before _read_mptcp_join is reached, so its own closing ProtocolError stays unreachable. Its cast is deliberately left alone — changing it would alter the connection value reported for every flagless parsed segment, which is not what #587 is about. Both halves of that reasoning are asserted, so the decision cannot rot silently.

Failing-then-passing evidence

New module, on the unfixed tree:

12 failed, 9 passed, 1 warning, 9 subtests passed in 1.37s

exit code 1. The representative failures, verbatim:

E       AttributeError: 'TCP' object has no attribute '_flags'
pcapkit/protocols/transport/tcp.py:2780: AttributeError
E       AssertionError: b'\x1e\x0c\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00' != b'\x1e\x18\x10\x00\xee\xee\xee\xee\xee\xee\xee\xe[45 chars]\xee' : the option must be figure 7, not the parsed segment's figure 5
E       AssertionError: 2390 not less than 1797 : TCP.make must assign self._flags before calling _make_tcp_options: the option makers reached from it read that attribute, and _make_mptcp_join branches on it to pick between RFC 8684 section 3.2 figures 5, 6 and 7
E               AssertionError: 0 is not an instance of <aenum 'Flags'>

With the fix:

17 passed, 1 warning, 16 subtests passed in 0.65s

exit code 0.

The round-trip suite

EXPECTED_FAILURES carried a tcp-mptcp/MP_JOIN entry that existed because of this defect, so it goes stale here. All three states measured (tests/protocols/test_option_roundtrip_unit.py):

State Result Exit
Unfixed tree, entry present 6 passed, 358 subtests passed 0
Fixed tree, entry still present 1 failed, 6 passed, 357 subtests passed 1
Fixed tree, entry deleted 6 passed, 358 subtests passed 0

The middle row is the suite's own contract working as designed — it failed with 'OK' != 'CONSTRUCT' and the message "If the defect is fixed, delete its EXPECTED_FAILURES entry." That one entry is deleted and no other is touched; the table goes from 46 entries to 45, and no MPTCP entry remains.

A finding worth recording

Statement and branch coverage of both TCP modules was already 100% on origin/main with the pristine tests, while the public MP_JOIN constructor was broken for every caller. Measured, pristine tree and pristine tests:

pcapkit/protocols/schema/transport/tcp.py     219      0     20      0   100%
pcapkit/protocols/transport/tcp.py            594      0    198      0   100%

The reason is that the pre-existing cases reach _make_mptcp_join by assigning a Python set to _flags on a bare TCP.__new__(TCP) (test_tcp_udp_unit.py:828-862) — Flags.SYN in {Flags.SYN} is true, so every branch executes, while both the statement ordering and the accumulator's real type are bypassed. 100% coverage of a path no caller can reach the way a caller reaches it. That is what the new module covers: everything in it goes through the public constructor or through pack on a parsed instance.

Testing

  • tests/protocols/transport/test_tcp_mptcp_join_flag_ordering_unit.py — new, 17 cases in six classes: the three layouts through the public constructor with their RFC octets, the construct/pack/parse cycle per layout, the stale-flags case, the statement order itself (so a future refactor cannot silently re-invert it), and controls for the parse path and for flag-independent options.
  • test_tcp_mptcp_subtype_unit.py — the MP_JOIN exclusion its docstring recorded is lifted, and the subtype assertion it could not make for MP_JOIN is added, once per layout. build_mptcp_option gains an optional header= override, defaulting to the previous behaviour.
  • test_tcp_mptcp_length_arithmetic_unit.py (_make_mptcp_*/_read_mptcp_* length arithmetic is wrong at six more sites beyond MP_CAPABLE #576/fix(tcp): correct MPTCP option length arithmetic at all six #576 sites #585) — untouched and still passing.
  • tests/protocols/615 passed, 1469 subtests passed, exit 0.
  • tests/project/, tests/test_docstring_contract.py, tests/test_tier_guard.py138 passed, 882 subtests passed, exit 0.
  • tests/integration/90 passed, 2 skipped, 115 subtests passed, exit 0, after regenerating the captures with examples/generators/make_samples.py against the fixed tree.
  • Coverage of the two TCP modules stays at 100% statement and branch; it was at the ceiling before, so the gain here is behavioural rather than numerical.

Every run was scoped with coverage run --include='pcapkit/protocols/transport/tcp.py,pcapkit/protocols/schema/transport/tcp.py' and a narrowed test path, and exit codes were read from a file rather than from the summary line, since pytest 9.1.1 prints PASSED for the parent of a failing subtest.

Changelog

A **Fixed** and an **Added** bullet in docs/source/changelog/1.5.0.rst; CHANGELOG.md regenerated with python util/changelog_md.py, and python util/changelog_md.py --check exits 0.

Found but deliberately not fixed

  1. test_tcp_udp_unit.py assigns a set to _flags at lines 828, 834, 840, 845, 937, 961, 977, 990 (and 490-526), where production code produces an aenum.IntFlag. The assertions pass either way, but they are asserting against a value shape the library never emits, which is exactly why neither defect here was caught despite 100% branch coverage. proto._flags = Flags.SYN and Flags(0) would be behaviour-identical; that is a test-fidelity change with its own blast radius and belongs in its own PR.
  2. examples/generators/options.py's TCP_BASE passes keywords TCP.make does not declare. 'seq': 1 and 'ack_flag': False are both swallowed by **kwargs, while 'ack': 0 binds to ack, the acknowledgement flag, not to ack_no. Measured: TCP(**TCP_BASE).info.seq is 0, not the 1 the mapping reads as. Harmless to what those cases assert, and changing it would churn every generated fixture, so it is only documented — the new module defines its own TCP_HEADER with make's real parameter names and says why.
  3. _read_mptcp_join's closing ProtocolError (tcp.py:1532) is unreachable, because mptcp_data_selector rejects a flagless MP_JOIN first. Left as it is; the reasoning is now recorded in code and pinned by a test rather than being rediscovered.
  4. _make_mptcp_*/_read_mptcp_* length arithmetic is wrong at six more sites beyond MP_CAPABLE #576/fix(tcp): set MPTCP.subtype on construction, fix MP_CAPABLE's length/rkey (#566, #567) #579/fix(tcp): correct MPTCP option length arithmetic at all six #576 sites #585 added no changelog bullets. Not backfilled here, to keep this PR to one issue.

Cross-review

Reviewed independently on a different model (Sonnet; this change was authored on Opus 5), briefed to falsify rather than confirm. Verdict: NEEDS CHANGES, one substantive finding, now addressed — so GOOD TO GO as of 14c400e65.

It reproduced C1, C3 and C10 independently rather than taking them on trust, by extracting a pristine origin/main with git archive into a scratch directory instead of mutating the worktree. Its pre-fix stale-flags measurement came back as option bytes 1e0c10000000000000000000 — figure 5, 12 octets, zeroed token and nonce, HMAC dropped — matching the byte string quoted above, and its independently re-measured pre-fix coverage came back as exactly 594 0 198 0 100% and 219 0 20 0 100%, matching the numbers quoted above.

The finding it raised, and what changed. test_packing_replaces_the_parsed_flags passed on the unfixed tree as well, so it was not a discriminator, while its docstring — "the mechanism behind the case above, asserted directly" — implied it was. That is a fair hit and the docstring was overclaiming.

It offered two remedies; the first does not work, and that is worth recording. Passing a real options= argument would not make the test discriminate: pre-fix, make assigned self._flags after building the options, so by the time pack returned the attribute held the new call's flags on either tree, and only the option octets built in between were wrong. The assertion that separates the trees is therefore on those octets, which test_parsed_instance_packs_the_requested_layout already owns. So the second remedy was taken — the case is renamed test_make_re_derives_the_flags_on_every_call, its docstring now states in bold that it is not a regression guard for #587 and passed pre-fix, explains the property the fix leans on that it does pin, and records why adding options= would not change that. A third flag combination was added so the assertion cannot be satisfied by a one-off rather than by a per-call derivation.

Also fixed from the same review: a prose miscount, "the other seven flags" where the flags dict has eight keys and excluding SYN/ACK leaves six. Now named explicitly (cwr, ece, urg, psh, rst, fin).

Its remaining verdicts — C2, C4, C5, C7, C8, C9 — all held, including every one of the line citations above and that the schema module's diff is empty. Two things it could not establish, stated as such rather than glossed: it derived the RFC 8684 figure/length map from the in-repo docstrings rather than from the RFC text, having no verified network access; and it did not exhaustively audit every other protocol's make() for an analogously-shaped ordering defect under a different attribute name, noting that as a separate, larger investigation.

Rebased

Rebased onto 0bd517a1c (after #595 merged). One commit per package, on top of the current remote. The only conflicts were in the two changelog files — no source conflict, so the tcp.py change and the tests are untouched by the rebase. docs/source/changelog/1.5.0.rst was resolved by keeping #595's bullet and these, not by choosing; CHANGELOG.md was not hand-merged but discarded and regenerated with python util/changelog_md.py, and python util/changelog_md.py --check exits 0.

Re-verified after the rebase: tests/protocols/transport/, tests/protocols/test_option_roundtrip_unit.py, tests/project/, tests/test_docstring_contract.py and tests/test_tier_guard.py together give 244 passed, 957 subtests passed, exit 0.

One thing deliberately left out

The mptcp_dss_ack_selector docstring at pcapkit/protocols/schema/transport/tcp.py:270-280, and the matching paragraphs in test_tcp_mptcp_length_arithmetic_unit.py, say that a corrected callable-length NumberField "would not have worked" and that fixing it "belongs to pcapkit.corekit.fields.numbers". #598 fixes exactly that, which will make those paragraphs stale.

Not corrected here, on purpose: #598 is still open and unmerged, so as of 0bd517a1c that prose still describes main accurately. Rewriting it now would make this PR assert something untrue of the tree it targets, and would be wrong again if #598 changes shape or is abandoned. The correction belongs in #598 itself — the change that makes the old wording false should be the change that replaces it — or in a follow-up once it has landed. Flagging it here so it is not lost. (Per #591's worker, the SwitchField workaround itself stays regardless: its NoValueField() branch handles the field being absent from the wire when the DSS flag is clear, which a callable-length NumberField cannot express. Only the justification goes stale, not the code.)

@JarryShaw

Copy link
Copy Markdown
Owner Author

✅ GOOD TO MERGE — head af5d7230cb816d8926d8f3dad05865c304ad8231 (note: GitHub reports mergeable: CONFLICTING against main because #595 merged first and both touch docs/source/changelog/1.5.0.rst/CHANGELOG.md — a rebase is needed before merge, but this is a git-history issue, not a code defect; see appendix for the code review).

@JarryShaw

Copy link
Copy Markdown
Owner Author

Cross-review appendix — PR #597

Reviewer: Sonnet; PR authored on Opus 5. Reviewed at head af5d7230cb816d8926d8f3dad05865c304ad8231 in an isolated worktree (/tmp/pcapkit-review/pr597, removed after this review).

Fixes keyword and CI

closingIssuesReferences = [587], matching Fixes #587. CI at review time: rollup PENDING, CheckRun tally: 6 SUCCESS, 2 SKIPPED (both COMPLETED), 14 QUEUED, 2 IN_PROGRESS, 0 FAILURE.

Note on mergeable: CONFLICTING: main has moved to 0bd517a1c (PR #595 merged while I was reviewing #596) since this PR branched from fa6d18e31, and both touch docs/source/changelog/1.5.0.rst/CHANGELOG.md. This is a rebase issue, not a code defect — flagging it so it doesn't get missed before merge.

RFC 8684 §3.2, independently re-derived

Fetched rfc-editor.org/rfc/rfc8684.txt directly rather than trusting the PR's table:

  • Line 1025: 3.2. Starting a New Subflow.
  • Line 1063: Figure 5: Join Connection (MP_JOIN) Option (for Initial SYN), Length = 12 (line 1055).
  • Line 1138: Figure 6: Join Connection (MP_JOIN) Option (for Responding SYN/ACK), Length = 16 (line 1129).
  • Line 1178: Figure 7: Join Connection (MP_JOIN) Option (for ACK), Length = 24 (line 1169).

This matches the PR's table exactly, and matches this programme's previously-established figure map (5=SYN, 6=SYN/ACK, 7=ACK, all §3.2), which I'm relying on as already vetted but re-confirmed the lengths myself.

The fix itself

Read pcapkit/protocols/transport/tcp.py's make() directly: the flags dict, the _flag accumulation loop, and self._flags = _flag are hoisted above the if options is not None: self._make_tcp_options(options) call; only the offset = math.ceil((20 + total_length) / 4) computation (which genuinely needs total_length) stays below. The accumulator seed is Enum_Flags(0), not cast('Enum_Flags', 0) — confirmed by reading the code, not just the diff.

Functionally reproduced both failure modes on the unfixed tree, and confirmed the fix resolves both

Swapped pcapkit/protocols/transport/tcp.py for main's version (test files untouched) and exercised the public API directly (via Enum_Option.Multipath_TCP + subtype=Enum_MPTCPOption.MP_JOIN, matching the call shape in the PR's own new test helper build_join):

  1. Fresh instance, single construction call (TCP(syn=True, ack=False, options=[...], **TCP_HEADER)): AttributeError: 'TCP' object has no attribute '_flags' — reproduced verbatim.
  2. Stale-flags corruption: parsed a genuine MP_JOIN-SYN segment from raw bytes, then called .pack(syn=False, ack=True, options=[(Multipath_TCP, {...MP_JOIN ACK args with a 20-byte hmac...})]) on that same instance. Unfixed tree emitted 1e0c10090000000000000000 — Kind 30, Length 12 (figure 5, the SYN form), with the caller's HMAC replaced by 8 zero bytes. This is the exact corruption shape the PR describes (wrong layout, dropped authentication payload, nothing raised).

Restored the fix and reran the identical two scenarios:

  1. Fresh single-call construction: succeeds, no exception.
  2. Same stale-flags scenario: now correctly emits 1e181000 + 20 \xee octets — Length 24, figure 7, the caller's real HMAC intact.

Also independently built all three layouts through the public API and checked the emitted option bytes against the RFC structure directly:

  • SYN: 1e0c1001deadbeef11111111 — Kind 30, Length 12, subtype/rsv/B, AddrID 1, token, nonce. Matches figure 5.
  • SYN/ACK: 1e101002cccccccccccccccc22222222 — Length 16, 8-octet truncated HMAC + 4-octet nonce. Matches figure 6.
  • ACK: 1e181000 + 20×\xee — Length 24, 2 octets subtype+reserved, 20-octet full HMAC. Matches figure 7.

Flagless case: on the fixed tree, TCP(syn=False, ack=False, options=[MP_JOIN...]) raises ProtocolError: TCP: : [OptNo 30] 1: invalid flags combination — the library's documented error, not a bare TypeError, confirming the "second, smaller fix" (seeding with Enum_Flags(0) rather than cast(..., 0)) is real and correctly wired.

Tests fail without the fix, pass with it

Reverted only pcapkit/protocols/transport/tcp.py to fa6d18e31, ran the new module:

12 failed, 9 passed, 1 warning, 9 subtests passed in 1.32s   (exit 1)

Exact match to the PR's claimed evidence. Restored the fix:

17 passed, 1 warning, 16 subtests passed in 0.64s   (exit 0)

Matches the PR's claimed 17 passed, 1 warning, 16 subtests passed in 0.65s almost to the second.

EXPECTED_FAILURES table — verified against the PR's actual base, not a stale local checkout

First attempt compared against a local main checkout that turned out to be at 8cfd6ab01 (stale, predates fa6d18e31) and got a confusing 54-vs-45 mismatch. Corrected by creating a worktree at the PR's declared base fa6d18e31 itself and importing the table there (grepping is unreliable — the table uses ** unpacking, per this programme's own earlier note on #596): 46 entries at fa6d18e31, including 'tcp-mptcp/MP_JOIN'; 45 entries on this PR's branch, with that key absent. Exactly matches "the table goes from 46 entries to 45, and no MPTCP entry remains."

Ran tests/protocols/test_option_roundtrip_unit.py on the PR's committed state: 6 passed, 1 warning, 358 subtests passed, exit 0 — matches the claimed "fixed tree, entry deleted" row exactly.

Regression

python util/changelog_md.py --check exits 0. Ran examples/generators/make_samples.py (fresh worktree, no committed fixtures) then tests/protocols/transport/: 142 passed, 99 subtests passed, exit 0.

Found-but-not-fixed disclosures

Spot-checked: tests/protocols/transport/test_tcp_udp_unit.py does assign Python set literals (proto._flags = {Flags.SYN}, etc.) at multiple lines including 490, 503, 516, 828, 834, 840, 937, 961, 977 — confirmed the shape of the claim (production code emits aenum.IntFlag, these tests bypass that with a set, which is why 100% branch coverage coexisted with a totally broken constructor). Did not chase every one of the PR's cited line numbers individually.

Not independently checked

  • mptcp_data_selector's guard at schema/transport/tcp.py:204-212 (claimed to make the read-path's equivalent ProtocolError branch unreachable) was read but not exercised with a crafted flagless-MP_JOIN wire capture.
  • The full tests/protocols/ (615 passed claim) and tests/project//tests/integration/ suites were not rerun in full — the scoped tests/protocols/transport/ run plus the two targeted test-evidence reproductions above were treated as sufficient given the size of the full suites and this repo's host-safety constraints.

Disagreement log

None. Every claim I checked — the RFC figures and lengths, the AttributeError, the stale-flags corruption and its fix, the flagless-case error type, the test pass/fail counts, and the EXPECTED_FAILURES table delta — held up exactly under independent reproduction.

)

Building any MP_JOIN option raised `AttributeError: 'TCP' object has no attribute
'_flags'`. `TCP.make` built the options at tcp.py:547 and assigned `self._flags`
only at tcp.py:567, but `_make_mptcp_join` (tcp.py:2780-2786) branches on that
attribute to choose between RFC 8684 s3.2's three MP_JOIN layouts: figure 5 for
SYN at 12 octets, figure 6 for SYN/ACK at 16, figure 7 for ACK at 24. The parse
path was always fine -- `read` assigns at tcp.py:485, before parsing options at
tcp.py:494, so the identical branches at tcp.py:1526-1532 always had them.

- Hoist the `flags` dict and the `_flag` accumulation above the
  `_make_tcp_options` call. Only the data-offset computation stays below it,
  needing the options' `total_length`. Not a two-line move: `_flag` derives from
  the `flags` dict, so the dict has to travel with it.
- Not fixed with a zero default. `Protocol.pack` is public and calls `make`, so
  an instance that had parsed already had `_flags` set and silently built the
  option for the segment it had *read*. Measured: a parsed MP_JOIN-SYN instance
  asked to pack an MP_JOIN-ACK segment emitted an ACK header carrying figure 5's
  12-octet SYN option, the caller's 20-octet HMAC replaced by an all-zero token
  and nonce, with nothing raised. A default fixes only the crash.
- Seed the accumulator with `Enum_Flags(0)` rather than `cast('Enum_Flags', 0)`.
  The hoist made the no-SYN-no-ACK branch reachable, and `typing.cast` being a
  runtime no-op left `_flags` a plain `int` on which the first membership test
  raised `TypeError` instead of the documented `ProtocolError`. The read path's
  seed is left alone: `mptcp_data_selector` rejects a flagless MP_JOIN before
  `_read_mptcp_join` runs.
- New test_tcp_mptcp_join_flag_ordering_unit.py, 17 cases: all three layouts
  through the public constructor, the pack/parse cycle per layout, the
  stale-flags case, the statement order, and parse-path and other-option
  controls. On the unfixed tree it reports `12 failed, 9 passed, 9 subtests
  passed` (exit 1); with the fix, `17 passed, 16 subtests passed` (exit 0).
- Delete the now-stale `tcp-mptcp/MP_JOIN` EXPECTED_FAILURES entry and lift the
  MP_JOIN exclusion in test_tcp_mptcp_subtype_unit.py.

tests/protocols/ 615 passed / 1469 subtests, exit 0. Coverage of the two TCP
modules was already 100% pre-fix while this path was wholly broken, because the
existing cases assign a Python `set` to `_flags` on a bare `TCP.__new__(TCP)`.
@JarryShaw
JarryShaw force-pushed the fix/587-mp-join-flag-ordering branch from af5d723 to 14c400e Compare September 21, 2026 23:25
@JarryShaw

Copy link
Copy Markdown
Owner Author

Cross-review (Sonnet, authored on Opus 5): GOOD TO GO as of 14c400e65 — opened NEEDS CHANGES on one non-discriminating test whose docstring overclaimed; renamed, honestly re-documented, and a third flag combination added. C1/C3/C10 were reproduced independently against a pristine origin/main and matched the quoted bytes and coverage numbers exactly; all line citations verified. Two items it explicitly could not verify are named in the PR body.

@JarryShaw
JarryShaw merged commit 83987fb into main Sep 21, 2026
25 checks passed
@JarryShaw
JarryShaw deleted the fix/587-mp-join-flag-ordering branch September 21, 2026 23:34
@JarryShaw

Copy link
Copy Markdown
Owner Author

Re-pointing my verdict — PR #597

My 23:24:12Z comment (**✅ GOOD TO MERGE**) was pinned to the pre-rebase head af5d7230cb816d8926d8f3dad05865c304ad8231. The PR has since rebased onto the new base 0bd517a1c (after #595 merged) and now sits at 14c400e6577c19438f9b84a7aeafab5fbbab98d1. Re-verified rather than assumed:

  • git diff af5d7230c 14c400e65 -- pcapkit/protocols/transport/tcp.py is empty — the fix code I reviewed is byte-identical across the rebase.
  • The naive git diff <old-head> <new-head> (not against the new base) shows changes in util/changelog_md.py and tests/project/test_changelog_md.py — that is fix(changelog-md): cite a line the entry has, and accept an anchored :rfc: role (#588, #592) #595's content arriving through the rebase, not anything this PR authored; comparing each head against its own base (fa6d18e31af5d7230c vs. 0bd517a1c14c400e65) for the files this PR actually owns shows only one genuine change.
  • That one genuine change: tests/protocols/transport/test_tcp_mptcp_join_flag_ordering_unit.py had one test renamed and rewritten — test_packing_replaces_the_parsed_flagstest_make_re_derives_the_flags_on_every_call, with a new docstring stating plainly "This case is not a regression guard for Constructing an MP_JOIN option raises AttributeError: TCP._make builds the options before assigning self._flags #587, and it passed on the unfixed tree too" (apparently in response to a separate cross-review that found the old docstring overclaimed what the test verified). I independently confirmed this claim rather than accepting it: reverted only tcp.py to fa6d18e31 and ran the renamed test in isolation — exit 0, it passes on the unfixed tree exactly as it now admits. I also reconfirmed the actual discriminator, TCPMPTCPJoinStaleFlagsUnitTests::test_parsed_instance_packs_the_requested_layout, still fails on the unfixed tree (exit 1) and passes on the fixed one, which is the property that matters. The full module still runs 17 passed, 1 warning, 16 subtests passed on the new head, unchanged. This is a legitimate, honest correction, not scope creep.

On the other comment on this PR (23:26:29Z, "Cross-review (Sonnet, authored on Opus 5): GOOD TO GO as of 14c400e65"): that is not my comment. I never opened NEEDS CHANGES on #597. Per the coordinator, that is the author relaying its own cross-review claim, not an independent second pass — treat it as the author's claim about its own work, same as this program's earlier #593 pattern. Note also it uses the string "GOOD TO GO", not this program's required **✅ GOOD TO MERGE** / **❌ NEEDS CHANGES** tokens — mine below is the one to grep for.

✅ GOOD TO MERGE — head 14c400e6577c19438f9b84a7aeafab5fbbab98d1. All findings in my prior appendix (comment at 23:24:12Z on this PR) stand unchanged: tcp.py is byte-identical to what I verified there.

JarryShaw added a commit that referenced this pull request Sep 22, 2026
…t a no-op cast

* `TCP.read` seeded `_flag` with `cast('Enum_Flags', 0)`. `typing.cast` is a
  runtime no-op, so a segment whose flags octet is all zero left `self._flags`
  a plain `int` -- the `|=` below is the only thing that promotes it, and it
  never runs. A membership test then raised `TypeError: argument of type 'int'
  is not a container or iterable`, and `TCP.connection` returned an `int` where
  it and `Data_TCP.connection` both annotate `Flags`. Seed `Enum_Flags(0)`,
  matching what #597 did to the sibling accumulator in `make`.
* Correct the prose that recorded the read path as deliberately keeping its
  `cast`: the NOTE in `make`, one NOTE in `test_tcp_udp_unit.py`, and two
  docstrings in `test_tcp_mptcp_join_flag_ordering_unit.py`.
* Add `test_a_flagless_segment_seeds_its_connection_flags_as_an_enum`, four
  subtests over the flags octet, asserting the *type* -- `0 == Enum_Flags(0)`,
  so equality cannot discriminate -- plus that `_read_mptcp_join` now reaches
  its own `ProtocolError` rather than a bare `TypeError`, plus the one
  observable difference: a flagless segment's `connection` dumps as
  `'Flags::None [0]'` where it dumped as `0`. That removes a type inconsistency
  (number when flagless, string otherwise); the literal `None` is a separate
  rendering defect in `pcapkit.dumpkit.common` and is left to its own change.

Coverage cannot see the fix: the line already executed, and `tcp.py` reads
100%/100% either side. Subtests over `tests/protocols/transport/` go 99 -> 103.
Verified against the unfixed snapshot: the new test fails `AssertionError:
<class 'int'> is not <aenum 'Flags'>`, exit 1; with the fix, exit 0. No
caller-visible parse change -- a flagless MP_JOIN still stops at
`mptcp_data_selector`'s `FieldError`, measured identical both sides.

Fixes #616
@JarryShaw JarryShaw added the fix Pull requests that fix a defect (fix: subject prefix) label Sep 22, 2026
JarryShaw added a commit that referenced this pull request Sep 22, 2026
…t a no-op cast

* `TCP.read` seeded `_flag` with `cast('Enum_Flags', 0)`. `typing.cast` is a
  runtime no-op, so a segment whose flags octet is all zero left `self._flags`
  a plain `int` -- the `|=` below is the only thing that promotes it, and it
  never runs. A membership test then raised `TypeError: argument of type 'int'
  is not a container or iterable`, and `TCP.connection` returned an `int` where
  it and `Data_TCP.connection` both annotate `Flags`. Seed `Enum_Flags(0)`,
  matching what #597 did to the sibling accumulator in `make`.
* Correct the prose that recorded the read path as deliberately keeping its
  `cast`: the NOTE in `make`, one NOTE in `test_tcp_udp_unit.py`, and two
  docstrings in `test_tcp_mptcp_join_flag_ordering_unit.py`.
* Add `test_a_flagless_segment_seeds_its_connection_flags_as_an_enum`, four
  subtests over the flags octet, asserting the *type* -- `0 == Enum_Flags(0)`,
  so equality cannot discriminate -- plus that `_read_mptcp_join` now reaches
  its own `ProtocolError` rather than a bare `TypeError`, plus the one
  observable difference: a flagless segment's `connection` dumps as
  `'Flags::None [0]'` where it dumped as `0`. That removes a type inconsistency
  (number when flagless, string otherwise); the literal `None` is a separate
  rendering defect in `pcapkit.dumpkit.common` and is left to its own change.

Coverage cannot see the fix: the line already executed, and `tcp.py` reads
100%/100% either side. Subtests over `tests/protocols/transport/` go 99 -> 103.
Verified against the unfixed snapshot: the new test fails `AssertionError:
<class 'int'> is not <aenum 'Flags'>`, exit 1; with the fix, exit 0. No
caller-visible parse change -- a flagless MP_JOIN still stops at
`mptcp_data_selector`'s `FieldError`, measured identical both sides.

Fixes #616
JarryShaw added a commit that referenced this pull request Sep 22, 2026
…t a no-op cast (#616) (#634)

* `TCP.read` seeded `_flag` with `cast('Enum_Flags', 0)`. `typing.cast` is a
  runtime no-op, so a segment whose flags octet is all zero left `self._flags`
  a plain `int` -- the `|=` below is the only thing that promotes it, and it
  never runs. A membership test then raised `TypeError: argument of type 'int'
  is not a container or iterable`, and `TCP.connection` returned an `int` where
  it and `Data_TCP.connection` both annotate `Flags`. Seed `Enum_Flags(0)`,
  matching what #597 did to the sibling accumulator in `make`.
* Correct the prose that recorded the read path as deliberately keeping its
  `cast`: the NOTE in `make`, one NOTE in `test_tcp_udp_unit.py`, and two
  docstrings in `test_tcp_mptcp_join_flag_ordering_unit.py`.
* Add `test_a_flagless_segment_seeds_its_connection_flags_as_an_enum`, four
  subtests over the flags octet, asserting the *type* -- `0 == Enum_Flags(0)`,
  so equality cannot discriminate -- plus that `_read_mptcp_join` now reaches
  its own `ProtocolError` rather than a bare `TypeError`, plus the one
  observable difference: a flagless segment's `connection` dumps as
  `'Flags::None [0]'` where it dumped as `0`. That removes a type inconsistency
  (number when flagless, string otherwise); the literal `None` is a separate
  rendering defect in `pcapkit.dumpkit.common` and is left to its own change.

Coverage cannot see the fix: the line already executed, and `tcp.py` reads
100%/100% either side. Subtests over `tests/protocols/transport/` go 99 -> 103.
Verified against the unfixed snapshot: the new test fails `AssertionError:
<class 'int'> is not <aenum 'Flags'>`, exit 1; with the fix, exit 0. No
caller-visible parse change -- a flagless MP_JOIN still stops at
`mptcp_data_selector`'s `FieldError`, measured identical both sides.

Fixes #616
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fix Pull requests that fix a defect (fix: subject prefix)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Constructing an MP_JOIN option raises AttributeError: TCP._make builds the options before assigning self._flags

1 participant