Skip to content

test: add UID_2445 population probe (gates portable event identity Phase 0) - #276

Merged
williscool merged 6 commits into
masterfrom
feat/uid2445_probe
Sep 21, 2026
Merged

williscool merged 6 commits into
masterfrom
feat/uid2445_probe

Conversation

@williscool

Copy link
Copy Markdown
Owner

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_2445 being 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:

  • Well populated → proceed as written; Phase 6 stays an optional safety net.
  • Sparse → the exact path isn't viable and Phase 6's content heuristic becomes the primary mechanism. Materially different plan — much cheaper to learn now than after five phases are built on it.

What it measures

Per account type, across every calendar on the device:

total events, excluding tombstones so deleted rows don't understate coverage
uid UID_2445 non-null, plus a percentage
syncId _SYNC_ID non-null — measures the fallback's real value instead of assuming it
neither events with no stable identifier at all; the exact path can never recover these

Ends with a VERDICT line 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 INCONCLUSIVE instead.

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:compileX8664DebugAndroidTestKotlin passes (exit 0, no Kotlin errors)
  • Not yet executed — needs a device with real Google-synced calendars
.\gradlew.bat :app:connectedX8664DebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.github.quarck.calnotify.calendar.Uid2445PopulationProbeTest
adb logcat -s Uid2445Probe:* | grep UID2445_PROBE

The plan doc is updated to point at this probe as the gating step.

🤖 Generated with Claude Code

https://claude.ai/code/session_01N34fouw76cNkoFVg6V3j4Z

williscool and others added 2 commits September 20, 2026 23:37
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
@github-actions

Copy link
Copy Markdown

Build artifacts for PR #276 (commit 599582b) are available:

You can download these artifacts from the "Artifacts" section of the workflow run.

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
@github-actions

Copy link
Copy Markdown

Code Coverage Report

Overall Project 27.68% ❌

There is no coverage information present for the Files changed

@github-actions

Copy link
Copy Markdown

📊 Code Coverage Summary

Coverage Type Coverage
Overall 27.68
Changed Files 100

View detailed coverage report

@github-actions

Copy link
Copy Markdown

Build artifacts for PR #276 (commit d1837c8) are available:

You can download these artifacts from the "Artifacts" section of the workflow run.

@github-actions

Copy link
Copy Markdown

📊 Code Coverage Summary

Coverage Type Coverage
Overall 27.68
Changed Files 100

View detailed coverage report

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
@github-actions

Copy link
Copy Markdown

Build artifacts for PR #276 (commit 13a106c) are available:

You can download these artifacts from the "Artifacts" section of the workflow run.

@github-actions

Copy link
Copy Markdown

📊 Code Coverage Summary

Coverage Type Coverage
Overall 27.68
Changed Files 100

View detailed coverage report

@github-actions

Copy link
Copy Markdown

Build artifacts for PR #276 (commit ad82816) are available:

You can download these artifacts from the "Artifacts" section of the workflow run.

@github-actions

Copy link
Copy Markdown

📊 Code Coverage Summary

Coverage Type Coverage
Overall 27.68
Changed Files 100

View detailed coverage report

williscool and others added 2 commits September 21, 2026 00:53
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
@github-actions

Copy link
Copy Markdown

Build artifacts for PR #276 (commit bf864ce) are available:

You can download these artifacts from the "Artifacts" section of the workflow run.

@github-actions

Copy link
Copy Markdown

📊 Code Coverage Summary

Coverage Type Coverage
Overall 27.68
Changed Files 100

View detailed coverage report

@github-actions

Copy link
Copy Markdown

Build artifacts for PR #276 (commit a033904) are available:

You can download these artifacts from the "Artifacts" section of the workflow run.

@github-actions

Copy link
Copy Markdown

📊 Code Coverage Summary

Coverage Type Coverage
Overall 27.68
Changed Files 100

View detailed coverage report

@williscool
williscool merged commit df42f7c into master Sep 21, 2026
10 of 16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant