C# Implementation (#2)
C# implementation of SEP-1763 --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
46d96779d6
commit
45f9e0ef27
3 changed files with 13862 additions and 21 deletions
|
|
@ -0,0 +1,504 @@
|
|||
using System.Text.Json.Nodes;
|
||||
using ModelContextProtocol.Interceptors;
|
||||
using ModelContextProtocol.Interceptors.Client;
|
||||
|
||||
namespace ModelContextProtocol.Interceptors.Tests;
|
||||
|
||||
public class InterceptorChainExecutorTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task ExecuteForSendingAsync_MutationsExecuteSequentiallyByPriority()
|
||||
{
|
||||
// Arrange
|
||||
var executionOrder = new List<string>();
|
||||
|
||||
var interceptors = new List<McpClientInterceptor>
|
||||
{
|
||||
CreateMutationInterceptor("high-priority", -100, payload =>
|
||||
{
|
||||
executionOrder.Add("high-priority");
|
||||
return MutationInterceptorResult.Unchanged(payload);
|
||||
}),
|
||||
CreateMutationInterceptor("medium-priority", 0, payload =>
|
||||
{
|
||||
executionOrder.Add("medium-priority");
|
||||
return MutationInterceptorResult.Unchanged(payload);
|
||||
}),
|
||||
CreateMutationInterceptor("low-priority", 100, payload =>
|
||||
{
|
||||
executionOrder.Add("low-priority");
|
||||
return MutationInterceptorResult.Unchanged(payload);
|
||||
})
|
||||
};
|
||||
|
||||
var executor = new InterceptorChainExecutor(interceptors);
|
||||
var payload = JsonNode.Parse("{}");
|
||||
|
||||
// Act
|
||||
await executor.ExecuteForSendingAsync(InterceptorEvents.ToolsCall, payload);
|
||||
|
||||
// Assert - Lower priority numbers execute first
|
||||
Assert.Equal(["high-priority", "medium-priority", "low-priority"], executionOrder);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteForSendingAsync_MutationsWithSamePriority_OrderAlphabetically()
|
||||
{
|
||||
// Arrange
|
||||
var executionOrder = new List<string>();
|
||||
|
||||
var interceptors = new List<McpClientInterceptor>
|
||||
{
|
||||
CreateMutationInterceptor("zebra", 0, payload =>
|
||||
{
|
||||
executionOrder.Add("zebra");
|
||||
return MutationInterceptorResult.Unchanged(payload);
|
||||
}),
|
||||
CreateMutationInterceptor("alpha", 0, payload =>
|
||||
{
|
||||
executionOrder.Add("alpha");
|
||||
return MutationInterceptorResult.Unchanged(payload);
|
||||
}),
|
||||
CreateMutationInterceptor("beta", 0, payload =>
|
||||
{
|
||||
executionOrder.Add("beta");
|
||||
return MutationInterceptorResult.Unchanged(payload);
|
||||
})
|
||||
};
|
||||
|
||||
var executor = new InterceptorChainExecutor(interceptors);
|
||||
var payload = JsonNode.Parse("{}");
|
||||
|
||||
// Act
|
||||
await executor.ExecuteForSendingAsync(InterceptorEvents.ToolsCall, payload);
|
||||
|
||||
// Assert - Same priority, alphabetical order
|
||||
Assert.Equal(["alpha", "beta", "zebra"], executionOrder);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteForSendingAsync_ValidationErrorBlocksExecution()
|
||||
{
|
||||
// Arrange
|
||||
var mutationExecuted = false;
|
||||
|
||||
var interceptors = new List<McpClientInterceptor>
|
||||
{
|
||||
CreateMutationInterceptor("mutator", -1000, payload =>
|
||||
{
|
||||
mutationExecuted = true;
|
||||
return MutationInterceptorResult.Mutated(JsonNode.Parse("{\"mutated\": true}"));
|
||||
}),
|
||||
CreateValidationInterceptor("validator", _ =>
|
||||
ValidationInterceptorResult.Error("Validation failed"))
|
||||
};
|
||||
|
||||
var executor = new InterceptorChainExecutor(interceptors);
|
||||
var payload = JsonNode.Parse("{}");
|
||||
|
||||
// Act
|
||||
var result = await executor.ExecuteForSendingAsync(InterceptorEvents.ToolsCall, payload);
|
||||
|
||||
// Assert
|
||||
// Mutations execute first in sending direction, so mutator runs before validator
|
||||
Assert.True(mutationExecuted, "Mutation should execute before validation in sending direction");
|
||||
Assert.Equal(InterceptorChainStatus.ValidationFailed, result.Status);
|
||||
Assert.NotNull(result.AbortedAt);
|
||||
Assert.Equal("validator", result.AbortedAt.Interceptor);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteForReceivingAsync_ValidationsExecuteBeforeMutations()
|
||||
{
|
||||
// Arrange
|
||||
var executionOrder = new List<string>();
|
||||
|
||||
var interceptors = new List<McpClientInterceptor>
|
||||
{
|
||||
CreateMutationInterceptor("mutator", 0, payload =>
|
||||
{
|
||||
executionOrder.Add("mutator");
|
||||
return MutationInterceptorResult.Unchanged(payload);
|
||||
}),
|
||||
CreateValidationInterceptor("validator", _ =>
|
||||
{
|
||||
executionOrder.Add("validator");
|
||||
return ValidationInterceptorResult.Success();
|
||||
})
|
||||
};
|
||||
|
||||
var executor = new InterceptorChainExecutor(interceptors);
|
||||
var payload = JsonNode.Parse("{}");
|
||||
|
||||
// Act
|
||||
await executor.ExecuteForReceivingAsync(InterceptorEvents.ToolsCall, payload);
|
||||
|
||||
// Assert - In receiving direction: Validate → Mutate
|
||||
Assert.Equal("validator", executionOrder[0]);
|
||||
Assert.Equal("mutator", executionOrder[1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteForReceivingAsync_ValidationErrorBlocksMutations()
|
||||
{
|
||||
// Arrange
|
||||
var mutationExecuted = false;
|
||||
|
||||
var interceptors = new List<McpClientInterceptor>
|
||||
{
|
||||
CreateMutationInterceptor("mutator", 0, payload =>
|
||||
{
|
||||
mutationExecuted = true;
|
||||
return MutationInterceptorResult.Mutated(JsonNode.Parse("{\"mutated\": true}"));
|
||||
}),
|
||||
CreateValidationInterceptor("validator", _ =>
|
||||
ValidationInterceptorResult.Error("Validation failed"))
|
||||
};
|
||||
|
||||
var executor = new InterceptorChainExecutor(interceptors);
|
||||
var payload = JsonNode.Parse("{}");
|
||||
|
||||
// Act
|
||||
var result = await executor.ExecuteForReceivingAsync(InterceptorEvents.ToolsCall, payload);
|
||||
|
||||
// Assert - In receiving direction, validation runs first and blocks mutation
|
||||
Assert.False(mutationExecuted);
|
||||
Assert.Equal(InterceptorChainStatus.ValidationFailed, result.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ObservabilityInterceptor_NeverBlocksExecution()
|
||||
{
|
||||
// Arrange
|
||||
var observabilityExecuted = false;
|
||||
var mutationExecuted = false;
|
||||
|
||||
var interceptors = new List<McpClientInterceptor>
|
||||
{
|
||||
CreateMutationInterceptor("mutator", 0, payload =>
|
||||
{
|
||||
mutationExecuted = true;
|
||||
return MutationInterceptorResult.Unchanged(payload);
|
||||
}),
|
||||
CreateObservabilityInterceptor("observer", _ =>
|
||||
{
|
||||
observabilityExecuted = true;
|
||||
throw new Exception("Observability failure should not block");
|
||||
})
|
||||
};
|
||||
|
||||
var executor = new InterceptorChainExecutor(interceptors);
|
||||
var payload = JsonNode.Parse("{}");
|
||||
|
||||
// Act
|
||||
var result = await executor.ExecuteForSendingAsync(InterceptorEvents.ToolsCall, payload);
|
||||
|
||||
// Assert - Observability failures don't block
|
||||
Assert.True(mutationExecuted);
|
||||
Assert.True(observabilityExecuted);
|
||||
Assert.Equal(InterceptorChainStatus.Success, result.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Timeout_AbortsChainExecution()
|
||||
{
|
||||
// Arrange
|
||||
var interceptors = new List<McpClientInterceptor>
|
||||
{
|
||||
CreateCancellableAsyncMutationInterceptor("slow-mutator", 0, async (payload, ct) =>
|
||||
{
|
||||
// Use the cancellation token so the delay can be cancelled
|
||||
await Task.Delay(5000, ct);
|
||||
return MutationInterceptorResult.Unchanged(payload);
|
||||
})
|
||||
};
|
||||
|
||||
var executor = new InterceptorChainExecutor(interceptors);
|
||||
var payload = JsonNode.Parse("{}");
|
||||
|
||||
// Act
|
||||
var result = await executor.ExecuteForSendingAsync(
|
||||
InterceptorEvents.ToolsCall,
|
||||
payload,
|
||||
timeoutMs: 100);
|
||||
|
||||
// Assert - The chain should abort (either as Timeout or MutationFailed due to cancellation)
|
||||
// The implementation catches OperationCanceledException from the mutation which gets reported
|
||||
// as MutationFailed rather than Timeout depending on timing
|
||||
Assert.NotEqual(InterceptorChainStatus.Success, result.Status);
|
||||
Assert.NotNull(result.AbortedAt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Mutations_PropagatePayloadThroughChain()
|
||||
{
|
||||
// Arrange
|
||||
var interceptors = new List<McpClientInterceptor>
|
||||
{
|
||||
CreateMutationInterceptor("first", -100, payload =>
|
||||
{
|
||||
var obj = payload!.AsObject();
|
||||
obj["step1"] = true;
|
||||
return MutationInterceptorResult.Mutated(obj);
|
||||
}),
|
||||
CreateMutationInterceptor("second", 0, payload =>
|
||||
{
|
||||
var obj = payload!.AsObject();
|
||||
obj["step2"] = true;
|
||||
return MutationInterceptorResult.Mutated(obj);
|
||||
}),
|
||||
CreateMutationInterceptor("third", 100, payload =>
|
||||
{
|
||||
var obj = payload!.AsObject();
|
||||
obj["step3"] = true;
|
||||
return MutationInterceptorResult.Mutated(obj);
|
||||
})
|
||||
};
|
||||
|
||||
var executor = new InterceptorChainExecutor(interceptors);
|
||||
var payload = JsonNode.Parse("{}");
|
||||
|
||||
// Act
|
||||
var result = await executor.ExecuteForSendingAsync(InterceptorEvents.ToolsCall, payload);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(InterceptorChainStatus.Success, result.Status);
|
||||
var final = result.FinalPayload!.AsObject();
|
||||
Assert.True(final["step1"]!.GetValue<bool>());
|
||||
Assert.True(final["step2"]!.GetValue<bool>());
|
||||
Assert.True(final["step3"]!.GetValue<bool>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidationWarning_DoesNotBlockExecution()
|
||||
{
|
||||
// Arrange
|
||||
var mutationExecuted = false;
|
||||
|
||||
var interceptors = new List<McpClientInterceptor>
|
||||
{
|
||||
CreateMutationInterceptor("mutator", -1000, payload =>
|
||||
{
|
||||
mutationExecuted = true;
|
||||
return MutationInterceptorResult.Unchanged(payload);
|
||||
}),
|
||||
CreateValidationInterceptor("validator", _ =>
|
||||
ValidationInterceptorResult.Warning("This is just a warning"))
|
||||
};
|
||||
|
||||
var executor = new InterceptorChainExecutor(interceptors);
|
||||
var payload = JsonNode.Parse("{}");
|
||||
|
||||
// Act
|
||||
var result = await executor.ExecuteForSendingAsync(InterceptorEvents.ToolsCall, payload);
|
||||
|
||||
// Assert - Warnings don't block
|
||||
Assert.True(mutationExecuted);
|
||||
Assert.Equal(InterceptorChainStatus.Success, result.Status);
|
||||
Assert.Equal(1, result.ValidationSummary.Warnings);
|
||||
}
|
||||
|
||||
// Helper methods to create test interceptors
|
||||
|
||||
private static McpClientInterceptor CreateMutationInterceptor(
|
||||
string name,
|
||||
int priority,
|
||||
Func<JsonNode?, MutationInterceptorResult> handler)
|
||||
{
|
||||
return new TestMutationInterceptor(name, priority, handler);
|
||||
}
|
||||
|
||||
private static McpClientInterceptor CreateAsyncMutationInterceptor(
|
||||
string name,
|
||||
int priority,
|
||||
Func<JsonNode?, Task<MutationInterceptorResult>> handler)
|
||||
{
|
||||
return new TestAsyncMutationInterceptor(name, priority, handler);
|
||||
}
|
||||
|
||||
private static McpClientInterceptor CreateCancellableAsyncMutationInterceptor(
|
||||
string name,
|
||||
int priority,
|
||||
Func<JsonNode?, CancellationToken, Task<MutationInterceptorResult>> handler)
|
||||
{
|
||||
return new TestCancellableAsyncMutationInterceptor(name, priority, handler);
|
||||
}
|
||||
|
||||
private static McpClientInterceptor CreateValidationInterceptor(
|
||||
string name,
|
||||
Func<JsonNode?, ValidationInterceptorResult> handler)
|
||||
{
|
||||
return new TestValidationInterceptor(name, handler);
|
||||
}
|
||||
|
||||
private static McpClientInterceptor CreateObservabilityInterceptor(
|
||||
string name,
|
||||
Action<JsonNode?> handler)
|
||||
{
|
||||
return new TestObservabilityInterceptor(name, handler);
|
||||
}
|
||||
}
|
||||
|
||||
// Test interceptor implementations
|
||||
|
||||
file class TestMutationInterceptor : McpClientInterceptor
|
||||
{
|
||||
private readonly Func<JsonNode?, MutationInterceptorResult> _handler;
|
||||
private readonly Interceptor _protocolInterceptor;
|
||||
private readonly IReadOnlyList<object> _metadata = [];
|
||||
|
||||
public TestMutationInterceptor(string name, int priority, Func<JsonNode?, MutationInterceptorResult> handler)
|
||||
{
|
||||
_protocolInterceptor = new Interceptor
|
||||
{
|
||||
Name = name,
|
||||
Type = InterceptorType.Mutation,
|
||||
Phase = InterceptorPhase.Both,
|
||||
Events = [InterceptorEvents.ToolsCall],
|
||||
PriorityHint = new InterceptorPriorityHint(priority)
|
||||
};
|
||||
_handler = handler;
|
||||
}
|
||||
|
||||
public override Interceptor ProtocolInterceptor => _protocolInterceptor;
|
||||
public override IReadOnlyList<object> Metadata => _metadata;
|
||||
|
||||
public override ValueTask<InterceptorResult> InvokeAsync(
|
||||
ClientInterceptorContext<InvokeInterceptorRequestParams> context,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = _handler(context.Params?.Payload);
|
||||
result.Interceptor = ProtocolInterceptor.Name;
|
||||
return new ValueTask<InterceptorResult>(result);
|
||||
}
|
||||
}
|
||||
|
||||
file class TestAsyncMutationInterceptor : McpClientInterceptor
|
||||
{
|
||||
private readonly Func<JsonNode?, Task<MutationInterceptorResult>> _handler;
|
||||
private readonly Interceptor _protocolInterceptor;
|
||||
private readonly IReadOnlyList<object> _metadata = [];
|
||||
|
||||
public TestAsyncMutationInterceptor(string name, int priority, Func<JsonNode?, Task<MutationInterceptorResult>> handler)
|
||||
{
|
||||
_protocolInterceptor = new Interceptor
|
||||
{
|
||||
Name = name,
|
||||
Type = InterceptorType.Mutation,
|
||||
Phase = InterceptorPhase.Both,
|
||||
Events = [InterceptorEvents.ToolsCall],
|
||||
PriorityHint = new InterceptorPriorityHint(priority)
|
||||
};
|
||||
_handler = handler;
|
||||
}
|
||||
|
||||
public override Interceptor ProtocolInterceptor => _protocolInterceptor;
|
||||
public override IReadOnlyList<object> Metadata => _metadata;
|
||||
|
||||
public override async ValueTask<InterceptorResult> InvokeAsync(
|
||||
ClientInterceptorContext<InvokeInterceptorRequestParams> context,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = await _handler(context.Params?.Payload);
|
||||
result.Interceptor = ProtocolInterceptor.Name;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
file class TestCancellableAsyncMutationInterceptor : McpClientInterceptor
|
||||
{
|
||||
private readonly Func<JsonNode?, CancellationToken, Task<MutationInterceptorResult>> _handler;
|
||||
private readonly Interceptor _protocolInterceptor;
|
||||
private readonly IReadOnlyList<object> _metadata = [];
|
||||
|
||||
public TestCancellableAsyncMutationInterceptor(string name, int priority, Func<JsonNode?, CancellationToken, Task<MutationInterceptorResult>> handler)
|
||||
{
|
||||
_protocolInterceptor = new Interceptor
|
||||
{
|
||||
Name = name,
|
||||
Type = InterceptorType.Mutation,
|
||||
Phase = InterceptorPhase.Both,
|
||||
Events = [InterceptorEvents.ToolsCall],
|
||||
PriorityHint = new InterceptorPriorityHint(priority)
|
||||
};
|
||||
_handler = handler;
|
||||
}
|
||||
|
||||
public override Interceptor ProtocolInterceptor => _protocolInterceptor;
|
||||
public override IReadOnlyList<object> Metadata => _metadata;
|
||||
|
||||
public override async ValueTask<InterceptorResult> InvokeAsync(
|
||||
ClientInterceptorContext<InvokeInterceptorRequestParams> context,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = await _handler(context.Params?.Payload, cancellationToken);
|
||||
result.Interceptor = ProtocolInterceptor.Name;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
file class TestValidationInterceptor : McpClientInterceptor
|
||||
{
|
||||
private readonly Func<JsonNode?, ValidationInterceptorResult> _handler;
|
||||
private readonly Interceptor _protocolInterceptor;
|
||||
private readonly IReadOnlyList<object> _metadata = [];
|
||||
|
||||
public TestValidationInterceptor(string name, Func<JsonNode?, ValidationInterceptorResult> handler)
|
||||
{
|
||||
_protocolInterceptor = new Interceptor
|
||||
{
|
||||
Name = name,
|
||||
Type = InterceptorType.Validation,
|
||||
Phase = InterceptorPhase.Both,
|
||||
Events = [InterceptorEvents.ToolsCall]
|
||||
};
|
||||
_handler = handler;
|
||||
}
|
||||
|
||||
public override Interceptor ProtocolInterceptor => _protocolInterceptor;
|
||||
public override IReadOnlyList<object> Metadata => _metadata;
|
||||
|
||||
public override ValueTask<InterceptorResult> InvokeAsync(
|
||||
ClientInterceptorContext<InvokeInterceptorRequestParams> context,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = _handler(context.Params?.Payload);
|
||||
result.Interceptor = ProtocolInterceptor.Name;
|
||||
result.Phase = context.Params?.Phase ?? InterceptorPhase.Request;
|
||||
return new ValueTask<InterceptorResult>(result);
|
||||
}
|
||||
}
|
||||
|
||||
file class TestObservabilityInterceptor : McpClientInterceptor
|
||||
{
|
||||
private readonly Action<JsonNode?> _handler;
|
||||
private readonly Interceptor _protocolInterceptor;
|
||||
private readonly IReadOnlyList<object> _metadata = [];
|
||||
|
||||
public TestObservabilityInterceptor(string name, Action<JsonNode?> handler)
|
||||
{
|
||||
_protocolInterceptor = new Interceptor
|
||||
{
|
||||
Name = name,
|
||||
Type = InterceptorType.Observability,
|
||||
Phase = InterceptorPhase.Both,
|
||||
Events = [InterceptorEvents.ToolsCall]
|
||||
};
|
||||
_handler = handler;
|
||||
}
|
||||
|
||||
public override Interceptor ProtocolInterceptor => _protocolInterceptor;
|
||||
public override IReadOnlyList<object> Metadata => _metadata;
|
||||
|
||||
public override ValueTask<InterceptorResult> InvokeAsync(
|
||||
ClientInterceptorContext<InvokeInterceptorRequestParams> context,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_handler(context.Params?.Payload);
|
||||
return new ValueTask<InterceptorResult>(new ObservabilityInterceptorResult
|
||||
{
|
||||
Interceptor = ProtocolInterceptor.Name,
|
||||
Observed = true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\ModelContextProtocol.Interceptors\ModelContextProtocol.Interceptors.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,328 @@
|
|||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using ModelContextProtocol.Interceptors;
|
||||
using ModelContextProtocol.Interceptors.Protocol.Llm;
|
||||
|
||||
namespace ModelContextProtocol.Interceptors.Tests;
|
||||
|
||||
public class ProtocolTypesTests
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
WriteIndented = false
|
||||
};
|
||||
|
||||
#region InterceptorType Serialization
|
||||
|
||||
[Theory]
|
||||
[InlineData(InterceptorType.Validation, "\"validation\"")]
|
||||
[InlineData(InterceptorType.Mutation, "\"mutation\"")]
|
||||
[InlineData(InterceptorType.Observability, "\"observability\"")]
|
||||
public void InterceptorType_SerializesToCorrectJsonString(InterceptorType type, string expected)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(type, JsonOptions);
|
||||
Assert.Equal(expected, json);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("\"validation\"", InterceptorType.Validation)]
|
||||
[InlineData("\"mutation\"", InterceptorType.Mutation)]
|
||||
[InlineData("\"observability\"", InterceptorType.Observability)]
|
||||
public void InterceptorType_DeserializesFromJsonString(string json, InterceptorType expected)
|
||||
{
|
||||
var type = JsonSerializer.Deserialize<InterceptorType>(json, JsonOptions);
|
||||
Assert.Equal(expected, type);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region InterceptorPhase Serialization
|
||||
|
||||
[Theory]
|
||||
[InlineData(InterceptorPhase.Request, "\"request\"")]
|
||||
[InlineData(InterceptorPhase.Response, "\"response\"")]
|
||||
[InlineData(InterceptorPhase.Both, "\"both\"")]
|
||||
public void InterceptorPhase_SerializesToCorrectJsonString(InterceptorPhase phase, string expected)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(phase, JsonOptions);
|
||||
Assert.Equal(expected, json);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region InterceptorPriorityHint Serialization
|
||||
|
||||
[Fact]
|
||||
public void InterceptorPriorityHint_SerializesAsNumber_WhenBothPhasesEqual()
|
||||
{
|
||||
var hint = new InterceptorPriorityHint(100);
|
||||
var json = JsonSerializer.Serialize(hint, JsonOptions);
|
||||
Assert.Equal("100", json);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InterceptorPriorityHint_SerializesAsObject_WhenPhasesDiffer()
|
||||
{
|
||||
var hint = new InterceptorPriorityHint(-100, 50);
|
||||
var json = JsonSerializer.Serialize(hint, JsonOptions);
|
||||
|
||||
var obj = JsonSerializer.Deserialize<JsonObject>(json);
|
||||
Assert.NotNull(obj);
|
||||
Assert.Equal(-100, obj["request"]!.GetValue<int>());
|
||||
Assert.Equal(50, obj["response"]!.GetValue<int>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InterceptorPriorityHint_DeserializesFromNumber()
|
||||
{
|
||||
var hint = JsonSerializer.Deserialize<InterceptorPriorityHint>("100", JsonOptions);
|
||||
Assert.Equal(100, hint.GetPriorityForPhase(InterceptorPhase.Request));
|
||||
Assert.Equal(100, hint.GetPriorityForPhase(InterceptorPhase.Response));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InterceptorPriorityHint_DeserializesFromObject()
|
||||
{
|
||||
var json = """{"request": -100, "response": 50}""";
|
||||
var hint = JsonSerializer.Deserialize<InterceptorPriorityHint>(json, JsonOptions);
|
||||
Assert.Equal(-100, hint.GetPriorityForPhase(InterceptorPhase.Request));
|
||||
Assert.Equal(50, hint.GetPriorityForPhase(InterceptorPhase.Response));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Interceptor Serialization
|
||||
|
||||
[Fact]
|
||||
public void Interceptor_SerializesWithAllFields()
|
||||
{
|
||||
var interceptor = new Interceptor
|
||||
{
|
||||
Name = "test-interceptor",
|
||||
Version = "1.0.0",
|
||||
Description = "A test interceptor",
|
||||
Events = [InterceptorEvents.ToolsCall, InterceptorEvents.LlmCompletion],
|
||||
Type = InterceptorType.Validation,
|
||||
Phase = InterceptorPhase.Request,
|
||||
PriorityHint = new InterceptorPriorityHint(100)
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(interceptor, JsonOptions);
|
||||
var deserialized = JsonSerializer.Deserialize<Interceptor>(json, JsonOptions);
|
||||
|
||||
Assert.NotNull(deserialized);
|
||||
Assert.Equal("test-interceptor", deserialized.Name);
|
||||
Assert.Equal("1.0.0", deserialized.Version);
|
||||
Assert.Equal("A test interceptor", deserialized.Description);
|
||||
Assert.Contains(InterceptorEvents.ToolsCall, deserialized.Events);
|
||||
Assert.Contains(InterceptorEvents.LlmCompletion, deserialized.Events);
|
||||
Assert.Equal(InterceptorType.Validation, deserialized.Type);
|
||||
Assert.Equal(InterceptorPhase.Request, deserialized.Phase);
|
||||
Assert.NotNull(deserialized.PriorityHint);
|
||||
Assert.Equal(100, deserialized.PriorityHint.Value.GetPriorityForPhase(InterceptorPhase.Request));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ValidationInterceptorResult Serialization
|
||||
|
||||
[Fact]
|
||||
public void ValidationInterceptorResult_SerializesCorrectly()
|
||||
{
|
||||
var result = new ValidationInterceptorResult
|
||||
{
|
||||
Valid = false,
|
||||
Severity = ValidationSeverity.Error,
|
||||
Messages =
|
||||
[
|
||||
new() { Path = "$.name", Message = "Name is required", Severity = ValidationSeverity.Error }
|
||||
],
|
||||
Suggestions =
|
||||
[
|
||||
new() { Path = "$.name", Value = JsonNode.Parse("\"default\"") }
|
||||
]
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(result, JsonOptions);
|
||||
var deserialized = JsonSerializer.Deserialize<ValidationInterceptorResult>(json, JsonOptions);
|
||||
|
||||
Assert.NotNull(deserialized);
|
||||
Assert.False(deserialized.Valid);
|
||||
Assert.Equal(ValidationSeverity.Error, deserialized.Severity);
|
||||
Assert.Single(deserialized.Messages!);
|
||||
Assert.Equal("$.name", deserialized.Messages![0].Path);
|
||||
Assert.Single(deserialized.Suggestions!);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region MutationInterceptorResult Serialization
|
||||
|
||||
[Fact]
|
||||
public void MutationInterceptorResult_SerializesCorrectly()
|
||||
{
|
||||
var result = MutationInterceptorResult.Mutated(JsonNode.Parse("{\"modified\": true}"));
|
||||
|
||||
var json = JsonSerializer.Serialize(result, JsonOptions);
|
||||
var deserialized = JsonSerializer.Deserialize<MutationInterceptorResult>(json, JsonOptions);
|
||||
|
||||
Assert.NotNull(deserialized);
|
||||
Assert.True(deserialized.Modified);
|
||||
Assert.NotNull(deserialized.Payload);
|
||||
Assert.True(deserialized.Payload["modified"]!.GetValue<bool>());
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region InterceptorChainResult Serialization
|
||||
|
||||
[Fact]
|
||||
public void InterceptorChainStatus_SerializesCorrectly()
|
||||
{
|
||||
// Test chain status enum serialization
|
||||
Assert.Equal("\"success\"", JsonSerializer.Serialize(InterceptorChainStatus.Success, JsonOptions));
|
||||
Assert.Equal("\"validation_failed\"", JsonSerializer.Serialize(InterceptorChainStatus.ValidationFailed, JsonOptions));
|
||||
Assert.Equal("\"mutation_failed\"", JsonSerializer.Serialize(InterceptorChainStatus.MutationFailed, JsonOptions));
|
||||
Assert.Equal("\"timeout\"", JsonSerializer.Serialize(InterceptorChainStatus.Timeout, JsonOptions));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidationSummary_SerializesCorrectly()
|
||||
{
|
||||
var summary = new ValidationSummary { Errors = 1, Warnings = 2, Infos = 3 };
|
||||
var json = JsonSerializer.Serialize(summary, JsonOptions);
|
||||
var deserialized = JsonSerializer.Deserialize<ValidationSummary>(json, JsonOptions);
|
||||
|
||||
Assert.NotNull(deserialized);
|
||||
Assert.Equal(1, deserialized.Errors);
|
||||
Assert.Equal(2, deserialized.Warnings);
|
||||
Assert.Equal(3, deserialized.Infos);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ChainAbortInfo_SerializesCorrectly()
|
||||
{
|
||||
var abortInfo = new ChainAbortInfo
|
||||
{
|
||||
Interceptor = "pii-validator",
|
||||
Reason = "PII detected in request",
|
||||
Type = "validation"
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(abortInfo, JsonOptions);
|
||||
var deserialized = JsonSerializer.Deserialize<ChainAbortInfo>(json, JsonOptions);
|
||||
|
||||
Assert.NotNull(deserialized);
|
||||
Assert.Equal("pii-validator", deserialized.Interceptor);
|
||||
Assert.Equal("PII detected in request", deserialized.Reason);
|
||||
Assert.Equal("validation", deserialized.Type);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region LlmCompletionRequest Serialization
|
||||
|
||||
[Fact]
|
||||
public void LlmCompletionRequest_SerializesCorrectly()
|
||||
{
|
||||
var request = new LlmCompletionRequest
|
||||
{
|
||||
Model = "gpt-4",
|
||||
Messages =
|
||||
[
|
||||
LlmMessage.System("You are a helpful assistant."),
|
||||
LlmMessage.User("Hello!")
|
||||
],
|
||||
Temperature = 0.7,
|
||||
MaxTokens = 1000,
|
||||
TopP = 0.9
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(request, JsonOptions);
|
||||
var deserialized = JsonSerializer.Deserialize<LlmCompletionRequest>(json, JsonOptions);
|
||||
|
||||
Assert.NotNull(deserialized);
|
||||
Assert.Equal("gpt-4", deserialized.Model);
|
||||
Assert.Equal(2, deserialized.Messages.Count);
|
||||
Assert.Equal(LlmMessageRole.System, deserialized.Messages[0].Role);
|
||||
Assert.Equal("You are a helpful assistant.", deserialized.Messages[0].Content);
|
||||
Assert.Equal(LlmMessageRole.User, deserialized.Messages[1].Role);
|
||||
Assert.Equal(0.7, deserialized.Temperature);
|
||||
Assert.Equal(1000, deserialized.MaxTokens);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LlmMessage_ToolMessage_SerializesCorrectly()
|
||||
{
|
||||
var message = LlmMessage.Tool("call_abc123", "{\"result\": \"success\"}", "get_weather");
|
||||
|
||||
var json = JsonSerializer.Serialize(message, JsonOptions);
|
||||
var deserialized = JsonSerializer.Deserialize<LlmMessage>(json, JsonOptions);
|
||||
|
||||
Assert.NotNull(deserialized);
|
||||
Assert.Equal(LlmMessageRole.Tool, deserialized.Role);
|
||||
Assert.Equal("call_abc123", deserialized.ToolCallId);
|
||||
Assert.Equal("{\"result\": \"success\"}", deserialized.Content);
|
||||
Assert.Equal("get_weather", deserialized.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LlmMessage_AssistantWithToolCalls_SerializesCorrectly()
|
||||
{
|
||||
var message = LlmMessage.Assistant(null, [
|
||||
new LlmToolCall
|
||||
{
|
||||
Id = "call_abc123",
|
||||
Type = "function",
|
||||
Function = new LlmFunctionCall
|
||||
{
|
||||
Name = "get_weather",
|
||||
Arguments = "{\"location\": \"NYC\"}"
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
var json = JsonSerializer.Serialize(message, JsonOptions);
|
||||
var deserialized = JsonSerializer.Deserialize<LlmMessage>(json, JsonOptions);
|
||||
|
||||
Assert.NotNull(deserialized);
|
||||
Assert.Equal(LlmMessageRole.Assistant, deserialized.Role);
|
||||
Assert.NotNull(deserialized.ToolCalls);
|
||||
Assert.Single(deserialized.ToolCalls);
|
||||
Assert.Equal("call_abc123", deserialized.ToolCalls[0].Id);
|
||||
Assert.Equal("get_weather", deserialized.ToolCalls[0].Function!.Name);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region InterceptorEvents Constants
|
||||
|
||||
[Fact]
|
||||
public void InterceptorEvents_HasAllRequiredEvents()
|
||||
{
|
||||
// Server features
|
||||
Assert.Equal("tools/list", InterceptorEvents.ToolsList);
|
||||
Assert.Equal("tools/call", InterceptorEvents.ToolsCall);
|
||||
Assert.Equal("prompts/list", InterceptorEvents.PromptsList);
|
||||
Assert.Equal("prompts/get", InterceptorEvents.PromptsGet);
|
||||
Assert.Equal("resources/list", InterceptorEvents.ResourcesList);
|
||||
Assert.Equal("resources/read", InterceptorEvents.ResourcesRead);
|
||||
Assert.Equal("resources/subscribe", InterceptorEvents.ResourcesSubscribe);
|
||||
|
||||
// Client features
|
||||
Assert.Equal("sampling/createMessage", InterceptorEvents.SamplingCreateMessage);
|
||||
Assert.Equal("elicitation/create", InterceptorEvents.ElicitationCreate);
|
||||
Assert.Equal("roots/list", InterceptorEvents.RootsList);
|
||||
|
||||
// LLM interactions
|
||||
Assert.Equal("llm/completion", InterceptorEvents.LlmCompletion);
|
||||
|
||||
// Wildcards
|
||||
Assert.Equal("*/request", InterceptorEvents.AllRequests);
|
||||
Assert.Equal("*/response", InterceptorEvents.AllResponses);
|
||||
Assert.Equal("*", InterceptorEvents.All);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Loading…
Reference in a new issue