Skip to content

refactor: dedupe provider zone fallback, journey leg steps, and logout write wait - #4871

Merged
tyler-dane merged 4 commits into
mainfrom
claude/kind-dirac-2qmerp
Oct 27, 2026
Merged

tyler-dane merged 4 commits into
mainfrom
claude/kind-dirac-2qmerp

Conversation

@tyler-dane

Copy link
Copy Markdown
Contributor

What and why

A debt pass over the last seven days of merges. Four items, one commit each,
no behavior change intended beyond the three dead journey selectors called
out below. Everything is on one branch because this session is scoped to
claude/kind-dirac-2qmerp; the commits are independent and revert cleanly
on their own.

1. refactor(sync): share the provider iana zone fallback
#4827 pulled Microsoft's zone resolution into microsoft-timezone.ts but
stopped at the provider boundary. The same IANA-or-UTC fallback was still
copied verbatim into the Google and Apple normalizers, and
microsoft-recurrence.ts kept a fourth near-copy (resolveIanaZone) that
guessed at IANA-ness with zone.includes("/"). The predicate and the
fallback now live in provider-timezone.util.ts, and the write path gets an
explicit requireIanaTimeZone (it must throw rather than anchor UNTIL to
the wrong wall clock). The includes("/") guess is gone, so a bogus
"Foo/Bar" raises instead of flowing into dayjs as an unusable zone.

