fix(nestsclient): skip Active announce when AnnouncePlease prefix doesn't match — moq-lite Lite-03 announce.rs

Per moq-lite Lite-03 (kixelated/moq `rs/moq-lite/src/lite/announce.rs`),
a publisher MUST only emit `Announce(Active)` for broadcasts whose path
starts with the requested AnnouncePlease prefix. Pre-fix, when
`MoqLitePath.stripPrefix(please.prefix, ourSuffix)` returned `null`, we
fell through to `?: announcePublisher.suffix` and emitted Active under
our full suffix anyway, falsely advertising the broadcast under a
prefix the subscriber didn't request.

Production never bit because the relay always opens its announce bidi
to us with `prefix=""` (which always matches), but a future peer-to-
peer or namespace-scoped subscriber would have observed a ghost
broadcast under whatever prefix they asked about.

The fix: if `stripPrefix` returns `null`, FIN the bidi cleanly without
writing any Announce body. The subscriber sees an empty announce
stream and moves on.

Regression test:
`MoqLiteSessionTest.publisher_skips_announce_when_announce_please_prefix_does_not_match`.

Audit doc: nestsClient/plans/2026-05-09-moq-lite-rfc-compliance.md (M4).

https://claude.ai/code/session_012TGfo99Ugz7fcCPv85a8Tq
This commit is contained in:
Claude
2026-05-09 13:51:24 +00:00
parent 3a346968db
commit 6c2d7efccb
2 changed files with 58 additions and 2 deletions
@@ -533,6 +533,43 @@ class MoqLiteSessionTest {
session.close()
}
@Test
fun publisher_skips_announce_when_announce_please_prefix_does_not_match() =
runBlocking {
// Lite-03 audit M4: when the relay opens an Announce bidi
// with a non-empty prefix that doesn't match our broadcast
// suffix, the publisher MUST NOT emit Active under the
// requested prefix (or under our own suffix) — kixelated's
// `rs/moq-lite/src/lite/announce.rs::Producer` only emits
// for matching prefixes. Pre-fix we'd fall through the
// `null` branch of `MoqLitePath.stripPrefix` and announce
// ourselves anyway. Verified here by FINing the bidi
// cleanly without writing any Announce body.
val (clientSide, serverSide) = FakeWebTransport.pair()
val session = MoqLiteSession.client(clientSide, pumpScope)
val publisher = session.publish(broadcastSuffix = "speakerPubkey", track = "audio/data")
// Relay opens Announce bidi with a non-matching prefix.
val relayBidi = serverSide.openBidiStream()
relayBidi.write(Varint.encode(MoqLiteControlType.Announce.code))
relayBidi.write(
MoqLiteCodec.encodeAnnouncePlease(
MoqLiteAnnouncePlease(prefix = "differentRoom"),
),
)
// The bidi MUST end with no body (peer FIN), and
// `relayBidi.incoming().toList()` therefore returns empty.
// Use a generous timeout so a slow fake doesn't false-pass
// by hanging instead of FINing.
val chunks = withTimeout(2_000) { relayBidi.incoming().toList() }
assertEquals(emptyList(), chunks, "non-matching prefix must FIN without writing any Announce body")
publisher.close()
session.close()
}
@Test
fun publisher_replies_subscribeDrop_when_track_is_not_published() =
runBlocking {