Promote the PUT captures into the exchange fixtures
Step 10's If-Match was rewritten to the seed card's etag: an etag
names server state, and the captured one names a dev database that no
longer exists, so the rewrite is what keeps the client behavior — send
back the etag the server last reported — replayable.

Assisted-by: GLM-5.3 via pi
change svzxvxyzvmnwvozlsmsskxtlwpouunqp
commit dbf03a36d2ddb0254342e57dbe88841dbedb0462
author Alpha Chen <alpha@kejadlen.dev>
date
parent oxwrqorl
diff --git a/test/fixtures/cards/oqmvpzooywwmtwwx.vcf b/test/fixtures/cards/oqmvpzooywwmtwwx.vcf
new file mode 100644
index 0000000..baa65f7
--- /dev/null
+++ b/test/fixtures/cards/oqmvpzooywwmtwwx.vcf
@@ -0,0 +1,10 @@
+BEGIN:VCARD
+VERSION:3.0
+N:Lovelace;Ada;;;
+FN:Ada Lovelace
+EMAIL:ada@example.com
+TEL;TYPE=mobile:+1-555-0100
+ADR;TYPE=home:;;12 Analytical Way;London;England;NW1 1AA;United Kingdom
+NOTE:Test
+UID:oqmvpzooywwmtwwx
+END:VCARD
diff --git a/test/fixtures/macos-exchange/05-propfind-addressbook-ctag/response b/test/fixtures/macos-exchange/05-propfind-addressbook-ctag/response
index 09f3e1d..862e97a 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>cfe366324ca3c5bb85010587e5fa778044ee67b5480e45441b562c662b7d1dc5</cs:getctag>
-      <d:sync-token>http://pro-tacts/sync/cfe366324ca3c5bb85010587e5fa778044ee67b5480e45441b562c662b7d1dc5</d:sync-token>
+      <cs:getctag>570e809cb07cb01027a3fd7a6404f564797e494fae2fadc4431eaeda89d95a5b</cs:getctag>
+      <d:sync-token>http://pro-tacts/sync/570e809cb07cb01027a3fd7a6404f564797e494fae2fadc4431eaeda89d95a5b</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/06-propfind-addressbook-listing/response b/test/fixtures/macos-exchange/06-propfind-addressbook-listing/response
index cd9cb4a..7848de9 100644
--- a/test/fixtures/macos-exchange/06-propfind-addressbook-listing/response
+++ b/test/fixtures/macos-exchange/06-propfind-addressbook-listing/response
@@ -13,5 +13,14 @@ Content-Type: text/xml
     <d:status>HTTP/1.1 200 OK</d:status>
   </d:propstat>
 </d:response>
+<d:response>
+  <d:href>/dav/addressbook/oqmvpzooywwmtwwx.vcf</d:href>
+  <d:propstat>
+    <d:prop>
+      <d:getetag>"0d07279dcc669c2424d9dc5d393d3d3b9bcad064d1639cdfb8170ccde76b233d"</d:getetag>
+    </d:prop>
+    <d:status>HTTP/1.1 200 OK</d:status>
+  </d:propstat>
+</d:response>
 
 </d:multistatus>
diff --git a/test/fixtures/macos-exchange/08-report-sync-collection/response b/test/fixtures/macos-exchange/08-report-sync-collection/response
index e7b9bc8..64109d7 100644
--- a/test/fixtures/macos-exchange/08-report-sync-collection/response
+++ b/test/fixtures/macos-exchange/08-report-sync-collection/response
@@ -12,5 +12,14 @@ Content-Type: text/xml
     <d:status>HTTP/1.1 200 OK</d:status>
   </d:propstat>
 </d:response>
+<d:response>
+  <d:href>/dav/addressbook/oqmvpzooywwmtwwx.vcf</d:href>
+  <d:propstat>
+    <d:prop>
+      <d:getetag>"0d07279dcc669c2424d9dc5d393d3d3b9bcad064d1639cdfb8170ccde76b233d"</d:getetag>
+    </d:prop>
+    <d:status>HTTP/1.1 200 OK</d:status>
+  </d:propstat>
+</d:response>
 
 </d:multistatus>
diff --git a/test/fixtures/macos-exchange/09-propfind-addressbook-bootstrap/response b/test/fixtures/macos-exchange/09-propfind-addressbook-bootstrap/response
index 259fb6b..302912a 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>cfe366324ca3c5bb85010587e5fa778044ee67b5480e45441b562c662b7d1dc5</cs:getctag>
-      <d:sync-token>http://pro-tacts/sync/cfe366324ca3c5bb85010587e5fa778044ee67b5480e45441b562c662b7d1dc5</d:sync-token>
+      <cs:getctag>570e809cb07cb01027a3fd7a6404f564797e494fae2fadc4431eaeda89d95a5b</cs:getctag>
+      <d:sync-token>http://pro-tacts/sync/570e809cb07cb01027a3fd7a6404f564797e494fae2fadc4431eaeda89d95a5b</d:sync-token>
       <d:current-user-privilege-set>
         <d:privilege><d:read/></d:privilege>
         <d:privilege><d:write/></d:privilege>
