diff --git a/TESTING_GUIDE.md b/TESTING_GUIDE.md index 0a293d5e7..b8ca8ae04 100644 --- a/TESTING_GUIDE.md +++ b/TESTING_GUIDE.md @@ -509,6 +509,7 @@ Use `docs/test-doc-maintenance-plan.md` before moving oversized suites or adding Keep `PathCasing.IgnoreCaseProbeForTesting` execution-context-local, and never write an injected cache-miss result into the shared path-casing cache. Existing production-seeded cache entries still take precedence; injected probe outcomes must flow to child tasks owned by that test without leaking into concurrently running LSP or other test classes through either the delegate or cached values. - `McpServer*Tests.cs` MCP JSON-RPC behavior and tool outputs. Large server coverage is split into focused partial suites for tool calls, tool listing, protocol/session handling, and error handling while the root `McpServerTests` part keeps shared seeded fixture state. Request-timeout tests use signal-gated delay hooks instead of fixed sleeps: start the request, confirm the hook has begun, then await the timeout response with a bounded wait so they pay only the configured timeout while still proving in-flight actions drain after the timeout response. + The single-request timeout-lease regression uses an ID-specific dispatch signal, a one-second execution timeout for scheduler headroom, typed response-node assertions, and `TestDeterminism.AssertTaskRemainsBlockedAsync` for the queued request. Keep those checks together so full-suite load produces an actionable assertion instead of a null dereference (#4807). The root seeded database/server fixture is initialized through a thread-safe `Lazy` only when a test accesses its default fixture path. Static helpers and tests that build their own server or transport must not pay schema creation and seed cost; concurrent fixture access must still publish exactly one database/server pair. Protocol negotiation coverage keeps `2025-06-18`, `2025-03-26`, and `2024-11-05` in one shared version-echo fixture. The Codex compatibility regression separately uses the lifecycle-enforcing transport to send a `2025-06-18` initialize, `notifications/initialized`, and `tools/list`, because a direct handler assertion would not catch initialization-gate failures. Request-id telemetry coverage uses credential-shaped and high-cardinality ids to prove raw values never reach stderr prefixes/events, Activity tags, MCP metrics, audit records, or timeout logs/status. Assert the fixed opaque token plus consistent type and decoded string-value UTF-16 code-unit length (`null` = `0`), different injected process salts, JSON type domain separation for equal textual values, and collapse to one overflow token after the distinct-id budget (including concurrent creation). Keep a CLI metrics negative case that omits all request-id fields. @@ -1356,6 +1357,7 @@ dotnet test --filter "FullyQualifiedName~GitHelperTests" `PathCasing.IgnoreCaseProbeForTesting` は execution context ごとに隔離し、注入した cache-miss 結果を共有 path-casing cache へ書き込まないでください。既存の production seed 済み cache entry は引き続き優先します。注入した probe 結果はその test が所有する child task には引き継ぎますが、delegate と cache のどちらからも並列実行中の LSP や他の test class へ漏らしてはいけません。 - `McpServer*Tests.cs` MCP の JSON-RPC 挙動とツール出力のテスト。大きな server coverage は tool call、tool listing、protocol/session handling、error handling ごとの focused partial suite に分割し、共有の seed 済み fixture 状態は root 側の `McpServerTests` に残します。request-timeout test は固定 sleep ではなく signal-gated delay hook を使います。request を開始し、hook が始まったことを確認してから timeout response を bounded wait で待つことで、timeout response 後に in-flight action が drain されることは保ったまま、設定した timeout 分だけを待つようにします。 + single-request の timeout-lease 回帰テストでは、ID 別の dispatch signal、scheduler の余裕を確保する 1 秒の execution timeout、型付き response-node assertion、queue 待ち request に対する `TestDeterminism.AssertTaskRemainsBlockedAsync` を使います。full-suite 負荷でも null 参照ではなく対応可能な assertion を返すよう、これらの検証をまとめて維持してください(#4807)。 root の seed 済み database/server fixture は、test が既定 fixture path へアクセスした場合だけ thread-safe な `Lazy` で初期化します。static helper や独自 server / transport を構築する test は未使用 schema の作成・seed cost を支払わず、並行 fixture access でも database/server pair を必ず1組だけ公開してください。 protocol negotiation coverage は `2025-06-18`、`2025-03-26`、`2024-11-05` を共通の version-echo fixture にまとめます。Codex 互換性の回帰テストでは別途 lifecycle を強制する transport を使い、`2025-06-18` の initialize、`notifications/initialized`、`tools/list` までを送ります。direct handler の assertion だけでは initialization gate の失敗を検出できないためです。 request-id telemetry coverage では credential 風および high-cardinality な id を使い、生値が stderr の prefix / event、Activity tag、MCP metrics、audit record、timeout log / status のどこにも出ないことを検証します。固定長 opaque token と、型および decode 後の string 値の UTF-16 code unit 数(`null` は `0`)が全 surface で一致することに加え、注入した process salt ごとの差、text が同じ JSON type 間の domain separation、concurrent creation を含む distinct-id budget 超過後の単一 overflow token への集約を確認してください。CLI metrics では request-id field をすべて省略する negative case を維持してください。 diff --git a/changelog.d/unreleased/4807.fixed.md b/changelog.d/unreleased/4807.fixed.md new file mode 100644 index 000000000..7181fe4aa --- /dev/null +++ b/changelog.d/unreleased/4807.fixed.md @@ -0,0 +1,16 @@ +--- +category: fixed +issues: + - 4807 +affected: + - tests/CodeIndex.Tests/McpServerTests.cs + - TESTING_GUIDE.md +--- + +## English + +- **MCP timeout-lease coverage no longer null-dereferences under full-suite load (#4807)** — the regression test now uses an ID-specific readiness signal, scheduler headroom, a bounded blocked-state observation, and typed JSON response assertions so concurrency regressions produce actionable test failures. + +## 日本語 + +- **MCP timeout-lease のカバレッジが full-suite 負荷時に null 参照しないようになりました(#4807)** — 回帰テストで ID 別の readiness signal、scheduler の余裕、上限付きの blocked-state 観測、型付き JSON response assertion を使い、並行処理の回帰時に対応可能なテスト失敗を返すようにしました。 diff --git a/tests/CodeIndex.Tests/McpServerTests.cs b/tests/CodeIndex.Tests/McpServerTests.cs index 3e76683b5..5b6686712 100644 --- a/tests/CodeIndex.Tests/McpServerTests.cs +++ b/tests/CodeIndex.Tests/McpServerTests.cs @@ -7031,7 +7031,7 @@ public async Task ProcessFrameAsync_TimedOutActionRetainsConcurrencyLeaseUntilIt toolFilter: null, maxConcurrency: 1) { - RequestTimeout = TimeSpan.FromMilliseconds(100), + RequestTimeout = TimeSpan.FromSeconds(1), }; Assert.NotNull(await server.ProcessFrameAsync( """{"jsonrpc":"2.0","id":"issue-4536-timeout-init","method":"initialize","params":{}}""")); @@ -7053,7 +7053,8 @@ public async Task ProcessFrameAsync_TimedOutActionRetainsConcurrencyLeaseUntilIt return releaseFirst.Task; } - secondStarted.TrySetResult(); + if (id?.GetValue() == 453652) + secondStarted.TrySetResult(); return Task.CompletedTask; }; @@ -7061,19 +7062,23 @@ public async Task ProcessFrameAsync_TimedOutActionRetainsConcurrencyLeaseUntilIt """{"jsonrpc":"2.0","id":453651,"method":"ping"}"""); await firstStarted.Task.WaitAsync(TestDeterminism.DefaultTimeout); var firstResponseText = await firstResponseTask.WaitAsync(TestDeterminism.DefaultTimeout); - Assert.Equal("Request timed out", JsonNode.Parse(firstResponseText!)!["error"]!["message"]!.GetValue()); + var firstResponse = Assert.IsType(JsonNode.Parse(Assert.IsType(firstResponseText))); + var firstError = Assert.IsType(firstResponse["error"]); + Assert.Equal("Request timed out", Assert.IsAssignableFrom(firstError["message"]).GetValue()); Assert.Equal(0, server.AvailableConcurrencySlotsForTests); var secondResponseTask = server.ProcessFrameAsync( """{"jsonrpc":"2.0","id":453652,"method":"ping"}"""); await secondRegistered.Task.WaitAsync(TestDeterminism.DefaultTimeout); - Assert.False(secondStarted.Task.IsCompleted); + await TestDeterminism.AssertTaskRemainsBlockedAsync(secondStarted.Task); Assert.Equal(0, server.AvailableConcurrencySlotsForTests); releaseFirst.TrySetResult(); await secondStarted.Task.WaitAsync(TestDeterminism.DefaultTimeout); var secondResponseText = await secondResponseTask.WaitAsync(TestDeterminism.DefaultTimeout); - Assert.Equal("ok", JsonNode.Parse(secondResponseText!)!["result"]!["status"]!.GetValue()); + var secondResponse = Assert.IsType(JsonNode.Parse(Assert.IsType(secondResponseText))); + var secondResult = Assert.IsType(secondResponse["result"]); + Assert.Equal("ok", Assert.IsAssignableFrom(secondResult["status"]).GetValue()); Assert.Equal(1, server.AvailableConcurrencySlotsForTests); }