fixtures: seed a contact that no book holds
Pat Private is in no sync: group, so the listing leaves that card out;
its put moves the ctag to 19, and that is all the replay diff is about.

Assisted-by: Claude Opus 5 via Claude Code
change olwvtvzworkosvrsotqvspzypnxxvkqz
commit 55439806d456b397a3a7f852a75630f358eef927
author Alpha Chen <alpha@kejadlen.dev>
date
parent wxmqulnp
diff --git a/test/fixture_data.rb b/test/fixture_data.rb
index 4881d5b..67bae0e 100644
--- a/test/fixture_data.rb
+++ b/test/fixture_data.rb
@@ -86,14 +86,23 @@ module FixtureData
     cards.keys.grep(/\Abday-/)
   end
 
-  # The third is `sync:*` holding every card, so every user's book is
-  # the whole seed — the replay's included, whose recorded sessions saw
-  # every card (docs/plans/2026-09-12-per-user-books.md).
+  # The seed's cards that no client syncs, read off the ids for
+  # birthday_cards' reason.
+  #: () -> Array[String]
+  def self.unsynced_cards
+    cards.keys.grep(/\Aunsynced-/)
+  end
+
+  # The third is `sync:*`, holding every card but the unsynced ones, so
+  # every user's book is the rest of the seed — the replay's included,
+  # whose recorded sessions saw every card they asked for — and the
+  # admin UI still shows a contact that is in nobody's
+  # (docs/plans/2026-09-12-per-user-books.md).
   #: (ProTacts::Store store) -> void
   def self.seed_groups(store)
     seed_group(store, name: "Booles", lines: HOUSEHOLD, members: MEMBERS)
     seed_group(store, members: birthday_cards)
-    seed_group(store, name: ProTacts::Store::EVERYONE, members: cards.keys)
+    seed_group(store, name: ProTacts::Store::EVERYONE, members: cards.keys - unsynced_cards)
   end
 
   # A group, seeded without a change-log entry for any member it moves
diff --git a/test/fixtures/cards/unsynced-pat.vcf b/test/fixtures/cards/unsynced-pat.vcf
new file mode 100644
index 0000000..7b9364b
--- /dev/null
+++ b/test/fixtures/cards/unsynced-pat.vcf
@@ -0,0 +1,7 @@
+BEGIN:VCARD
+VERSION:3.0
+N:Private;Pat;;;
+FN:Pat Private
+EMAIL:pat@example.com
+UID:unsynced-pat
+END:VCARD
diff --git a/test/fixtures/ios-exchange/02-propfind-addressbook-ctag/response b/test/fixtures/ios-exchange/02-propfind-addressbook-ctag/response
index b7d86f1..d298041 100644
--- a/test/fixtures/ios-exchange/02-propfind-addressbook-ctag/response
+++ b/test/fixtures/ios-exchange/02-propfind-addressbook-ctag/response
@@ -16,8 +16,8 @@ Content-Type: text/xml
           <d:report><d:sync-collection/></d:report>
         </d:supported-report>
       </d:supported-report-set>
-      <cs:getctag>18</cs:getctag>
-      <d:sync-token>http://pro-tacts/sync/18/c8dae6371b33b8d6</d:sync-token>
+      <cs:getctag>19</cs:getctag>
+      <d:sync-token>http://pro-tacts/sync/19/c8dae6371b33b8d6</d:sync-token>
       <d:current-user-privilege-set>
         <d:privilege><d:read/></d:privilege>
         <d:privilege><d:write/></d:privilege>
diff --git a/test/fixtures/ios-exchange/03-propfind-addressbook-bootstrap/response b/test/fixtures/ios-exchange/03-propfind-addressbook-bootstrap/response
index c949537..ee3f601 100644
--- a/test/fixtures/ios-exchange/03-propfind-addressbook-bootstrap/response
+++ b/test/fixtures/ios-exchange/03-propfind-addressbook-bootstrap/response
@@ -16,8 +16,8 @@ Content-Type: text/xml
           <d:report><d:sync-collection/></d:report>
         </d:supported-report>
       </d:supported-report-set>
-      <cs:getctag>18</cs:getctag>
-      <d:sync-token>http://pro-tacts/sync/18/c8dae6371b33b8d6</d:sync-token>
+      <cs:getctag>19</cs:getctag>
+      <d:sync-token>http://pro-tacts/sync/19/c8dae6371b33b8d6</d:sync-token>
       <d:current-user-privilege-set>
         <d:privilege><d:read/></d:privilege>
         <d:privilege><d:write/></d:privilege>
diff --git a/test/fixtures/macos-exchange/05-propfind-addressbook-ctag/response b/test/fixtures/macos-exchange/05-propfind-addressbook-ctag/response
index b7d86f1..d298041 100644
--- a/test/fixtures/macos-exchange/05-propfind-addressbook-ctag/response
+++ b/test/fixtures/macos-exchange/05-propfind-addressbook-ctag/response
@@ -16,8 +16,8 @@ Content-Type: text/xml
           <d:report><d:sync-collection/></d:report>
         </d:supported-report>
       </d:supported-report-set>
-      <cs:getctag>18</cs:getctag>
-      <d:sync-token>http://pro-tacts/sync/18/c8dae6371b33b8d6</d:sync-token>
+      <cs:getctag>19</cs:getctag>
+      <d:sync-token>http://pro-tacts/sync/19/c8dae6371b33b8d6</d:sync-token>
       <d:current-user-privilege-set>
         <d:privilege><d:read/></d:privilege>
         <d:privilege><d:write/></d:privilege>
diff --git a/test/fixtures/macos-exchange/09-propfind-addressbook-bootstrap/response b/test/fixtures/macos-exchange/09-propfind-addressbook-bootstrap/response
index c949537..ee3f601 100644
--- a/test/fixtures/macos-exchange/09-propfind-addressbook-bootstrap/response
+++ b/test/fixtures/macos-exchange/09-propfind-addressbook-bootstrap/response
@@ -16,8 +16,8 @@ Content-Type: text/xml
           <d:report><d:sync-collection/></d:report>
         </d:supported-report>
       </d:supported-report-set>
-      <cs:getctag>18</cs:getctag>
-      <d:sync-token>http://pro-tacts/sync/18/c8dae6371b33b8d6</d:sync-token>
+      <cs:getctag>19</cs:getctag>
+      <d:sync-token>http://pro-tacts/sync/19/c8dae6371b33b8d6</d:sync-token>
       <d:current-user-privilege-set>
         <d:privilege><d:read/></d:privilege>
         <d:privilege><d:write/></d:privilege>
diff --git a/test/pro_tacts/test_web.rb b/test/pro_tacts/test_web.rb
index dc7030d..7250a89 100644
--- a/test/pro_tacts/test_web.rb
+++ b/test/pro_tacts/test_web.rb
@@ -97,6 +97,17 @@ class WebTest < Minitest::Test
     assert_includes last_response.body, "END:VCARD"
   end
 
+  # The seed's unsynced card is in no `sync:` group, so no book holds it
+  # (FixtureData.unsynced_cards).
+  def test_a_seed_card_in_no_book_is_not_served
+    request "/dav/addressbook/", method: "PROPFIND", "HTTP_DEPTH" => "1", input: etag_only_propfind
+    assert_includes last_response.body, "household-george.vcf"
+    refute_includes last_response.body, "unsynced-"
+
+    get "/dav/addressbook/unsynced-pat.vcf"
+    assert_equal 404, last_response.status
+  end
+
   def test_propfind_addressbook_includes_ctag
     request "/dav/addressbook/", method: "PROPFIND"