mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-07 04:08:45 +02:00
feat: add packaging and validation coverage
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
using System.Text.Json;
|
||||
using Kopaya.AgentHost.Contracts;
|
||||
using Kopaya.AgentHost.Services;
|
||||
|
||||
namespace Kopaya.AgentHost.Tests;
|
||||
|
||||
public sealed class SidecarProtocolHostTests
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public async Task DescribeCapabilitiesCommand_ReturnsCapabilitiesAndCompletion()
|
||||
{
|
||||
IReadOnlyList<JsonElement> events = await RunHostAsync(new DescribeCapabilitiesCommandDto
|
||||
{
|
||||
Type = "describe-capabilities",
|
||||
RequestId = "cap-1",
|
||||
});
|
||||
|
||||
Assert.Collection(
|
||||
events,
|
||||
capabilitiesEvent =>
|
||||
{
|
||||
Assert.Equal("capabilities", capabilitiesEvent.GetProperty("type").GetString());
|
||||
Assert.Equal("cap-1", capabilitiesEvent.GetProperty("requestId").GetString());
|
||||
|
||||
JsonElement capabilities = capabilitiesEvent.GetProperty("capabilities");
|
||||
Assert.Equal("dotnet-maf", capabilities.GetProperty("runtime").GetString());
|
||||
|
||||
JsonElement modes = capabilities.GetProperty("modes");
|
||||
Assert.True(modes.GetProperty("single").GetProperty("available").GetBoolean());
|
||||
Assert.False(modes.GetProperty("magentic").GetProperty("available").GetBoolean());
|
||||
|
||||
string magenticReason = modes.GetProperty("magentic").GetProperty("reason").GetString() ?? string.Empty;
|
||||
Assert.Contains("unsupported", magenticReason, StringComparison.OrdinalIgnoreCase);
|
||||
},
|
||||
completionEvent =>
|
||||
{
|
||||
Assert.Equal("command-complete", completionEvent.GetProperty("type").GetString());
|
||||
Assert.Equal("cap-1", completionEvent.GetProperty("requestId").GetString());
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidatePatternCommand_ReturnsIssuesAndCompletion()
|
||||
{
|
||||
IReadOnlyList<JsonElement> events = await RunHostAsync(new ValidatePatternCommandDto
|
||||
{
|
||||
Type = "validate-pattern",
|
||||
RequestId = "validate-1",
|
||||
Pattern = new PatternDefinitionDto
|
||||
{
|
||||
Id = "single-pattern",
|
||||
Name = "",
|
||||
Mode = "single",
|
||||
Availability = "available",
|
||||
Agents =
|
||||
[
|
||||
CreateAgent(),
|
||||
CreateAgent(id: "agent-2", name: "Reviewer", model: ""),
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
Assert.Collection(
|
||||
events,
|
||||
validationEvent =>
|
||||
{
|
||||
Assert.Equal("pattern-validation", validationEvent.GetProperty("type").GetString());
|
||||
Assert.Equal("validate-1", validationEvent.GetProperty("requestId").GetString());
|
||||
|
||||
JsonElement[] issues = validationEvent.GetProperty("issues").EnumerateArray().ToArray();
|
||||
Assert.Contains(issues, issue =>
|
||||
issue.GetProperty("field").GetString() == "name"
|
||||
&& issue.GetProperty("message").GetString() == "Pattern name is required.");
|
||||
Assert.Contains(issues, issue =>
|
||||
issue.GetProperty("field").GetString() == "agents"
|
||||
&& issue.GetProperty("message").GetString() == "Single-agent chat requires exactly one agent.");
|
||||
Assert.Contains(issues, issue =>
|
||||
issue.GetProperty("field").GetString() == "agents.model"
|
||||
&& issue.GetProperty("message").GetString() == "Agent \"Reviewer\" requires a model identifier.");
|
||||
},
|
||||
completionEvent =>
|
||||
{
|
||||
Assert.Equal("command-complete", completionEvent.GetProperty("type").GetString());
|
||||
Assert.Equal("validate-1", completionEvent.GetProperty("requestId").GetString());
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task<IReadOnlyList<JsonElement>> RunHostAsync<TCommand>(TCommand command)
|
||||
{
|
||||
string input = JsonSerializer.Serialize(command, JsonOptions) + Environment.NewLine;
|
||||
|
||||
using StringReader reader = new(input);
|
||||
using StringWriter writer = new();
|
||||
|
||||
SidecarProtocolHost host = new();
|
||||
await host.RunAsync(reader, writer, CancellationToken.None);
|
||||
|
||||
return ParseEvents(writer.ToString());
|
||||
}
|
||||
|
||||
private static IReadOnlyList<JsonElement> ParseEvents(string output)
|
||||
{
|
||||
List<JsonElement> events = [];
|
||||
using StringReader reader = new(output);
|
||||
|
||||
string? line;
|
||||
while ((line = reader.ReadLine()) is not null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
using JsonDocument document = JsonDocument.Parse(line);
|
||||
events.Add(document.RootElement.Clone());
|
||||
}
|
||||
|
||||
return events;
|
||||
}
|
||||
|
||||
private static PatternAgentDefinitionDto CreateAgent(
|
||||
string id = "agent-1",
|
||||
string name = "Primary",
|
||||
string model = "gpt-5.4",
|
||||
string instructions = "Help with the user's request.")
|
||||
{
|
||||
return new PatternAgentDefinitionDto
|
||||
{
|
||||
Id = id,
|
||||
Name = name,
|
||||
Model = model,
|
||||
Instructions = instructions,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -10,60 +10,99 @@ public sealed class PatternValidatorTests
|
||||
[Fact]
|
||||
public void SingleAgentPattern_WithExactlyOneAgent_IsValid()
|
||||
{
|
||||
PatternDefinitionDto pattern = new()
|
||||
{
|
||||
Id = "single",
|
||||
Name = "Single",
|
||||
Mode = "single",
|
||||
Availability = "available",
|
||||
Agents =
|
||||
[
|
||||
new PatternAgentDefinitionDto
|
||||
{
|
||||
Id = "agent-1",
|
||||
Name = "Primary",
|
||||
Model = "gpt-5.4",
|
||||
Instructions = "Help with the user's request.",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
IReadOnlyList<PatternValidationIssueDto> issues = _validator.Validate(pattern);
|
||||
IReadOnlyList<PatternValidationIssueDto> issues = _validator.Validate(
|
||||
CreatePattern(
|
||||
"single",
|
||||
[CreateAgent()]));
|
||||
|
||||
Assert.Empty(issues);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HandoffPattern_WithSingleAgent_IsReportedAsInvalid()
|
||||
{
|
||||
IReadOnlyList<PatternValidationIssueDto> issues = _validator.Validate(
|
||||
CreatePattern(
|
||||
"handoff",
|
||||
[CreateAgent()]));
|
||||
|
||||
Assert.Contains(issues, issue =>
|
||||
issue.Field == "agents"
|
||||
&& issue.Message == "Handoff orchestration requires at least two agents.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AgentWithoutModel_IsReportedAsInvalid()
|
||||
{
|
||||
IReadOnlyList<PatternValidationIssueDto> issues = _validator.Validate(
|
||||
CreatePattern(
|
||||
"sequential",
|
||||
[
|
||||
CreateAgent(model: ""),
|
||||
CreateAgent(id: "agent-2", name: "Reviewer"),
|
||||
]));
|
||||
|
||||
Assert.Contains(issues, issue =>
|
||||
issue.Field == "agents.model"
|
||||
&& issue.Message == "Agent \"Primary\" requires a model identifier.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MagenticPattern_IsReportedAsUnavailable()
|
||||
{
|
||||
PatternDefinitionDto pattern = new()
|
||||
IReadOnlyList<PatternValidationIssueDto> issues = _validator.Validate(
|
||||
CreatePattern(
|
||||
"magentic",
|
||||
[
|
||||
CreateAgent(id: "agent-1", name: "Planner", instructions: "Plan the task."),
|
||||
CreateAgent(
|
||||
id: "agent-2",
|
||||
name: "Specialist",
|
||||
model: "claude-opus-4.5",
|
||||
instructions: "Complete the task."),
|
||||
],
|
||||
availability: "unavailable",
|
||||
unavailabilityReason: "Unsupported in C#.",
|
||||
name: "Magentic"));
|
||||
|
||||
Assert.Contains(issues, issue =>
|
||||
issue.Field == "availability"
|
||||
&& issue.Message.Contains("Unsupported", StringComparison.OrdinalIgnoreCase));
|
||||
Assert.Contains(issues, issue =>
|
||||
issue.Field == "mode"
|
||||
&& issue.Message.Contains("Unsupported", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private static PatternDefinitionDto CreatePattern(
|
||||
string mode,
|
||||
IReadOnlyList<PatternAgentDefinitionDto> agents,
|
||||
string availability = "available",
|
||||
string? unavailabilityReason = null,
|
||||
string name = "Pattern")
|
||||
{
|
||||
return new PatternDefinitionDto
|
||||
{
|
||||
Id = "magentic",
|
||||
Name = "Magentic",
|
||||
Mode = "magentic",
|
||||
Availability = "unavailable",
|
||||
UnavailabilityReason = "Unsupported in C#.",
|
||||
Agents =
|
||||
[
|
||||
new PatternAgentDefinitionDto
|
||||
{
|
||||
Id = "agent-1",
|
||||
Name = "Planner",
|
||||
Model = "gpt-5.4",
|
||||
Instructions = "Plan the task.",
|
||||
},
|
||||
new PatternAgentDefinitionDto
|
||||
{
|
||||
Id = "agent-2",
|
||||
Name = "Specialist",
|
||||
Model = "claude-opus-4.5",
|
||||
Instructions = "Complete the task.",
|
||||
},
|
||||
],
|
||||
Id = $"{mode}-pattern",
|
||||
Name = name,
|
||||
Mode = mode,
|
||||
Availability = availability,
|
||||
UnavailabilityReason = unavailabilityReason,
|
||||
Agents = agents,
|
||||
};
|
||||
}
|
||||
|
||||
IReadOnlyList<PatternValidationIssueDto> issues = _validator.Validate(pattern);
|
||||
|
||||
Assert.Contains(issues, issue => issue.Message.Contains("Unsupported", StringComparison.OrdinalIgnoreCase));
|
||||
private static PatternAgentDefinitionDto CreateAgent(
|
||||
string id = "agent-1",
|
||||
string name = "Primary",
|
||||
string model = "gpt-5.4",
|
||||
string instructions = "Help with the user's request.")
|
||||
{
|
||||
return new PatternAgentDefinitionDto
|
||||
{
|
||||
Id = id,
|
||||
Name = name,
|
||||
Model = model,
|
||||
Instructions = instructions,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user