test: add UID_2445 population probe (gates portable event identity Phase 0) - #276
Merged
Merged
Conversation
Diagnostic that answers the question the portable event identity plan gates Phase 0 on: is CalendarContract.Events.UID_2445 actually populated on real calendars? The plan's exact-match path (Phases 0-5) depends entirely on that column having values, but there is a long-standing unresolved Android issue claiming it is always null. If the column is sparse, the exact path is not viable and the Phase 6 content heuristic becomes the primary mechanism -- a materially different plan, and much cheaper to discover now than after five phases are built on it. Reports rather than asserts, and never fails on low coverage: "this device has no synced calendars" is a property of the device, not a bug. Tallies _SYNC_ID alongside UID_2445 so the fallback's real value is measured rather than assumed, and counts events carrying neither -- those are the ones the exact path can never recover. Excludes tombstones so deleted rows do not understate coverage. Verdict maps coverage onto the decision the plan needs, so the output is actionable rather than just numbers. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N34fouw76cNkoFVg6V3j4Z
Adds scripts/probe_uid2445.sh, which reads the Calendar Provider over `adb shell content query` and prints the same per-account-type table and verdict as the instrumentation test -- but with no compile, install, or test run. Faster path to the answer that gates Phase 0. Records why this cannot run against an app backup, since the question comes up naturally and the answer constrains the wider plan: UID_2445 lives in com.android.providers.calendar's database, a different app. Our backup covers only this app's own databases and prefs, so it holds no calendar rows to measure. run-as cannot reach another package without root either. The provider is therefore only readable live -- which is precisely why identity must be captured at write time rather than reconstructed later, and why an already-restored device has only row content to match on. The awk parser had two real bugs caught by fixture testing: substring key matching let "_id" match inside "calendar_id", and regex trimming broke on values containing "@" or "." (every UID and email). Now splits on ", " and compares keys exactly. Verified against fixtures covering the viable, not-viable, and empty-provider paths, including tombstone exclusion. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N34fouw76cNkoFVg6V3j4Z
The probe was one file with a 50-line awk program embedded in a shell pipe, so neither language was readable and the awk could not be run without extracting it by hand first. Now three pieces: scripts/probe_uid2445.sh - adb plumbing, small named functions scripts/lib/uid2445_tally.awk - the tallying logic, on its own scripts/lib/test_uid2445_tally.sh + testdata/ - tests Also drops the ---SPLIT--- sentinel (awk takes two files and uses FNR), renames single-letter arrays to what they hold, and moves the verdict thresholds into a named function. Splitting it surfaced a real bug: the row-type detection I first wrote matched calendar rows with /(^|, )_id=/, which also matches "_sync_id=" on every event row -- so every event was consumed as a calendar and the probe reported INCONCLUSIVE against perfectly good data. Now keyed on file position instead of field names. Fixtures encode the bugs actually hit while writing this: substring key matching, "@" and "." in values, _sync_id confusion, and tombstones inflating the denominator. Verified the tests are not vacuous by mutation -- removing the tombstone skip fails 3 of the 7. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N34fouw76cNkoFVg6V3j4Z
Code Coverage Report
|
📊 Code Coverage Summary
|
📊 Code Coverage Summary
|
Ran the probe on a real device (Pixel 10 Pro Fold, API 37, 16 Google calendars across two accounts, 4761 events). UID_2445 is null for every single event. The long-standing Android issue is accurate and current: the column exists and is queryable, but Google's sync adapter never populates it. Building the exact-match path on it would have failed completely and silently. _SYNC_ID is the opposite: populated for 100% of events, and 4761 unique values across 4761 events -- no collisions. So the exact path survives intact; only the column name changes. _SYNC_ID becomes primary and UID_2445 is read opportunistically, since non-Google providers may populate it. Recurrence also checked, since it drives the Phase 1b re-key: a series carries ONE _SYNC_ID for the parent rather than one per instance, so (sync_id, instanceStartTime) identifies an occurrence, and the 1021 recurrence exceptions carry original_sync_id back to the parent. Caveat recorded: one device, com.google only. The never-synced local event case still degrades to unresolved as described; this device has no local calendars to demonstrate it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N34fouw76cNkoFVg6V3j4Z
📊 Code Coverage Summary
|
📊 Code Coverage Summary
|
Checked the plan's premises against the real device and the real app install (368 stored events, Room active) while it was attached. Phase 2 backfill would capture _SYNC_ID for 368/368 stored events -- 100%. Direct instance resolution is 362/368; the 6 gaps are snoozed occurrences of deleted recurring series whose parent event rows still exist, so backfill reaches them anyway. The reserved s2 column is empty on all 368 rows, confirming the premise that nothing reads or writes it today. Also validated findMatchingCalendarId's design: all 16 calendars produce a UNIQUE tier-1 key (account_name + account_type + ownerAccount) despite sharing only two account names between them. ownerAccount is what disambiguates, so the existing three-tier matcher is right for this setup rather than merely plausible. Records the adb gotchas found along the way: exec-out rather than shell (shell corrupts binary), and pull the -wal file or the database reads as malformed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N34fouw76cNkoFVg6V3j4Z
Saves everything the portable-event-identity work needs so a device no longer has to stay attached: provider contents, this app's databases, and shared prefs. Note what "backup of the calendar db" can actually mean here. The Calendar Provider's own database belongs to another app and cannot be pulled without root, so its CONTENTS are exported as content-query text instead -- enough to re-run read-only analysis, but not a restorable database file. Our own app databases are pulled verbatim, since that package is debuggable. Bakes in the two adb gotchas found earlier: exec-out rather than shell (shell mangles binary and the DB reads as malformed), and pulling the -wal file (without it recent writes are missing). Events are exported including deleted=1 tombstones so tombstone handling stays testable offline. Verified the snapshot is equivalent to a live device: replaying the Phase 2 backfill simulation against it reproduces the same 368/368, and all three Room databases pass integrity_check. Output lands in ./tmp/ (gitignored) and contains real personal calendar data -- the script and its README say so explicitly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N34fouw76cNkoFVg6V3j4Z
📊 Code Coverage Summary
|
📊 Code Coverage Summary
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
First step of portable event identity (#273). Diagnostic only — no production code changes.
Why this before Phase 0
The plan's exact-match path (Phases 0–5) depends entirely on
CalendarContract.Events.UID_2445being populated. There is a long-standing Android issue titled "UID_2445 column is always null" whose resolution isn't publicly readable, so the plan gates Phase 0 on testing that assumption rather than trusting it.The outcome decides which plan we're on:
What it measures
Per account type, across every calendar on the device:
totaluidUID_2445non-null, plus a percentagesyncId_SYNC_IDnon-null — measures the fallback's real value instead of assuming itneitherEnds with a
VERDICTline mapping coverage onto the actual decision (VIABLE/VIABLE WITH FALLBACK/MIXED/NOT VIABLE), so the output is actionable rather than raw numbers.Two deliberate choices
It reports, it does not assert. A device with no synced calendars isn't a bug, so failing the build on low coverage would be wrong. It emits
INCONCLUSIVEinstead.It needs a real device. A clean emulator has no Google-synced events and will report
INCONCLUSIVE— which is not evidence either way. This has to run against a phone with real calendars, so I have not been able to run it; only the compile is verified.Status
:app:compileX8664DebugAndroidTestKotlinpasses (exit 0, no Kotlin errors)The plan doc is updated to point at this probe as the gating step.
🤖 Generated with Claude Code
https://claude.ai/code/session_01N34fouw76cNkoFVg6V3j4Z