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 )\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 [--json] [--verbose]\n", .{}); std.debug.print(" ml validate --task [--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); }