Fix the flaky DebouncedSearchField test: debounce on TimeProvider #21

Merged
bjoern merged 1 commit from fix/debounce-flake into main 2026-07-10 08:10:15 +02:00
Member

Clearing_cancels_a_pending_debounce_so_it_never_reports has failed intermittently twice in two days (expected [""], got ["hero", ""]) — most recently on PR #20's local runs. Hunted it down: it never fails solo (0 in 10 runs), only under full-suite parallel load (1 in 6 runs), which is why it kept slipping through.

Root cause: the component debounces with wall-clock Task.Delay, and the test used a 50ms window. Under load, the 50ms elapsed between Input("hero") and the clear click — the stale value legitimately reported before the cancel existed. The test's assumption ("my next line runs within 50ms") is exactly the kind that CI load breaks.

Fix: DebouncedSearchField gains a Clock parameter (TimeProvider, default System) and delays via Task.Delay(…, Clock, token). A parameter rather than an injection keeps it a dumb component per ADR 0023, with no DI expectations on consumers. Production behaviour is unchanged.

The timing tests now run on a FakeTimeProvider and advance it explicitly — the same pattern the DomainChangedBridge tests use in PR #20. The previously-flaky test now proves the actual property instead of a probability: the clear cancels the pending delay before time advances, so no amount of time can make the stale value fire. No test sleeps remain in the file.

Evidence: reproduced the flake before fixing (1 failure in 6 full-suite runs); after, 0 failures in 8 full-suite runs.

One coordination note: this adds the Microsoft.Extensions.TimeProvider.Testing package version line to Directory.Packages.props, which PR #20 adds identically — whichever merges second will see a trivial (identical-line) conflict at worst.

🤖 Generated with Claude Code

`Clearing_cancels_a_pending_debounce_so_it_never_reports` has failed intermittently twice in two days (expected `[""]`, got `["hero", ""]`) — most recently on PR #20's local runs. Hunted it down: it never fails solo (0 in 10 runs), only under full-suite parallel load (1 in 6 runs), which is why it kept slipping through. **Root cause:** the component debounces with wall-clock `Task.Delay`, and the test used a 50ms window. Under load, the 50ms elapsed *between* `Input("hero")` and the clear click — the stale value legitimately reported before the cancel existed. The test's assumption ("my next line runs within 50ms") is exactly the kind that CI load breaks. **Fix:** `DebouncedSearchField` gains a `Clock` parameter (`TimeProvider`, default `System`) and delays via `Task.Delay(…, Clock, token)`. A *parameter* rather than an injection keeps it a dumb component per ADR 0023, with no DI expectations on consumers. Production behaviour is unchanged. The timing tests now run on a `FakeTimeProvider` and advance it explicitly — the same pattern the `DomainChangedBridge` tests use in PR #20. The previously-flaky test now proves the actual property instead of a probability: the clear cancels the pending delay *before* time advances, so **no amount of time** can make the stale value fire. No test sleeps remain in the file. **Evidence:** reproduced the flake before fixing (1 failure in 6 full-suite runs); after, 0 failures in 8 full-suite runs. One coordination note: this adds the `Microsoft.Extensions.TimeProvider.Testing` package version line to `Directory.Packages.props`, which PR #20 adds identically — whichever merges second will see a trivial (identical-line) conflict at worst. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix(ui): deterministic debounce — DebouncedSearchField on TimeProvider
All checks were successful
CI / build (pull_request) Successful in 14s
CI / test (pull_request) Successful in 22s
d91b6e8f20
Clearing_cancels_a_pending_debounce_so_it_never_reports failed intermittently
under full-suite CI load (twice in two days: expected [""], got ["hero", ""]).
The component debounced with wall-clock Task.Delay, so the test's 50ms window
raced the scheduler: under load, the delay elapsed between Input("hero") and
the clear click, and the stale value reported before the cancel could land.

- DebouncedSearchField gains a Clock parameter (TimeProvider, default System)
  and delays via Task.Delay(…, Clock, token). A parameter rather than an
  injection keeps it a dumb component (ADR 0023) with no DI expectations.
