diff --git a/src/Netclaw.SkillServer.Cli/Commands/VersionsCommand.cs b/src/Netclaw.SkillServer.Cli/Commands/VersionsCommand.cs index 097c5a0..bef38e4 100644 --- a/src/Netclaw.SkillServer.Cli/Commands/VersionsCommand.cs +++ b/src/Netclaw.SkillServer.Cli/Commands/VersionsCommand.cs @@ -4,6 +4,7 @@ // // ----------------------------------------------------------------------- +using System.Net; using System.Text.Json; using Netclaw.SkillClient; using Netclaw.SkillServer.Cli.Json; @@ -22,7 +23,21 @@ public static async Task ExecuteAsync(ParsedArgs args, SkillServerClient cl } var name = args.Positional[0]; - var versions = await client.GetSkillVersionsAsync(name); + + IReadOnlyList 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") { diff --git a/tests/Netclaw.SkillServer.Cli.Tests/VersionsCommandTests.cs b/tests/Netclaw.SkillServer.Cli.Tests/VersionsCommandTests.cs new file mode 100644 index 0000000..c36356a --- /dev/null +++ b/tests/Netclaw.SkillServer.Cli.Tests/VersionsCommandTests.cs @@ -0,0 +1,93 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2026 - 2026 Petabridge, LLC +// +// ----------------------------------------------------------------------- + +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)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 _responses; + + public QueuedHandler(params HttpResponseMessage[] responses) + { + _responses = new Queue(responses); + } + + public List Requests { get; } = []; + + protected override Task SendAsync( + HttpRequestMessage request, + CancellationToken cancellationToken) + { + Requests.Add(request.RequestUri?.AbsolutePath ?? ""); + + return Task.FromResult(_responses.Count == 0 + ? new HttpResponseMessage(HttpStatusCode.OK) + : _responses.Dequeue()); + } + } +}