diff --git a/TESTING_GUIDE.md b/TESTING_GUIDE.md index b33a0c5a1..d632d326f 100644 --- a/TESTING_GUIDE.md +++ b/TESTING_GUIDE.md @@ -499,6 +499,7 @@ Use `docs/test-doc-maintenance-plan.md` before moving oversized suites or adding LSP JSON-RPC behavior and indexed editor semantics. Reference parity coverage must compare LSP locations with the matching CLI candidate bundle, including overload identity and both `includeDeclaration` states. Keep document/workspace identifier ranges and explicit-versus-inferred inlay hints in one source-semantics fixture so persisted-column anchoring, source confirmation, and hint suppression cannot drift independently. Document-symbol hierarchy coverage keeps positional and body record members in one same-line fixture, asserts the complete root-to-member shape and deterministic sibling order, and retains the separate same-range top-level controls so order-independent parent resolution cannot over-nest unrelated symbols. Keep a same-line duplicate-container fixture with distinct positional members so selection-column disambiguation cannot move an earlier member beneath a later same-named container. Symbol-progress coverage crosses the production chunk item limit with one sentinel, verifies the complete deterministic sequence across partial-result frames, and keeps live work-done delivery, truncation, typed request-ID cancellation, full-queue cancellation, queue-pressure notification preservation, backpressured `Server busy` response retention, emitted-count cancellation reporting, output-failure propagation, and invalid-token variants in the same protocol fixture. Assert both the item and UTF-8 JSON body budgets for every emitted chunk. + The server-busy backpressure cancellation fixture must stage the cancel frame until both the active symbol request and blocked response write have been observed. Block the request on its cancellation token instead of matching fixed delays so suite load cannot convert the expected cancellation into a successful response. The suite runs in parallel with other test classes: every process-global CodeIndex telemetry listener fixture, including LSP, MCP, and DB coverage, must capture stopped activities in a thread-safe collection and filter by the request trace, while fixtures that seed process-global `PathCasing` state must hold `PathCasingTestLock.Gate` for their full lifetime. Keep `PathCasing.IgnoreCaseProbeForTesting` execution-context-local, and never write an injected cache-miss result into the shared path-casing cache. Existing production-seeded cache entries still take precedence; injected probe outcomes must flow to child tasks owned by that test without leaking into concurrently running LSP or other test classes through either the delegate or cached values. - `McpServer*Tests.cs` @@ -1340,6 +1341,7 @@ dotnet test --filter "FullyQualifiedName~GitHelperTests" LSP の JSON-RPC 挙動と indexed editor semantics のテスト。reference parity coverage では、overload identity と `includeDeclaration` の両状態を含め、LSP location と対応する CLI candidate bundle を比較してください。document/workspace の identifier range と explicit/inferred inlay hint は 1 つの source-semantics fixture にまとめ、保存済み column の anchoring、source 上の確認、hint 抑制が別々に drift しないようにします。 document-symbol hierarchy coverage は positional member と body member を同一行の record fixture にまとめ、root から member までの完全な形状と決定的な sibling 順序を assert します。順序非依存の親解決が無関係な symbol を過剰に nest しないよう、same-range top-level の control は独立したまま維持してください。selection column による曖昧性解消で前の member が行内で後にある同名 container の配下へ移動しないよう、異なる positional member を持つ同一行の duplicate-container fixture も維持してください。 symbol-progress coverage は production の chunk item limit を sentinel 1 件だけで超え、partial-result frame 全体の決定的な完全 sequence を検証します。live work-done delivery、truncation、型付き request ID の cancellation、full-queue cancellation、queue pressure 下の notification 保持、backpressure された `Server busy` response の保持、cancellation 時の送信済み件数、output-failure propagation、invalid-token variant は同じ protocol fixture にまとめ、送信した全 chunk の item 上限と UTF-8 JSON body budget の両方を assert してください。 + server-busy backpressure cancellation fixture では、active symbol request と block された response write の両方を観測するまで cancel frame を stage してください。固定 delay の一致に依存せず cancellation token 上で request を block することで、suite load により期待する cancellation が成功 response に変わる競合を防ぎます。 suite は他の test class と並列実行されます。LSP、MCP、DB coverage を含む process-global な CodeIndex telemetry listener fixture はすべて、停止した activity を thread-safe collection に記録して request trace で絞り込み、process-global な `PathCasing` state を seed する fixture は全 lifetime にわたって `PathCasingTestLock.Gate` を保持してください。 `PathCasing.IgnoreCaseProbeForTesting` は execution context ごとに隔離し、注入した cache-miss 結果を共有 path-casing cache へ書き込まないでください。既存の production seed 済み cache entry は引き続き優先します。注入した probe 結果はその test が所有する child task には引き継ぎますが、delegate と cache のどちらからも並列実行中の LSP や他の test class へ漏らしてはいけません。 - `McpServer*Tests.cs` diff --git a/changelog.d/unreleased/4785.internal.md b/changelog.d/unreleased/4785.internal.md new file mode 100644 index 000000000..63751430e --- /dev/null +++ b/changelog.d/unreleased/4785.internal.md @@ -0,0 +1,16 @@ +--- +category: internal +issues: + - 4785 +affected: + - tests/CodeIndex.Tests/LspServerTests.cs + - TESTING_GUIDE.md +--- + +## English + +- **Made LSP server-busy cancellation coverage deterministic (#4785)** — the backpressure test now holds the active symbol request at an explicit synchronization boundary and stages the cancellation frame until both the request and blocked response write are observed, removing its fixed-delay race under full-suite load. + +## 日本語 + +- **LSP の server-busy cancellation coverage を決定的にしました (#4785)** — backpressure test は active symbol request を明示的な同期境界で保持し、request と block された response write の両方を観測するまで cancellation frame を stage するようになり、full-suite load 下の固定 delay 競合を解消しました。 diff --git a/tests/CodeIndex.Tests/LspServerTests.cs b/tests/CodeIndex.Tests/LspServerTests.cs index 1f7b4a3ec..212d9321d 100644 --- a/tests/CodeIndex.Tests/LspServerTests.cs +++ b/tests/CodeIndex.Tests/LspServerTests.cs @@ -1493,6 +1493,8 @@ public async Task RunAsync_QueuePressurePreservesDocumentSyncNotifications_Issue public async Task RunAsync_ServerBusyBackpressureRetainsEveryRejectedResponse_Issue4721() { var projectRoot = TestProjectHelper.CreateTempProject("cdidx_lsp_busy_response_backpressure"); + using var requestEntered = new ManualResetEventSlim(); + using var requestRelease = new ManualResetEventSlim(); try { var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); @@ -1501,8 +1503,8 @@ public async Task RunAsync_ServerBusyBackpressureRetainsEveryRejectedResponse_Is { BeforeSymbolRequestForTesting = cancellationToken => { - cancellationToken.WaitHandle.WaitOne(TimeSpan.FromSeconds(5)); - cancellationToken.ThrowIfCancellationRequested(); + requestEntered.Set(); + requestRelease.Wait(cancellationToken); }, }; var frames = new StringBuilder(); @@ -1520,16 +1522,21 @@ public async Task RunAsync_ServerBusyBackpressureRetainsEveryRejectedResponse_Is @params = new { }, }))); } - frames.Append(Frame( + const string cancel = """ {"jsonrpc":"2.0","method":"$/cancelRequest","params":{"id":"active-backpressure-4721"}} - """)); - using var input = new MemoryStream(Encoding.UTF8.GetBytes(frames.ToString())); + """; + using var input = new StagedReadStream( + Encoding.UTF8.GetBytes(frames.ToString()), + Encoding.UTF8.GetBytes(Frame(cancel))); using var output = new FirstWriteGateMemoryStream(); var runTask = server.RunAsync(input, output); + Assert.True(requestEntered.Wait(TimeSpan.FromSeconds(5))); await output.WaitForBlockedWriteAsync().WaitAsync(TimeSpan.FromSeconds(5)); + Assert.False(runTask.IsCompleted); + input.ReleaseSuffix(); output.ReleaseWrites(); Assert.Equal(CommandExitCodes.Success, await runTask.WaitAsync(TimeSpan.FromSeconds(5))); @@ -1551,6 +1558,7 @@ public async Task RunAsync_ServerBusyBackpressureRetainsEveryRejectedResponse_Is } finally { + requestRelease.Set(); TestProjectHelper.DeleteDirectory(projectRoot); } }