diff --git a/changelog.d/unreleased/3306-3308.fixed.md b/changelog.d/unreleased/3306-3308.fixed.md new file mode 100644 index 0000000000..99801f947b --- /dev/null +++ b/changelog.d/unreleased/3306-3308.fixed.md @@ -0,0 +1,17 @@ +--- +category: fixed +issues: + - 3306 + - 3307 + - 3308 +affected: + - tests/CodeIndex.Tests/McpAuditLogTests.cs +--- + +## English + +- **MCP audit request-id truncation coverage now uses a protocol-valid escaped id (#3306, #3307, #3308)**: the regression test now keeps the JSON-RPC id within request validation limits while still making the serialized audit request id exceed the audit display cap. + +## 日本語 + +- **MCP audit request-id truncation のカバレッジが protocol-valid な escaped id を使うようになりました (#3306, #3307, #3308)**: 回帰テストは JSON-RPC id を request validation の上限内に保ちつつ、serialized audit request id が audit 表示上限を超える入力で検証するようになりました。 diff --git a/tests/CodeIndex.Tests/McpAuditLogTests.cs b/tests/CodeIndex.Tests/McpAuditLogTests.cs index 507161074c..94e0ee9ba9 100644 --- a/tests/CodeIndex.Tests/McpAuditLogTests.cs +++ b/tests/CodeIndex.Tests/McpAuditLogTests.cs @@ -1,4 +1,5 @@ using System.Globalization; +using System.Text; using System.Text.Json; using System.Text.Json.Nodes; using CodeIndex.Cli; @@ -126,7 +127,7 @@ public void ToolsCall_MissingToolName_StillEmitsAuditRecord() } [Fact] - public void ToolsCall_OversizedRequestId_IsRejectedBeforeAuditRecord_Issue3104() + public void ToolsCall_OversizedRequestId_IsRejectedBeforeAuditRecord_Issue3104_3308() { using var sink = new AuditLogSink(_auditPath, AuditLogSink.DefaultMaxBytes, includeValues: false); using var server = CreateServer(sink); @@ -462,12 +463,13 @@ public void ToolsCall_CapsAuditArgumentKeyCount_Issue3237() } [Fact] - public void ToolsCall_MaxLengthRequestId_PreservesAuditRequestId_Issue3237() + public void ToolsCall_MaxLengthRequestId_PreservesAuditRequestId_Issue3237_3307() { using var sink = new AuditLogSink(_auditPath, AuditLogSink.DefaultMaxBytes, includeValues: false); using var server = CreateServer(sink); var id = new string('r', McpServer.MaxRequestIdCharacterCount); var serializedId = JsonSerializer.Serialize(id); + Assert.True(serializedId.Length <= AuditLogSink.MaxRequestIdChars); var request = new JsonObject { ["jsonrpc"] = "2.0", @@ -492,6 +494,39 @@ public void ToolsCall_MaxLengthRequestId_PreservesAuditRequestId_Issue3237() Assert.False(record.TryGetProperty("request_id_truncated", out _)); } + [Fact] + public void ToolsCall_EscapedRequestId_TruncatesAuditRequestId_Issue3306() + { + using var sink = new AuditLogSink(_auditPath, AuditLogSink.DefaultMaxBytes, includeValues: false); + using var server = CreateServer(sink); + var id = new string('\u3042', 43); + Assert.True(id.Length <= McpServer.MaxRequestIdCharacterCount); + Assert.True(Encoding.UTF8.GetByteCount(id) <= McpServer.MaxRequestIdByteLength); + var serializedId = JsonSerializer.Serialize(id); + Assert.True(serializedId.Length > AuditLogSink.MaxRequestIdChars); + var requestIdDisplay = McpBoundedText.ForDisplay(serializedId, AuditLogSink.MaxRequestIdChars); + var request = new JsonObject + { + ["jsonrpc"] = "2.0", + ["id"] = id, + ["method"] = "tools/call", + ["params"] = new JsonObject + { + ["name"] = "ping", + ["arguments"] = new JsonObject(), + }, + }; + + var response = server.HandleMessage(request)!; + + Assert.False(response.AsObject().ContainsKey("error")); + Assert.NotNull(response["result"]); + var record = ReadOnlyRecord(); + Assert.Equal(requestIdDisplay.Text, record.GetProperty("request_id").GetString()); + Assert.Equal(serializedId.Length, record.GetProperty("request_id_length").GetInt32()); + Assert.True(record.GetProperty("request_id_truncated").GetBoolean()); + } + [Fact] public void ToolsCall_ValuesOmitted_ByDefault() {