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
17 changes: 16 additions & 1 deletion src/Netclaw.SkillServer.Cli/Commands/VersionsCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
// </copyright>
// -----------------------------------------------------------------------

using System.Net;
using System.Text.Json;
using Netclaw.SkillClient;
using Netclaw.SkillServer.Cli.Json;
Expand All @@ -22,7 +23,21 @@ public static async Task<int> ExecuteAsync(ParsedArgs args, SkillServerClient cl
}

var name = args.Positional[0];
var versions = await client.GetSkillVersionsAsync(name);

IReadOnlyList<SkillVersionSummary> versions;
try
{
versions = await client.GetSkillVersionsAsync(name);
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
{
ConsoleOutput.WriteError($"Skill '{name}' not found.");
return 1;
}
catch (HttpRequestException ex)
{
return ConsoleOutput.HandleHttpError(ex);
}

if (args.OutputFormat == "json")
{
Expand Down
93 changes: 93 additions & 0 deletions tests/Netclaw.SkillServer.Cli.Tests/VersionsCommandTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// -----------------------------------------------------------------------
// <copyright file="VersionsCommandTests.cs" company="Petabridge, LLC">
// Copyright (C) 2026 - 2026 Petabridge, LLC <https://petabridge.com>
// </copyright>
// -----------------------------------------------------------------------

using System.Net;
using System.Text;
using System.Text.Json;
using Netclaw.SkillClient;
using Netclaw.SkillServer.Cli.Commands;
using Xunit;

namespace Netclaw.SkillServer.Cli.Tests;

public sealed class VersionsCommandTests
{
[Fact]
public async Task ExecuteAsync_MissingSkill_ReturnsOneWithoutThrowing()
{
// Server returns 404 for a skill that isn't published (regression test for #142).
var handler = new QueuedHandler(new HttpResponseMessage(HttpStatusCode.NotFound));
using var client = CreateClient(handler);

var exitCode = await VersionsCommand.ExecuteAsync(
new ParsedArgs { Positional = ["no-such-skill"] },
client);

Assert.Equal(1, exitCode);
Assert.Single(handler.Requests);
}

[Fact]
public async Task ExecuteAsync_ExistingSkill_ReturnsZeroAndListsVersions()
{
var versions = new[]
{
new SkillVersionSummary
{
Name = "example-skill",
Version = "1.0.0",
Sha256 = "abc123",
PublishedAt = DateTimeOffset.UtcNow,
IsLatest = true
}
};
var json = JsonSerializer.Serialize(
(IReadOnlyList<SkillVersionSummary>)versions,
SkillServerClientJsonContext.Default.IReadOnlyListSkillVersionSummary);
var handler = new QueuedHandler(new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(json, Encoding.UTF8, "application/json")
});
using var client = CreateClient(handler);

var exitCode = await VersionsCommand.ExecuteAsync(
new ParsedArgs { Positional = ["example-skill"] },
client);

Assert.Equal(0, exitCode);
}

private static SkillServerClient CreateClient(HttpMessageHandler handler)
{
return new SkillServerClient(new HttpClient(handler)
{
BaseAddress = new Uri("https://example.test/")
});
}

private sealed class QueuedHandler : HttpMessageHandler
{
private readonly Queue<HttpResponseMessage> _responses;

public QueuedHandler(params HttpResponseMessage[] responses)
{
_responses = new Queue<HttpResponseMessage>(responses);
}

public List<string> Requests { get; } = [];

protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
Requests.Add(request.RequestUri?.AbsolutePath ?? "");

return Task.FromResult(_responses.Count == 0
? new HttpResponseMessage(HttpStatusCode.OK)
: _responses.Dequeue());
}
}
}
Loading