- The timing tests run on a FakeTimeProvider and advance it explicitly. The
  flaky test now proves the actual property: the clear cancels the pending
  delay BEFORE time advances, so no amount of time can make the stale value
  fire — an absence the wall-clock version could not pin down, only make
  probable. No test sleeps remain in the file.

Reproduced the flake beforehand (1 failure in 6 full-suite runs; never solo in
10 — it needed parallel-suite load); with the fix, 0 failures in 8 full-suite
runs. Production behaviour is unchanged: the default clock is the system one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Summary

Summary
Generated on: 07/10/2026 - 06:04:03
Coverage date: 07/10/2026 - 06:03:58 - 07/10/2026 - 06:04:01
Parser: MultiReport (4x Cobertura)
Assemblies: 7
Classes: 138
Files: 119
Line coverage: 93.2% (2930 of 3143)
Covered lines: 2930
Uncovered lines: 213
Coverable lines: 3143
Total lines: 6597
Branch coverage: 85.7% (516 of 602)
Covered branches: 516
Total branches: 602
Method coverage: Feature is only available for sponsors

Coverage

Kagura.BlazorAdapter - 71.3%
Name Line Branch
Kagura.BlazorAdapter 71.3% 75.2%
Kagura.BlazorAdapter.BlazorAdapterAssembly 100%
Kagura.BlazorAdapter.Design 0% 0%
Kagura.BlazorAdapter.KnowledgeBase.CharacterCreated 100%
Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorEffects 100% 100%
Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorLoaded 100%
Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorPage 86.6% 72.7%
Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorReducers 100%
Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorState 100%
Kagura.BlazorAdapter.KnowledgeBase.CharactersEffects 93.3% 75%
Kagura.BlazorAdapter.KnowledgeBase.CharactersLoaded 100%
Kagura.BlazorAdapter.KnowledgeBase.CharactersPage 90% 75%
Kagura.BlazorAdapter.KnowledgeBase.CharactersReducers 100% 87.5%
Kagura.BlazorAdapter.KnowledgeBase.CharactersState 100% 100%
Kagura.BlazorAdapter.KnowledgeBase.CreateCharacterFailed 0%
Kagura.BlazorAdapter.KnowledgeBase.CreateCharacterRequested 100%
Kagura.BlazorAdapter.KnowledgeBase.LoadCharacter 100%
Kagura.BlazorAdapter.KnowledgeBase.LoadCharacters 100%
Kagura.BlazorAdapter.KnowledgeBase.SetCharactersFilter 100%
Kagura.BlazorAdapter.Notifications.DomainChangedBridge 88.8% 58.3%
Kagura.BlazorAdapter.Notifications.DomainChangesReceived 100% 50%
Kagura.BlazorAdapter.OverlayDemo 0% 0%
Kagura.BlazorAdapter.Projects.CreateProjectRequested 100%
Kagura.BlazorAdapter.Projects.DeleteProjectRequested 100%
Kagura.BlazorAdapter.Projects.LoadWorkspace 100%
Kagura.BlazorAdapter.Projects.ProjectCreated 100%
Kagura.BlazorAdapter.Projects.ProjectCreateFailed 100%
Kagura.BlazorAdapter.Projects.ProjectDeleted 100%
Kagura.BlazorAdapter.Projects.ProjectSaved 100%
Kagura.BlazorAdapter.Projects.ProjectSaveFailed 100%
Kagura.BlazorAdapter.Projects.ProjectsEffects 100% 100%
Kagura.BlazorAdapter.Projects.ProjectsLoaded 100%
Kagura.BlazorAdapter.Projects.ProjectsPage 94.7% 100%
Kagura.BlazorAdapter.Projects.ProjectsReducers 100% 100%
Kagura.BlazorAdapter.Projects.ProjectsState 100% 100%
Kagura.BlazorAdapter.Projects.ProjectWorkspacePage 93.7% 75%
Kagura.BlazorAdapter.Projects.SaveProjectRequested 100%
Kagura.BlazorAdapter.Projects.SetProjectsFilter 100%
Kagura.BlazorAdapter.Projects.WorkspaceEffects 100% 100%
Kagura.BlazorAdapter.Projects.WorkspaceLoaded 100%
Kagura.BlazorAdapter.Projects.WorkspaceReducers 100%
Kagura.BlazorAdapter.Projects.WorkspaceSectionPage 95.2% 66.6%
Kagura.BlazorAdapter.Projects.WorkspaceShell 100% 93.7%
Kagura.BlazorAdapter.Projects.WorkspaceState 100%
Kagura.Domain - 96.4%
Name Line Branch
Kagura.Domain 96.4% 83.9%
Kagura.Domain.Graph.Entry 100% 100%
Kagura.Domain.Graph.Link 100% 100%
Kagura.Domain.Graph.LinkRole 100% 100%
Kagura.Domain.Graph.LinkRoles 92.3%
Kagura.Domain.Journal.ChangeLogEntry 100%
Kagura.Domain.KnowledgeBase.Character 100%
Kagura.Domain.Projects.Project 100% 100%
Kagura.Domain.Projects.Slug 100% 100%
System.Text.RegularExpressions.Generated 90.2% 72.2%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>FE06CC341D340484
D04ADFED3A21D401C2764A1D17367E35BEB556CBB3B4B0B74__NonSlugChars_0
89.4% 75%
Kagura.Infrastructure - 95.9%
Name Line Branch
Kagura.Infrastructure 95.9% 87.2%
Kagura.Infrastructure.DependencyInjection 100%
Kagura.Infrastructure.Graph.EfGraphStore 95.5% 66.6%
Kagura.Infrastructure.Journal.EfChangeJournal 100%
Kagura.Infrastructure.Journal.EfUndoStore 97.5% 90.6%
Kagura.Infrastructure.Journal.OperationContext 100% 100%
Kagura.Infrastructure.KnowledgeBase.EfCharacterStore 100%
Kagura.Infrastructure.Notifications.InProcessDomainChangedBus 100% 100%
Kagura.Infrastructure.Persistence.Configurations.ChangeLogEntryConfiguratio
n
100%
Kagura.Infrastructure.Persistence.Configurations.CharacterConfiguration 100%
Kagura.Infrastructure.Persistence.Configurations.EntryConfiguration 100%
Kagura.Infrastructure.Persistence.Configurations.LinkConfiguration 100%
Kagura.Infrastructure.Persistence.Configurations.ProjectConfiguration 100%
Kagura.Infrastructure.Persistence.Converters.UtcTicksConverter 100%
Kagura.Infrastructure.Persistence.KaguraDbContext 83.5% 82.5%
Kagura.Infrastructure.Persistence.KaguraDbContextFactory 0%
Kagura.Infrastructure.Persistence.Migrations.AddChangeLogUndoFlag 96.8%
Kagura.Infrastructure.Persistence.Migrations.AddCharacters 98.7%
Kagura.Infrastructure.Persistence.Migrations.AddGraphEntryAndLink 97.7%
Kagura.Infrastructure.Persistence.Migrations.AddProjectDescription 98.1%
Kagura.Infrastructure.Persistence.Migrations.AddSoftDeleteAndChangeLog 90.3%
Kagura.Infrastructure.Persistence.Migrations.InitialCreate 94.4%
Kagura.Infrastructure.Persistence.Migrations.KaguraDbContextModelSnapshot 100%
Kagura.Infrastructure.Projects.EfProjectStore 100% 100%
Kagura.Kernel - 90%
Name Line Branch
Kagura.Kernel 90% 75%
Kagura.Kernel.Err`1 100%
Kagura.Kernel.Ok`1 100%
Kagura.Kernel.Result`1 87.5% 75%
Kagura.Server - 100%
Name Line Branch
Kagura.Server 100% 78.9%
Kagura.Server.Components.App 100%
Kagura.Server.Components.Layout.MainLayout 100%
Kagura.Server.Components.Pages.Error 100% 50%
Kagura.Server.Components.Pages.Gate 100% 100%
Kagura.Server.Security.AccessGate 100% 83.3%
Kagura.Server.Security.AccessSecret 100% 100%
Program 100% 80%
Kagura.UI - 97.7%
Name Line Branch
Kagura.UI 97.7% 94.4%
Kagura.UI.Badge 100% 100%
Kagura.UI.Breadcrumb 100%
Kagura.UI.BreadcrumbItem 100% 100%
Kagura.UI.Button 100% 100%
Kagura.UI.Card 100% 100%
Kagura.UI.ConfirmDialog 100%
Kagura.UI.CssClassExtensions 100%
Kagura.UI.DebouncedSearchField 100% 88.8%
Kagura.UI.EmptyState 100% 100%
Kagura.UI.Field 100% 100%
Kagura.UI.Icon 100% 100%
Kagura.UI.IconCatalog 100%
Kagura.UI.InputFieldBase 94.2% 87.5%
Kagura.UI.Modal 87.1% 90%
Kagura.UI.NavGroup 100% 100%
Kagura.UI.NavItem 100% 100%
Kagura.UI.NavList 100%
Kagura.UI.PreviewImage 100% 100%
Kagura.UI.RelativeTime 100% 93.7%
Kagura.UI.Separator 100%
Kagura.UI.StatusDot 100%
Kagura.UI.Table`1 100% 92.3%
Kagura.UI.TableColumn`1 100%
Kagura.UI.TextArea 100% 100%
Kagura.UI.TextField 100%
Kagura.UseCases - 96.1%
Name Line Branch
Kagura.UseCases 96.1% 96.1%
Kagura.UseCases.DependencyInjection 100%
Kagura.UseCases.Graph.EdgeGroup 100%
Kagura.UseCases.Graph.GetNodeGraph 96.4% 83.3%
Kagura.UseCases.Graph.GraphEdgeView 85.7%
Kagura.UseCases.Graph.LinkNodes 100% 100%
Kagura.UseCases.Graph.NodeGraphView 100%
Kagura.UseCases.Graph.NodeSummary 100%
Kagura.UseCases.Graph.RemoveLink 100% 100%
Kagura.UseCases.Graph.RestoreLink 100% 100%
Kagura.UseCases.Journal.ChangeRecordView 57.1%
Kagura.UseCases.Journal.GetEntityHistory 100%
Kagura.UseCases.Journal.GetUndoStatus 100%
Kagura.UseCases.Journal.Redo 100% 100%
Kagura.UseCases.Journal.Undo 100% 100%
Kagura.UseCases.Journal.UndoOutcome 100%
Kagura.UseCases.Journal.UndoStatus 100%
Kagura.UseCases.KnowledgeBase.CharacterDto 80%
Kagura.UseCases.KnowledgeBase.CreateCharacter 100%
Kagura.UseCases.KnowledgeBase.GetCharacter 100% 100%
Kagura.UseCases.KnowledgeBase.ListCharacters 100%
Kagura.UseCases.Notifications.DomainChanged 100%
Kagura.UseCases.Projects.CreateProject 100% 100%
Kagura.UseCases.Projects.DeleteProject 100% 100%
Kagura.UseCases.Projects.GetProject 100% 100%
Kagura.UseCases.Projects.ListProjects 100%
Kagura.UseCases.Projects.ProjectDto 100%
Kagura.UseCases.Projects.UpdateProject 100% 100%
<!-- coverage-comment --> # Summary <details open><summary>Summary</summary> ||| |:---|:---| | Generated on: | 07/10/2026 - 06:04:03 | | Coverage date: | 07/10/2026 - 06:03:58 - 07/10/2026 - 06:04:01 | | Parser: | MultiReport (4x Cobertura) | | Assemblies: | 7 | | Classes: | 138 | | Files: | 119 | | **Line coverage:** | 93.2% (2930 of 3143) | | Covered lines: | 2930 | | Uncovered lines: | 213 | | Coverable lines: | 3143 | | Total lines: | 6597 | | **Branch coverage:** | 85.7% (516 of 602) | | Covered branches: | 516 | | Total branches: | 602 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>Kagura.BlazorAdapter - 71.3%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.BlazorAdapter**|**71.3%**|**75.2%**| |Kagura.BlazorAdapter.BlazorAdapterAssembly|100%|| |Kagura.BlazorAdapter.Design|0%|0%| |Kagura.BlazorAdapter.KnowledgeBase.CharacterCreated|100%|| |Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorEffects|100%|100%| |Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorLoaded|100%|| |Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorPage|86.6%|72.7%| |Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorReducers|100%|| |Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorState|100%|| |Kagura.BlazorAdapter.KnowledgeBase.CharactersEffects|93.3%|75%| |Kagura.BlazorAdapter.KnowledgeBase.CharactersLoaded|100%|| |Kagura.BlazorAdapter.KnowledgeBase.CharactersPage|90%|75%| |Kagura.BlazorAdapter.KnowledgeBase.CharactersReducers|100%|87.5%| |Kagura.BlazorAdapter.KnowledgeBase.CharactersState|100%|100%| |Kagura.BlazorAdapter.KnowledgeBase.CreateCharacterFailed|0%|| |Kagura.BlazorAdapter.KnowledgeBase.CreateCharacterRequested|100%|| |Kagura.BlazorAdapter.KnowledgeBase.LoadCharacter|100%|| |Kagura.BlazorAdapter.KnowledgeBase.LoadCharacters|100%|| |Kagura.BlazorAdapter.KnowledgeBase.SetCharactersFilter|100%|| |Kagura.BlazorAdapter.Notifications.DomainChangedBridge|88.8%|58.3%| |Kagura.BlazorAdapter.Notifications.DomainChangesReceived|100%|50%| |Kagura.BlazorAdapter.OverlayDemo|0%|0%| |Kagura.BlazorAdapter.Projects.CreateProjectRequested|100%|| |Kagura.BlazorAdapter.Projects.DeleteProjectRequested|100%|| |Kagura.BlazorAdapter.Projects.LoadWorkspace|100%|| |Kagura.BlazorAdapter.Projects.ProjectCreated|100%|| |Kagura.BlazorAdapter.Projects.ProjectCreateFailed|100%|| |Kagura.BlazorAdapter.Projects.ProjectDeleted|100%|| |Kagura.BlazorAdapter.Projects.ProjectSaved|100%|| |Kagura.BlazorAdapter.Projects.ProjectSaveFailed|100%|| |Kagura.BlazorAdapter.Projects.ProjectsEffects|100%|100%| |Kagura.BlazorAdapter.Projects.ProjectsLoaded|100%|| |Kagura.BlazorAdapter.Projects.ProjectsPage|94.7%|100%| |Kagura.BlazorAdapter.Projects.ProjectsReducers|100%|100%| |Kagura.BlazorAdapter.Projects.ProjectsState|100%|100%| |Kagura.BlazorAdapter.Projects.ProjectWorkspacePage|93.7%|75%| |Kagura.BlazorAdapter.Projects.SaveProjectRequested|100%|| |Kagura.BlazorAdapter.Projects.SetProjectsFilter|100%|| |Kagura.BlazorAdapter.Projects.WorkspaceEffects|100%|100%| |Kagura.BlazorAdapter.Projects.WorkspaceLoaded|100%|| |Kagura.BlazorAdapter.Projects.WorkspaceReducers|100%|| |Kagura.BlazorAdapter.Projects.WorkspaceSectionPage|95.2%|66.6%| |Kagura.BlazorAdapter.Projects.WorkspaceShell|100%|93.7%| |Kagura.BlazorAdapter.Projects.WorkspaceState|100%|| </details> <details><summary>Kagura.Domain - 96.4%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.Domain**|**96.4%**|**83.9%**| |Kagura.Domain.Graph.Entry|100%|100%| |Kagura.Domain.Graph.Link|100%|100%| |Kagura.Domain.Graph.LinkRole|100%|100%| |Kagura.Domain.Graph.LinkRoles|92.3%|| |Kagura.Domain.Journal.ChangeLogEntry|100%|| |Kagura.Domain.KnowledgeBase.Character|100%|| |Kagura.Domain.Projects.Project|100%|100%| |Kagura.Domain.Projects.Slug|100%|100%| |System.Text.RegularExpressions.Generated|90.2%|72.2%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>FE06CC341D340484<br/>D04ADFED3A21D401C2764A1D17367E35BEB556CBB3B4B0B74__NonSlugChars_0|89.4%|75%| </details> <details><summary>Kagura.Infrastructure - 95.9%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.Infrastructure**|**95.9%**|**87.2%**| |Kagura.Infrastructure.DependencyInjection|100%|| |Kagura.Infrastructure.Graph.EfGraphStore|95.5%|66.6%| |Kagura.Infrastructure.Journal.EfChangeJournal|100%|| |Kagura.Infrastructure.Journal.EfUndoStore|97.5%|90.6%| |Kagura.Infrastructure.Journal.OperationContext|100%|100%| |Kagura.Infrastructure.KnowledgeBase.EfCharacterStore|100%|| |Kagura.Infrastructure.Notifications.InProcessDomainChangedBus|100%|100%| |Kagura.Infrastructure.Persistence.Configurations.ChangeLogEntryConfiguratio<br/>n|100%|| |Kagura.Infrastructure.Persistence.Configurations.CharacterConfiguration|100%|| |Kagura.Infrastructure.Persistence.Configurations.EntryConfiguration|100%|| |Kagura.Infrastructure.Persistence.Configurations.LinkConfiguration|100%|| |Kagura.Infrastructure.Persistence.Configurations.ProjectConfiguration|100%|| |Kagura.Infrastructure.Persistence.Converters.UtcTicksConverter|100%|| |Kagura.Infrastructure.Persistence.KaguraDbContext|83.5%|82.5%| |Kagura.Infrastructure.Persistence.KaguraDbContextFactory|0%|| |Kagura.Infrastructure.Persistence.Migrations.AddChangeLogUndoFlag|96.8%|| |Kagura.Infrastructure.Persistence.Migrations.AddCharacters|98.7%|| |Kagura.Infrastructure.Persistence.Migrations.AddGraphEntryAndLink|97.7%|| |Kagura.Infrastructure.Persistence.Migrations.AddProjectDescription|98.1%|| |Kagura.Infrastructure.Persistence.Migrations.AddSoftDeleteAndChangeLog|90.3%|| |Kagura.Infrastructure.Persistence.Migrations.InitialCreate|94.4%|| |Kagura.Infrastructure.Persistence.Migrations.KaguraDbContextModelSnapshot|100%|| |Kagura.Infrastructure.Projects.EfProjectStore|100%|100%| </details> <details><summary>Kagura.Kernel - 90%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.Kernel**|**90%**|**75%**| |Kagura.Kernel.Err`1|100%|| |Kagura.Kernel.Ok`1|100%|| |Kagura.Kernel.Result`1|87.5%|75%| </details> <details><summary>Kagura.Server - 100%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.Server**|**100%**|**78.9%**| |Kagura.Server.Components.App|100%|| |Kagura.Server.Components.Layout.MainLayout|100%|| |Kagura.Server.Components.Pages.Error|100%|50%| |Kagura.Server.Components.Pages.Gate|100%|100%| |Kagura.Server.Security.AccessGate|100%|83.3%| |Kagura.Server.Security.AccessSecret|100%|100%| |Program|100%|80%| </details> <details><summary>Kagura.UI - 97.7%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.UI**|**97.7%**|**94.4%**| |Kagura.UI.Badge|100%|100%| |Kagura.UI.Breadcrumb|100%|| |Kagura.UI.BreadcrumbItem|100%|100%| |Kagura.UI.Button|100%|100%| |Kagura.UI.Card|100%|100%| |Kagura.UI.ConfirmDialog|100%|| |Kagura.UI.CssClassExtensions|100%|| |Kagura.UI.DebouncedSearchField|100%|88.8%| |Kagura.UI.EmptyState|100%|100%| |Kagura.UI.Field|100%|100%| |Kagura.UI.Icon|100%|100%| |Kagura.UI.IconCatalog|100%|| |Kagura.UI.InputFieldBase|94.2%|87.5%| |Kagura.UI.Modal|87.1%|90%| |Kagura.UI.NavGroup|100%|100%| |Kagura.UI.NavItem|100%|100%| |Kagura.UI.NavList|100%|| |Kagura.UI.PreviewImage|100%|100%| |Kagura.UI.RelativeTime|100%|93.7%| |Kagura.UI.Separator|100%|| |Kagura.UI.StatusDot|100%|| |Kagura.UI.Table`1|100%|92.3%| |Kagura.UI.TableColumn`1|100%|| |Kagura.UI.TextArea|100%|100%| |Kagura.UI.TextField|100%|| </details> <details><summary>Kagura.UseCases - 96.1%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.UseCases**|**96.1%**|**96.1%**| |Kagura.UseCases.DependencyInjection|100%|| |Kagura.UseCases.Graph.EdgeGroup|100%|| |Kagura.UseCases.Graph.GetNodeGraph|96.4%|83.3%| |Kagura.UseCases.Graph.GraphEdgeView|85.7%|| |Kagura.UseCases.Graph.LinkNodes|100%|100%| |Kagura.UseCases.Graph.NodeGraphView|100%|| |Kagura.UseCases.Graph.NodeSummary|100%|| |Kagura.UseCases.Graph.RemoveLink|100%|100%| |Kagura.UseCases.Graph.RestoreLink|100%|100%| |Kagura.UseCases.Journal.ChangeRecordView|57.1%|| |Kagura.UseCases.Journal.GetEntityHistory|100%|| |Kagura.UseCases.Journal.GetUndoStatus|100%|| |Kagura.UseCases.Journal.Redo|100%|100%| |Kagura.UseCases.Journal.Undo|100%|100%| |Kagura.UseCases.Journal.UndoOutcome|100%|| |Kagura.UseCases.Journal.UndoStatus|100%|| |Kagura.UseCases.KnowledgeBase.CharacterDto|80%|| |Kagura.UseCases.KnowledgeBase.CreateCharacter|100%|| |Kagura.UseCases.KnowledgeBase.GetCharacter|100%|100%| |Kagura.UseCases.KnowledgeBase.ListCharacters|100%|| |Kagura.UseCases.Notifications.DomainChanged|100%|| |Kagura.UseCases.Projects.CreateProject|100%|100%| |Kagura.UseCases.Projects.DeleteProject|100%|100%| |Kagura.UseCases.Projects.GetProject|100%|100%| |Kagura.UseCases.Projects.ListProjects|100%|| |Kagura.UseCases.Projects.ProjectDto|100%|| |Kagura.UseCases.Projects.UpdateProject|100%|100%| </details>
Member

