Author unit tests for the AG-UI .NET SDK (the *.UnitTests projects), following the SDK's serialization and compatibility conventions. USE FOR: adding unit-test coverage for a new type/method in AGUI.Abstractions/Formatting/Protobuf/Client/Server, event serialization round-trips, JsonDocument property-name assertions, backward-compatibility fixtures against TypeScript JSON, protobuf codec round-trips, client builder/handler tests, server ChatResponseUpdate conversion tests, SSE formatter tests. DO NOT USE FOR: HTTP pipeline / WebApplicationFactory end-to-end tests (use the agui-dotnet-integration-tests skill), cross-language TS↔C# server-parity tests (use the cross-language test skill).
95%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
Conventions for the tests/*.UnitTests projects. Mined from sdks/dotnet/AGENTS.md
and existing tests. An agent gets these wrong by default: asserting on deserialized
objects (misses naming bugs), comparing full JSON strings (fragile), using reflection,
or skipping the FixtureLoader compatibility pattern.
All commands run from sdks/dotnet/.
| Project | Focus |
|---|---|
tests/AGUI.Abstractions.UnitTests/ | Event serialization round-trips; backward compat vs TypeScript fixtures (Compatibility/) |
tests/AGUI.Protobuf.UnitTests/ | Protobuf codec round-trips; JsonElement↔protobuf Value conversion |
tests/AGUI.Client.UnitTests/ | Client builders, content-negotiation handler, transport, protocol rules |
tests/AGUI.Server.UnitTests/ | ChatResponseUpdate → AG-UI event conversion |
tests/AGUI.Formatting.UnitTests/ | SSE event-stream formatter (read/write, media type) |
Run one project: dotnet test tests/AGUI.Abstractions.UnitTests/
Events/ subfolders).
Compatibility tests go in the Compatibility/ subfolder.{TypeUnderTest}Test (RunStartedEventTest, ToolCallBuilderTest).
Compatibility class: {Category}CompatibilityTest (RunEventsCompatibilityTest).public sealed class, one class per file, file name matches type.[Fact] for single cases, [Theory] + [InlineData] for parameterized cases.AGUIJsonSerializerContext.Default.{Type} —
never JsonSerializer.Serialize<object>(...) or hand-rolled options.Serialize via the source-gen context, parse with JsonDocument, assert each camelCase
property name AND the type discriminator. This is what catches [JsonPropertyName] bugs.
[Fact]
public void Serialization_RoundTrips()
{
var evt = new RunStartedEvent { ThreadId = "t1", RunId = "r1", Timestamp = 1234567890 };
var json = JsonSerializer.Serialize(evt, AGUIJsonSerializerContext.Default.RunStartedEvent);
using var doc = JsonDocument.Parse(json);
Assert.Equal("RUN_STARTED", doc.RootElement.GetProperty("type").GetString());
Assert.Equal("t1", doc.RootElement.GetProperty("threadId").GetString());
Assert.Equal("r1", doc.RootElement.GetProperty("runId").GetString());
Assert.Equal(1234567890, doc.RootElement.GetProperty("timestamp").GetInt64());
}Compatibility/Fixtures/*.json are produced by the TypeScript reference implementation.
Deserialize them into .NET types and assert the values — this catches wire-format drift.
Load arrays with FixtureLoader; write one test method per event shape.
public sealed class RunEventsCompatibilityTest
{
private readonly JsonElement[] _fixtures = FixtureLoader.LoadFixture("run-events.json");
[Fact]
public void RunStartedEvent_DeserializesFromTypeScriptPayload()
{
var evt = FixtureLoader.DeserializeAsBaseEvent(_fixtures[0]);
var typed = Assert.IsType<RunStartedEvent>(evt);
Assert.Equal("thread-1234", typed.ThreadId);
Assert.Equal("run-5678", typed.RunId);
}
}Adding a fixture: place the TS-produced JSON array under Compatibility/Fixtures/
(embedded resource), then index into the loaded array per shape.
Encode→decode and assert typed values. Compare JsonElement payloads with
JsonTestHelpers.AssertEqual (deep-equals), not string compare. ProtoValueConverter
tests verify JsonElement↔Value for each ValueKind.
var result = RoundTrip(new RunStartedEvent { ThreadId = "thread-1", RunId = "run-1" });
Assert.Equal("thread-1", result.ThreadId);
JsonTestHelpers.AssertEqual(expectedElement, result.RawEvent!.Value);Drive the builder/handler through its real API and assert the produced MEAI types.
ToolCallBuilder/TextMessageBuilder: feed events, flush, assert FunctionCallContent,
ConversationId, ResponseId. AGUIEventStreamHandler: assert content-negotiation
Accept header ordering via a test inner handler. Standard xunit assertions.
Convert a ChatResponseUpdate stream to AG-UI events and assert the sequence with
Assert.Collection, type-checking each event and its fields (e.g. run lifecycle wraps
content with RunStartedEvent/RunFinishedEvent).
Assert MediaType, CanRead(contentType) (use [Theory]/[InlineData] for case and
null/empty handling), and that WriteAsync produces the data: {json} shape per event.
[JsonPropertyName] mistakes. Parse the JSON with
JsonDocument and assert the literal camelCase property names + type discriminator.JsonTestHelpers.AssertEqual for payloads).AGUIJsonSerializerContext.Default.{Type} so tests exercise the real AOT context.ag-ui-protocol/ag-ui
73b04fd
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.