Pin the vCard redaction to the end of the marker
(?~) is greedy to the longest run holding no complete END:VCARD, so it runs
into the marker and stops at END:VCAR; the trailing alternation is what
backtracks it onto the boundary. Dropping it left a stray "D" behind, and
every existing test passed anyway, because they checked that card content
was gone rather than what replaced it.

Assisted-by: Claude Opus 5 via Claude Code
change knkmrzvzzuxtmxsvollyvlukpmslvvpr
commit 5038c15e30b952cf9442786409b8723b73931b04
author Alpha Chen <alpha@kejadlen.dev>
date
parent rqpzwkrl
diff --git a/lib/pro_tacts/sentry_scrubber.rb b/lib/pro_tacts/sentry_scrubber.rb
index 66ea3db..053c8ad 100644
--- a/lib/pro_tacts/sentry_scrubber.rb
+++ b/lib/pro_tacts/sentry_scrubber.rb
@@ -7,13 +7,11 @@ module ProTacts
   #
   # Sentry truncates bodies at 16KB (RequestInterface::MAX_BODY_LIMIT), so a
   # card can arrive with its BEGIN and no END. The trailing alternation
-  # redacts to the end of the body in that case rather than missing the
-  # match; a card with an END redacts only to its own END, leaving the rest
-  # of the body intact.
+  # handles that, and is load-bearing for the ordinary case too: (?~) is
+  # greedy up to the longest run with no complete END:VCARD in it, which
+  # runs into the marker and stops at END:VCAR. Requiring END:VCARD or the
+  # end of the body is what backtracks it onto the real boundary.
   module SentryScrubber
-    # (?~exp) is Ruby's absence operator: the run of characters not
-    # containing exp. Says "up to the first END:VCARD" more directly than a
-    # lazy quantifier, whose stopping point depends on alternation order.
     VCARD = /BEGIN:VCARD(?~END:VCARD)(?:END:VCARD|\z)/mi
     VCARD_CONTENT_TYPE = %r{\Atext/vcard}i
     REDACTED = "[vcard redacted]".freeze #: String
diff --git a/test/pro_tacts/test_sentry_scrubber.rb b/test/pro_tacts/test_sentry_scrubber.rb
index a99d42a..5c53688 100644
--- a/test/pro_tacts/test_sentry_scrubber.rb
+++ b/test/pro_tacts/test_sentry_scrubber.rb
@@ -43,6 +43,14 @@ class SentryScrubberTest < Minitest::Test
     assert_includes scrubbed, "<href>/keep/me</href>"
   end
 
+  # (?~) is greedy to the longest run holding no complete END:VCARD, which
+  # runs into the marker and stops at END:VCAR. Without something forcing it
+  # back onto the boundary a stray "D" survives, so assert the exact result
+  # rather than just the absence of card content.
+  def test_no_fragment_of_the_end_marker_survives
+    assert_equal "<d>#{ProTacts::SentryScrubber::REDACTED}</d>TAIL", scrub("<d>#{CARD}</d>TAIL")
+  end
+
   def test_content_between_two_cards_survives
     scrubbed = scrub("#{CARD}\nMIDDLE\n#{CARD.sub('Real Person', 'Other Person')}\nTRAILING")