@@ -38,5 +38,14 @@ Content-Type: text/xml
     <d:status>HTTP/1.1 200 OK</d:status>
   </d:propstat>
 </d:response>
+<d:response>
+  <d:href>/dav/addressbook/oqmvpzooywwmtwwx.vcf</d:href>
+  <d:propstat>
+    <d:prop>
+      <d:getetag>"0d07279dcc669c2424d9dc5d393d3d3b9bcad064d1639cdfb8170ccde76b233d"</d:getetag>
+    </d:prop>
+    <d:status>HTTP/1.1 200 OK</d:status>
+  </d:propstat>
+</d:response>
 
 </d:multistatus>
diff --git a/test/fixtures/macos-exchange/10-put-contact-edit/request b/test/fixtures/macos-exchange/10-put-contact-edit/request
new file mode 100644
index 0000000..3580901
--- /dev/null
+++ b/test/fixtures/macos-exchange/10-put-contact-edit/request
@@ -0,0 +1,18 @@
+PUT /dav/addressbook/oqmvpzooywwmtwwx.vcf HTTP/1.1
+Content-Type: text/vcard; charset=utf-8
+If-Match: "0d07279dcc669c2424d9dc5d393d3d3b9bcad064d1639cdfb8170ccde76b233d"
+
+BEGIN:VCARD
+VERSION:3.0
+PRODID:-//Apple Inc.//macOS 26.5.1//EN
+N:Lovelace;Ada;;;
+FN:Ada Lovelace
+EMAIL;type=INTERNET;type=pref:ada@example.com
+TEL;type=CELL;type=VOICE;type=pref:+1-555-0100
+X-ADDRESSING-GRAMMAR;type=pref:W7cARX37SsOaCHm/dwdsAGskOe0lLH1g4FfqDq/F300BtTs6d5DYOioIK0dc5jXGXP3pSXjpCZ2FwY35ll4hqn3SCPvSfOW1cZFde5Xs3dCT/e9xiEsOj7lEc7f1llgmeXSTLUUdophTM3F1P4NejNmYueSrIaJxDYJRnEBW6S0khBIUFR/UDyXWi/Nuf+bI67I6VoFxfBwhhXiRZqUJCi0CBabLuQKVFLZFtPvqHjq1N9dgY0GPYZV5KCVxuMr3kuSPCxWEvOG+QI0QQRTh2HWKjAae/PHzdOrQeDHsiVQ38eePGr/2wLzdJmzCLpjptZo3lJ+eFV9KqPQqgyvb1Wxc053QeEYe87Zeba2crGFHey9Ie89KIpJ2Cva0zFs3mRR02G1q9NJeoSCuQOFZjB37dso=
+ADR;type=HOME;type=pref:;;12 Analytical Way;London;England;NW1 1AA;United Kingdom
+NOTE:Test
+BDAY;X-APPLE-OMIT-YEAR=1604:1604-01-01
+REV:2026-08-25T03:01:37Z
+UID:oqmvpzooywwmtwwx
+END:VCARD
diff --git a/test/fixtures/macos-exchange/10-put-contact-edit/response b/test/fixtures/macos-exchange/10-put-contact-edit/response
new file mode 100644
index 0000000..0ca9ef3
--- /dev/null
+++ b/test/fixtures/macos-exchange/10-put-contact-edit/response
@@ -0,0 +1,3 @@
+204
+ETag: "07436a908594751e83c07cf99e4ed9b6a0fca38a14c492b90d977aba1780f63a"
+
diff --git a/test/fixtures/macos-exchange/11-put-contact-edit-retried/request b/test/fixtures/macos-exchange/11-put-contact-edit-retried/request
new file mode 100644
index 0000000..b1f0a9c
--- /dev/null
+++ b/test/fixtures/macos-exchange/11-put-contact-edit-retried/request
@@ -0,0 +1,18 @@
+PUT /dav/addressbook/oqmvpzooywwmtwwx.vcf HTTP/1.1
+Content-Type: text/vcard; charset=utf-8
+If-Match: "0ffed8916af1ab276ff37b202c309c6dbbe3b5da9e99659fff93e71edb55aa1b"
+
+BEGIN:VCARD
+VERSION:3.0
+PRODID:-//Apple Inc.//macOS 26.5.1//EN
+N:Lovelace;Ada;;;
+FN:Ada Lovelace
+EMAIL;type=INTERNET;type=pref:ada@example.com
+TEL;type=CELL;type=VOICE;type=pref:+1-555-0100
+X-ADDRESSING-GRAMMAR;type=pref:W7cARX37SsOaCHm/dwdsAGskOe0lLH1g4FfqDq/F300BtTs6d5DYOioIK0dc5jXGXP3pSXjpCZ2FwY35ll4hqn3SCPvSfOW1cZFde5Xs3dCT/e9xiEsOj7lEc7f1llgmeXSTLUUdophTM3F1P4NejNmYueSrIaJxDYJRnEBW6S0khBIUFR/UDyXWi/Nuf+bI67I6VoFxfBwhhXiRZqUJCi0CBabLuQKVFLZFtPvqHjq1N9dgY0GPYZV5KCVxuMr3kuSPCxWEvOG+QI0QQRTh2HWKjAae/PHzdOrQeDHsiVQ38eePGr/2wLzdJmzCLpjptZo3lJ+eFV9KqPQqgyvb1Wxc053QeEYe87Zeba2crGFHey9Ie89KIpJ2Cva0zFs3mRR02G1q9NJeoSCuQOFZjB37dso=
+ADR;type=HOME;type=pref:;;12 Analytical Way;London;England;NW1 1AA;United Kingdom
+NOTE:Test
+BDAY;X-APPLE-OMIT-YEAR=1604:1604-01-01
+REV:2026-08-25T03:12:11Z
+UID:oqmvpzooywwmtwwx
+END:VCARD
diff --git a/test/fixtures/macos-exchange/11-put-contact-edit-retried/response b/test/fixtures/macos-exchange/11-put-contact-edit-retried/response
new file mode 100644
index 0000000..19dd161
--- /dev/null
+++ b/test/fixtures/macos-exchange/11-put-contact-edit-retried/response
@@ -0,0 +1,3 @@
+412
+Content-Type: text/html
+
diff --git a/test/fixtures/macos-exchange/README.md b/test/fixtures/macos-exchange/README.md
index 7e10ad0..bec4442 100644
--- a/test/fixtures/macos-exchange/README.md
+++ b/test/fixtures/macos-exchange/README.md
@@ -1,25 +1,51 @@
 # macOS exchange fixtures
 
