diff --git a/TESTING_GUIDE.md b/TESTING_GUIDE.md index 0b37e41be..dd715e8c9 100644 --- a/TESTING_GUIDE.md +++ b/TESTING_GUIDE.md @@ -396,7 +396,7 @@ Use `docs/test-doc-maintenance-plan.md` before moving oversized suites or adding blocks at the active update-extraction boundary, cancels through the watch sub-run token, and uses an unrelated stdout sentinel to prove the injected runner writer does not replace process-global stdout. Keep the explicit synchronization point and bounded wait; a fixed delay cannot prove that cancellation happened during active work. - The top-level issue-4591 watch cancellation fixtures use the ready callback to cover both an idle loop and a deterministically enqueued active update. They run the synchronous command runner on a dedicated worker and await asynchronous readiness signals so full-suite thread-pool pressure cannot starve startup. A bounded wait must distinguish an early watch exit or fault from a still-running readiness observation timeout, and cleanup must cancel and drain the worker before deleting its temporary project. The FTS/planner fixtures cancel at the existing maintenance hooks; keep the bulk-FTS assertion that synchronization triggers are restored and the marker is recoverable without an active PID owner. - `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. + 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` likewise writes the real ancestor ignore file and, only on macOS where runtime delivery can be silently delayed or dropped, invokes the production enqueue callback captured at readiness. Preserve the real watcher-delivery assertion on Linux and Windows (#4966). - `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. - 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. @@ -1332,7 +1332,7 @@ dotnet test --filter "FullyQualifiedName~GitHelperTests" update extraction の実行中境界で停止させ、watch sub-run token 経由でキャンセルし、無関係な stdout sentinel によって注入 writer が process-global stdout を置き換えないことを検証する。明示的な同期点と bounded wait を維持すること。固定 delay では active work 中のキャンセルを証明できない。 - issue-4591 の top-level watch cancellation fixture は ready callback を使い、idle loop と決定的に enqueue した active update の両方を検証する。同期 command runner は専用 worker で実行し、非同期 readiness signal を await することで、full-suite の thread-pool 負荷が startup を枯渇させないようにする。bounded wait は watch の早期終了または fault と、watch が実行中のまま readiness を観測できない timeout を区別し、cleanup は一時 project を削除する前に worker を cancel して drain しなければならない。FTS / planner fixture は既存の maintenance hook で cancel する。bulk FTS では同期 trigger が復元され、active PID owner を持たない marker から recovery できることの assertion を維持する。 - `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 境界を直接検証する。 + 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` も実際の祖先 ignore file を書き、runtime 配信で無通知の遅延・欠落が起こり得る macOS だけで、ready 時に取得した本番 enqueue callback を呼び出す。Linux と Windows では実際の watcher 配信 assertion を維持すること (#4966)。 - `IndexWatchRunnerTests.RunCore_StartupHandoff_ReconcilesMutationAndDrainsBeforeReady_Issue4594` subscribe 後の startup hook から index 済み symbol を変更して event を明示的に enqueue し、最終 database content と `watching` より前の `rescanned` / `updated` 順序を検証する。platform watcher の timing だけに依存すると handoff 回帰が非決定的になるため、注入した enqueue を維持すること。 - 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 のどちらも許容する。 diff --git a/changelog.d/unreleased/4966.internal.md b/changelog.d/unreleased/4966.internal.md new file mode 100644 index 000000000..44a799b8b --- /dev/null +++ b/changelog.d/unreleased/4966.internal.md @@ -0,0 +1,16 @@ +--- +category: internal +issues: + - 4966 +affected: + - tests/CodeIndex.Tests/IndexWatchRunnerTests.cs + - TESTING_GUIDE.md +--- + +## English + +- **Made ancestor-ignore watcher reconciliation coverage deterministic on macOS (#4966)** — The subproject watcher test now injects the production enqueue callback after writing the ancestor `.gitignore` only on macOS, where delivery proved unreliable under both .NET 8 and .NET 9. Linux and Windows retain real watcher-delivery coverage, and every configuration retains the bounded end-to-end database assertion. + +## 日本語 + +- **macOS で祖先 ignore watcher の reconciliation coverage を決定的にしました (#4966)** — subproject watcher test は .NET 8 と .NET 9 の両方で配信が不安定と確認された macOS だけで祖先 `.gitignore` の書き込み後に本番 enqueue callback を注入します。Linux と Windows では実際の watcher 配信 coverage を、全構成では上限付きの end-to-end database assertion を維持します。 diff --git a/tests/CodeIndex.Tests/IndexWatchRunnerTests.cs b/tests/CodeIndex.Tests/IndexWatchRunnerTests.cs index 616c0b13b..fcb1680ff 100644 --- a/tests/CodeIndex.Tests/IndexWatchRunnerTests.cs +++ b/tests/CodeIndex.Tests/IndexWatchRunnerTests.cs @@ -2016,6 +2016,7 @@ public void RunCore_SubprojectObservesAncestorIgnoreFileChanges_Issue4592() var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); using var cts = new CancellationTokenSource(); using var ready = new ManualResetEventSlim(); + Action? enqueue = null; Task? loopTask = null; try { @@ -2036,11 +2037,26 @@ public void RunCore_SubprojectObservesAncestorIgnoreFileChanges_Issue4592() Watch = true, WatchDebounceMs = 50, }; - IndexWatchRunner.WatchReadyForTesting = _ => ready.Set(); + IndexWatchRunner.WatchReadyForTesting = callback => + { + enqueue = callback; + ready.Set(); + }; loopTask = IndexWatchRunner.RunCoreAsync(options, _jsonOptions, projectRoot, dbPath, cts.Token); Assert.True(ready.Wait(TimeSpan.FromSeconds(15)), "The subproject watcher did not become ready."); - File.WriteAllText(Path.Combine(repoRoot, ".gitignore"), "subproj/ignored.py\n"); + var ignorePath = Path.Combine(repoRoot, ".gitignore"); + File.WriteAllText(ignorePath, "subproj/ignored.py\n"); + + if (OperatingSystem.IsMacOS()) + { + // macOS FileSystemWatcher delivery can be silently delayed or dropped. + // Exercise the production enqueue/reconcile path without making runtime delivery a test oracle. + // macOS の FileSystemWatcher 配信は無通知の遅延・欠落が起こり得るため、 + // runtime 配信を test oracle にせず、本番 enqueue / reconcile 経路を検証する。 + var enqueueCallback = Assert.IsType>(enqueue); + enqueueCallback(ignorePath); + } Assert.True( SpinWait.SpinUntil( () =>