fix(core): escape quotes again, keep the reference's window label, and sniff the hyper body
Three divergences found by reading the ABI-3 diff and pinned by new parity cases in prime-agent. All three are cases the parity suite could not see before, because it drove Zig one round per process and never compared an HTTP request step. - writeJsonString emitted a bare `"` for a quote inside a value, so any provider string containing one produced invalid JSON: `"notes":["Team: Hyper "Squad""]`. The escape table is the boundary between provider-controlled text and the JSON contract, so it now has a test that parses its own output. - epochToWindowLabel dropped the label for a reset already in the past. The reference (prime-agent's driver.ts) floors it to `Math.max(1, …)`, and a host that shows nothing looks like a provider with no window at all. - hyper classified its round by response count alone, so a caller handing in the billing body as the only response got "no hyper teams found". The reference distinguishes by body shape (`plan` or `balances`), and the host loop is not the only caller. Two tests added for the first two; prime-agent's parity suite grows a multi-round hyper case and a past-due reset case for all three.
160 unmodified linesExpand all161162163164165165166167168169106 unmodified linesExpand all276277278279280281282283281284285286287349 unmodified linesExpand all160 unmodified linesExpand allpub fn writeJsonString(w: *Writer, value: []const u8) !void { try w.byte('"'); for (value) |c| { switch (c) { '"' => try w.raw("\""), '"' => try w.raw("\\\""), '\\' => try w.raw("\\\\"), '\n' => try w.raw("\\n"), '\r' => try w.raw("\\r"), '\t' => try w.raw("\\t"),106 unmodified linesExpand all}
pub fn epochToWindowLabel(alloc: std.mem.Allocator, resets_at_ms: ?i64, now_ms: i64) !?[]const u8 { const ra = resets_at_ms orelse return null; // A reset already in the past floors to the 1h label rather than disappearing: the reference // (prime-agent's driver.ts epochToWindowLabel) does `Math.max(1, …)`, and a host that shows // nothing would look like a provider with no window at all. const diff = ra - now_ms; if (diff <= 0) return null; const diff_f = @as(f64, @floatFromInt(diff)) / 1000.0; const seconds = @max(1, roundToInt(i64, diff_f) orelse return null); if (seconds >= 86_400) { const days = roundToInt(i64, @as(f64, @floatFromInt(seconds)) / 86_400.0) orelse return null;349 unmodified linesExpand all513 unmodified linesExpand all514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543513 unmodified linesExpand all const text = try advance(a, "zai", cred, string_body, 0); try std.testing.expect(std.mem.indexOf(u8, text, "\"planType\":\"Pro\"") != null); _ = try std.json.parseFromSlice(std.json.Value, a, text, .{});}
test "json escaping: a quote or backslash in a value stays parseable" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const a = arena.allocator();
var w = usage.Writer.init(a); try usage.writeJsonString(&w, "Team: Hyper \"Squad\" \\ back"); const text = w.items(); // The whole point of the escape table: a value the provider controls must not be able to end // the JSON string early. const parsed = try std.json.parseFromSlice([]const u8, a, text, .{}); try std.testing.expectEqualStrings("Team: Hyper \"Squad\" \\ back", parsed.value);}
test "reset already in the past keeps the reference's 1h window" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const a = arena.allocator();
// driver.ts epochToWindowLabel is `Math.max(1, Math.round((resetsAt - nowMs) / 1000))`, so a // past reset still names a window instead of vanishing from the report. Parity depends on this. const overdue = try usage.epochToWindowLabel(a, 1_000_000, 9_000_000); try std.testing.expect(overdue != null); try std.testing.expectEqualStrings("1h window", overdue.?);}166 unmodified linesExpand all16716816917017117117217317417517617217317417517617717817918018117818218318418518618741 unmodified lines22923023123222822923023123223323423523623423523723823924044 unmodified lines166 unmodified linesExpand all }; return .{ .requests = requests }; }
// Round 2: the answer is either the teams list or already a billing body. The reference // Round 2: Teams response received if (resp_array.items.len == 1) { const first = switch (try usage.parseFirstResponse(alloc, resp_array, "teams body is not JSON")) { .ok => |f| f, .err => |msg| return .{ .report = .{ .error_message = msg } }, }; // (hyper.ts) distinguishes them by shape, not by round number, so a caller that hands the // billing body in as the only response gets a report instead of "no teams found". const first_payload = switch (try usage.parseFirstResponse(alloc, resp_array, "teams body is not JSON")) { .ok => |f| f, .err => |msg| return .{ .report = .{ .error_message = msg } }, }; const first_object = usage.asObject(first_payload.parsed.value); const is_billing_response = first_object != null and (first_object.?.get("plan") != null or first_object.?.get("balances") != null);
const teams_root = usage.asObject(first.parsed.value); if (!is_billing_response and resp_array.items.len == 1) { const teams_root = first_object; const items = if (teams_root) |tr| usage.asArray(tr.get("items")) else null; if (items == null or items.?.items.len == 0) { return .{ .report = .{ .error_message = "no hyper teams found" } }; }41 unmodified lines }; return .{ .requests = requests }; }
// Round 3: Billing & members responses handling const first = switch (try usage.parseFirstResponse(alloc, resp_array, "billing body is not JSON")) { .ok => |f| f, .err => |msg| return .{ .report = .{ .error_message = msg } }, }; // Round 3: Billing & members responses handling. The billing body is whichever response the // shaping above identified, so it is already parsed. const parsed_billing = try parseHyperBilling(alloc, first_payload.parsed.value, now_ms);
const parsed_billing = try parseHyperBilling(alloc, first.parsed.value, now_ms);
var member_email: ?[]const u8 = null; if (resp_array.items.len >= 2) { const resp1_obj = usage.asObject(resp_array.items[1]); if (resp1_obj) |r1| {44 unmodified lines