Skip to content

tests: port 0 is written only in test_support::ports; per-session checkpoint fault count - #155

Merged
jaredLunde merged 2 commits into
mainfrom
jared/port-zero-only-in-ports
Oct 7, 2026
Merged

jaredLunde merged 2 commits into
mainfrom
jared/port-zero-only-in-ports

Conversation

@jaredLunde

@jaredLunde jaredLunde commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Two follow-ups from #151.

1. Port 0 is written only in beyond_ai_test_support::ports

This replaces the released-port data-flow lint (serve_harness_deadlines.rs, about 580 lines removed). That lint had to follow a socket through bindings, Options, closures and helpers in other files, and every audit found a shape it missed.

The new rule (crates/agent/tests/ports_lint.rs) needs no data-flow analysis. It looks at how code is written, not at what happens to the socket. A zero port must not be written anywhere in the workspace except crates/test-support/src/ports.rs. A zero port is any of:

  • an address literal ending in :0: "127.0.0.1:0", "[::1]:0", "localhost:0", "{host}:0", ":0";

  • a zero used as the port of a bind, SocketAddr{,V4,V6}::new, set_port, or an IP tuple. A zero means 0, 0u16, 0_u16, u16::MIN, 0 as u16, Default::default(), or a const/static/let bound to one.

  • a zero formatted in as an address's port: format!("127.0.0.1:{}", 0), format!("{}:{}", host, 0), port = 0, or {port} capturing let port = 0. Placeholders are mapped to their arguments, so only a zero in the port position counts;

  • a zero port field in a struct literal: port: 0, listen_port: 0. This caught the gateway harness's Nats, which held port: 0 until the port was read back. Nats is now built only once the port is known, and a NatsProcess guard owns the server until then.

Comments are skipped, and string literals are only checked for addresses. Whitespace is ignored, so layout cannot hide a case.

Tests get ports from ports helpers. The new ones are listener(), tokio_listener(), bound_socket() and bound_reuseport_socket(); the existing ones are DeadPort and children that report their port. About 100 binds moved to these helpers. agent-core gains test-support as a dev-dependency. ExecMock and the routed model server now take a held listener.

Allowed exceptions. A few sites bind port 0 and keep the socket for real:

  • agent mcp login's callback listener;
  • the CallbackServer unit tests;
  • the three MCP fixture servers;
  • child command lines (--listen 127.0.0.1:0).

The SSRF resolver's lookup_host((host, 0)) is also allowed: it is a lookup, not a bind. Each exception is annotated // port-0: <why it is held> in place and counted in the lint's ALLOWED, so adding one shows up as a reviewed change to the lint file.

Tests.

  • The old lint's pinned bypass cases are kept as cases the new rule must flag: closure helper, Option, const ANY: u16 = 0, cross-file helper, socket2, and the rest. New spellings are added too: IPv6, localhost, format!, raw strings, the SocketAddr constructors, set_port, .into(), and a multi-line tuple.
  • The fine cases include model ids ending in v1:0, JSON "index":0, URLs, assert_eq!(n, 0), and annotated lines.
  • Mutation-checked: removing an annotation, adding an unlisted annotated site, and putting a reverted helper back all fail the lint.
  • On rebase the lint immediately caught a new port-0 bind that agent: OAuth fixture coverage (DELETE, streamed in-POST question); session lock is a record lock (serve_ws flake root cause) #144 had just merged (mcp_http_exit.rs); it is migrated.

The SO_REUSEPORT Linux caveat now lives on ports::bound_reuseport_socket; RestartableNats points to it.

Not caught, and listed in the lint's doc: a zero that only becomes a port through a value the rule cannot follow. Examples:

  • an env default unwrap_or(0) later passed to a bind;
  • a "0" string default later formatted into an address;
  • n - n.

The first two can happen by accident, so ports that come from configuration still need review. The last would be deliberate evasion.

2. BEYOND_AI_AGENT_TEST_FAIL_CHECKPOINT=n counts per session

The counter was process-wide. On a daemon, one session's writes shifted which write of another session failed. It now counts per session id, in debug builds only, and release builds still carry none of the seam names (release_seams passes).

Test: serve_session_isolation::an_injected_checkpoint_failure_counts_each_sessions_writes runs two sessions on one daemon. Each must fail closed at its own second write, so neither tool runs. With the counter put back to a single shared count, the test fails.

Verification

fmt, dprint and clippy --workspace --all-targets -D warnings are clean. Workspace nextest passes 4871/4871. ARCHITECTURE.md is updated (the harness row and the env table).

🤖 Generated with Claude Code

https://claude.ai/code/session_01JimHGjsfk2Ktm5GxyZJKKk

jaredLunde and others added 2 commits October 7, 2026 05:49
…ckpoint fault count

The released-port lint tried to tell a held port-0 socket from a
released one by following it through bindings, wrappers, closures and
helpers in other files, and kept missing new shapes. Replaced with a rule
that needs no data flow: a zero port (an address literal ending `:0`, or
a zero / zero-valued const used as the port of a bind, a SocketAddr
constructor, set_port, or an IP tuple) is written nowhere but
`crates/test-support/src/ports.rs`. Tests get ports from its helpers:
`listener()`, `tokio_listener()`, `bound_socket()`,
`bound_reuseport_socket()`, `DeadPort`, or a child that binds port 0 and
reports it. The few sites that really bind port 0 and keep the socket
(the MCP OAuth callback, the fixture servers, child command lines, plus
one DNS lookup that is not a bind) are annotated `// port-0:` in place
and counted in the lint's ALLOWED list. The old lint's bypass cases are
pinned as cases the new rule must flag. ~100 test binds moved to the
helpers; agent-core gains test-support as a dev-dependency.

`BEYOND_AI_AGENT_TEST_FAIL_CHECKPOINT=n` counted writes process-wide, so
on a daemon one session's writes shifted which write of another session
failed. It now counts per session id (debug builds only), proven with two
sessions on one daemon.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JimHGjsfk2Ktm5GxyZJKKk
…; list what a text rule cannot see

- `format!("127.0.0.1:{}", 0)`, `format!("{}:{}", host, 0)`, `port = 0` named and
  `{port}` capturing `let port = 0`: placeholders are mapped to their arguments, so only
  the zero that lands in the port position is flagged.
- `port: 0` / `*_port: 0` in a struct literal. It found the gateway harness's `Nats`
  carrying `port: 0` until the port was read back; `Nats` is now built only once the
  port is known (`NatsProcess` guards the server meanwhile).
- The doc lists what passes a text rule: an env default `unwrap_or(0)` later bound, a
  `"0"` string default later formatted into an address, and `n - n`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JimHGjsfk2Ktm5GxyZJKKk
@jaredLunde
jaredLunde force-pushed the jared/port-zero-only-in-ports branch from 9207c3d to 79a22d5 Compare October 7, 2026 12:58
@jaredLunde
jaredLunde merged commit e75d2d1 into main Oct 7, 2026
21 checks passed
@jaredLunde
jaredLunde deleted the jared/port-zero-only-in-ports branch October 7, 2026 13:19
jaredLunde added a commit that referenced this pull request Oct 7, 2026
…pport::ports (#155)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JimHGjsfk2Ktm5GxyZJKKk
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant