Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions TESTING_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>` 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.
Expand Down Expand Up @@ -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<T>` で初期化します。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 を維持してください。
Expand Down
16 changes: 16 additions & 0 deletions changelog.d/unreleased/4807.fixed.md
Original file line number Diff line number Diff line change
@@ -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 を使い、並行処理の回帰時に対応可能なテスト失敗を返すようにしました。
15 changes: 10 additions & 5 deletions tests/CodeIndex.Tests/McpServerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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":{}}"""));
Expand All @@ -7053,27 +7053,32 @@ public async Task ProcessFrameAsync_TimedOutActionRetainsConcurrencyLeaseUntilIt
return releaseFirst.Task;
}

secondStarted.TrySetResult();
if (id?.GetValue<int>() == 453652)
secondStarted.TrySetResult();
return Task.CompletedTask;
};

var firstResponseTask = server.ProcessFrameAsync(
"""{"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<string>());
var firstResponse = Assert.IsType<JsonObject>(JsonNode.Parse(Assert.IsType<string>(firstResponseText)));
var firstError = Assert.IsType<JsonObject>(firstResponse["error"]);
Assert.Equal("Request timed out", Assert.IsAssignableFrom<JsonValue>(firstError["message"]).GetValue<string>());
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<string>());
var secondResponse = Assert.IsType<JsonObject>(JsonNode.Parse(Assert.IsType<string>(secondResponseText)));
var secondResult = Assert.IsType<JsonObject>(secondResponse["result"]);
Assert.Equal("ok", Assert.IsAssignableFrom<JsonValue>(secondResult["status"]).GetValue<string>());
Assert.Equal(1, server.AvailableConcurrencySlotsForTests);
}

Expand Down
Loading