diff --git a/CHANGELOG.md b/CHANGELOG.md
index f1cf597..61f17e9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -21,6 +21,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
still bind the keystore to this machine/user, and the security boundary is
unchanged (filesystem permissions on the credentials directory).
+- **The Keychain test-retry budget is raised so `macos-15` stops flaking.** The
+ `RetryHelper` default was 20 attempts × 25 ms ≈ 500 ms, which was too tight on
+ a contended `macos-15` runner: a selection item written by
+ `RestoreCredentialAsync` was not always visible within it, so
+ `RestoreCredentialAsync_PreservesAccountIdAndSelection` timed out its
+ `IsSelected` retry and the assertion failed intermittently. The default is now
+ 40 × 50 ms ≈ 2 s; retries still return as soon as the store is consistent, so
+ passing runs are unaffected. Test-only change.
+
### Changed
- **Keystore format bumped to version 2** (`.keystore` header). A version-1
diff --git a/tests/NextIteration.SpectreConsole.Auth.Tests/Infrastructure/RetryHelper.cs b/tests/NextIteration.SpectreConsole.Auth.Tests/Infrastructure/RetryHelper.cs
index ce1aa73..5385bb4 100644
--- a/tests/NextIteration.SpectreConsole.Auth.Tests/Infrastructure/RetryHelper.cs
+++ b/tests/NextIteration.SpectreConsole.Auth.Tests/Infrastructure/RetryHelper.cs
@@ -18,6 +18,15 @@ namespace NextIteration.SpectreConsole.Auth.Tests.Infrastructure
/// final attempt still fails. A persistent error still fails the test with its real
/// message; only a transient one is ridden out.
///
+ ///
+ /// The default budget is 40 attempts × 50 ms ≈ 2 s. The original 20 × 25 ms ≈ 500 ms
+ /// proved too tight on a contended macos-15 runner: a selection item written by
+ /// RestoreCredentialAsync moments earlier was not always visible within it, so
+ /// the IsSelected retry in RestoreCredentialAsync_PreservesAccountIdAndSelection
+ /// timed out and the following assertion failed. Retries return
+ /// as soon as the store is consistent, so a passing run does not pay the ceiling; only a
+ /// genuine failure waits the full 2 s before surfacing.
+ ///
///
internal static class RetryHelper
{
@@ -29,8 +38,8 @@ internal static class RetryHelper
///
internal static async Task UntilTrueAsync(
Func> action,
- int maxAttempts = 20,
- int delayMs = 25)
+ int maxAttempts = 40,
+ int delayMs = 50)
{
for (var attempt = 1; attempt <= maxAttempts; attempt++)
{
@@ -69,8 +78,8 @@ internal static async Task UntilTrueAsync(
internal static async Task UntilAsync(
Func> action,
Func predicate,
- int maxAttempts = 20,
- int delayMs = 25)
+ int maxAttempts = 40,
+ int delayMs = 50)
{
T result = default!;
var satisfied = false;