Skip to content

test: retry the Keychain store-wide reads that raced their own writes - #79

Merged
StuartMeeks merged 1 commit into
mainfrom
fix/keychain-visibility-race-in-tests
Aug 26, 2026
Merged

test: retry the Keychain store-wide reads that raced their own writes#79
StuartMeeks merged 1 commit into
mainfrom
fix/keychain-visibility-race-in-tests

Conversation

@StuartMeeks

Copy link
Copy Markdown
Owner

Fixes the red main.

What happened

The post-merge run of #78 failed on test (macos-14):

failed KeychainCredentialManagerTests.GetProviderNamesAsync_ReturnsDistinctProviders
  Assert.Equal() Failure: Values differ
  Expected: 2
  Actual:   0

The same commit passed that same leg on the PR run minutes earlier, so it is timing rather than a regression. It is also not a consequence of the #55 owner-attribute change: when no owner is recorded IsOwnedByThisApp falls back to the prefix test, so filtering cannot zero the result.

Root cause — the same mismatch as #61

AddCredentialAsync confirms its write is visible via QuerySingleItem, an exact service+account query. ListCredentialsAsync and GetProviderNamesAsync go through QueryItems/QueryAllItemsForApp, a class-wide query filtered in memory. Visibility to one does not imply visibility to the other, so a read on the second path can legitimately see nothing straight after a write confirmed on the first.

That is precisely the mismatch #61 fixed for the libsecret Restore test. Same shape, different file.

The fix, and why six tests rather than one

Fourteen reads in KeychainCredentialManagerTests already used RetryHelper. Six did notGetProviderNamesAsync was simply the one that lost the race first, and the other five were equally exposed. All six now retry.

Each predicate matches what the test then asserts, not merely that something appeared:

Test Predicate
ListCredentialsAsync_ReturnsAddedCredential r.Any(c => c.AccountId == accountId)
ListCredentialsAsync_FiltersByProvider r.Count() == 1 per provider
GetCredentialByIdAsync_DoesNotMutateSelection r.Count() == 2 && r.Any(c => c.IsSelected)
DeleteCredentialAsync_RemovesCredential !r.Any()
GetProviderNamesAsync_ReturnsDistinctProviders r.Count() == 2
ListCredentialsAsync_IncludesDisplayFields_FromSummaryProvider r.Count() == 1

A count-only predicate guarding a content assertion is exactly what let #61 through, so the third row waits on the selection being visible too, not just on two rows existing.

Verification

Build clean, 416 tests locally. The macOS legs are the ones that matter here and only CI runs them — worth watching test (macos-14) and test (macos-15) specifically on this PR, and on main after merge, since a timing fix cannot be proven by a single green run.

🤖 Generated with Claude Code

main went red on macos-14: GetProviderNamesAsync_ReturnsDistinctProviders
reported "Expected: 2, Actual: 0" after adding three credentials. The same
commit passed that leg on the PR run minutes earlier, so it is timing, not a
regression -- and not a consequence of the #55 owner-attribute change, which
falls back to the prefix test when no owner is recorded.

AddCredentialAsync confirms visibility via QuerySingleItem, an exact
service+account query. ListCredentialsAsync and GetProviderNamesAsync go through
QueryItems/QueryAllItemsForApp, a class-wide query filtered in memory. Being
visible to one does not imply being visible to the other, which is exactly the
mismatch #61 fixed for the libsecret Restore test.

Fourteen reads in this file already used RetryHelper. Six did not, and any of
them could have lost the same race; GetProviderNamesAsync was simply first. All
six now retry, each on the condition the test then asserts rather than on
something merely having appeared -- a count-only predicate is what let #61
through.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@StuartMeeks
StuartMeeks merged commit e95eeaf into main Aug 26, 2026
11 checks passed
@StuartMeeks
StuartMeeks deleted the fix/keychain-visibility-race-in-tests branch August 26, 2026 05:17
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