Skip to content
Open
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
82 changes: 82 additions & 0 deletions src/gh.zig
Original file line number Diff line number Diff line change
Expand Up @@ -189,3 +189,85 @@ pub fn errorMessage(err: GhError) []const u8 {
GhError.Unexpected => "Unexpected gh error",
};
}

// ── Tests ─────────────────────────────────────────────────────────────────────

test "classifyError: auth required" {
try std.testing.expectEqual(GhError.AuthRequired, classifyError("error: not logged in"));
try std.testing.expectEqual(GhError.AuthRequired, classifyError("authentication failed"));
try std.testing.expectEqual(GhError.AuthRequired, classifyError("GITHUB_TOKEN not set"));
}

test "classifyError: not found" {
try std.testing.expectEqual(GhError.NotFound, classifyError("resource not found"));
try std.testing.expectEqual(GhError.NotFound, classifyError("Could not resolve host"));
try std.testing.expectEqual(GhError.NotFound, classifyError("No such repository"));
}

test "classifyError: rate limited" {
try std.testing.expectEqual(GhError.RateLimited, classifyError("rate limit exceeded"));
try std.testing.expectEqual(GhError.RateLimited, classifyError("HTTP 429 Too Many Requests"));
try std.testing.expectEqual(GhError.RateLimited, classifyError("secondary rate limit"));
}

test "classifyError: permission denied" {
try std.testing.expectEqual(GhError.PermissionDenied, classifyError("403 Forbidden"));
try std.testing.expectEqual(GhError.PermissionDenied, classifyError("permission denied"));
try std.testing.expectEqual(GhError.PermissionDenied, classifyError("forbidden"));
}

test "classifyError: unexpected for unknown" {
try std.testing.expectEqual(GhError.Unexpected, classifyError("something went wrong"));
try std.testing.expectEqual(GhError.Unexpected, classifyError(""));
try std.testing.expectEqual(GhError.Unexpected, classifyError("unknown error occurred"));
}

test "classifyError: priority — auth beats not-found" {
try std.testing.expectEqual(GhError.AuthRequired, classifyError("not logged in, not found"));
}

test "classifyError: priority — not-found beats rate-limit" {
try std.testing.expectEqual(GhError.NotFound, classifyError("not found rate limit"));
}

test "errorMessage: all variants return non-empty" {
const errors = [_]GhError{
GhError.AuthRequired,
GhError.NotFound,
GhError.RateLimited,
GhError.PermissionDenied,
GhError.MalformedOutput,
GhError.SpawnFailed,
GhError.OutOfMemory,
GhError.Unexpected,
};
for (errors) |err| {
try std.testing.expect(errorMessage(err).len > 0);
}
}

test "errorMessage: specific strings" {
try std.testing.expectEqualStrings("GitHub auth required. Run: gh auth login", errorMessage(GhError.AuthRequired));
try std.testing.expectEqualStrings("Resource not found on GitHub", errorMessage(GhError.NotFound));
try std.testing.expectEqualStrings("Unexpected gh error", errorMessage(GhError.Unexpected));
}

test "containsAny: basic matching" {
try std.testing.expect(containsAny("hello world", &.{ "world", "foo" }));
try std.testing.expect(containsAny("hello world", &.{ "foo", "hello" }));
try std.testing.expect(!containsAny("hello world", &.{ "foo", "bar" }));
}

test "containsAny: empty inputs" {
try std.testing.expect(!containsAny("", &.{"something"}));
try std.testing.expect(!containsAny("hello", &.{}));
try std.testing.expect(!containsAny("", &.{}));
}

test "GhResult.deinit frees memory" {
const alloc = std.testing.allocator;
const stdout = try alloc.dupe(u8, "output");
const stderr = try alloc.dupe(u8, "error");
const result = GhResult{ .stdout = stdout, .stderr = stderr, .exit_code = 0 };
result.deinit(alloc);
}
37 changes: 37 additions & 0 deletions src/tools.zig
Original file line number Diff line number Diff line change
Expand Up @@ -3215,3 +3215,40 @@ test "buildIssueBody appends parent issue annotation for context only" {
try std.testing.expect(std.mem.endsWith(u8, with_parent, "\n\nParent issue: #393"));
try std.testing.expect(std.mem.indexOf(u8, with_parent, "## Acceptance criteria") != null);
}

// ── parse + Tool enum tests (#167) ──────────────────────────────────────────

test "parse: valid tool names" {
try std.testing.expectEqual(Tool.create_issue, parse("create_issue").?);
try std.testing.expectEqual(Tool.run_task, parse("run_task").?);
try std.testing.expectEqual(Tool.blast_radius, parse("blast_radius").?);
try std.testing.expectEqual(Tool.set_repo, parse("set_repo").?);
try std.testing.expectEqual(Tool.run_swarm, parse("run_swarm").?);
}

test "parse: invalid names return null" {
try std.testing.expectEqual(@as(?Tool, null), parse("nonexistent_tool"));
try std.testing.expectEqual(@as(?Tool, null), parse(""));
try std.testing.expectEqual(@as(?Tool, null), parse("CREATE_ISSUE"));
}

test "parse: every Tool variant round-trips through @tagName" {
const info = @typeInfo(Tool);
inline for (info.@"enum".fields) |f| {
const parsed = parse(f.name);
try std.testing.expect(parsed != null);
}
}

test "dispatch: all tool enum variants are covered by switch" {
const info = @typeInfo(Tool);
try std.testing.expectEqual(@as(usize, 36), info.@"enum".fields.len);
}

test "tools_list JSON is valid and contains expected tool names" {
try std.testing.expect(std.mem.indexOf(u8, tools_list, "\"create_issue\"") != null);
try std.testing.expect(std.mem.indexOf(u8, tools_list, "\"run_task\"") != null);
try std.testing.expect(std.mem.indexOf(u8, tools_list, "\"blast_radius\"") != null);
try std.testing.expect(std.mem.indexOf(u8, tools_list, "\"run_swarm\"") != null);
try std.testing.expect(std.mem.indexOf(u8, tools_list, "\"run_agent\"") != null);
}
Loading