probe: separate the renumber from the binding
Two groups on one card and a gap in between,
item1 holds still while item3 has to lean.
If the marker moves with it the binding is real —
if it stays where it sat, it was never a seal.
Assisted-by: Claude Opus 5 via Claude Code
diff --git a/tasks/probe.rake b/tasks/probe.rake
index e4f625d..6eea939 100644
--- a/tasks/probe.rake
+++ b/tasks/probe.rake
@@ -9,6 +9,47 @@
# cooperation beyond running.
namespace :probe do
+ # Puts a probe card where a client will sync it. The id is the UID:
+ # this server's cards are named by it (see ProTacts::Contact), and a
+ # PUT whose body disagrees is refused.
+ def seed(id, card)
+ require "net/http"
+ require "uri"
+
+ # rackup's default, which is what `rake dev` starts.
+ url = URI.parse(ENV.fetch("PROBE_URL", "http://localhost:9292"))
+ url.path = "/dav/addressbook/#{id}.vcf"
+
+ request = Net::HTTP::Put.new(url)
+ request["Content-Type"] = "text/vcard"
+ # Serve strips this header from incoming requests and sets it from
+ # the tailnet identity, so supplying one is only possible — and only
+ # meaningful — against a dev server on localhost
+ # (ProTacts::TailscaleAuth).
+ request["Tailscale-User-Login"] = ENV.fetch("PROBE_LOGIN", "probe@example.com")
+ request.body = card
+
+ response = Net::HTTP.start(url.hostname, url.port) { it.request(request) }
+ abort "#{url} answered #{response.code}: #{response.body}" unless response.code.start_with?("2")
+ response.code
+ end
+
+ # The shared half of every probe's instructions: a successful PUT is
+ # not an unhandled request, so the debug log is the only place it
+ # lands.
+ def reading_the_result(id, name)
+ <<~STEPS
+ 1. The server needs PRO_TACTS_DEBUG=1 for the write to be logged —
+ a successful PUT is not an unhandled request, so log/unhandled
+ will not have it. Restart with `PRO_TACTS_DEBUG=1 rake dev` if
+ it is not set.
+ 2. Resync the client, edit "#{name}" — any field, the edit itself
+ does not matter — and let the write go through.
+ 3. The PUT body lands in log/dev.log after a `>>` line, under
+ #{id}.vcf.
+ STEPS
+ end
+
# The annotation forms under test, one card so a single edit answers
# all five. Each is independent: whatever the client keeps, drops, or
# renumbers, it does to each of these on its own terms.
@@ -28,9 +69,6 @@ namespace :probe do
# item8.X-PT-GROUP a companion beside an X-ABLabel that it
# does, in case the known line is what
# anchors the group through a renumber
- #
- # The id is the UID: this server's cards are named by it (see
- # ProTacts::Contact), and a PUT whose body disagrees is refused.
PROBE_ID = "probe-annotations"
PROBE_CARD = <<~CARD.gsub("\n", "\r\n")
BEGIN:VCARD
@@ -49,43 +87,76 @@ namespace :probe do
END:VCARD
CARD
+ # The follow-up to that one, which answered the companion question
+ # with numbering nothing here chose deliberately: item8 and item9,
+ # picked to dodge collisions, both got renumbered or stripped, so
+ # "the marker does not follow its group" and "the numbering confused
+ # the client" were not told apart.
+ #
+ # This card separates them. Two groups, both spelled the way a client
+ # spells its own — dense, in document order, each anchored by an
+ # X-ABLabel macOS models — with one gap:
+ #
+ # item1 needs no renumbering. If its marker comes back at item1,
+ # an untouched group keeps its companion.
+ # item3 has to move to item2 for the numbering to be dense again.
+ # Its marker either follows the group it belongs to, or it
+ # stays at item3 while the group moves out from under it.
+ #
+ # The second is the whole experiment: a marker that stays put is a
+ # binding the client never maintained, and one that follows is a
+ # binding provenance could rest on. The first is the control that
+ # says the card was well-formed enough to be kept at all.
+ RENUMBER_ID = "probe-renumber"
+ RENUMBER_CARD = <<~CARD.gsub("\n", "\r\n")
+ BEGIN:VCARD
+ VERSION:3.0
+ N:Boole;Renumber;;;
+ FN:Probe Renumber
+ item1.TEL;TYPE=cell:+44 20 5555 0100
+ item1.X-ABLabel:household
+ item1.X-PT-GROUP:kxsv
+ item3.EMAIL;TYPE=internet:probe@example.com
+ item3.X-ABLabel:household
+ item3.X-PT-GROUP:mnop
+ UID:#{RENUMBER_ID}
+ END:VCARD
+ CARD
+
desc "Seed the annotation probe card into a running dev server (PROBE_URL to override)"
task :annotations do
- require "net/http"
- require "uri"
+ code = seed(PROBE_ID, PROBE_CARD)
- # rackup's default, which is what `rake dev` starts.
- url = URI.parse(ENV.fetch("PROBE_URL", "http://localhost:9292"))
- url.path = "/dav/addressbook/#{PROBE_ID}.vcf"
+ puts <<~NEXT
+ Seeded #{PROBE_ID} (#{code}).
- request = Net::HTTP::Put.new(url)
- request["Content-Type"] = "text/vcard"
- # Serve strips this header from incoming requests and sets it from
- # the tailnet identity, so supplying one is only possible — and only
- # meaningful — against a dev server on localhost
- # (ProTacts::TailscaleAuth).
- request["Tailscale-User-Login"] = ENV.fetch("PROBE_LOGIN", "probe@example.com")
- request.body = PROBE_CARD
+ #{reading_the_result(PROBE_ID, "Probe Boole").chomp}
+ 4. Compare it with the card in tasks/probe.rake: which of the five
+ forms came back, whether itemN was renumbered, and whether a
+ companion stayed with the line it was grouped with.
+ 5. Record what happened in docs/macos-contacts.md. Repeat on iOS,
+ which numbers item groups differently (see
+ test/fixtures/ios-exchange/05-put-contact-edit/request).
+ NEXT
+ end
- response = Net::HTTP.start(url.hostname, url.port) { it.request(request) }
- abort "#{url} answered #{response.code}: #{response.body}" unless response.code.start_with?("2")
+ desc "Seed the renumbering probe: does a companion follow its group?"
+ task :renumber do
+ code = seed(RENUMBER_ID, RENUMBER_CARD)
puts <<~NEXT
- Seeded #{PROBE_ID} (#{response.code}).
+ Seeded #{RENUMBER_ID} (#{code}).
- 1. The server needs PRO_TACTS_DEBUG=1 for the write to be logged —
- a successful PUT is not an unhandled request, so log/unhandled
- will not have it. Restart with `PRO_TACTS_DEBUG=1 rake dev` if
- it is not set.
- 2. Resync the client, edit "Probe Boole" — any field, the edit
- itself does not matter — and let the write go through.
- 3. The PUT body lands in log/dev.log after a `>>` line. Compare it
- with the card above: which of the five forms came back, whether
- itemN was renumbered, and whether a companion stayed with the
- line it was grouped with.
- 4. Record what happened in docs/macos-contacts.md. Repeat on iOS,
- which numbers item groups differently (see
- test/fixtures/ios-exchange/05-put-contact-edit/request).
+ #{reading_the_result(RENUMBER_ID, "Probe Renumber").chomp}
+ 4. Read the two groups that come back:
+ - item1.TEL and its X-ABLabel should not have to move. Did
+ item1.X-PT-GROUP come back beside them?
+ - item3.EMAIL and its X-ABLabel should land on item2. Did
+ item3.X-PT-GROUP follow them there, or stay at item3?
+ The second answers whether a companion is a binding the client
+ maintains or two numbers that happened to agree.
+ 5. Record what happened in docs/macos-contacts.md, under "An
+ annotation survives only on its own line".
NEXT
end
end