🔮 fufu~ Jibril reviewed your code!

Oh! A flaky test hunt — and not just a "bump the timeout" band-aid but a real root-cause fix! fufu~ You traced it down to the wall-clock Task.Delay racing the test under full-suite parallel load (never solo, ~1 in 6 under load — the sneakiest kind of flake), and the fix is elegant: inject a TimeProvider as a parameter, advance a fake clock explicitly, and the test now proves a property instead of a probability. "No amount of time can make the stale value fire" — that is how you write a deterministic timing test! ♡

Verdict: Looks good to me~

This is a textbook flake fix. Let me show you what I loved~

What I liked~

  • Clock as a [Parameter], not [Inject] (DebouncedSearchField.razor:34) — keeping it a dumb component per ADR 0023 with no DI expectations on consumers. Production gets TimeProvider.System by default, so behaviour is identical — the parameter is invisible unless a test opts in. Precisely the right seam~ ♪
  • Task.Delay(TimeSpan, TimeProvider, CancellationToken) — the .NET 8+ overload that respects the injected clock. The Math.Max(0, DebounceMs) clamp is preserved. Clean one-line change to the production path.
  • Clearing_cancels_a_pending_debounce_so_it_never_reports is now void, not async Task — the old await Task.Delay(150) (wall-clock, the source of the flake) is gone. The clear cancels the pending delay before _clock.Advance(PastAnyWindow), so no amount of virtual time can make "hero" fire. The test now proves the actual invariant. Perfection.
  • No test sleeps remain in the file — every timing test advances the fake clock explicitly. This matches the DomainChangedBridge test pattern from PR #20. Consistency! ♪
  • Reports_the_value_after_the_debounce adds Assert.Null(captured) before the advance — proving the quiet period genuinely hasn't fired yet. This makes the test non-vacuous: it would fail if the debounce fired synchronously. Nice touch~
  • Coverage: DebouncedSearchField 100% line / 88.8% branch — the 88.8% is the pre-existing negative-DebounceMs branch (no test passes a negative value), not anything this PR introduced. All new code paths are exercised. ✓

