fetch_ml/cli/src/commands/validate.zig
Jeremie Fraeys cb826b74a3
feat: WebSocket API infrastructure improvements
Enhance WebSocket client and server components:
- Add new WebSocket opcodes (CompareRuns, FindRuns, ExportRun, SetRunOutcome)
- Improve WebSocket client with additional response handlers
- Add crypto utilities for secure WebSocket communications
- Add I/O utilities for WebSocket payload handling
- Enhance validation for WebSocket message payloads
- Update routes for new WebSocket endpoints
- Improve monitor and validate command WebSocket integrations
2026-02-18 21:27:48 -05:00

261 lines
8.5 KiB
Zig

const std = @import("std");
const testing = std.testing;
const Config = @import("../config.zig").Config;
const ws = @import("../net/ws/client.zig");
const protocol = @import("../net/protocol.zig");
const colors = @import("../utils/colors.zig");
const crypto = @import("../utils/crypto.zig");
const io = @import("../utils/io.zig");
pub const Options = struct {
json: bool = false,
verbose: bool = false,
task_id: ?[]const u8 = null,
};
pub fn run(allocator: std.mem.Allocator, args: []const []const u8) !void {
if (args.len == 0) {
try printUsage();
return error.InvalidArgs;
}
var opts = Options{};
var commit_hex: ?[]const u8 = null;
var i: usize = 0;
while (i < args.len) : (i += 1) {
const arg = args[i];
if (std.mem.eql(u8, arg, "--json")) {
opts.json = true;
} else if (std.mem.eql(u8, arg, "--verbose")) {
opts.verbose = true;
} else if (std.mem.eql(u8, arg, "--task") and i + 1 < args.len) {
opts.task_id = args[i + 1];
i += 1;
} else if (std.mem.startsWith(u8, arg, "--help")) {
try printUsage();
return;
} else if (std.mem.startsWith(u8, arg, "--")) {
colors.printError("Unknown option: {s}\n", .{arg});
try printUsage();
return error.InvalidArgs;
} else {
commit_hex = arg;
}
}
const config = try Config.load(allocator);
defer {
var mut_config = config;
mut_config.deinit(allocator);
}
if (config.api_key.len == 0) return error.APIKeyMissing;
const ws_url = try config.getWebSocketUrl(allocator);
defer allocator.free(ws_url);
var client = try ws.Client.connect(allocator, ws_url, config.api_key);
defer client.close();
const api_key_hash = try crypto.hashApiKey(allocator, config.api_key);
defer allocator.free(api_key_hash);
if (opts.task_id) |tid| {
try client.sendValidateRequestTask(api_key_hash, tid);
} else {
if (commit_hex == null or commit_hex.?.len != 40) {
colors.printError("validate requires a 40-char commit id (or --task <task_id>)\n", .{});
try printUsage();
return error.InvalidArgs;
}
const commit_bytes = try crypto.decodeHex(allocator, commit_hex.?);
defer allocator.free(commit_bytes);
if (commit_bytes.len != 20) return error.InvalidCommitId;
try client.sendValidateRequestCommit(api_key_hash, commit_bytes);
}
// Expect Data packet with data_type "validate" and JSON payload.
const msg = try client.receiveMessage(allocator);
defer allocator.free(msg);
const packet = protocol.ResponsePacket.deserialize(msg, allocator) catch {
if (opts.json) {
var out = io.stdoutWriter();
try out.print("{s}\n", .{msg});
} else {
std.debug.print("{s}\n", .{msg});
}
return error.InvalidPacket;
};
defer packet.deinit(allocator);
if (packet.packet_type == .error_packet) {
try client.handleResponsePacket(packet, "validate");
return error.ValidationFailed;
}
if (packet.packet_type != .data or packet.data_payload == null) {
colors.printError("unexpected response for validate\n", .{});
return error.InvalidPacket;
}
const payload = packet.data_payload.?;
if (opts.json) {
var out = io.stdoutWriter();
try out.print("{s}\n", .{payload});
} else {
const parsed = try std.json.parseFromSlice(std.json.Value, allocator, payload, .{});
defer parsed.deinit();
const root = parsed.value.object;
const ok = try printHumanReport(root, opts.verbose);
if (!ok) return error.ValidationFailed;
}
}
fn printHumanReport(root: std.json.ObjectMap, verbose: bool) !bool {
const ok_val = root.get("ok") orelse return error.InvalidPacket;
if (ok_val != .bool) return error.InvalidPacket;
const ok = ok_val.bool;
if (root.get("commit_id")) |cid| {
if (cid != .null) {
std.debug.print("commit_id: {s}\n", .{cid.string});
}
}
if (root.get("task_id")) |tid| {
if (tid != .null) {
std.debug.print("task_id: {s}\n", .{tid.string});
}
}
if (ok) {
std.debug.print("validate: OK\n", .{});
} else {
std.debug.print("validate: FAILED\n", .{});
}
if (root.get("errors")) |errs| {
if (errs == .array and errs.array.items.len > 0) {
std.debug.print("errors:\n", .{});
for (errs.array.items) |e| {
if (e == .string) {
std.debug.print("- {s}\n", .{e.string});
}
}
}
}
if (root.get("warnings")) |warns| {
if (warns == .array and warns.array.items.len > 0) {
std.debug.print("warnings:\n", .{});
for (warns.array.items) |w| {
if (w == .string) {
std.debug.print("- {s}\n", .{w.string});
}
}
}
}
if (root.get("checks")) |checks_val| {
if (checks_val == .object) {
if (verbose) {
std.debug.print("checks:\n", .{});
} else {
std.debug.print("failed_checks:\n", .{});
}
var it = checks_val.object.iterator();
var any_failed: bool = false;
while (it.next()) |entry| {
const name = entry.key_ptr.*;
const check_val = entry.value_ptr.*;
if (check_val != .object) continue;
const check_obj = check_val.object;
var check_ok: bool = false;
if (check_obj.get("ok")) |cok| {
if (cok == .bool) check_ok = cok.bool;
}
if (!check_ok) any_failed = true;
if (!verbose and check_ok) continue;
if (check_ok) {
std.debug.print("- {s}: OK\n", .{name});
} else {
std.debug.print("- {s}: FAILED\n", .{name});
}
if (verbose or !check_ok) {
if (check_obj.get("expected")) |exp| {
if (exp != .null) {
std.debug.print(" expected: {s}\n", .{exp.string});
}
}
if (check_obj.get("actual")) |act| {
if (act != .null) {
std.debug.print(" actual: {s}\n", .{act.string});
}
}
if (check_obj.get("details")) |det| {
if (det != .null) {
std.debug.print(" details: {s}\n", .{det.string});
}
}
}
}
if (!verbose and !any_failed) {
std.debug.print("- none\n", .{});
}
}
}
return ok;
}
fn printUsage() !void {
colors.printInfo("Usage:\n", .{});
std.debug.print(" ml validate <commit_id> [--json] [--verbose]\n", .{});
std.debug.print(" ml validate --task <task_id> [--json] [--verbose]\n", .{});
}
test "validate human report formatting" {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
defer _ = gpa.deinit();
const payload =
\\{
\\ "ok": false,
\\ "commit_id": "abc",
\\ "task_id": "t1",
\\ "checks": {
\\ "a": {"ok": true},
\\ "b": {"ok": false, "expected": "x", "actual": "y", "details": "d"}
\\ },
\\ "errors": ["e1"],
\\ "warnings": ["w1"],
\\ "ts": "now"
\\}
;
const parsed = try std.json.parseFromSlice(std.json.Value, allocator, payload, .{});
defer parsed.deinit();
var buf = std.ArrayList(u8).empty;
defer buf.deinit(allocator);
_ = try printHumanReport(buf.writer(), parsed.value.object, false);
try testing.expect(std.mem.indexOf(u8, buf.items, "failed_checks") != null);
try testing.expect(std.mem.indexOf(u8, buf.items, "- b: FAILED") != null);
try testing.expect(std.mem.indexOf(u8, buf.items, "expected: x") != null);
buf.clearRetainingCapacity();
_ = try printHumanReport(buf.writer(), parsed.value.object, true);
try testing.expect(std.mem.indexOf(u8, buf.items, "checks") != null);
try testing.expect(std.mem.indexOf(u8, buf.items, "- a: OK") != null);
try testing.expect(std.mem.indexOf(u8, buf.items, "- b: FAILED") != null);
}