-Request-level recordings of the eight distinct requests macOS Contacts
-(26.5.1, AddressBookCore/2732.600.11) sent to pro-tacts on 2026-08-14, the
-day the one-card milestone landed — see
-`docs/plans/2026-08-12-one-card-on-macos.md`. That exchange is the only
-confirmed-working client session, so these requests are the baseline every
+Request-level recordings of what macOS Contacts (26.5.1,
+AddressBookCore/2732.600.11) has sent to pro-tacts, promoted from
+`log/unhandled` as each session landed. Steps 01–09 are the read-only
+exchange of 2026-08-14, the day the one-card milestone landed (see
+`docs/plans/2026-08-12-one-card-on-macos.md`). Steps 10–11 are the
+writes the client began sending once the collection advertised write
+privileges, captured 2026-08-25. The requests are the baseline every
 later response change is measured against.
 
 Each step directory holds:
 
 - `request` — the request line, the headers that affect routing
-  (`Brief`, `Content-Type`, `Depth`, `Prefer`), a blank line, then the exact
-  body. `Authorization`, `Host`, `User-Agent`, and the `Tailscale-*` /
-  `X-Forwarded-*` / `X-Mme-Client-Info` headers were stripped: they identify
-  the tailnet and user, and the app ignores them.
+  (`Brief`, `Content-Type`, `Depth`, `If-Match`, `Prefer`), a blank
+  line, then the exact body. `Authorization`, `Host`, `User-Agent`, and
+  the `Tailscale-*` / `X-Forwarded-*` / `X-Mme-Client-Info` headers
+  were stripped: they identify the tailnet and user, and the app
+  ignores them.
 - `response` — the status line, the significant response headers
-  (`Content-Type`, `ETag`, `DAV`, `Allow`, `Location`), a blank line, then
-  the exact body the app returned.
+  (`Content-Type`, `ETag`, `DAV`, `Allow`, `Location`), a blank line,
+  then the exact body the app returned.
 
 `request` files are evidence: edit them only when a new client session
 supersedes this one. `response` files are a snapshot of current app
 behavior: regenerate them with `rake fixtures` after changing responses
 deliberately, and review the diff — accidental drift is exactly what the
 comparison test exists to catch.
+
+## The PUT steps
+
+Both are the same edit to the same contact. Step 10 is the edit, which
+replaces the seed card; step 11 is the client's retry minutes later,
+byte-identical apart from `REV`, still carrying the etag of the
+pre-edit card — so it fails its `If-Match`, which is what a lost
+update has to look like.
+
+Two liberties were taken promoting them:
+
+- Step 10's `If-Match` was rewritten to the etag of the replay's seed
+  card. An etag names server state, and the captured one names a dev
+  database that no longer exists; the client's behavior — send back
+  the etag the server last reported — is the part that is evidence,
+  and it survives the rewrite.
+- The seed carries that pre-edit card (`test/fixtures/cards/`), so the
+  read steps list two contacts where the original session saw one.
+
+The capture also lost the body's final newline — 835 bytes declared,
+834 recorded — so the promoted card ends at `END:VCARD`. Captures with
+`PHOTO` bodies (34 KB and 338 KB cards) were left unpromoted: possibly
+real images, and the same code path as the 834-byte card that was.