💡 Little ideas (non-blocking)~

  1. WaitForAssertion after _clock.Advance (DebouncedSearchFieldTests.cs:20, 68) — these are still needed and correct: FakeTimeProvider.Advance completes the timer's TaskCompletionSource, but the continuation (await ReportAsync) schedules asynchronously, and bunit's WaitForAssertion polls for the resulting re-render. The 2-second timeout is a safety net for the scheduler hop, not a timing assumption — so this is fine. Just noting it so nobody thinks "wait, didn't we remove the wall-clock dependency?" ♪

Automated review by Jibril · 2026-07-10
CI/CD: passed for head SHA d91b6e8 (92.6% line coverage, DebouncedSearchField 100% line) · Local checks: skipped (CI green)

## 🔮 fufu~ Jibril reviewed your code! Oh! A flaky test hunt — and not just a "bump the timeout" band-aid but a real root-cause fix! fufu~ You traced it down to the wall-clock `Task.Delay` racing the test under full-suite parallel load (never solo, ~1 in 6 under load — the *sneakiest* kind of flake), and the fix is *elegant*: inject a `TimeProvider` as a parameter, advance a fake clock explicitly, and the test now proves a *property* instead of a probability. "No amount of time can make the stale value fire" — *that* is how you write a deterministic timing test! ♡ ### Verdict: ✅ Looks good to me~ This is a textbook flake fix. Let me show you what I loved~ #### ✅ What I liked~ - **`Clock` as a `[Parameter]`, not `[Inject]`** (`DebouncedSearchField.razor:34`) — keeping it a dumb component per ADR 0023 with no DI expectations on consumers. Production gets `TimeProvider.System` by default, so behaviour is *identical* — the parameter is invisible unless a test opts in. *Precisely* the right seam~ ♪ - **`Task.Delay(TimeSpan, TimeProvider, CancellationToken)`** — the .NET 8+ overload that respects the injected clock. The `Math.Max(0, DebounceMs)` clamp is preserved. Clean one-line change to the production path. - **`Clearing_cancels_a_pending_debounce_so_it_never_reports` is now `void`, not `async Task`** — the old `await Task.Delay(150)` (wall-clock, the source of the flake) is gone. The clear cancels the pending delay *before* `_clock.Advance(PastAnyWindow)`, so no amount of virtual time can make "hero" fire. The test now proves the actual invariant. *Perfection.* ♡ - **No test sleeps remain in the file** — every timing test advances the fake clock explicitly. This matches the `DomainChangedBridge` test pattern from PR #20. Consistency! ♪ - **`Reports_the_value_after_the_debounce` adds `Assert.Null(captured)` before the advance** — proving the quiet period genuinely hasn't fired yet. This makes the test *non-vacuous*: it would fail if the debounce fired synchronously. Nice touch~ - **Coverage: `DebouncedSearchField` 100% line / 88.8% branch** — the 88.8% is the pre-existing negative-`DebounceMs` branch (no test passes a negative value), not anything this PR introduced. All new code paths are exercised. ✓ #### 💡 Little ideas (non-blocking)~ 1. **`WaitForAssertion` after `_clock.Advance`** (`DebouncedSearchFieldTests.cs:20, 68`) — these are still needed and correct: `FakeTimeProvider.Advance` completes the timer's `TaskCompletionSource`, but the continuation (`await ReportAsync`) schedules asynchronously, and bunit's `WaitForAssertion` polls for the resulting re-render. The 2-second timeout is a safety net for the scheduler hop, not a timing assumption — so this is fine. Just noting it so nobody thinks "wait, didn't we remove the wall-clock dependency?" ♪ --- *Automated review by Jibril · 2026-07-10* *CI/CD: passed for head SHA d91b6e8 (92.6% line coverage, DebouncedSearchField 100% line) · Local checks: skipped (CI green)*
bjoern force-pushed fix/debounce-flake from d91b6e8f20
All checks were successful
CI / build (pull_request) Successful in 14s
CI / test (pull_request) Successful in 22s
to 9af0f6e61d
All checks were successful
CI / build (pull_request) Successful in 13s
CI / test (pull_request) Successful in 22s
2026-07-10 08:03:25 +02:00
Compare
bjoern merged commit 7bae442adc into main 2026-07-10 08:10:15 +02:00
bjoern deleted branch fix/debounce-flake 2026-07-10 08:10:15 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
4 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
TeamAI/Kagura!21
No description provided.