From 3fe69f7a97369d6c472cb382966abcd2782be7f1 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sat, 1 Aug 2026 17:07:00 +0900 Subject: [PATCH 1/6] Make batch timeout assertions actionable (#4935) --- TESTING_GUIDE.md | 4 ++-- changelog.d/unreleased/4935.internal.md | 16 ++++++++++++++++ tests/CodeIndex.Tests/McpServerTests.cs | 18 ++++++++++++++---- 3 files changed, 32 insertions(+), 6 deletions(-) create mode 100644 changelog.d/unreleased/4935.internal.md diff --git a/TESTING_GUIDE.md b/TESTING_GUIDE.md index b10b91aff..62a7bbb22 100644 --- a/TESTING_GUIDE.md +++ b/TESTING_GUIDE.md @@ -579,7 +579,7 @@ Use `docs/test-doc-maintenance-plan.md` before moving oversized suites or adding Keep failed-then-success initialize isolation in the protocol/session suite: assert caller, client info, roots, capabilities, initialization lifecycle, and session ID after negotiation or success-response serialization failure and again after the corrected handshake, so failed metadata cannot poison the accepted session (#4540). Signal-gated duplicate-initialize coverage must hold a concurrent status reader on its captured snapshot and an in-flight `roots/list` response from the accepted handshake, proving readers see one complete state and a rejected duplicate cannot replace caller metadata or roots. Stdio response-order tests use the same signal-gated pattern: make the synthetic transport signal the parse-error path instead of sleeping in the response serializer. Stdio request-concurrency tests also use signal gates: prove two requests overlap at `maxConcurrency: 2`, a third waits, and a cancellation frame still bypasses a saturated execution gate. Cover the separate accepted-frame cap, retry-safe overflow, more than 64 registered queued cancellations, and timeout leases that remain held until cancellation-insensitive actions drain. Run a non-concurrent base `IMcpTransport` at `maxConcurrency: 1` to prove frame handling does not double-acquire the execution gate. Initialization-order tests must hold the initialize dispatch explicitly and verify later requests wait for its protocol barrier. - JSON-RPC batch-concurrency tests use ID-aware signal gates: prove `maxConcurrency: 1` cannot deadlock on an outer frame slot, prove the global peak and stable input-order response independently of completion order, and hold initialize, duplicate initialize, duplicate-ID, cancellation targets, and timed-out live actions explicitly to verify their fence/isolation/lease semantics. A duplicate initialize must retain the accepted generation while preserving the adjacent-request fence. Fill and age the general cancellation tombstone cache to prove a queued batch target remains cancellable, then prove a pre-dispatch return releases its queued registration before ID reuse. Do not use wall-clock sleeps. + JSON-RPC batch-concurrency tests use ID-aware signal gates: prove `maxConcurrency: 1` cannot deadlock on an outer frame slot, prove the global peak and stable input-order response independently of completion order, and hold initialize, duplicate initialize, duplicate-ID, cancellation targets, and timed-out live actions explicitly to verify their fence/isolation/lease semantics. A duplicate initialize must retain the accepted generation while preserving the adjacent-request fence. Fill and age the general cancellation tombstone cache to prove a queued batch target remains cancellable, then prove a pre-dispatch return releases its queued registration before ID reuse. Do not use wall-clock sleeps. Parse returned batch frames through typed string, array, and per-item object assertions before reading members so a missing or malformed response fails with actionable shape diagnostics instead of a null dereference (#4935). Transport-teardown tests gate a deliberately cancellation-insensitive request, inject EOF, invalid UTF-8, or an oversized line, and set both drain windows to zero. They must prove malformed-input errors are attempted before shutdown and that every path returns with diagnostics for the actual unfinished request, terminal write, or shutdown callback. When an assertion pins an unfinished-request count, keep every counted request behind its own cancellation-insensitive gate until stderr capture and assertions finish; do not include a cancellation-responsive task whose completion depends on callback scheduling. Cover shutdown beginning after the initial EOF snapshot, base-transport callback and completion drains, external cancellation during an inline control write, the initiating HTTP shutdown POST completing with `204`, and stdio output disposal deferred behind a late writer even when input disposal fails. Include token-ignoring writers, write-gate contention, blocking callbacks, and throwing callbacks; use signal gates rather than wall-clock sleeps. Rate-limit-disabled coverage uses the lightweight `languages` tool for repeated successful calls; do not pay repeated `status` database aggregation cost when the assertion only concerns limiter bypass. `RateLimiterTests.cs` uses an injected deterministic clock for bucket-cap saturation and recovery. Assert that bucket count never exceeds the configured cap, a single-partition cap rejection reports the earliest idle expiry, and advancing exactly to the advertised retry boundary allows a legitimate new bucket. Layered coverage must also use burst 1 with a coarse refill slower than the earliest secondary-cap expiry, proving that a charged coarse token is included in the combined retry boundary. MCP integration coverage proves that one caller-wide coarse quota spans canonical names while known tools retain secondary per-tool partitions, drives missing/non-string/empty/oversized/case-variant/unknown names plus invalid arguments through pre-validation, and verifies that unknown `batch_query` slot names share one fixed bounded partition. @@ -1528,7 +1528,7 @@ dotnet test --filter "FullyQualifiedName~GitHelperTests" failed-then-success initialize の分離は protocol/session suite に維持し、交渉失敗または success response の serialization 失敗直後と、修正した handshake 後の caller、client info、roots、capabilities、initialization lifecycle、session ID を検証して、失敗 metadata が受理済み session を汚染できないようにします(#4540)。signal-gated な duplicate-initialize coverage では、並行 status reader を取得済み snapshot で保持し、受理済み handshake の進行中 `roots/list` response を保持することで、reader が完全な 1 state だけを見ることと、拒否された重複 initialize が caller metadata や roots を置き換えられないことも検証します。 stdio response-order test も同じ signal-gated pattern を使い、response serializer で sleep する代わりに synthetic transport が parse-error path を signal するようにします。 stdio request-concurrency test も signal gate を使い、`maxConcurrency: 2` で 2 request が実際に overlap し、3 件目が待ち、実行 gate 飽和中も cancellation frame が bypass することを検証します。別枠の accepted-frame 上限、retry-safe な overflow、64 件を超える登録済み queued cancellation、cancellation を無視する action が drain するまで保持される timeout lease も検証してください。non-concurrent な base `IMcpTransport` を `maxConcurrency: 1` で実行し、frame handling が execution gate を二重取得しないことも確認します。initialization-order test は initialize dispatch を明示的に保持し、後続 request が protocol barrier を待つことを確認してください。 - JSON-RPC batch-concurrency test は ID-aware signal gate を使います。`maxConcurrency: 1` で outer frame slot による deadlock が起きないこと、完了順と独立した global peak と入力順 response、initialize・重複 initialize・重複 ID・cancellation target・timeout 後も動く action の fence/isolation/lease semantics を、各処理を明示的に保持して検証してください。重複 initialize は受理済み generation を保持しながら、隣接 request の fence を維持する必要があります。一般 cancellation tombstone cache を満杯にして TTL も経過させ、queue 待ち batch target が引き続き cancellation を受けることと、dispatch 前の return が queued registration を解放して ID を再利用できることも検証します。wall-clock sleep は使わないでください。 + JSON-RPC batch-concurrency test は ID-aware signal gate を使います。`maxConcurrency: 1` で outer frame slot による deadlock が起きないこと、完了順と独立した global peak と入力順 response、initialize・重複 initialize・重複 ID・cancellation target・timeout 後も動く action の fence/isolation/lease semantics を、各処理を明示的に保持して検証してください。重複 initialize は受理済み generation を保持しながら、隣接 request の fence を維持する必要があります。一般 cancellation tombstone cache を満杯にして TTL も経過させ、queue 待ち batch target が引き続き cancellation を受けることと、dispatch 前の return が queued registration を解放して ID を再利用できることも検証します。wall-clock sleep は使わないでください。返された batch frame は member を読む前に、型付きの string、array、各 item の object assertion を通し、response の欠落や malformed shape が null 参照ではなく対応可能な shape diagnostic として失敗するようにしてください(#4935)。 transport teardown test は cancellation を意図的に無視する request を signal gate で保持し、EOF、不正 UTF-8、oversized line を注入して両 drain window を 0 にします。不正入力 error を shutdown 前に試行し、実際に未完了な request、terminal write、shutdown callback ごとの diagnostic を残して全経路が戻ることを検証してください。未完了 request 数を固定する assertion では、stderr の capture と assertion が終わるまで、数に含める全 request をそれぞれ cancellation-insensitive な gate の後ろで保持してください。callback の scheduling によって完了時点が変わる cancellation-responsive な task をその件数に含めてはいけません。初回 EOF snapshot 後に始まる shutdown、base transport の callback/completion drain、inline control write 中の external cancellation、shutdown 起点 HTTP POST の `204` completion、input dispose 失敗時を含む late writer 後まで defer される stdio output dispose を含めます。token を無視する writer、write-gate contention、停止 callback、例外 callback を使い、wall-clock sleep ではなく signal gate で検証してください。 rate-limit-disabled coverage の繰り返し成功 call には軽量な `languages` tool を使い、limiter bypass だけの assertion で `status` の database aggregation cost を繰り返し支払わないでください。 `RateLimiterTests.cs` は bucket 上限の飽和と回復に注入した決定論的 clock を使います。bucket 数が設定上限を超えないこと、単一 partition の上限拒否が最も早い idle expiry を返すこと、通知された retry 境界まで正確に進めると正規の新規 bucket を作成できることを検証してください。layered coverage では burst 1 と、最短 secondary-cap expiry より遅い coarse refill を使い、消費済み coarse token が結合 retry 境界に含まれることも証明してください。MCP integration coverage では 1 つの caller-wide coarse quota が canonical 名をまたぐ一方で既知 tool が secondary per-tool partition を維持することを証明し、missing/non-string/empty/oversized/case-variant/unknown 名と invalid argument を pre-validation に通し、unknown な `batch_query` slot 名が 1 つの固定 bounded partition を共有することを検証してください。 diff --git a/changelog.d/unreleased/4935.internal.md b/changelog.d/unreleased/4935.internal.md new file mode 100644 index 000000000..7aceee5d5 --- /dev/null +++ b/changelog.d/unreleased/4935.internal.md @@ -0,0 +1,16 @@ +--- +category: internal +issues: + - 4935 +affected: + - tests/CodeIndex.Tests/McpServerTests.cs + - TESTING_GUIDE.md +--- + +## English + +- **MCP batch-timeout test failures now report the response shape (#4935)** — the concurrency-lease regression test validates the returned frame, batch array, and item objects before reading timeout and success members, replacing load-sensitive null dereferences with actionable assertion failures. + +## 日本語 + +- **MCP batch timeout テストが response shape を報告するようになりました(#4935)** — concurrency lease の回帰テストは timeout と success の member を読む前に、返された frame、batch array、item object を検証し、負荷時の null 参照を対応可能な assertion failure に置き換えます。 diff --git a/tests/CodeIndex.Tests/McpServerTests.cs b/tests/CodeIndex.Tests/McpServerTests.cs index 314e45173..566d5c87a 100644 --- a/tests/CodeIndex.Tests/McpServerTests.cs +++ b/tests/CodeIndex.Tests/McpServerTests.cs @@ -7522,10 +7522,20 @@ public async Task ProcessFrameAsync_BatchTimedOutActionRetainsConcurrencyLeaseUn releaseFirst.TrySetResult(); await secondStarted.Task.WaitAsync(TestDeterminism.DefaultTimeout); - var responseText = await batchResponseTask.WaitAsync(TestDeterminism.DefaultTimeout); - var responses = JsonNode.Parse(responseText!)!.AsArray(); - Assert.Equal("Request timed out", responses[0]!["error"]!["message"]!.GetValue()); - Assert.Equal("ok", responses[1]!["result"]!["status"]!.GetValue()); + var responseText = Assert.IsType( + await batchResponseTask.WaitAsync(TestDeterminism.DefaultTimeout)); + var responses = Assert.IsType(JsonNode.Parse(responseText)); + Assert.Equal(2, responses.Count); + var firstResponse = Assert.IsType(responses[0]); + var firstError = Assert.IsType(firstResponse["error"]); + Assert.Equal( + "Request timed out", + Assert.IsAssignableFrom(firstError["message"]).GetValue()); + var secondResponse = Assert.IsType(responses[1]); + var secondResult = Assert.IsType(secondResponse["result"]); + Assert.Equal( + "ok", + Assert.IsAssignableFrom(secondResult["status"]).GetValue()); Assert.Equal(1, server.AvailableConcurrencySlotsForTests); } From 23a7f349448b891f003d4fab9b0f7525c56cf023 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sat, 1 Aug 2026 17:27:45 +0900 Subject: [PATCH 2/6] Synchronize the batch timeout lease test (#4947) --- TESTING_GUIDE.md | 4 ++-- changelog.d/unreleased/4947.internal.md | 16 ++++++++++++++++ tests/CodeIndex.Tests/McpServerTests.cs | 5 ++++- 3 files changed, 22 insertions(+), 3 deletions(-) create mode 100644 changelog.d/unreleased/4947.internal.md diff --git a/TESTING_GUIDE.md b/TESTING_GUIDE.md index 62a7bbb22..c016293bc 100644 --- a/TESTING_GUIDE.md +++ b/TESTING_GUIDE.md @@ -579,7 +579,7 @@ Use `docs/test-doc-maintenance-plan.md` before moving oversized suites or adding Keep failed-then-success initialize isolation in the protocol/session suite: assert caller, client info, roots, capabilities, initialization lifecycle, and session ID after negotiation or success-response serialization failure and again after the corrected handshake, so failed metadata cannot poison the accepted session (#4540). Signal-gated duplicate-initialize coverage must hold a concurrent status reader on its captured snapshot and an in-flight `roots/list` response from the accepted handshake, proving readers see one complete state and a rejected duplicate cannot replace caller metadata or roots. Stdio response-order tests use the same signal-gated pattern: make the synthetic transport signal the parse-error path instead of sleeping in the response serializer. Stdio request-concurrency tests also use signal gates: prove two requests overlap at `maxConcurrency: 2`, a third waits, and a cancellation frame still bypasses a saturated execution gate. Cover the separate accepted-frame cap, retry-safe overflow, more than 64 registered queued cancellations, and timeout leases that remain held until cancellation-insensitive actions drain. Run a non-concurrent base `IMcpTransport` at `maxConcurrency: 1` to prove frame handling does not double-acquire the execution gate. Initialization-order tests must hold the initialize dispatch explicitly and verify later requests wait for its protocol barrier. - JSON-RPC batch-concurrency tests use ID-aware signal gates: prove `maxConcurrency: 1` cannot deadlock on an outer frame slot, prove the global peak and stable input-order response independently of completion order, and hold initialize, duplicate initialize, duplicate-ID, cancellation targets, and timed-out live actions explicitly to verify their fence/isolation/lease semantics. A duplicate initialize must retain the accepted generation while preserving the adjacent-request fence. Fill and age the general cancellation tombstone cache to prove a queued batch target remains cancellable, then prove a pre-dispatch return releases its queued registration before ID reuse. Do not use wall-clock sleeps. Parse returned batch frames through typed string, array, and per-item object assertions before reading members so a missing or malformed response fails with actionable shape diagnostics instead of a null dereference (#4935). + JSON-RPC batch-concurrency tests use ID-aware signal gates: prove `maxConcurrency: 1` cannot deadlock on an outer frame slot, prove the global peak and stable input-order response independently of completion order, and hold initialize, duplicate initialize, duplicate-ID, cancellation targets, and timed-out live actions explicitly to verify their fence/isolation/lease semantics. A duplicate initialize must retain the accepted generation while preserving the adjacent-request fence. Fill and age the general cancellation tombstone cache to prove a queued batch target remains cancellable, then prove a pre-dispatch return releases its queued registration before ID reuse. Do not use wall-clock sleeps. Parse returned batch frames through typed string, array, and per-item object assertions before reading members so a missing or malformed response fails with actionable shape diagnostics instead of a null dereference (#4935). Timeout-lease fixtures must observe the blocked action's cancellation token before releasing its gate, so parallel-suite scheduling cannot turn the expected timeout into a successful response (#4947). Transport-teardown tests gate a deliberately cancellation-insensitive request, inject EOF, invalid UTF-8, or an oversized line, and set both drain windows to zero. They must prove malformed-input errors are attempted before shutdown and that every path returns with diagnostics for the actual unfinished request, terminal write, or shutdown callback. When an assertion pins an unfinished-request count, keep every counted request behind its own cancellation-insensitive gate until stderr capture and assertions finish; do not include a cancellation-responsive task whose completion depends on callback scheduling. Cover shutdown beginning after the initial EOF snapshot, base-transport callback and completion drains, external cancellation during an inline control write, the initiating HTTP shutdown POST completing with `204`, and stdio output disposal deferred behind a late writer even when input disposal fails. Include token-ignoring writers, write-gate contention, blocking callbacks, and throwing callbacks; use signal gates rather than wall-clock sleeps. Rate-limit-disabled coverage uses the lightweight `languages` tool for repeated successful calls; do not pay repeated `status` database aggregation cost when the assertion only concerns limiter bypass. `RateLimiterTests.cs` uses an injected deterministic clock for bucket-cap saturation and recovery. Assert that bucket count never exceeds the configured cap, a single-partition cap rejection reports the earliest idle expiry, and advancing exactly to the advertised retry boundary allows a legitimate new bucket. Layered coverage must also use burst 1 with a coarse refill slower than the earliest secondary-cap expiry, proving that a charged coarse token is included in the combined retry boundary. MCP integration coverage proves that one caller-wide coarse quota spans canonical names while known tools retain secondary per-tool partitions, drives missing/non-string/empty/oversized/case-variant/unknown names plus invalid arguments through pre-validation, and verifies that unknown `batch_query` slot names share one fixed bounded partition. @@ -1528,7 +1528,7 @@ dotnet test --filter "FullyQualifiedName~GitHelperTests" failed-then-success initialize の分離は protocol/session suite に維持し、交渉失敗または success response の serialization 失敗直後と、修正した handshake 後の caller、client info、roots、capabilities、initialization lifecycle、session ID を検証して、失敗 metadata が受理済み session を汚染できないようにします(#4540)。signal-gated な duplicate-initialize coverage では、並行 status reader を取得済み snapshot で保持し、受理済み handshake の進行中 `roots/list` response を保持することで、reader が完全な 1 state だけを見ることと、拒否された重複 initialize が caller metadata や roots を置き換えられないことも検証します。 stdio response-order test も同じ signal-gated pattern を使い、response serializer で sleep する代わりに synthetic transport が parse-error path を signal するようにします。 stdio request-concurrency test も signal gate を使い、`maxConcurrency: 2` で 2 request が実際に overlap し、3 件目が待ち、実行 gate 飽和中も cancellation frame が bypass することを検証します。別枠の accepted-frame 上限、retry-safe な overflow、64 件を超える登録済み queued cancellation、cancellation を無視する action が drain するまで保持される timeout lease も検証してください。non-concurrent な base `IMcpTransport` を `maxConcurrency: 1` で実行し、frame handling が execution gate を二重取得しないことも確認します。initialization-order test は initialize dispatch を明示的に保持し、後続 request が protocol barrier を待つことを確認してください。 - JSON-RPC batch-concurrency test は ID-aware signal gate を使います。`maxConcurrency: 1` で outer frame slot による deadlock が起きないこと、完了順と独立した global peak と入力順 response、initialize・重複 initialize・重複 ID・cancellation target・timeout 後も動く action の fence/isolation/lease semantics を、各処理を明示的に保持して検証してください。重複 initialize は受理済み generation を保持しながら、隣接 request の fence を維持する必要があります。一般 cancellation tombstone cache を満杯にして TTL も経過させ、queue 待ち batch target が引き続き cancellation を受けることと、dispatch 前の return が queued registration を解放して ID を再利用できることも検証します。wall-clock sleep は使わないでください。返された batch frame は member を読む前に、型付きの string、array、各 item の object assertion を通し、response の欠落や malformed shape が null 参照ではなく対応可能な shape diagnostic として失敗するようにしてください(#4935)。 + JSON-RPC batch-concurrency test は ID-aware signal gate を使います。`maxConcurrency: 1` で outer frame slot による deadlock が起きないこと、完了順と独立した global peak と入力順 response、initialize・重複 initialize・重複 ID・cancellation target・timeout 後も動く action の fence/isolation/lease semantics を、各処理を明示的に保持して検証してください。重複 initialize は受理済み generation を保持しながら、隣接 request の fence を維持する必要があります。一般 cancellation tombstone cache を満杯にして TTL も経過させ、queue 待ち batch target が引き続き cancellation を受けることと、dispatch 前の return が queued registration を解放して ID を再利用できることも検証します。wall-clock sleep は使わないでください。返された batch frame は member を読む前に、型付きの string、array、各 item の object assertion を通し、response の欠落や malformed shape が null 参照ではなく対応可能な shape diagnostic として失敗するようにしてください(#4935)。timeout lease の fixture は block 中 action の cancellation token を観測してから gate を解放し、parallel suite の scheduling により期待する timeout が success response に変わらないようにしてください(#4947)。 transport teardown test は cancellation を意図的に無視する request を signal gate で保持し、EOF、不正 UTF-8、oversized line を注入して両 drain window を 0 にします。不正入力 error を shutdown 前に試行し、実際に未完了な request、terminal write、shutdown callback ごとの diagnostic を残して全経路が戻ることを検証してください。未完了 request 数を固定する assertion では、stderr の capture と assertion が終わるまで、数に含める全 request をそれぞれ cancellation-insensitive な gate の後ろで保持してください。callback の scheduling によって完了時点が変わる cancellation-responsive な task をその件数に含めてはいけません。初回 EOF snapshot 後に始まる shutdown、base transport の callback/completion drain、inline control write 中の external cancellation、shutdown 起点 HTTP POST の `204` completion、input dispose 失敗時を含む late writer 後まで defer される stdio output dispose を含めます。token を無視する writer、write-gate contention、停止 callback、例外 callback を使い、wall-clock sleep ではなく signal gate で検証してください。 rate-limit-disabled coverage の繰り返し成功 call には軽量な `languages` tool を使い、limiter bypass だけの assertion で `status` の database aggregation cost を繰り返し支払わないでください。 `RateLimiterTests.cs` は bucket 上限の飽和と回復に注入した決定論的 clock を使います。bucket 数が設定上限を超えないこと、単一 partition の上限拒否が最も早い idle expiry を返すこと、通知された retry 境界まで正確に進めると正規の新規 bucket を作成できることを検証してください。layered coverage では burst 1 と、最短 secondary-cap expiry より遅い coarse refill を使い、消費済み coarse token が結合 retry 境界に含まれることも証明してください。MCP integration coverage では 1 つの caller-wide coarse quota が canonical 名をまたぐ一方で既知 tool が secondary per-tool partition を維持することを証明し、missing/non-string/empty/oversized/case-variant/unknown 名と invalid argument を pre-validation に通し、unknown な `batch_query` slot 名が 1 つの固定 bounded partition を共有することを検証してください。 diff --git a/changelog.d/unreleased/4947.internal.md b/changelog.d/unreleased/4947.internal.md new file mode 100644 index 000000000..f223368fc --- /dev/null +++ b/changelog.d/unreleased/4947.internal.md @@ -0,0 +1,16 @@ +--- +category: internal +issues: + - 4947 +affected: + - tests/CodeIndex.Tests/McpServerTests.cs + - TESTING_GUIDE.md +--- + +## English + +- **The MCP batch timeout lease test now waits for the timeout signal (#4947)** — the fixture observes cancellation of the blocked action before releasing it, keeping the expected timeout and queued-request ordering deterministic during parallel full-suite execution. + +## 日本語 + +- **MCP batch timeout lease テストが timeout signal を待つようになりました(#4947)** — fixture は block 中 action の cancellation を観測してから解放するため、parallel full-suite 実行時も期待する timeout と queue 待ち request の順序が決定的になります。 diff --git a/tests/CodeIndex.Tests/McpServerTests.cs b/tests/CodeIndex.Tests/McpServerTests.cs index 566d5c87a..f54329f08 100644 --- a/tests/CodeIndex.Tests/McpServerTests.cs +++ b/tests/CodeIndex.Tests/McpServerTests.cs @@ -7494,6 +7494,7 @@ public async Task ProcessFrameAsync_BatchTimedOutActionRetainsConcurrencyLeaseUn """{"jsonrpc":"2.0","id":"issue-4536-batch-timeout-init","method":"initialize","params":{}}""")); var firstStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var firstTimedOut = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var releaseFirst = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var secondRegistered = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var secondStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -7502,11 +7503,12 @@ public async Task ProcessFrameAsync_BatchTimedOutActionRetainsConcurrencyLeaseUn if (id?.GetValue() == 453654) secondRegistered.TrySetResult(); }; - server.RequestDelayForTestsWithId = (id, _) => + server.RequestDelayForTestsWithId = (id, cancellationToken) => { if (id?.GetValue() == 453653) { firstStarted.TrySetResult(); + cancellationToken.Register(() => firstTimedOut.TrySetResult()); return releaseFirst.Task; } @@ -7519,6 +7521,7 @@ public async Task ProcessFrameAsync_BatchTimedOutActionRetainsConcurrencyLeaseUn await Task.WhenAll(firstStarted.Task, secondRegistered.Task).WaitAsync(TestDeterminism.DefaultTimeout); Assert.False(secondStarted.Task.IsCompleted); Assert.Equal(0, server.AvailableConcurrencySlotsForTests); + await firstTimedOut.Task.WaitAsync(TestDeterminism.DefaultTimeout); releaseFirst.TrySetResult(); await secondStarted.Task.WaitAsync(TestDeterminism.DefaultTimeout); From 14a639ccf7e55c7d802ddd85bbcf95b50dd3979a Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sat, 1 Aug 2026 17:53:58 +0900 Subject: [PATCH 3/6] Make cancelled EOF drain coverage deterministic (#4978) --- TESTING_GUIDE.md | 4 ++-- changelog.d/unreleased/4978.internal.md | 16 ++++++++++++++++ tests/CodeIndex.Tests/McpServerTests.cs | 8 +++----- 3 files changed, 21 insertions(+), 7 deletions(-) create mode 100644 changelog.d/unreleased/4978.internal.md diff --git a/TESTING_GUIDE.md b/TESTING_GUIDE.md index c016293bc..739507da4 100644 --- a/TESTING_GUIDE.md +++ b/TESTING_GUIDE.md @@ -580,7 +580,7 @@ Use `docs/test-doc-maintenance-plan.md` before moving oversized suites or adding Stdio response-order tests use the same signal-gated pattern: make the synthetic transport signal the parse-error path instead of sleeping in the response serializer. Stdio request-concurrency tests also use signal gates: prove two requests overlap at `maxConcurrency: 2`, a third waits, and a cancellation frame still bypasses a saturated execution gate. Cover the separate accepted-frame cap, retry-safe overflow, more than 64 registered queued cancellations, and timeout leases that remain held until cancellation-insensitive actions drain. Run a non-concurrent base `IMcpTransport` at `maxConcurrency: 1` to prove frame handling does not double-acquire the execution gate. Initialization-order tests must hold the initialize dispatch explicitly and verify later requests wait for its protocol barrier. JSON-RPC batch-concurrency tests use ID-aware signal gates: prove `maxConcurrency: 1` cannot deadlock on an outer frame slot, prove the global peak and stable input-order response independently of completion order, and hold initialize, duplicate initialize, duplicate-ID, cancellation targets, and timed-out live actions explicitly to verify their fence/isolation/lease semantics. A duplicate initialize must retain the accepted generation while preserving the adjacent-request fence. Fill and age the general cancellation tombstone cache to prove a queued batch target remains cancellable, then prove a pre-dispatch return releases its queued registration before ID reuse. Do not use wall-clock sleeps. Parse returned batch frames through typed string, array, and per-item object assertions before reading members so a missing or malformed response fails with actionable shape diagnostics instead of a null dereference (#4935). Timeout-lease fixtures must observe the blocked action's cancellation token before releasing its gate, so parallel-suite scheduling cannot turn the expected timeout into a successful response (#4947). - Transport-teardown tests gate a deliberately cancellation-insensitive request, inject EOF, invalid UTF-8, or an oversized line, and set both drain windows to zero. They must prove malformed-input errors are attempted before shutdown and that every path returns with diagnostics for the actual unfinished request, terminal write, or shutdown callback. When an assertion pins an unfinished-request count, keep every counted request behind its own cancellation-insensitive gate until stderr capture and assertions finish; do not include a cancellation-responsive task whose completion depends on callback scheduling. Cover shutdown beginning after the initial EOF snapshot, base-transport callback and completion drains, external cancellation during an inline control write, the initiating HTTP shutdown POST completing with `204`, and stdio output disposal deferred behind a late writer even when input disposal fails. Include token-ignoring writers, write-gate contention, blocking callbacks, and throwing callbacks; use signal gates rather than wall-clock sleeps. + Transport-teardown tests gate a deliberately cancellation-insensitive request, inject EOF, invalid UTF-8, or an oversized line, and set both drain windows to zero. They must prove malformed-input errors are attempted before shutdown and that every path returns with diagnostics for the actual unfinished request, terminal write, or shutdown callback. When an assertion pins an unfinished-request count, keep every counted request behind its own cancellation-insensitive gate until stderr capture and assertions finish; do not include a cancellation-responsive task whose completion depends on callback scheduling. Cover shutdown beginning after the initial EOF snapshot, base-transport callback and completion drains, external cancellation during an inline control write, the initiating HTTP shutdown POST completing with `204`, and stdio output disposal deferred behind a late writer even when input disposal fails. Include token-ignoring writers, write-gate contention, blocking callbacks, and throwing callbacks; use signal gates rather than wall-clock sleeps. Pre-cancelled EOF-drain coverage must pass a deliberately large grace period, await the returned task with the shared bounded timeout, and prove the pending operation remains incomplete instead of enforcing a sub-second wall-clock threshold (#4978). Rate-limit-disabled coverage uses the lightweight `languages` tool for repeated successful calls; do not pay repeated `status` database aggregation cost when the assertion only concerns limiter bypass. `RateLimiterTests.cs` uses an injected deterministic clock for bucket-cap saturation and recovery. Assert that bucket count never exceeds the configured cap, a single-partition cap rejection reports the earliest idle expiry, and advancing exactly to the advertised retry boundary allows a legitimate new bucket. Layered coverage must also use burst 1 with a coarse refill slower than the earliest secondary-cap expiry, proving that a charged coarse token is included in the combined retry boundary. MCP integration coverage proves that one caller-wide coarse quota spans canonical names while known tools retain secondary per-tool partitions, drives missing/non-string/empty/oversized/case-variant/unknown names plus invalid arguments through pre-validation, and verifies that unknown `batch_query` slot names share one fixed bounded partition. State-changing notification authentication coverage stays table-driven across cancellation, roots, shutdown, and exit methods; every denied notification must remain response-free, emit only a bounded diagnostic, and leave cancellation, roots, and lifecycle state unchanged. @@ -1529,7 +1529,7 @@ dotnet test --filter "FullyQualifiedName~GitHelperTests" stdio response-order test も同じ signal-gated pattern を使い、response serializer で sleep する代わりに synthetic transport が parse-error path を signal するようにします。 stdio request-concurrency test も signal gate を使い、`maxConcurrency: 2` で 2 request が実際に overlap し、3 件目が待ち、実行 gate 飽和中も cancellation frame が bypass することを検証します。別枠の accepted-frame 上限、retry-safe な overflow、64 件を超える登録済み queued cancellation、cancellation を無視する action が drain するまで保持される timeout lease も検証してください。non-concurrent な base `IMcpTransport` を `maxConcurrency: 1` で実行し、frame handling が execution gate を二重取得しないことも確認します。initialization-order test は initialize dispatch を明示的に保持し、後続 request が protocol barrier を待つことを確認してください。 JSON-RPC batch-concurrency test は ID-aware signal gate を使います。`maxConcurrency: 1` で outer frame slot による deadlock が起きないこと、完了順と独立した global peak と入力順 response、initialize・重複 initialize・重複 ID・cancellation target・timeout 後も動く action の fence/isolation/lease semantics を、各処理を明示的に保持して検証してください。重複 initialize は受理済み generation を保持しながら、隣接 request の fence を維持する必要があります。一般 cancellation tombstone cache を満杯にして TTL も経過させ、queue 待ち batch target が引き続き cancellation を受けることと、dispatch 前の return が queued registration を解放して ID を再利用できることも検証します。wall-clock sleep は使わないでください。返された batch frame は member を読む前に、型付きの string、array、各 item の object assertion を通し、response の欠落や malformed shape が null 参照ではなく対応可能な shape diagnostic として失敗するようにしてください(#4935)。timeout lease の fixture は block 中 action の cancellation token を観測してから gate を解放し、parallel suite の scheduling により期待する timeout が success response に変わらないようにしてください(#4947)。 - transport teardown test は cancellation を意図的に無視する request を signal gate で保持し、EOF、不正 UTF-8、oversized line を注入して両 drain window を 0 にします。不正入力 error を shutdown 前に試行し、実際に未完了な request、terminal write、shutdown callback ごとの diagnostic を残して全経路が戻ることを検証してください。未完了 request 数を固定する assertion では、stderr の capture と assertion が終わるまで、数に含める全 request をそれぞれ cancellation-insensitive な gate の後ろで保持してください。callback の scheduling によって完了時点が変わる cancellation-responsive な task をその件数に含めてはいけません。初回 EOF snapshot 後に始まる shutdown、base transport の callback/completion drain、inline control write 中の external cancellation、shutdown 起点 HTTP POST の `204` completion、input dispose 失敗時を含む late writer 後まで defer される stdio output dispose を含めます。token を無視する writer、write-gate contention、停止 callback、例外 callback を使い、wall-clock sleep ではなく signal gate で検証してください。 + transport teardown test は cancellation を意図的に無視する request を signal gate で保持し、EOF、不正 UTF-8、oversized line を注入して両 drain window を 0 にします。不正入力 error を shutdown 前に試行し、実際に未完了な request、terminal write、shutdown callback ごとの diagnostic を残して全経路が戻ることを検証してください。未完了 request 数を固定する assertion では、stderr の capture と assertion が終わるまで、数に含める全 request をそれぞれ cancellation-insensitive な gate の後ろで保持してください。callback の scheduling によって完了時点が変わる cancellation-responsive な task をその件数に含めてはいけません。初回 EOF snapshot 後に始まる shutdown、base transport の callback/completion drain、inline control write 中の external cancellation、shutdown 起点 HTTP POST の `204` completion、input dispose 失敗時を含む late writer 後まで defer される stdio output dispose を含めます。token を無視する writer、write-gate contention、停止 callback、例外 callback を使い、wall-clock sleep ではなく signal gate で検証してください。事前 cancel 済み EOF drain の coverage は意図的に大きい grace period を渡し、共有の bounded timeout で返却 task を待ち、1 秒未満という wall-clock threshold の代わりに pending operation が未完了のままであることを証明してください(#4978)。 rate-limit-disabled coverage の繰り返し成功 call には軽量な `languages` tool を使い、limiter bypass だけの assertion で `status` の database aggregation cost を繰り返し支払わないでください。 `RateLimiterTests.cs` は bucket 上限の飽和と回復に注入した決定論的 clock を使います。bucket 数が設定上限を超えないこと、単一 partition の上限拒否が最も早い idle expiry を返すこと、通知された retry 境界まで正確に進めると正規の新規 bucket を作成できることを検証してください。layered coverage では burst 1 と、最短 secondary-cap expiry より遅い coarse refill を使い、消費済み coarse token が結合 retry 境界に含まれることも証明してください。MCP integration coverage では 1 つの caller-wide coarse quota が canonical 名をまたぐ一方で既知 tool が secondary per-tool partition を維持することを証明し、missing/non-string/empty/oversized/case-variant/unknown 名と invalid argument を pre-validation に通し、unknown な `batch_query` slot 名が 1 つの固定 bounded partition を共有することを検証してください。 state-changing notification の認証 coverage は cancellation、roots、shutdown、exit の各 method を table-driven のまま検証し、拒否されたすべての notification が応答を返さず bounded な診断だけを出力し、cancellation、roots、lifecycle state を変更しないことを確認してください。 diff --git a/changelog.d/unreleased/4978.internal.md b/changelog.d/unreleased/4978.internal.md new file mode 100644 index 000000000..79f3dd5a3 --- /dev/null +++ b/changelog.d/unreleased/4978.internal.md @@ -0,0 +1,16 @@ +--- +category: internal +issues: + - 4978 +affected: + - tests/CodeIndex.Tests/McpServerTests.cs + - TESTING_GUIDE.md +--- + +## English + +- **Cancelled EOF-drain coverage no longer depends on a one-second threshold (#4978)** — the test now combines a deliberately large grace period with a bounded completion wait and verifies that pending work remains untouched, preserving the cancellation contract without failing on .NET 9 scheduler delays. + +## 日本語 + +- **cancel 済み EOF drain の coverage が 1 秒の閾値に依存しなくなりました(#4978)** — テストは意図的に大きい grace period と bounded completion wait を組み合わせ、pending work が未変更のままであることを検証するため、.NET 9 の scheduler 遅延で失敗せず cancellation 契約を維持します。 diff --git a/tests/CodeIndex.Tests/McpServerTests.cs b/tests/CodeIndex.Tests/McpServerTests.cs index f54329f08..bba450495 100644 --- a/tests/CodeIndex.Tests/McpServerTests.cs +++ b/tests/CodeIndex.Tests/McpServerTests.cs @@ -11432,16 +11432,14 @@ public async Task DrainInFlightTasksAsync_CancelledTokenSkipsEofDelay_Issue3400( var pending = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); using var cts = new CancellationTokenSource(); cts.Cancel(); - var stopwatch = Stopwatch.StartNew(); await _server.DrainInFlightTasksAsync( [pending.Task], - McpServer.DefaultEofDrainTimeout, + TimeSpan.FromDays(1), McpServer.DefaultEofPostCancelDrainTimeout, - cts.Token); + cts.Token).WaitAsync(TestDeterminism.DefaultTimeout); - stopwatch.Stop(); - Assert.True(stopwatch.Elapsed < TimeSpan.FromSeconds(1), $"EOF drain cancellation took {stopwatch.Elapsed}."); + Assert.False(pending.Task.IsCompleted); } private sealed class QueuedFrameTransport : IMcpTransport From 1fa1c5a18f58cc6da1ab8a3f35424d6a0f1266b0 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sat, 1 Aug 2026 18:38:57 +0900 Subject: [PATCH 4/6] Synchronize duplicate-initialize timeout coverage (#5001) --- TESTING_GUIDE.md | 4 ++-- changelog.d/unreleased/5001.internal.md | 16 +++++++++++++ tests/CodeIndex.Tests/McpServerTests.cs | 32 +++++++++++++++++++------ 3 files changed, 43 insertions(+), 9 deletions(-) create mode 100644 changelog.d/unreleased/5001.internal.md diff --git a/TESTING_GUIDE.md b/TESTING_GUIDE.md index 739507da4..de4b8c73a 100644 --- a/TESTING_GUIDE.md +++ b/TESTING_GUIDE.md @@ -579,7 +579,7 @@ Use `docs/test-doc-maintenance-plan.md` before moving oversized suites or adding Keep failed-then-success initialize isolation in the protocol/session suite: assert caller, client info, roots, capabilities, initialization lifecycle, and session ID after negotiation or success-response serialization failure and again after the corrected handshake, so failed metadata cannot poison the accepted session (#4540). Signal-gated duplicate-initialize coverage must hold a concurrent status reader on its captured snapshot and an in-flight `roots/list` response from the accepted handshake, proving readers see one complete state and a rejected duplicate cannot replace caller metadata or roots. Stdio response-order tests use the same signal-gated pattern: make the synthetic transport signal the parse-error path instead of sleeping in the response serializer. Stdio request-concurrency tests also use signal gates: prove two requests overlap at `maxConcurrency: 2`, a third waits, and a cancellation frame still bypasses a saturated execution gate. Cover the separate accepted-frame cap, retry-safe overflow, more than 64 registered queued cancellations, and timeout leases that remain held until cancellation-insensitive actions drain. Run a non-concurrent base `IMcpTransport` at `maxConcurrency: 1` to prove frame handling does not double-acquire the execution gate. Initialization-order tests must hold the initialize dispatch explicitly and verify later requests wait for its protocol barrier. - JSON-RPC batch-concurrency tests use ID-aware signal gates: prove `maxConcurrency: 1` cannot deadlock on an outer frame slot, prove the global peak and stable input-order response independently of completion order, and hold initialize, duplicate initialize, duplicate-ID, cancellation targets, and timed-out live actions explicitly to verify their fence/isolation/lease semantics. A duplicate initialize must retain the accepted generation while preserving the adjacent-request fence. Fill and age the general cancellation tombstone cache to prove a queued batch target remains cancellable, then prove a pre-dispatch return releases its queued registration before ID reuse. Do not use wall-clock sleeps. Parse returned batch frames through typed string, array, and per-item object assertions before reading members so a missing or malformed response fails with actionable shape diagnostics instead of a null dereference (#4935). Timeout-lease fixtures must observe the blocked action's cancellation token before releasing its gate, so parallel-suite scheduling cannot turn the expected timeout into a successful response (#4947). + JSON-RPC batch-concurrency tests use ID-aware signal gates: prove `maxConcurrency: 1` cannot deadlock on an outer frame slot, prove the global peak and stable input-order response independently of completion order, and hold initialize, duplicate initialize, duplicate-ID, cancellation targets, and timed-out live actions explicitly to verify their fence/isolation/lease semantics. A duplicate initialize must retain the accepted generation while preserving the adjacent-request fence. Fill and age the general cancellation tombstone cache to prove a queued batch target remains cancellable, then prove a pre-dispatch return releases its queued registration before ID reuse. Do not use wall-clock sleeps. Parse returned batch frames through typed string, array, and per-item object assertions before reading members so a missing or malformed response fails with actionable shape diagnostics instead of a null dereference (#4935). Timeout-lease fixtures must observe the blocked action's cancellation token before releasing its gate, so parallel-suite scheduling cannot turn the expected timeout into a successful response (#4947). Duplicate-initialize timeout-generation fixtures must also observe the timed-out action's cancellation before release and type every adjacent response, preserving the accepted generation and its fence under suite load (#5001). Transport-teardown tests gate a deliberately cancellation-insensitive request, inject EOF, invalid UTF-8, or an oversized line, and set both drain windows to zero. They must prove malformed-input errors are attempted before shutdown and that every path returns with diagnostics for the actual unfinished request, terminal write, or shutdown callback. When an assertion pins an unfinished-request count, keep every counted request behind its own cancellation-insensitive gate until stderr capture and assertions finish; do not include a cancellation-responsive task whose completion depends on callback scheduling. Cover shutdown beginning after the initial EOF snapshot, base-transport callback and completion drains, external cancellation during an inline control write, the initiating HTTP shutdown POST completing with `204`, and stdio output disposal deferred behind a late writer even when input disposal fails. Include token-ignoring writers, write-gate contention, blocking callbacks, and throwing callbacks; use signal gates rather than wall-clock sleeps. Pre-cancelled EOF-drain coverage must pass a deliberately large grace period, await the returned task with the shared bounded timeout, and prove the pending operation remains incomplete instead of enforcing a sub-second wall-clock threshold (#4978). Rate-limit-disabled coverage uses the lightweight `languages` tool for repeated successful calls; do not pay repeated `status` database aggregation cost when the assertion only concerns limiter bypass. `RateLimiterTests.cs` uses an injected deterministic clock for bucket-cap saturation and recovery. Assert that bucket count never exceeds the configured cap, a single-partition cap rejection reports the earliest idle expiry, and advancing exactly to the advertised retry boundary allows a legitimate new bucket. Layered coverage must also use burst 1 with a coarse refill slower than the earliest secondary-cap expiry, proving that a charged coarse token is included in the combined retry boundary. MCP integration coverage proves that one caller-wide coarse quota spans canonical names while known tools retain secondary per-tool partitions, drives missing/non-string/empty/oversized/case-variant/unknown names plus invalid arguments through pre-validation, and verifies that unknown `batch_query` slot names share one fixed bounded partition. @@ -1528,7 +1528,7 @@ dotnet test --filter "FullyQualifiedName~GitHelperTests" failed-then-success initialize の分離は protocol/session suite に維持し、交渉失敗または success response の serialization 失敗直後と、修正した handshake 後の caller、client info、roots、capabilities、initialization lifecycle、session ID を検証して、失敗 metadata が受理済み session を汚染できないようにします(#4540)。signal-gated な duplicate-initialize coverage では、並行 status reader を取得済み snapshot で保持し、受理済み handshake の進行中 `roots/list` response を保持することで、reader が完全な 1 state だけを見ることと、拒否された重複 initialize が caller metadata や roots を置き換えられないことも検証します。 stdio response-order test も同じ signal-gated pattern を使い、response serializer で sleep する代わりに synthetic transport が parse-error path を signal するようにします。 stdio request-concurrency test も signal gate を使い、`maxConcurrency: 2` で 2 request が実際に overlap し、3 件目が待ち、実行 gate 飽和中も cancellation frame が bypass することを検証します。別枠の accepted-frame 上限、retry-safe な overflow、64 件を超える登録済み queued cancellation、cancellation を無視する action が drain するまで保持される timeout lease も検証してください。non-concurrent な base `IMcpTransport` を `maxConcurrency: 1` で実行し、frame handling が execution gate を二重取得しないことも確認します。initialization-order test は initialize dispatch を明示的に保持し、後続 request が protocol barrier を待つことを確認してください。 - JSON-RPC batch-concurrency test は ID-aware signal gate を使います。`maxConcurrency: 1` で outer frame slot による deadlock が起きないこと、完了順と独立した global peak と入力順 response、initialize・重複 initialize・重複 ID・cancellation target・timeout 後も動く action の fence/isolation/lease semantics を、各処理を明示的に保持して検証してください。重複 initialize は受理済み generation を保持しながら、隣接 request の fence を維持する必要があります。一般 cancellation tombstone cache を満杯にして TTL も経過させ、queue 待ち batch target が引き続き cancellation を受けることと、dispatch 前の return が queued registration を解放して ID を再利用できることも検証します。wall-clock sleep は使わないでください。返された batch frame は member を読む前に、型付きの string、array、各 item の object assertion を通し、response の欠落や malformed shape が null 参照ではなく対応可能な shape diagnostic として失敗するようにしてください(#4935)。timeout lease の fixture は block 中 action の cancellation token を観測してから gate を解放し、parallel suite の scheduling により期待する timeout が success response に変わらないようにしてください(#4947)。 + JSON-RPC batch-concurrency test は ID-aware signal gate を使います。`maxConcurrency: 1` で outer frame slot による deadlock が起きないこと、完了順と独立した global peak と入力順 response、initialize・重複 initialize・重複 ID・cancellation target・timeout 後も動く action の fence/isolation/lease semantics を、各処理を明示的に保持して検証してください。重複 initialize は受理済み generation を保持しながら、隣接 request の fence を維持する必要があります。一般 cancellation tombstone cache を満杯にして TTL も経過させ、queue 待ち batch target が引き続き cancellation を受けることと、dispatch 前の return が queued registration を解放して ID を再利用できることも検証します。wall-clock sleep は使わないでください。返された batch frame は member を読む前に、型付きの string、array、各 item の object assertion を通し、response の欠落や malformed shape が null 参照ではなく対応可能な shape diagnostic として失敗するようにしてください(#4935)。timeout lease の fixture は block 中 action の cancellation token を観測してから gate を解放し、parallel suite の scheduling により期待する timeout が success response に変わらないようにしてください(#4947)。重複 initialize の timeout-generation fixture でも timeout 済み action の cancellation を観測してから解放し、隣接する全 response を型付きで検証して、suite load 下でも受理済み generation とその fence を維持してください(#5001)。 transport teardown test は cancellation を意図的に無視する request を signal gate で保持し、EOF、不正 UTF-8、oversized line を注入して両 drain window を 0 にします。不正入力 error を shutdown 前に試行し、実際に未完了な request、terminal write、shutdown callback ごとの diagnostic を残して全経路が戻ることを検証してください。未完了 request 数を固定する assertion では、stderr の capture と assertion が終わるまで、数に含める全 request をそれぞれ cancellation-insensitive な gate の後ろで保持してください。callback の scheduling によって完了時点が変わる cancellation-responsive な task をその件数に含めてはいけません。初回 EOF snapshot 後に始まる shutdown、base transport の callback/completion drain、inline control write 中の external cancellation、shutdown 起点 HTTP POST の `204` completion、input dispose 失敗時を含む late writer 後まで defer される stdio output dispose を含めます。token を無視する writer、write-gate contention、停止 callback、例外 callback を使い、wall-clock sleep ではなく signal gate で検証してください。事前 cancel 済み EOF drain の coverage は意図的に大きい grace period を渡し、共有の bounded timeout で返却 task を待ち、1 秒未満という wall-clock threshold の代わりに pending operation が未完了のままであることを証明してください(#4978)。 rate-limit-disabled coverage の繰り返し成功 call には軽量な `languages` tool を使い、limiter bypass だけの assertion で `status` の database aggregation cost を繰り返し支払わないでください。 `RateLimiterTests.cs` は bucket 上限の飽和と回復に注入した決定論的 clock を使います。bucket 数が設定上限を超えないこと、単一 partition の上限拒否が最も早い idle expiry を返すこと、通知された retry 境界まで正確に進めると正規の新規 bucket を作成できることを検証してください。layered coverage では burst 1 と、最短 secondary-cap expiry より遅い coarse refill を使い、消費済み coarse token が結合 retry 境界に含まれることも証明してください。MCP integration coverage では 1 つの caller-wide coarse quota が canonical 名をまたぐ一方で既知 tool が secondary per-tool partition を維持することを証明し、missing/non-string/empty/oversized/case-variant/unknown 名と invalid argument を pre-validation に通し、unknown な `batch_query` slot 名が 1 つの固定 bounded partition を共有することを検証してください。 diff --git a/changelog.d/unreleased/5001.internal.md b/changelog.d/unreleased/5001.internal.md new file mode 100644 index 000000000..0abf4afb9 --- /dev/null +++ b/changelog.d/unreleased/5001.internal.md @@ -0,0 +1,16 @@ +--- +category: internal +issues: + - 5001 +affected: + - tests/CodeIndex.Tests/McpServerTests.cs + - TESTING_GUIDE.md +--- + +## English + +- **Duplicate-initialize timeout coverage now waits for the timeout signal (#5001)** — the batch test observes the blocked action's cancellation before releasing it and validates every response shape explicitly, preventing scheduler load from turning the expected timeout into a success or a null dereference. + +## 日本語 + +- **重複 initialize の timeout coverage が timeout signal を待つようになりました(#5001)** — batch テストは block 中 action を解放する前に cancellation を観測し、全 response shape を明示的に検証するため、scheduler load により期待する timeout が成功応答や null 参照に変わることを防ぎます。 diff --git a/tests/CodeIndex.Tests/McpServerTests.cs b/tests/CodeIndex.Tests/McpServerTests.cs index bba450495..6566b7bf6 100644 --- a/tests/CodeIndex.Tests/McpServerTests.cs +++ b/tests/CodeIndex.Tests/McpServerTests.cs @@ -7560,16 +7560,18 @@ public async Task ProcessFrameAsync_BatchDuplicateInitializeDoesNotReplaceTimedO """{"jsonrpc":"2.0","id":"batch-generation-init","method":"initialize","params":{}}""")); var firstStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var firstTimedOut = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var releaseFirst = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var followingPingStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var releaseFollowingPing = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var observedDrainingCaller = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - server.RequestDelayForTestsWithId = async (id, _) => + server.RequestDelayForTestsWithId = async (id, cancellationToken) => { switch (id?.GetValue()) { case 454031: firstStarted.TrySetResult(); + cancellationToken.Register(() => firstTimedOut.TrySetResult()); await releaseFirst.Task; observedDrainingCaller.TrySetResult(server.CurrentCaller); break; @@ -7588,6 +7590,7 @@ public async Task ProcessFrameAsync_BatchDuplicateInitializeDoesNotReplaceTimedO { await firstStarted.Task.WaitAsync(TestDeterminism.DefaultTimeout); await followingPingStarted.Task.WaitAsync(TestDeterminism.DefaultTimeout); + await firstTimedOut.Task.WaitAsync(TestDeterminism.DefaultTimeout); releaseFirst.TrySetResult(); Assert.Equal( @@ -7595,14 +7598,29 @@ public async Task ProcessFrameAsync_BatchDuplicateInitializeDoesNotReplaceTimedO await observedDrainingCaller.Task.WaitAsync(TestDeterminism.DefaultTimeout)); releaseFollowingPing.TrySetResult(); - var responseText = await batchTask.WaitAsync(TestDeterminism.DefaultTimeout); - var responses = Assert.IsType(JsonNode.Parse(responseText!)); - Assert.Equal("Request timed out", responses[0]!["error"]!["message"]!.GetValue()); - Assert.Equal(-32600, responses[1]!["error"]!["code"]!.GetValue()); + var responseText = Assert.IsType( + await batchTask.WaitAsync(TestDeterminism.DefaultTimeout)); + var responses = Assert.IsType(JsonNode.Parse(responseText)); + Assert.Equal(3, responses.Count); + var timedOutResponse = Assert.IsType(responses[0]); + var timedOutError = Assert.IsType(timedOutResponse["error"]); + Assert.Equal( + "Request timed out", + Assert.IsAssignableFrom(timedOutError["message"]).GetValue()); + var duplicateResponse = Assert.IsType(responses[1]); + var duplicateError = Assert.IsType(duplicateResponse["error"]); + Assert.Equal( + -32600, + Assert.IsAssignableFrom(duplicateError["code"]).GetValue()); + var duplicateData = Assert.IsType(duplicateError["data"]); Assert.Equal( "duplicate_initialize", - responses[1]!["error"]!["data"]!["reason"]!.GetValue()); - Assert.Equal("ok", responses[2]!["result"]!["status"]!.GetValue()); + Assert.IsAssignableFrom(duplicateData["reason"]).GetValue()); + var pingResponse = Assert.IsType(responses[2]); + var pingResult = Assert.IsType(pingResponse["result"]); + Assert.Equal( + "ok", + Assert.IsAssignableFrom(pingResult["status"]).GetValue()); Assert.Equal("unknown", server.CurrentCaller); } finally From 84fd613f827a5f4cf17c63e10f97e3c72ce7ef10 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sat, 1 Aug 2026 19:58:01 +0900 Subject: [PATCH 5/6] Isolate batch timeout test setup (#4947) --- TESTING_GUIDE.md | 4 ++-- changelog.d/unreleased/4947.internal.md | 4 ++-- tests/CodeIndex.Tests/McpServerTests.cs | 11 +++++------ 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/TESTING_GUIDE.md b/TESTING_GUIDE.md index de4b8c73a..2b682c047 100644 --- a/TESTING_GUIDE.md +++ b/TESTING_GUIDE.md @@ -579,7 +579,7 @@ Use `docs/test-doc-maintenance-plan.md` before moving oversized suites or adding Keep failed-then-success initialize isolation in the protocol/session suite: assert caller, client info, roots, capabilities, initialization lifecycle, and session ID after negotiation or success-response serialization failure and again after the corrected handshake, so failed metadata cannot poison the accepted session (#4540). Signal-gated duplicate-initialize coverage must hold a concurrent status reader on its captured snapshot and an in-flight `roots/list` response from the accepted handshake, proving readers see one complete state and a rejected duplicate cannot replace caller metadata or roots. Stdio response-order tests use the same signal-gated pattern: make the synthetic transport signal the parse-error path instead of sleeping in the response serializer. Stdio request-concurrency tests also use signal gates: prove two requests overlap at `maxConcurrency: 2`, a third waits, and a cancellation frame still bypasses a saturated execution gate. Cover the separate accepted-frame cap, retry-safe overflow, more than 64 registered queued cancellations, and timeout leases that remain held until cancellation-insensitive actions drain. Run a non-concurrent base `IMcpTransport` at `maxConcurrency: 1` to prove frame handling does not double-acquire the execution gate. Initialization-order tests must hold the initialize dispatch explicitly and verify later requests wait for its protocol barrier. - JSON-RPC batch-concurrency tests use ID-aware signal gates: prove `maxConcurrency: 1` cannot deadlock on an outer frame slot, prove the global peak and stable input-order response independently of completion order, and hold initialize, duplicate initialize, duplicate-ID, cancellation targets, and timed-out live actions explicitly to verify their fence/isolation/lease semantics. A duplicate initialize must retain the accepted generation while preserving the adjacent-request fence. Fill and age the general cancellation tombstone cache to prove a queued batch target remains cancellable, then prove a pre-dispatch return releases its queued registration before ID reuse. Do not use wall-clock sleeps. Parse returned batch frames through typed string, array, and per-item object assertions before reading members so a missing or malformed response fails with actionable shape diagnostics instead of a null dereference (#4935). Timeout-lease fixtures must observe the blocked action's cancellation token before releasing its gate, so parallel-suite scheduling cannot turn the expected timeout into a successful response (#4947). Duplicate-initialize timeout-generation fixtures must also observe the timed-out action's cancellation before release and type every adjacent response, preserving the accepted generation and its fence under suite load (#5001). + JSON-RPC batch-concurrency tests use ID-aware signal gates: prove `maxConcurrency: 1` cannot deadlock on an outer frame slot, prove the global peak and stable input-order response independently of completion order, and hold initialize, duplicate initialize, duplicate-ID, cancellation targets, and timed-out live actions explicitly to verify their fence/isolation/lease semantics. A duplicate initialize must retain the accepted generation while preserving the adjacent-request fence. Fill and age the general cancellation tombstone cache to prove a queued batch target remains cancellable, then prove a pre-dispatch return releases its queued registration before ID reuse. Do not use wall-clock sleeps. Parse returned batch frames through typed string, array, and per-item object assertions before reading members so a missing or malformed response fails with actionable shape diagnostics instead of a null dereference (#4935). Timeout-lease fixtures must establish and type-check setup initialization outside the shortened timed dispatch, then observe the blocked action's cancellation token before releasing its gate, so parallel-suite scheduling cannot turn the expected timeout into a successful response (#4947). Duplicate-initialize timeout-generation fixtures must also observe the timed-out action's cancellation before release and type every adjacent response, preserving the accepted generation and its fence under suite load (#5001). Transport-teardown tests gate a deliberately cancellation-insensitive request, inject EOF, invalid UTF-8, or an oversized line, and set both drain windows to zero. They must prove malformed-input errors are attempted before shutdown and that every path returns with diagnostics for the actual unfinished request, terminal write, or shutdown callback. When an assertion pins an unfinished-request count, keep every counted request behind its own cancellation-insensitive gate until stderr capture and assertions finish; do not include a cancellation-responsive task whose completion depends on callback scheduling. Cover shutdown beginning after the initial EOF snapshot, base-transport callback and completion drains, external cancellation during an inline control write, the initiating HTTP shutdown POST completing with `204`, and stdio output disposal deferred behind a late writer even when input disposal fails. Include token-ignoring writers, write-gate contention, blocking callbacks, and throwing callbacks; use signal gates rather than wall-clock sleeps. Pre-cancelled EOF-drain coverage must pass a deliberately large grace period, await the returned task with the shared bounded timeout, and prove the pending operation remains incomplete instead of enforcing a sub-second wall-clock threshold (#4978). Rate-limit-disabled coverage uses the lightweight `languages` tool for repeated successful calls; do not pay repeated `status` database aggregation cost when the assertion only concerns limiter bypass. `RateLimiterTests.cs` uses an injected deterministic clock for bucket-cap saturation and recovery. Assert that bucket count never exceeds the configured cap, a single-partition cap rejection reports the earliest idle expiry, and advancing exactly to the advertised retry boundary allows a legitimate new bucket. Layered coverage must also use burst 1 with a coarse refill slower than the earliest secondary-cap expiry, proving that a charged coarse token is included in the combined retry boundary. MCP integration coverage proves that one caller-wide coarse quota spans canonical names while known tools retain secondary per-tool partitions, drives missing/non-string/empty/oversized/case-variant/unknown names plus invalid arguments through pre-validation, and verifies that unknown `batch_query` slot names share one fixed bounded partition. @@ -1528,7 +1528,7 @@ dotnet test --filter "FullyQualifiedName~GitHelperTests" failed-then-success initialize の分離は protocol/session suite に維持し、交渉失敗または success response の serialization 失敗直後と、修正した handshake 後の caller、client info、roots、capabilities、initialization lifecycle、session ID を検証して、失敗 metadata が受理済み session を汚染できないようにします(#4540)。signal-gated な duplicate-initialize coverage では、並行 status reader を取得済み snapshot で保持し、受理済み handshake の進行中 `roots/list` response を保持することで、reader が完全な 1 state だけを見ることと、拒否された重複 initialize が caller metadata や roots を置き換えられないことも検証します。 stdio response-order test も同じ signal-gated pattern を使い、response serializer で sleep する代わりに synthetic transport が parse-error path を signal するようにします。 stdio request-concurrency test も signal gate を使い、`maxConcurrency: 2` で 2 request が実際に overlap し、3 件目が待ち、実行 gate 飽和中も cancellation frame が bypass することを検証します。別枠の accepted-frame 上限、retry-safe な overflow、64 件を超える登録済み queued cancellation、cancellation を無視する action が drain するまで保持される timeout lease も検証してください。non-concurrent な base `IMcpTransport` を `maxConcurrency: 1` で実行し、frame handling が execution gate を二重取得しないことも確認します。initialization-order test は initialize dispatch を明示的に保持し、後続 request が protocol barrier を待つことを確認してください。 - JSON-RPC batch-concurrency test は ID-aware signal gate を使います。`maxConcurrency: 1` で outer frame slot による deadlock が起きないこと、完了順と独立した global peak と入力順 response、initialize・重複 initialize・重複 ID・cancellation target・timeout 後も動く action の fence/isolation/lease semantics を、各処理を明示的に保持して検証してください。重複 initialize は受理済み generation を保持しながら、隣接 request の fence を維持する必要があります。一般 cancellation tombstone cache を満杯にして TTL も経過させ、queue 待ち batch target が引き続き cancellation を受けることと、dispatch 前の return が queued registration を解放して ID を再利用できることも検証します。wall-clock sleep は使わないでください。返された batch frame は member を読む前に、型付きの string、array、各 item の object assertion を通し、response の欠落や malformed shape が null 参照ではなく対応可能な shape diagnostic として失敗するようにしてください(#4935)。timeout lease の fixture は block 中 action の cancellation token を観測してから gate を解放し、parallel suite の scheduling により期待する timeout が success response に変わらないようにしてください(#4947)。重複 initialize の timeout-generation fixture でも timeout 済み action の cancellation を観測してから解放し、隣接する全 response を型付きで検証して、suite load 下でも受理済み generation とその fence を維持してください(#5001)。 + JSON-RPC batch-concurrency test は ID-aware signal gate を使います。`maxConcurrency: 1` で outer frame slot による deadlock が起きないこと、完了順と独立した global peak と入力順 response、initialize・重複 initialize・重複 ID・cancellation target・timeout 後も動く action の fence/isolation/lease semantics を、各処理を明示的に保持して検証してください。重複 initialize は受理済み generation を保持しながら、隣接 request の fence を維持する必要があります。一般 cancellation tombstone cache を満杯にして TTL も経過させ、queue 待ち batch target が引き続き cancellation を受けることと、dispatch 前の return が queued registration を解放して ID を再利用できることも検証します。wall-clock sleep は使わないでください。返された batch frame は member を読む前に、型付きの string、array、各 item の object assertion を通し、response の欠落や malformed shape が null 参照ではなく対応可能な shape diagnostic として失敗するようにしてください(#4935)。timeout lease の fixture は短い timed dispatch の外側で setup initialization を確立して型も検証し、その後 block 中 action の cancellation token を観測してから gate を解放して、parallel suite の scheduling により期待する timeout が success response に変わらないようにしてください(#4947)。重複 initialize の timeout-generation fixture でも timeout 済み action の cancellation を観測してから解放し、隣接する全 response を型付きで検証して、suite load 下でも受理済み generation とその fence を維持してください(#5001)。 transport teardown test は cancellation を意図的に無視する request を signal gate で保持し、EOF、不正 UTF-8、oversized line を注入して両 drain window を 0 にします。不正入力 error を shutdown 前に試行し、実際に未完了な request、terminal write、shutdown callback ごとの diagnostic を残して全経路が戻ることを検証してください。未完了 request 数を固定する assertion では、stderr の capture と assertion が終わるまで、数に含める全 request をそれぞれ cancellation-insensitive な gate の後ろで保持してください。callback の scheduling によって完了時点が変わる cancellation-responsive な task をその件数に含めてはいけません。初回 EOF snapshot 後に始まる shutdown、base transport の callback/completion drain、inline control write 中の external cancellation、shutdown 起点 HTTP POST の `204` completion、input dispose 失敗時を含む late writer 後まで defer される stdio output dispose を含めます。token を無視する writer、write-gate contention、停止 callback、例外 callback を使い、wall-clock sleep ではなく signal gate で検証してください。事前 cancel 済み EOF drain の coverage は意図的に大きい grace period を渡し、共有の bounded timeout で返却 task を待ち、1 秒未満という wall-clock threshold の代わりに pending operation が未完了のままであることを証明してください(#4978)。 rate-limit-disabled coverage の繰り返し成功 call には軽量な `languages` tool を使い、limiter bypass だけの assertion で `status` の database aggregation cost を繰り返し支払わないでください。 `RateLimiterTests.cs` は bucket 上限の飽和と回復に注入した決定論的 clock を使います。bucket 数が設定上限を超えないこと、単一 partition の上限拒否が最も早い idle expiry を返すこと、通知された retry 境界まで正確に進めると正規の新規 bucket を作成できることを検証してください。layered coverage では burst 1 と、最短 secondary-cap expiry より遅い coarse refill を使い、消費済み coarse token が結合 retry 境界に含まれることも証明してください。MCP integration coverage では 1 つの caller-wide coarse quota が canonical 名をまたぐ一方で既知 tool が secondary per-tool partition を維持することを証明し、missing/non-string/empty/oversized/case-variant/unknown 名と invalid argument を pre-validation に通し、unknown な `batch_query` slot 名が 1 つの固定 bounded partition を共有することを検証してください。 diff --git a/changelog.d/unreleased/4947.internal.md b/changelog.d/unreleased/4947.internal.md index f223368fc..72efe46c9 100644 --- a/changelog.d/unreleased/4947.internal.md +++ b/changelog.d/unreleased/4947.internal.md @@ -9,8 +9,8 @@ affected: ## English -- **The MCP batch timeout lease test now waits for the timeout signal (#4947)** — the fixture observes cancellation of the blocked action before releasing it, keeping the expected timeout and queued-request ordering deterministic during parallel full-suite execution. +- **The MCP batch timeout lease test now isolates its short timeout (#4947)** — the fixture establishes and verifies setup initialization outside the shortened timed dispatch, then observes cancellation of the blocked action before releasing it, keeping the expected timeout and queued-request ordering deterministic during parallel full-suite execution. ## 日本語 -- **MCP batch timeout lease テストが timeout signal を待つようになりました(#4947)** — fixture は block 中 action の cancellation を観測してから解放するため、parallel full-suite 実行時も期待する timeout と queue 待ち request の順序が決定的になります。 +- **MCP batch timeout lease テストが短い timeout を分離するようになりました(#4947)** — fixture は短い timed dispatch の外側で setup initialization を確立して成功を検証し、その後 block 中 action の cancellation を観測してから解放するため、parallel full-suite 実行時も期待する timeout と queue 待ち request の順序が決定的になります。 diff --git a/tests/CodeIndex.Tests/McpServerTests.cs b/tests/CodeIndex.Tests/McpServerTests.cs index 6566b7bf6..a6f2837a3 100644 --- a/tests/CodeIndex.Tests/McpServerTests.cs +++ b/tests/CodeIndex.Tests/McpServerTests.cs @@ -7490,8 +7490,9 @@ public async Task ProcessFrameAsync_BatchTimedOutActionRetainsConcurrencyLeaseUn { RequestTimeout = TimeSpan.FromMilliseconds(100), }; - Assert.NotNull(await server.ProcessFrameAsync( - """{"jsonrpc":"2.0","id":"issue-4536-batch-timeout-init","method":"initialize","params":{}}""")); + var initializeResponse = Assert.IsType(server.HandleMessage(JsonNode.Parse( + """{"jsonrpc":"2.0","id":"issue-4536-batch-timeout-init","method":"initialize","params":{}}""")!)); + Assert.IsType(initializeResponse["result"]); var firstStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var firstTimedOut = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -7517,7 +7518,7 @@ public async Task ProcessFrameAsync_BatchTimedOutActionRetainsConcurrencyLeaseUn }; var batchResponseTask = server.ProcessFrameAsync( - """[{"jsonrpc":"2.0","id":453653,"method":"ping"},{"jsonrpc":"2.0","id":453654,"method":"ping"}]"""); + """[{"jsonrpc":"2.0","id":453653,"method":"ping"},{"jsonrpc":"2.0","id":453654,"method":"prompts/list"}]"""); await Task.WhenAll(firstStarted.Task, secondRegistered.Task).WaitAsync(TestDeterminism.DefaultTimeout); Assert.False(secondStarted.Task.IsCompleted); Assert.Equal(0, server.AvailableConcurrencySlotsForTests); @@ -7536,9 +7537,7 @@ public async Task ProcessFrameAsync_BatchTimedOutActionRetainsConcurrencyLeaseUn Assert.IsAssignableFrom(firstError["message"]).GetValue()); var secondResponse = Assert.IsType(responses[1]); var secondResult = Assert.IsType(secondResponse["result"]); - Assert.Equal( - "ok", - Assert.IsAssignableFrom(secondResult["status"]).GetValue()); + Assert.IsType(secondResult["prompts"]); Assert.Equal(1, server.AvailableConcurrencySlotsForTests); } From 292ac4c6d3edda27fc4d23e36aa2a8152ba80673 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sat, 1 Aug 2026 21:03:09 +0900 Subject: [PATCH 6/6] Isolate duplicate-initialize timeout setup (#5001) --- TESTING_GUIDE.md | 4 ++-- changelog.d/unreleased/5001.internal.md | 4 ++-- tests/CodeIndex.Tests/McpServerTests.cs | 29 ++++++++++++------------- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/TESTING_GUIDE.md b/TESTING_GUIDE.md index 2b682c047..8d840119a 100644 --- a/TESTING_GUIDE.md +++ b/TESTING_GUIDE.md @@ -579,7 +579,7 @@ Use `docs/test-doc-maintenance-plan.md` before moving oversized suites or adding Keep failed-then-success initialize isolation in the protocol/session suite: assert caller, client info, roots, capabilities, initialization lifecycle, and session ID after negotiation or success-response serialization failure and again after the corrected handshake, so failed metadata cannot poison the accepted session (#4540). Signal-gated duplicate-initialize coverage must hold a concurrent status reader on its captured snapshot and an in-flight `roots/list` response from the accepted handshake, proving readers see one complete state and a rejected duplicate cannot replace caller metadata or roots. Stdio response-order tests use the same signal-gated pattern: make the synthetic transport signal the parse-error path instead of sleeping in the response serializer. Stdio request-concurrency tests also use signal gates: prove two requests overlap at `maxConcurrency: 2`, a third waits, and a cancellation frame still bypasses a saturated execution gate. Cover the separate accepted-frame cap, retry-safe overflow, more than 64 registered queued cancellations, and timeout leases that remain held until cancellation-insensitive actions drain. Run a non-concurrent base `IMcpTransport` at `maxConcurrency: 1` to prove frame handling does not double-acquire the execution gate. Initialization-order tests must hold the initialize dispatch explicitly and verify later requests wait for its protocol barrier. - JSON-RPC batch-concurrency tests use ID-aware signal gates: prove `maxConcurrency: 1` cannot deadlock on an outer frame slot, prove the global peak and stable input-order response independently of completion order, and hold initialize, duplicate initialize, duplicate-ID, cancellation targets, and timed-out live actions explicitly to verify their fence/isolation/lease semantics. A duplicate initialize must retain the accepted generation while preserving the adjacent-request fence. Fill and age the general cancellation tombstone cache to prove a queued batch target remains cancellable, then prove a pre-dispatch return releases its queued registration before ID reuse. Do not use wall-clock sleeps. Parse returned batch frames through typed string, array, and per-item object assertions before reading members so a missing or malformed response fails with actionable shape diagnostics instead of a null dereference (#4935). Timeout-lease fixtures must establish and type-check setup initialization outside the shortened timed dispatch, then observe the blocked action's cancellation token before releasing its gate, so parallel-suite scheduling cannot turn the expected timeout into a successful response (#4947). Duplicate-initialize timeout-generation fixtures must also observe the timed-out action's cancellation before release and type every adjacent response, preserving the accepted generation and its fence under suite load (#5001). + JSON-RPC batch-concurrency tests use ID-aware signal gates: prove `maxConcurrency: 1` cannot deadlock on an outer frame slot, prove the global peak and stable input-order response independently of completion order, and hold initialize, duplicate initialize, duplicate-ID, cancellation targets, and timed-out live actions explicitly to verify their fence/isolation/lease semantics. A duplicate initialize must retain the accepted generation while preserving the adjacent-request fence. Fill and age the general cancellation tombstone cache to prove a queued batch target remains cancellable, then prove a pre-dispatch return releases its queued registration before ID reuse. Do not use wall-clock sleeps. Parse returned batch frames through typed string, array, and per-item object assertions before reading members so a missing or malformed response fails with actionable shape diagnostics instead of a null dereference (#4935). Timeout-lease fixtures must establish and type-check setup initialization outside the shortened timed dispatch, then observe the blocked action's cancellation token before releasing its gate, so parallel-suite scheduling cannot turn the expected timeout into a successful response (#4947). Duplicate-initialize timeout-generation fixtures must likewise establish setup initialization outside the shortened timed dispatch, observe the timed-out action's cancellation before release, use a database-independent adjacent success request, and type every response, preserving the accepted generation and its fence under suite load (#5001). Transport-teardown tests gate a deliberately cancellation-insensitive request, inject EOF, invalid UTF-8, or an oversized line, and set both drain windows to zero. They must prove malformed-input errors are attempted before shutdown and that every path returns with diagnostics for the actual unfinished request, terminal write, or shutdown callback. When an assertion pins an unfinished-request count, keep every counted request behind its own cancellation-insensitive gate until stderr capture and assertions finish; do not include a cancellation-responsive task whose completion depends on callback scheduling. Cover shutdown beginning after the initial EOF snapshot, base-transport callback and completion drains, external cancellation during an inline control write, the initiating HTTP shutdown POST completing with `204`, and stdio output disposal deferred behind a late writer even when input disposal fails. Include token-ignoring writers, write-gate contention, blocking callbacks, and throwing callbacks; use signal gates rather than wall-clock sleeps. Pre-cancelled EOF-drain coverage must pass a deliberately large grace period, await the returned task with the shared bounded timeout, and prove the pending operation remains incomplete instead of enforcing a sub-second wall-clock threshold (#4978). Rate-limit-disabled coverage uses the lightweight `languages` tool for repeated successful calls; do not pay repeated `status` database aggregation cost when the assertion only concerns limiter bypass. `RateLimiterTests.cs` uses an injected deterministic clock for bucket-cap saturation and recovery. Assert that bucket count never exceeds the configured cap, a single-partition cap rejection reports the earliest idle expiry, and advancing exactly to the advertised retry boundary allows a legitimate new bucket. Layered coverage must also use burst 1 with a coarse refill slower than the earliest secondary-cap expiry, proving that a charged coarse token is included in the combined retry boundary. MCP integration coverage proves that one caller-wide coarse quota spans canonical names while known tools retain secondary per-tool partitions, drives missing/non-string/empty/oversized/case-variant/unknown names plus invalid arguments through pre-validation, and verifies that unknown `batch_query` slot names share one fixed bounded partition. @@ -1528,7 +1528,7 @@ dotnet test --filter "FullyQualifiedName~GitHelperTests" failed-then-success initialize の分離は protocol/session suite に維持し、交渉失敗または success response の serialization 失敗直後と、修正した handshake 後の caller、client info、roots、capabilities、initialization lifecycle、session ID を検証して、失敗 metadata が受理済み session を汚染できないようにします(#4540)。signal-gated な duplicate-initialize coverage では、並行 status reader を取得済み snapshot で保持し、受理済み handshake の進行中 `roots/list` response を保持することで、reader が完全な 1 state だけを見ることと、拒否された重複 initialize が caller metadata や roots を置き換えられないことも検証します。 stdio response-order test も同じ signal-gated pattern を使い、response serializer で sleep する代わりに synthetic transport が parse-error path を signal するようにします。 stdio request-concurrency test も signal gate を使い、`maxConcurrency: 2` で 2 request が実際に overlap し、3 件目が待ち、実行 gate 飽和中も cancellation frame が bypass することを検証します。別枠の accepted-frame 上限、retry-safe な overflow、64 件を超える登録済み queued cancellation、cancellation を無視する action が drain するまで保持される timeout lease も検証してください。non-concurrent な base `IMcpTransport` を `maxConcurrency: 1` で実行し、frame handling が execution gate を二重取得しないことも確認します。initialization-order test は initialize dispatch を明示的に保持し、後続 request が protocol barrier を待つことを確認してください。 - JSON-RPC batch-concurrency test は ID-aware signal gate を使います。`maxConcurrency: 1` で outer frame slot による deadlock が起きないこと、完了順と独立した global peak と入力順 response、initialize・重複 initialize・重複 ID・cancellation target・timeout 後も動く action の fence/isolation/lease semantics を、各処理を明示的に保持して検証してください。重複 initialize は受理済み generation を保持しながら、隣接 request の fence を維持する必要があります。一般 cancellation tombstone cache を満杯にして TTL も経過させ、queue 待ち batch target が引き続き cancellation を受けることと、dispatch 前の return が queued registration を解放して ID を再利用できることも検証します。wall-clock sleep は使わないでください。返された batch frame は member を読む前に、型付きの string、array、各 item の object assertion を通し、response の欠落や malformed shape が null 参照ではなく対応可能な shape diagnostic として失敗するようにしてください(#4935)。timeout lease の fixture は短い timed dispatch の外側で setup initialization を確立して型も検証し、その後 block 中 action の cancellation token を観測してから gate を解放して、parallel suite の scheduling により期待する timeout が success response に変わらないようにしてください(#4947)。重複 initialize の timeout-generation fixture でも timeout 済み action の cancellation を観測してから解放し、隣接する全 response を型付きで検証して、suite load 下でも受理済み generation とその fence を維持してください(#5001)。 + JSON-RPC batch-concurrency test は ID-aware signal gate を使います。`maxConcurrency: 1` で outer frame slot による deadlock が起きないこと、完了順と独立した global peak と入力順 response、initialize・重複 initialize・重複 ID・cancellation target・timeout 後も動く action の fence/isolation/lease semantics を、各処理を明示的に保持して検証してください。重複 initialize は受理済み generation を保持しながら、隣接 request の fence を維持する必要があります。一般 cancellation tombstone cache を満杯にして TTL も経過させ、queue 待ち batch target が引き続き cancellation を受けることと、dispatch 前の return が queued registration を解放して ID を再利用できることも検証します。wall-clock sleep は使わないでください。返された batch frame は member を読む前に、型付きの string、array、各 item の object assertion を通し、response の欠落や malformed shape が null 参照ではなく対応可能な shape diagnostic として失敗するようにしてください(#4935)。timeout lease の fixture は短い timed dispatch の外側で setup initialization を確立して型も検証し、その後 block 中 action の cancellation token を観測してから gate を解放して、parallel suite の scheduling により期待する timeout が success response に変わらないようにしてください(#4947)。重複 initialize の timeout-generation fixture も短い timed dispatch の外側で setup initialization を確立し、timeout 済み action の cancellation を観測してから解放し、DB 非依存の隣接 success request と全 response の型を検証して、suite load 下でも受理済み generation とその fence を維持してください(#5001)。 transport teardown test は cancellation を意図的に無視する request を signal gate で保持し、EOF、不正 UTF-8、oversized line を注入して両 drain window を 0 にします。不正入力 error を shutdown 前に試行し、実際に未完了な request、terminal write、shutdown callback ごとの diagnostic を残して全経路が戻ることを検証してください。未完了 request 数を固定する assertion では、stderr の capture と assertion が終わるまで、数に含める全 request をそれぞれ cancellation-insensitive な gate の後ろで保持してください。callback の scheduling によって完了時点が変わる cancellation-responsive な task をその件数に含めてはいけません。初回 EOF snapshot 後に始まる shutdown、base transport の callback/completion drain、inline control write 中の external cancellation、shutdown 起点 HTTP POST の `204` completion、input dispose 失敗時を含む late writer 後まで defer される stdio output dispose を含めます。token を無視する writer、write-gate contention、停止 callback、例外 callback を使い、wall-clock sleep ではなく signal gate で検証してください。事前 cancel 済み EOF drain の coverage は意図的に大きい grace period を渡し、共有の bounded timeout で返却 task を待ち、1 秒未満という wall-clock threshold の代わりに pending operation が未完了のままであることを証明してください(#4978)。 rate-limit-disabled coverage の繰り返し成功 call には軽量な `languages` tool を使い、limiter bypass だけの assertion で `status` の database aggregation cost を繰り返し支払わないでください。 `RateLimiterTests.cs` は bucket 上限の飽和と回復に注入した決定論的 clock を使います。bucket 数が設定上限を超えないこと、単一 partition の上限拒否が最も早い idle expiry を返すこと、通知された retry 境界まで正確に進めると正規の新規 bucket を作成できることを検証してください。layered coverage では burst 1 と、最短 secondary-cap expiry より遅い coarse refill を使い、消費済み coarse token が結合 retry 境界に含まれることも証明してください。MCP integration coverage では 1 つの caller-wide coarse quota が canonical 名をまたぐ一方で既知 tool が secondary per-tool partition を維持することを証明し、missing/non-string/empty/oversized/case-variant/unknown 名と invalid argument を pre-validation に通し、unknown な `batch_query` slot 名が 1 つの固定 bounded partition を共有することを検証してください。 diff --git a/changelog.d/unreleased/5001.internal.md b/changelog.d/unreleased/5001.internal.md index 0abf4afb9..4f5d578ce 100644 --- a/changelog.d/unreleased/5001.internal.md +++ b/changelog.d/unreleased/5001.internal.md @@ -9,8 +9,8 @@ affected: ## English -- **Duplicate-initialize timeout coverage now waits for the timeout signal (#5001)** — the batch test observes the blocked action's cancellation before releasing it and validates every response shape explicitly, preventing scheduler load from turning the expected timeout into a success or a null dereference. +- **Duplicate-initialize timeout coverage now isolates its short timeout (#5001)** — the batch test establishes setup initialization outside timed dispatch, observes the blocked action's cancellation before releasing it, uses a database-independent adjacent success request, and validates every response shape explicitly so scheduler or database load cannot replace the expected duplicate-initialize error. ## 日本語 -- **重複 initialize の timeout coverage が timeout signal を待つようになりました(#5001)** — batch テストは block 中 action を解放する前に cancellation を観測し、全 response shape を明示的に検証するため、scheduler load により期待する timeout が成功応答や null 参照に変わることを防ぎます。 +- **重複 initialize の timeout coverage が短い timeout を分離するようになりました(#5001)** — batch テストは timed dispatch の外側で setup initialization を確立し、block 中 action を解放する前に cancellation を観測して、DB 非依存の隣接 success request と全 response shape を検証するため、scheduler やDB負荷が期待する重複 initialize error を置き換えることを防ぎます。 diff --git a/tests/CodeIndex.Tests/McpServerTests.cs b/tests/CodeIndex.Tests/McpServerTests.cs index a6f2837a3..f1f595ee6 100644 --- a/tests/CodeIndex.Tests/McpServerTests.cs +++ b/tests/CodeIndex.Tests/McpServerTests.cs @@ -7555,14 +7555,15 @@ public async Task ProcessFrameAsync_BatchDuplicateInitializeDoesNotReplaceTimedO { RequestTimeout = TimeSpan.FromMilliseconds(250), }; - Assert.NotNull(await server.ProcessFrameAsync( - """{"jsonrpc":"2.0","id":"batch-generation-init","method":"initialize","params":{}}""")); + var initializeResponse = Assert.IsType(server.HandleMessage(JsonNode.Parse( + """{"jsonrpc":"2.0","id":"batch-generation-init","method":"initialize","params":{}}""")!)); + Assert.IsType(initializeResponse["result"]); var firstStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var firstTimedOut = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var releaseFirst = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var followingPingStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var releaseFollowingPing = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var followingRequestStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var releaseFollowingRequest = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var observedDrainingCaller = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); server.RequestDelayForTestsWithId = async (id, cancellationToken) => { @@ -7575,8 +7576,8 @@ public async Task ProcessFrameAsync_BatchDuplicateInitializeDoesNotReplaceTimedO observedDrainingCaller.TrySetResult(server.CurrentCaller); break; case 454033: - followingPingStarted.TrySetResult(); - await releaseFollowingPing.Task; + followingRequestStarted.TrySetResult(); + await releaseFollowingRequest.Task; break; default: break; @@ -7584,11 +7585,11 @@ public async Task ProcessFrameAsync_BatchDuplicateInitializeDoesNotReplaceTimedO }; var batchTask = server.ProcessFrameAsync( - """[{"jsonrpc":"2.0","id":454031,"method":"tools/call","params":{"name":"status","arguments":{}}},{"jsonrpc":"2.0","id":454032,"method":"initialize","params":{"clientInfo":{"name":"later-generation"}}},{"jsonrpc":"2.0","id":454033,"method":"ping"}]"""); + """[{"jsonrpc":"2.0","id":454031,"method":"tools/call","params":{"name":"status","arguments":{}}},{"jsonrpc":"2.0","id":454032,"method":"initialize","params":{"clientInfo":{"name":"later-generation"}}},{"jsonrpc":"2.0","id":454033,"method":"prompts/list"}]"""); try { await firstStarted.Task.WaitAsync(TestDeterminism.DefaultTimeout); - await followingPingStarted.Task.WaitAsync(TestDeterminism.DefaultTimeout); + await followingRequestStarted.Task.WaitAsync(TestDeterminism.DefaultTimeout); await firstTimedOut.Task.WaitAsync(TestDeterminism.DefaultTimeout); releaseFirst.TrySetResult(); @@ -7596,7 +7597,7 @@ public async Task ProcessFrameAsync_BatchDuplicateInitializeDoesNotReplaceTimedO "unknown", await observedDrainingCaller.Task.WaitAsync(TestDeterminism.DefaultTimeout)); - releaseFollowingPing.TrySetResult(); + releaseFollowingRequest.TrySetResult(); var responseText = Assert.IsType( await batchTask.WaitAsync(TestDeterminism.DefaultTimeout)); var responses = Assert.IsType(JsonNode.Parse(responseText)); @@ -7615,17 +7616,15 @@ public async Task ProcessFrameAsync_BatchDuplicateInitializeDoesNotReplaceTimedO Assert.Equal( "duplicate_initialize", Assert.IsAssignableFrom(duplicateData["reason"]).GetValue()); - var pingResponse = Assert.IsType(responses[2]); - var pingResult = Assert.IsType(pingResponse["result"]); - Assert.Equal( - "ok", - Assert.IsAssignableFrom(pingResult["status"]).GetValue()); + var followingResponse = Assert.IsType(responses[2]); + var followingResult = Assert.IsType(followingResponse["result"]); + Assert.IsType(followingResult["prompts"]); Assert.Equal("unknown", server.CurrentCaller); } finally { releaseFirst.TrySetResult(); - releaseFollowingPing.TrySetResult(); + releaseFollowingRequest.TrySetResult(); await batchTask.WaitAsync(TestDeterminism.DefaultTimeout); } }