Skip to content

Commit ab3f203

Browse files
committed
Fix #4653: stabilize MCP teardown deadline test
1 parent f0a1e76 commit ab3f203

3 files changed

Lines changed: 48 additions & 23 deletions

File tree

TESTING_GUIDE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -417,7 +417,7 @@ Use `docs/test-doc-maintenance-plan.md` before moving oversized suites or adding
417417
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.
418418
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.
419419
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-ID, cancellation targets, and timed-out live actions explicitly to verify their fence/isolation/lease semantics. 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.
420-
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. 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.
420+
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.
421421
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.
422422
`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.
423423
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.
@@ -1125,7 +1125,7 @@ dotnet test --filter "FullyQualifiedName~GitHelperTests"
11251125
stdio response-order test も同じ signal-gated pattern を使い、response serializer で sleep する代わりに synthetic transport が parse-error path を signal するようにします。
11261126
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 を待つことを確認してください。
11271127
JSON-RPC batch-concurrency test は ID-aware signal gate を使います。`maxConcurrency: 1` で outer frame slot による deadlock が起きないこと、完了順と独立した global peak と入力順 response、initialize・重複 ID・cancellation target・timeout 後も動く action の fence/isolation/lease semantics を、各処理を明示的に保持して検証してください。一般 cancellation tombstone cache を満杯にして TTL も経過させ、queue 待ち batch target が引き続き cancellation を受けることと、dispatch 前の return が queued registration を解放して ID を再利用できることも検証します。wall-clock sleep は使わないでください。
1128-
transport teardown test は cancellation を意図的に無視する request を signal gate で保持し、EOF、不正 UTF-8、oversized line を注入して両 drain window を 0 にします。不正入力 error を shutdown 前に試行し、実際に未完了な request、terminal write、shutdown callback ごとの diagnostic を残して全経路が戻ることを検証してください。初回 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 で検証してください。
1128+
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 で検証してください。
11291129
rate-limit-disabled coverage の繰り返し成功 call には軽量な `languages` tool を使い、limiter bypass だけの assertion で `status` の database aggregation cost を繰り返し支払わないでください。
11301130
`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 を共有することを検証してください。
11311131
state-changing notification の認証 coverage は cancellation、roots、shutdown、exit の各 method を table-driven のまま検証し、拒否されたすべての notification が応答を返さず bounded な診断だけを出力し、cancellation、roots、lifecycle state を変更しないことを確認してください。
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
category: internal
3+
issues:
4+
- 4653
5+
affected:
6+
- tests/CodeIndex.Tests/McpServerTests.cs
7+
- TESTING_GUIDE.md
8+
---
9+
10+
## English
11+
12+
- **Release MCP teardown deadline coverage is now deterministic (#4653)** — the unfinished-request diagnostic test keeps every counted request behind a cancellation-insensitive gate until stderr capture and assertions complete, so isolated Release scheduling cannot prune a request early.
13+
14+
## 日本語
15+
16+
- **Release 構成の MCP teardown deadline テストを決定的にしました (#4653)** — 未完了 request の診断テストでは、stderr の capture と assertion が完了するまで件数対象の全 request を cancellation-insensitive な gate で保持し、isolated Release 実行の scheduling による早期 prune を防ぎます。

tests/CodeIndex.Tests/McpServerTests.cs

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -189,37 +189,46 @@ public async Task DrainInFlightTasksAsync_CleanEofWaitsWithinGraceForLegitimateR
189189
public async Task DrainInFlightTasksAsync_DiagnosticCountsOnlyUnfinishedRequests_Issue4435()
190190
{
191191
var completesOnShutdown = Task.Delay(Timeout.InfiniteTimeSpan, _server.ShutdownTokenForTests);
192-
var unfinished = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
193-
var tasks = new List<Task> { completesOnShutdown, unfinished.Task };
192+
var firstUnfinished = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
193+
var secondUnfinished = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
194+
var tasks = new List<Task> { firstUnfinished.Task, secondUnfinished.Task };
194195
var previousError = Console.Error;
195196
using var stderr = new StringWriter();
196197
Console.SetError(stderr);
197198

198199
try
199200
{
200-
var drain = _server.DrainInFlightTasksAsync(
201-
tasks,
202-
TimeSpan.Zero,
203-
TimeSpan.Zero);
204-
await drain.WaitAsync(TimeSpan.FromSeconds(5));
201+
try
202+
{
203+
var drain = _server.DrainInFlightTasksAsync(
204+
tasks,
205+
TimeSpan.Zero,
206+
TimeSpan.Zero);
207+
await drain.WaitAsync(TimeSpan.FromSeconds(5));
208+
}
209+
finally
210+
{
211+
Console.SetError(previousError);
212+
}
213+
214+
Assert.Contains(
215+
"Transport teardown has 2 in-flight request(s); cancelling after 0ms grace period.",
216+
stderr.ToString(),
217+
StringComparison.Ordinal);
218+
Assert.Contains(
219+
"Transport teardown final deadline expired with 2 in-flight request(s) remaining after 0ms post-cancel grace period.",
220+
stderr.ToString(),
221+
StringComparison.Ordinal);
222+
await Assert.ThrowsAnyAsync<OperationCanceledException>(
223+
() => completesOnShutdown.WaitAsync(TestDeterminism.DefaultTimeout));
205224
}
206225
finally
207226
{
208-
Console.SetError(previousError);
227+
firstUnfinished.TrySetResult();
228+
secondUnfinished.TrySetResult();
229+
await Task.WhenAll(firstUnfinished.Task, secondUnfinished.Task)
230+
.WaitAsync(TestDeterminism.DefaultTimeout);
209231
}
210-
211-
Assert.Contains(
212-
"Transport teardown has 2 in-flight request(s); cancelling after 0ms grace period.",
213-
stderr.ToString(),
214-
StringComparison.Ordinal);
215-
Assert.Contains(
216-
"Transport teardown final deadline expired with 2 in-flight request(s) remaining after 0ms post-cancel grace period.",
217-
stderr.ToString(),
218-
StringComparison.Ordinal);
219-
await Assert.ThrowsAnyAsync<OperationCanceledException>(
220-
() => completesOnShutdown.WaitAsync(TestDeterminism.DefaultTimeout));
221-
unfinished.SetResult();
222-
await unfinished.Task.WaitAsync(TestDeterminism.DefaultTimeout);
223232
}
224233

225234
private static bool MethodCallsType(MethodInfo method, Type declaringType)

0 commit comments

Comments
 (0)