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
261 lines
8.5 KiB
Zig
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);
|
|
}
|