2. refactor(e2e): share journey leg steps so selector fixes reach both legs
The staging and prod journey specs drive the same account lifecycle against
different deployments, with the scaffolding copy-pasted. Last week's seven
staging fixes (#4854-#4860) only landed on the staging copy, so the prod leg
had three selectors that cannot match the app:

The shared steps moved to journey-leg.util.ts; both legs call them, so the
prod leg picks up the fixed selectors and the next fix lands once. The prod
leg also now opens its draft with the shared keyboard helper and
disambiguates its grid card with .last() (#4854, #4856), and its
cancel-subscription confirm is scoped to that dialog instead of matching any
open dialog while the settings panel is also one. Its checkout-session
response listener now attaches before signup rather than after the trial
dialog appears, so the response cannot land before the listener and leave the
expect.poll waiting out its full 120s.

3. simplify(web): hoist the logout pending-write timer out of its retry loop
#4866 gave waitForPendingEventMutations a timeout by recomputing the
remaining budget and allocating a fresh setTimeout on every pass, plus a
remainingMs <= 0 guard for the pass where nothing was left. One timer for
the whole budget does the same job; the clear moved into a finally so no
timer outlives the wait.

4. refactor(backend): route the synthetic email check through one predicate
resolveSyntheticAccountFromEmail restated isSyntheticAccount's inbox
match instead of calling it, so the two could drift on what counts as a
journey address. It delegates now, keeping the request-header check that is
the only thing it adds.

Also noticed but left alone, as neither is from this window:
patchedDependencies still pins @tanstack/hotkeys@0.3.3 while the resolved
transitive version is 0.16.0, so bun install prints patch no longer applies on every fresh worktree; and the onboarding localStorage seeding
is open-coded as raw key strings in ~30 e2e specs.

Verify

Selected packages: sync, web, backend
Checks run: test:sync:fast, test:web, type-check, lint, knip, test:a11y
Checks skipped: (none)
Failed: test:backend:fast, test:e2e
VERDICT: FAIL

Green: test:sync:fast (899 pass), test:web (738 pass), test:a11y
(67 passed), type-check, lint, knip.

Both failures are this sandbox rather than this change, and I would rather
show that than paste a PASS I did not get:

  • test:backend:fast — 13 failures, all pre-existing. They are
    GET /api/config 500s and SSE Server event timeouts, i.e. exactly the
    suites AGENTS.md lists as needing compass.yaml; the config schema wants
    MONGO_URI, SUPERTOKENS_URI, SUPERTOKENS_KEY and
    SYNC_INTERNAL_AUTH_TOKEN, which this sandbox has no secrets for. Checked
    rather than assumed: with the one backend file in this PR reverted to
    origin/main, the same suite fails the same 13 tests (619 pass / 13 fail
    either way), and none of the 13 goes anywhere near synthetic accounts.
  • test:e2e — 1 failure, in a file this diff cannot reach.
    e2e/billing/explore-before-trial.spec.ts:273 spent its 15s waiting on the
    draft title input and failed at 16.4s under full-suite load. Re-run on its
    own it passes in 3.2s (5 passed), so this is sandbox load: the only e2e
    files here are the two journey specs and their new helper, and both legs
    skip unless *_JOURNEY_ENABLED=1.

The journey legs cannot be executed from here (they need staging/prod and
real accounts), so their evidence is type-check over e2e/tsconfig.json,
bun lint, and a Playwright run confirming both legs still collect and skip
under their own configs (playwright.staging-journey.config.ts,
playwright.prod-journey.config.ts). The three selector corrections were
each read off the component that renders them, cited above.


Generated by Claude Code

#4827 pulled Microsoft's zone resolution into microsoft-timezone.ts but
stopped at the provider boundary, leaving the same IANA-or-UTC fallback
copied verbatim in the Google and Apple normalizers and a fourth
near-copy (resolveIanaZone) inside microsoft-recurrence.ts.

Move the predicate and the UTC fallback to provider-timezone.util.ts and
have all four call sites use it. microsoft-timezone.ts now also exports
requireIanaTimeZone for the write path, which must throw rather than
anchor UNTIL to the wrong wall clock; it replaces resolveIanaZone's
`includes("/")` guess with the Intl-backed check, so a bogus "Foo/Bar"
raises instead of flowing into dayjs as an unusable zone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HmwVQGs5UYvpcKv3TCDt5Y
The staging and prod journey specs ran the same account lifecycle against
different deployments with the scaffolding copy-pasted: provider parsing,
credential lookup, onboarding seeding, signup, login, logout, and
delete-account. Last week's seven staging fixes (#4854-#4860) only ever
landed on the staging copy, so the prod leg had drifted into three dead
selectors:

- `button "Settings"` matched the panel title, not the sidebar control,
  which is labelled "Open settings" (#4857).
- The delete dialog is titled "Delete your Compass account?", so
  `/delete your account/i` never matched (#4860).
- The confirmation input is compared with `===` against
  "Delete my Compass account", so typing it lower case left the submit
  button disabled and the leg could never finish.

Move the shared steps into journey-leg.util.ts and have both legs call
them, so the prod leg picks up the fixed selectors and the next one lands
once. The prod leg also now opens its draft with the shared keyboard
helper and disambiguates the grid card with `.last()`, matching #4854 and
#4856.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HmwVQGs5UYvpcKv3TCDt5Y
…loop

#4866 gave waitForPendingEventMutations a timeout by recomputing the
remaining budget and allocating a fresh setTimeout on every pass, with an
extra `remainingMs <= 0` guard to cover the pass where nothing was left.
One timer for the whole budget does the same job: the loop re-reads the
pending set (a settled batch can enqueue follow-up writes) and races it
against that single timer, with the clear in a `finally` so no timer
outlives the wait.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HmwVQGs5UYvpcKv3TCDt5Y
resolveSyntheticAccountFromEmail restated isSyntheticAccount's inbox
match instead of calling it, so the two could disagree about what counts
as a journey address. It now delegates, keeping the header check that is
the only thing it adds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HmwVQGs5UYvpcKv3TCDt5Y
@tyler-dane
tyler-dane marked this pull request as ready for review October 27, 2026 10:38
@tyler-dane tyler-dane added the agent-automerge Agent-loop finished; merge-guard may squash-merge label Oct 27, 2026 — with Claude
@tyler-dane
tyler-dane enabled auto-merge October 27, 2026 10:39

tyler-dane commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor Author

Tech-debt sweep over merges from 2026-10-20 to 2026-10-27 (~50 commits). Debt items addressed, one commit each:

# Debt Introduced by Commit
1 Incomplete dedupe: the IANA-or-UTC zone fallback left copied in 3 more places after the shared module was created #4827 refactor(sync): share the provider iana zone fallback
2 Copy-paste drift: 7 staging journey fixes never reached the duplicated prod leg, leaving 3 selectors that cannot match the app #4854-#4860 refactor(e2e): share journey leg steps so selector fixes reach both legs
3 Per-iteration timer + manual deadline arithmetic in the logout pending-write wait #4866 simplify(web): hoist the logout pending-write timer out of its retry loop
4 resolveSyntheticAccountFromEmail restating isSyntheticAccount's inbox match instead of calling it #4866 refactor(backend): route the synthetic email check through one predicate

Item 2 is the one with teeth. The prod journey leg could not have completed: it looked for button "Settings" where the sidebar control is aria-label="Open settings", matched /delete your account/i against a dialog titled "Delete your Compass account?", and typed the confirmation phrase in lower case against an exact === comparison, which leaves the submit button disabled. Each of those was fixed on the staging copy last week and never ported, because the two legs were duplicated rather than shared.

Scanned and found clean, having already had a dedupe pass in this window: the hidden-draft / pointer-gate / deck-layout cluster (#4835, #4833), the email dispatch send tail (#4827, #4850), the OAuth scopes-recovery scaffolding (#4866), and the Microsoft recurrence mapping (#4844).

Two items noticed and deliberately left out of scope, both older than this window: patchedDependencies still pins @tanstack/hotkeys@0.3.3 while the resolved transitive version is 0.16.0, so every fresh worktree's bun install prints patch no longer applies for a patch that is not being applied; and the onboarding localStorage seeding is open-coded as raw key strings across ~30 e2e specs. Happy to take either as a follow-up.

Merged via the merge queue. On the verify caveat recorded in the PR body: local bun run verify --strict returned VERDICT: FAIL on test:backend:fast (13) and test:e2e (1), which I argued were sandbox-only. CI settled it, both green: unit-leg (backend) passed, and the e2e shards passed including shard 2 (e2e/billing, home of the spec that timed out locally) and shard 5 (e2e/journey). Boot bundle came in at +0.0 KB vs main.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

Boot bundle: 528.8 KB gzip (+0.0 KB vs main)

@tyler-dane
tyler-dane added this pull request to the merge queue Oct 27, 2026
Merged via the queue into main with commit cb8093e Oct 27, 2026
39 checks passed
@tyler-dane
tyler-dane deleted the claude/kind-dirac-2qmerp branch October 27, 2026 10:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent-automerge Agent-loop finished; merge-guard may squash-merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants