diff --git a/TESTING_GUIDE.md b/TESTING_GUIDE.md index 7fee787a7..80437c8f4 100644 --- a/TESTING_GUIDE.md +++ b/TESTING_GUIDE.md @@ -415,7 +415,7 @@ Use `docs/test-doc-maintenance-plan.md` before moving oversized suites or adding - `IndexWatchRunnerTests.ClassifyWatchPath_ReconcilesInputsAndUsesSharedCdidxMembership_Issue4592`, `IndexCommandRunnerTests.RunStatusCheck_CdidxSidecarIsExcludedFromScanAndWorkspaceMembership_Issue4592`, and the issue-4592 update-mode fixtures keep scan, `status --check`, and watch on one `.cdidx` membership policy. Ignore/unignore changes and pattern-config add/edit/delete events must reach a debounced full scan, while pattern/plugin inputs and ordinary `.cdidx` sidecars remain outside indexed source rows. Pattern extension edits must purge the old extension and index the new one in the same process. The registry refresh test also removes a loaded workspace plugin and verifies that its collectible context is unloaded while a host-registered fallback survives. Test the classification boundary directly instead of waiting for platform-specific `FileSystemWatcher` delivery. `RunCore_SubprojectObservesAncestorIgnoreFileChanges_Issue4592` is the narrower integration exception: it must create the real ancestor `.gitignore` after readiness. On macOS, its .NET 8 target relies on bounded polling delivery for the exact ancestor ignore paths while project-tree delivery remains on FSEvents. The .NET 9 target retains FSEvents in the backend-selection matrix but explicitly forwards the real path through the readiness callback's production enqueue because host EventStream delivery is not deterministic under concurrent target execution. Linux and Windows retain the real watcher-delivery assertion (#4966). Keep the selection matrix and exact-path enumeration assertion so the workaround cannot expand into recursive project polling. - `IndexWatchRunnerTests.RunCore_StartupHandoff_ReconcilesMutationAndDrainsBeforeReady_Issue4594` - mutates an already indexed symbol through the post-subscription startup hook, explicitly enqueues that event, and verifies both final database content and `rescanned` / `updated` ordering before `watching`. Keep the injected enqueue: relying only on platform watcher timing would make the handoff regression nondeterministic. + mutates an already indexed symbol through the post-subscription startup hook, explicitly enqueues that event, and verifies the reconciled database content from the readiness callback. The transcript must place the startup `rescanned` event before `watching`; when the queued path still requires a startup `updated` event, that event must remain between them. The baseline rescan may already observe the mutation under load, in which case omitting the redundant `updated` event is equivalent. Keep the injected enqueue and readiness-state assertion: relying only on platform watcher timing or post-shutdown database state would make the handoff regression nondeterministic. - The issue-4858 watch startup fixtures include a top-level command test that counts the real full-scan write phase, plus focused tests that inject `IWatchBackend` implementations and explicit baseline/recovery delegates. They require asynchronous backend-start failure to switch from FSEvents to polling around exactly one baseline, replace FSEvents after a fatal error that arrives after readiness, ignore late callbacks from the disposed generation, collapse repeated event-loss callbacks into one recovery generation, stop before the baseline when both starts fail, and emit `stopped` when polling startup is canceled. The polling snapshot fixture requires indexer-equivalent pruning for ignored and internal trees, and human-output lifecycle tests use a fake backend instead of assuming the host FSEvents service is available. Keep the machine-readable `backend` / `recovery_reason` assertions and avoid wall-clock sleeps. The guarded macOS case uses the real backend selection and accepts either FSEvents or its operational polling fallback. - `BackgroundTaskObserverTests` relies on `BackgroundTaskObserver`'s fault-only continuation contract: canceled @@ -1372,7 +1372,7 @@ dotnet test --filter "FullyQualifiedName~GitHelperTests" - `IndexWatchRunnerTests.ClassifyWatchPath_ReconcilesInputsAndUsesSharedCdidxMembership_Issue4592`、`IndexCommandRunnerTests.RunStatusCheck_CdidxSidecarIsExcludedFromScanAndWorkspaceMembership_Issue4592`、issue-4592 の update-mode fixture scan、`status --check`、watch が単一の `.cdidx` membership policy を使うことを固定する。ignore / unignore 変更と pattern-config の追加 / 編集 / 削除は debounce 付き full scan に到達し、pattern / plugin 入力と通常の `.cdidx` sidecar は indexed source row から除外されたままでなければならない。pattern extension の編集では同一 process 内で旧 extension を purge して新 extension を index する。registry refresh test は読み込み済み workspace plugin を削除し、host 登録 fallback を維持しながら collectible context が unload されることも検証する。platform 固有の `FileSystemWatcher` 配信を待たず、classification 境界を直接検証する。`RunCore_SubprojectObservesAncestorIgnoreFileChanges_Issue4592` は限定的な integration 例外であり、ready 後に実際の祖先 `.gitignore` を作成する。macOS の .NET 8 target は project tree の FSEvents を維持しつつ、祖先 ignore path のみを対象にした bounded polling の実配信に依存させる。.NET 9 target は backend 選択 matrix で FSEvents を維持するが、host EventStream 配信は target 並行実行時に決定的ではないため、実 path を readiness callback の production enqueue へ明示転送する。Linux と Windows では実際の watcher 配信 assertion を維持すること (#4966)。workaround が project の再帰 polling へ拡大しないよう、選択 matrix と exact-path 列挙 assertion を維持する。 - `IndexWatchRunnerTests.RunCore_StartupHandoff_ReconcilesMutationAndDrainsBeforeReady_Issue4594` - subscribe 後の startup hook から index 済み symbol を変更して event を明示的に enqueue し、最終 database content と `watching` より前の `rescanned` / `updated` 順序を検証する。platform watcher の timing だけに依存すると handoff 回帰が非決定的になるため、注入した enqueue を維持すること。 + subscribe 後の startup hook から index 済み symbol を変更して event を明示的に enqueue し、readiness callback から reconciliation 済みの database content を検証する。transcript では startup `rescanned` event が `watching` より前でなければならず、queued path に startup `updated` event が引き続き必要な場合はその間に位置しなければならない。負荷下では baseline rescan が mutation を先に観測でき、その場合は冗長な `updated` event の省略を同等と扱う。platform watcher の timing や shutdown 後の database state だけに依存すると handoff 回帰が非決定的になるため、注入した enqueue と readiness 時点の assertion を維持すること。 - issue-4858 の watch startup fixture には、実際の full-scan write phase を数える top-level command test と、`IWatchBackend` 実装および明示的な baseline / recovery delegate を注入する focused test がある。非同期 backend 起動失敗が baseline 1 回の前後で FSEvents から polling へ切り替わること、ready 後に届く fatal error でも FSEvents を置換すること、dispose 済み generation から遅れて届く callback を無視すること、複数の event-loss callback が recovery generation 1 回へ集約されること、2 回とも起動に失敗した場合は baseline 前に停止すること、polling startup の cancellation でも `stopped` を出力することを固定する。polling snapshot fixture は ignored / internal tree を indexer と同じ規則で剪定することを要求し、human-output lifecycle test は host の FSEvents availability を仮定せず fake backend を使う。機械可読な `backend` / `recovery_reason` assertion を維持し、wall-clock sleep は使わないこと。macOS guard 付き case は実際の backend 選択を使い、FSEvents と実運用 polling fallback のどちらも許容する。 - `BackgroundTaskObserverTests` は `BackgroundTaskObserver` の fault-only continuation 契約に依存します。canceled diff --git a/changelog.d/unreleased/4939.fixed.md b/changelog.d/unreleased/4939.fixed.md new file mode 100644 index 000000000..53848f9c5 --- /dev/null +++ b/changelog.d/unreleased/4939.fixed.md @@ -0,0 +1,16 @@ +--- +category: fixed +issues: + - 4939 +affected: + - tests/CodeIndex.Tests/IndexWatchRunnerTests.cs + - TESTING_GUIDE.md +--- + +## English + +- **Stabilized the startup-handoff watch regression test under parallel suite load (#4939)** — the fixture now verifies reconciled symbols at the readiness boundary and accepts both a queued startup update and the equivalent case where the baseline rescan already observed the mutation. + +## 日本語 + +- **並列 full-suite 負荷下で startup-handoff watch 回帰テストを安定化しました (#4939)** — fixture は readiness 境界で reconciliation 済み symbol を検証し、startup update が queue 処理される経路と baseline rescan が mutation を先に観測した同等経路の両方を許容するようになりました。 diff --git a/tests/CodeIndex.Tests/IndexWatchRunnerTests.cs b/tests/CodeIndex.Tests/IndexWatchRunnerTests.cs index 1513cfae4..765f4b972 100644 --- a/tests/CodeIndex.Tests/IndexWatchRunnerTests.cs +++ b/tests/CodeIndex.Tests/IndexWatchRunnerTests.cs @@ -1835,6 +1835,8 @@ public void RunCore_StartupHandoff_ReconcilesMutationAndDrainsBeforeReady_Issue4 using var cts = new CancellationTokenSource(); var handoffInvoked = false; + var afterHandoffAtReady = false; + var beforeHandoffAtReady = true; string capturedOut; int exitCode; @@ -1846,6 +1848,12 @@ public void RunCore_StartupHandoff_ReconcilesMutationAndDrainsBeforeReady_Issue4 Console.SetOut(stdout); try { + IndexWatchRunner.WatchReadyForTesting = _ => + { + afterHandoffAtReady = HasIndexedSymbol(dbPath, "AfterHandoff"); + beforeHandoffAtReady = HasIndexedSymbol(dbPath, "BeforeHandoff"); + cts.Cancel(); + }; loopTask = IndexWatchRunner.RunCoreAsync( options, _jsonOptions, @@ -1858,7 +1866,6 @@ public void RunCore_StartupHandoff_ReconcilesMutationAndDrainsBeforeReady_Issue4 File.WriteAllText(sourcePath, "public sealed class AfterHandoff { }\n"); enqueue(sourcePath); }); - cts.Cancel(); #pragma warning disable xUnit1031 // Console redirection lock requires synchronous bounded drain. exitCode = loopTask.WaitAsync(TimeSpan.FromSeconds(30)).GetAwaiter().GetResult(); #pragma warning restore xUnit1031 @@ -1875,16 +1882,19 @@ public void RunCore_StartupHandoff_ReconcilesMutationAndDrainsBeforeReady_Issue4 Assert.Equal(CommandExitCodes.Success, exitCode); Assert.True(HasIndexedSymbol(dbPath, "AfterHandoff")); Assert.False(HasIndexedSymbol(dbPath, "BeforeHandoff")); + Assert.True(afterHandoffAtReady); + Assert.False(beforeHandoffAtReady); var startupRescan = capturedOut.IndexOf("\"status\":\"rescanned\",\"phase\":\"startup\"", StringComparison.Ordinal); var startupDrain = capturedOut.IndexOf("\"status\":\"updated\",\"phase\":\"startup\"", StringComparison.Ordinal); var ready = capturedOut.IndexOf("\"status\":\"watching\"", StringComparison.Ordinal); Assert.True(startupRescan >= 0, capturedOut); - Assert.True(startupDrain > startupRescan, capturedOut); - Assert.True(ready > startupDrain, capturedOut); + Assert.True(startupDrain < 0 || startupDrain > startupRescan, capturedOut); + Assert.True(ready > (startupDrain >= 0 ? startupDrain : startupRescan), capturedOut); } finally { + IndexWatchRunner.WatchReadyForTesting = null; DeleteDirectory(projectRoot); } }