diff --git a/docs/reviews/2026-08-18-r3-test-truth-ledger.md b/docs/reviews/2026-08-18-r3-test-truth-ledger.md index 8394e952..776eb20b 100644 --- a/docs/reviews/2026-08-18-r3-test-truth-ledger.md +++ b/docs/reviews/2026-08-18-r3-test-truth-ledger.md @@ -50,6 +50,7 @@ inventory refreshed for Batch G reports: | Mechanical output-only candidates | 2 | | Reviewed diagnostic methods | 51 | | Methods directly using `Thread.Sleep` / `Task.Delay` | 15 / 23 | +| Cancellable-infinite-only / elapsed-time methods | 5 / 33 | | Methods directly reading environment variables | 47 | | Methods directly reading `.cs` source text | 63 | @@ -138,6 +139,7 @@ identifiable in the lane report. | T-016 historical test taxonomy | open | Rename/re-home only after each test's durable owner and oracle are established. | | T-017 Avalonia ownership | reopened and closed in batch D | The full gate exposed the same compositor ownership class between six newer `MainWindowViewTests` facts. Their six named assertion phases now run in one owned Avalonia application session; 25 fresh-process stress iterations and the complete gate pass. | | T-018 stderr observer race | resolved in R2 | Live reader shares writes/deletes; 25 focused repetitions plus the complete gate. | +| T-019 remaining explicit waits | classified in batch H; nine fixed-delay negative oracles need cleanup | Five of the 38 methods are deterministic cancellation stubs, not wall-clock tests. Preserve four intentional real-time contracts and bounded integration polling; replace the nine tests that infer “still blocked” from a fixed delay with observable synchronization state. | ## Preserved rationale for removals in batch A @@ -423,3 +425,34 @@ The fixture-generation task was intentionally not executed: its documented purpose is to rewrite fixture files, while this R3 batch is review/test-only. The no-retry complete hermetic Release gate passed 14,391/14,391 with zero skips or failures across all 12 test assemblies. + +## Batch H explicit-wait classification + +The syntax inventory now records every direct `Thread.Sleep` and `Task.Delay` +invocation instead of treating both method-level booleans as equivalent. The +38 attributed methods divide into six materially different groups: + +| Wait purpose | Methods | R3 disposition | +|---|---:|---| +| Cancellable infinite suspension in a fake | 5 | Keep. `Task.Delay(Timeout.InfiniteTimeSpan, token)` advances only through the cancellation being tested and consumes no elapsed-time oracle. | +| Intentional real-time protocol/timeout contract | 4 | Keep with explicit bounds: two Live handshake race delays, the one-second net-probe cadence, and continuous unrelated shutdown drain. | +| Cooperative yield while virtual-clock/background transport work drains | 8 | Retain for now; prefer an observable receiver/worker signal when that seam exists. The virtual behavioral oracle does not derive from the sleep duration. | +| Bounded completion/readiness polling | 11 | Retain as integration polling with a terminal assertion and deadline; improve opportunistically with events, not by busy-spinning. | +| Fixed delay used to prove another operation is still blocked | 9 | Cleanup priority. Replace elapsed-time absence-of-completion with an observed wait state or test seam. | +| Positive completion timeout guard | 1 | Keep. The two-second `WhenAny` in `RuntimeCharacterStateTests` fails only if the operation does not complete; it does not delay a passing run. | + +The nine fixed-delay negative oracles are: + +1. `LiveSessionCommandRouterTests.ConcurrentDispose_WaitsForInFlightTransportThenMakesRouterInert`; +2. `HostQuiescenceGateTests.ExternalStopWaitsForAdmittedCallbackToReturn`; +3. both `SilkWindowCallbackBindingTests.ConcurrentDispose...` contracts; +4. `LandblockStreamerPoolTests.Dispose_JoinsEveryWorkerInThePool`; +5. `LandblockStreamerTests.DisposeAndConcurrentDisposeWaitForInFlightLoad`; +6. `HeadlessPluginSessionTests.LateSubscriberReplayQueuesConcurrentRegistrationExactlyOnceInOrder`; +7. `LauncherInstallerTests.IndependentInstallersSerializeAndWaitingCancellationTouchesNothing`; and +8. `LauncherInstallerTests.OrphanBakeCanNeverPublishAfterRestartRecovery`. + +This classification is review evidence, not a claim that the remaining waits +are flaky. The inventory run at `6faeb4a1` parsed all 1,256 tracked C# test +files and reproduced 15 sleep methods, 23 delay methods, five cancellation- +only methods, and 33 methods with some elapsed-time wait. diff --git a/tools/audit-test-inventory.ps1 b/tools/audit-test-inventory.ps1 index 61648097..fcfd5795 100644 --- a/tools/audit-test-inventory.ps1 +++ b/tools/audit-test-inventory.ps1 @@ -189,6 +189,41 @@ function Test-DirectOutputSignal { return $false } +function Get-WaitSites { + param( + [Parameter(Mandatory)]$Tree, + [Parameter(Mandatory)]$Method + ) + + $sites = [Collections.Generic.List[object]]::new() + foreach ($invocation in @($Method.DescendantNodes() | Where-Object { + $_.GetType().Name -eq 'InvocationExpressionSyntax' + })) { + $expression = $invocation.Expression.ToString() + $kind = if ($expression -match '(^|\.)Thread\.Sleep$') { + 'ThreadSleep' + } elseif ($expression -match '(^|\.)Task\.Delay$') { + 'TaskDelay' + } else { + $null + } + if ($null -eq $kind) { + continue + } + + $text = [regex]::Replace($invocation.ToString(), '\s+', ' ').Trim() + $sites.Add([ordered]@{ + Line = Get-NodeLine $Tree $invocation + Kind = $kind + Invocation = $text + IsCancellableInfiniteDelay = $kind -eq 'TaskDelay' -and + $text -match 'Timeout\.InfiniteTimeSpan' -and + $invocation.ArgumentList.Arguments.Count -ge 2 + }) + } + return @($sites) +} + function Test-RecursiveSignal { param( [Parameter(Mandatory)][string]$MethodKey, @@ -352,6 +387,7 @@ foreach ($relativePath in $trackedFiles) { } $bodyText = $method.ToString() + $waitSites = @(Get-WaitSites $tree $method) $environmentVariables = @([regex]::Matches( $bodyText, 'GetEnvironmentVariable\s*\(\s*"([A-Za-z0-9_]+)"') | @@ -376,8 +412,17 @@ foreach ($relativePath in $trackedFiles) { HasOutputSignal = $hasOutputSignal OutputOnlyCandidate = $hasOutputSignal -and -not $hasFailureSignal EnvironmentVariables = $environmentVariables - HasThreadSleep = $bodyText -match '\bThread\.Sleep\s*\(' - HasTaskDelay = $bodyText -match '\bTask\.Delay\s*\(' + WaitSites = $waitSites + HasThreadSleep = @($waitSites | Where-Object { + $_.Kind -eq 'ThreadSleep' + }).Count -gt 0 + HasTaskDelay = @($waitSites | Where-Object { + $_.Kind -eq 'TaskDelay' + }).Count -gt 0 + HasOnlyCancellableInfiniteDelay = $waitSites.Count -gt 0 -and + @($waitSites | Where-Object { + -not $_.IsCancellableInfiniteDelay + }).Count -eq 0 ReadsSourceText = $bodyText -match '(ReadAllText|ReadAllLines)\s*\(' -and $bodyText -match '\.cs' }) @@ -453,6 +498,12 @@ $summary = [ordered]@{ } ThreadSleepMethods = @($orderedRecords | Where-Object { $_.HasThreadSleep }).Count TaskDelayMethods = @($orderedRecords | Where-Object { $_.HasTaskDelay }).Count + CancellableInfiniteDelayOnlyMethods = @($orderedRecords | Where-Object { + $_.HasOnlyCancellableInfiniteDelay + }).Count + FixedWallClockWaitMethods = @($orderedRecords | Where-Object { + $_.WaitSites.Count -gt 0 -and -not $_.HasOnlyCancellableInfiniteDelay + }).Count DirectEnvironmentVariableMethods = @($orderedRecords | Where-Object { $_.EnvironmentVariables.Count -gt 0 }).Count