a73x

f1e1cc8f

test: the QUIC absence checks wait long enough to be a claim

a73x   2026-09-03 20:08

Commit message
test: the QUIC absence checks wait long enough to be a claim

probeGotAnything kept an immediate read for the two negative assertions,
and this branch's own measurement says loopback UDP is not delivered inside
the sendto on Darwin. So the test's central negative — a packet addressed
to a CID this connection advertised is delivered, not answered — could no
longer fail on a Mac: the read ran before any datagram could arrive. A
routing regression that answered an advertised CID would have shipped green.

Both negatives wait 500 ms now. Timed on both systems, a Retry this
listener does send is readable in under a millisecond once the reader
yields to a poll, so the budget is three orders of magnitude of slack.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SakwJEwD9dXBoRP5kWbemW

src/server/quic_server.zig
Old New
@@ -1391,17 +1391,25 @@ test "Listener: a packet addressed to any advertised CID reaches its connection"
1391 var probe_len: std.posix.socklen_t = @sizeOf(@TypeOf(probe_addr)); 1391 var probe_len: std.posix.socklen_t = @sizeOf(@TypeOf(probe_addr));
1392 try std.posix.getsockname(probe, @ptrCast(&probe_addr), &probe_len); 1392 try std.posix.getsockname(probe, @ptrCast(&probe_addr), &probe_len);
1393 1393
1394 // The baseline: nothing has been routed, so nothing may arrive. It waits
1395 // the same budget the real absence check does, so the two are the same
1396 // question asked twice and a budget too short to matter would show up
1397 // here as well.
1394 var pkt: [1300]u8 = undefined; 1398 var pkt: [1300]u8 = undefined;
1395 try std.testing.expect(!probeGotAnything(probe, 0)); 1399 try std.testing.expect(!probeGotAnything(probe, probe_absence_ms));
1396 1400
1397 // Addressed to a CID this connection advertised: delivered, not 1401 // Addressed to a CID this connection advertised: delivered, not
1398 // answered. 1402 // answered. This is the test's central negative and it has to be a WAIT,
1403 // not a glance — a read that returns before any datagram could have
1404 // crossed loopback is true of a listener that answered wrongly.
1399 buildInitial(&pkt, sec.data[0..sec.datalen]); 1405 buildInitial(&pkt, sec.data[0..sec.datalen]);
1400 setup.l.route(&pkt, &probe_addr, probe_len); 1406 setup.l.route(&pkt, &probe_addr, probe_len);
1401 try std.testing.expect(!probeGotAnything(probe, 0)); 1407 try std.testing.expect(!probeGotAnything(probe, probe_absence_ms));
1402 1408
1403 // Addressed to nobody: `accept` answers with a Retry, which is what 1409 // Addressed to nobody: `accept` answers with a Retry, which is what
1404 // every migrated packet used to get. 1410 // every migrated packet used to get. The budget here is generous because
1411 // a MISSING reply is the failure this checks for; how long the reply
1412 // really takes is what `probe_absence_ms` is sized against.
1405 buildInitial(&pkt, &stranger_cid); 1413 buildInitial(&pkt, &stranger_cid);
1406 setup.l.route(&pkt, &probe_addr, probe_len); 1414 setup.l.route(&pkt, &probe_addr, probe_len);
1407 try std.testing.expect(probeGotAnything(probe, 2000)); 1415 try std.testing.expect(probeGotAnything(probe, 2000));
@@ -1432,19 +1440,32 @@ fn buildInitial(pkt: *[1300]u8, dcid: []const u8) void {
1432 std.mem.writeInt(u16, pkt[i..][0..2], rest | 0x4000, .big); 1440 std.mem.writeInt(u16, pkt[i..][0..2], rest | 0x4000, .big);
1433 } 1441 }
1434 1442
1443 /// True when a datagram arrives within `budget_ms`. Every caller waits,
1444 /// including the ones asserting ABSENCE: loopback UDP is not synchronous
1445 /// everywhere — Linux hands the datagram over inside the `sendto`, so an
1446 /// immediate recv finds it, and Darwin does not. A read with no budget
1447 /// therefore CANNOT SEE a Retry on a Mac, which would make "nothing came
1448 /// back" true of a listener that answered wrongly. The absence budget is
1449 /// what makes the negative a claim rather than a race the Mac always wins;
1450 /// `probe_absence_ms` says how it was chosen.
1435 fn probeGotAnything(fd: std.posix.fd_t, budget_ms: i32) bool { 1451 fn probeGotAnything(fd: std.posix.fd_t, budget_ms: i32) bool {
1436 var buf: [2048]u8 = undefined; 1452 var buf: [2048]u8 = undefined;
1437 var fds = [_]std.posix.pollfd{.{ .fd = fd, .events = std.posix.POLL.IN, .revents = 0 }}; 1453 var fds = [_]std.posix.pollfd{.{ .fd = fd, .events = std.posix.POLL.IN, .revents = 0 }};
1438 // A budget, because loopback UDP is not synchronous everywhere: Linux
1439 // hands the datagram over inside the sendto, so an immediate recv finds
1440 // it, and Darwin does not — the Retry this test is waiting for arrived
1441 // after the read that was looking for it. A caller asserting ABSENCE
1442 // passes 0 and keeps the immediate read it always did.
1443 if (budget_ms > 0) _ = std.posix.poll(&fds, budget_ms) catch return false; 1454 if (budget_ms > 0) _ = std.posix.poll(&fds, budget_ms) catch return false;
1444 const n = std.posix.recv(fd, &buf, 0) catch return false; 1455 const n = std.posix.recv(fd, &buf, 0) catch return false;
1445 return n > 0; 1456 return n > 0;
1446 } 1457 }
1447 1458
1459 /// How long "nothing came back" waits before it is believed. Sized against
1460 /// the presence check at the end of the same test, on the same socket:
1461 /// timed on both systems 2026-09-03, a Retry this listener DOES send is
1462 /// readable in under a millisecond once the reader yields to a poll. Half a
1463 /// second is three orders of magnitude of slack, so a wrongly-emitted one has
1464 /// had every chance to arrive. What it is NOT sized against is `sendto`
1465 /// itself: Linux delivers inside the call and Darwin does not, which is why
1466 /// a glance with no budget at all could never fail on a Mac.
1467 const probe_absence_ms: i32 = 500;
1468
1448 test "Listener: a reply queued just before a close still reaches the peer" { 1469 test "Listener: a reply queued just before a close still reaches the peer" {
1449 const alloc = std.testing.allocator; 1470 const alloc = std.testing.allocator;
1450 const key: quic.Key = .{ .bytes = [_]u8{0x5C} ** quic.key_len }; 1471 const key: quic.Key = .{ .bytes = [_]u8{0x5C} ** quic.key_len };