fix(alibaba): quote a CLI's JSON error message, not its braces
bl failed with exit 3 and wrote its error as JSON on stderr: {"error": {"message": "No console access token found.", "hint": "Run `bl auth login --console`."}} and the failure reason took the body's first line, so the tray showed "bailian CLI failed (exit 3): {" — less useful than the exit code alone. A JSON body now contributes its error.message and error.hint (the action the user needs); a plain-text body keeps its first non-empty line; JSON without a message leaves the reason as the bare exit code. Mirrored in the TypeScript reference with tests on both.
343 unmodified linesExpand all344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376192 unmodified linesExpand all343 unmodified linesExpand all try std.testing.expect(std.mem.indexOf(u8, req_cn, "\"cn-beijing\"") != null); try std.testing.expect(std.mem.indexOf(u8, req_cn, "\"domestic\"") != null);}
test "alibaba CLI failure quotes a JSON error body's message and hint" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const a = arena.allocator();
const cred = "{\"kind\":\"cli\",\"config\":{\"region\":\"intl-personal\"}}"; const body = "[{\"status\":3,\"body\":\"{\\n \\\"error\\\": {\\n \\\"code\\\": 3,\\n \\\"message\\\": \\\"No console access token found.\\\",\\n \\\"hint\\\": \\\"Run `bl auth login --console`.\\\"\\n }\\n}\"}]"; const text = try advance(a, "alibaba", cred, body, 0); try std.testing.expect(std.mem.indexOf(u8, text, "bailian CLI failed (exit 3)") != null); try std.testing.expect(std.mem.indexOf(u8, text, "No console access token found. Run `bl auth login --console`.") != null); try std.testing.expect(std.mem.indexOf(u8, text, ": {") == null);}
test "alibaba CLI failure stays the bare exit code when the JSON body has no message" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const a = arena.allocator();
const cred = "{\"kind\":\"cli\",\"config\":{\"region\":\"intl-personal\"}}"; const body = "[{\"status\":3,\"body\":\"{\\\"trace\\\":\\\"abc\\\"}\"}]"; const text = try advance(a, "alibaba", cred, body, 0); try std.testing.expect(std.mem.indexOf(u8, text, "bailian CLI failed (exit 3)\"") != null); try std.testing.expect(std.mem.indexOf(u8, text, "trace") == null);}
test "alibaba 127 with session fallback" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const a = arena.allocator();192 unmodified linesExpand all466 unmodified linesExpand all467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510475476477478479511512513514515516517135 unmodified linesExpand all466 unmodified linesExpand all report: ?usage.ReportOutcome = null, reason: ?[]const u8 = null,};
/// The human part of a CLI failure. A JSON error body carries its own message and usually a hint —/// "No console access token found. Run `bl auth login --console`." — which is the action the user/// needs; anything else explains itself in its first non-empty line. JSON without a message says/// nothing useful, so the reason stays the bare exit code rather than quoting a brace.fn cliFailureDetail(alloc: std.mem.Allocator, text: []const u8) !?[]const u8 { if (std.json.parseFromSlice(std.json.Value, alloc, text, .{})) |parsed_val| { const root = usage.asObject(parsed_val.value); const err_obj = if (root) |r| usage.asObject(r.get("error")) else null; const message = if (err_obj) |e| usage.stringOf(e.get("message")) else null; if (message) |msg| { if (msg.len == 0) return null; if (err_obj) |e| { if (usage.stringOf(e.get("hint"))) |hint| { if (hint.len > 0) { const join: []const u8 = if (msg[msg.len - 1] == '.') " " else ". "; const joined: ?[]const u8 = try std.fmt.allocPrint(alloc, "{s}{s}{s}", .{ msg, join, hint }); return joined; } } } const bare: ?[]const u8 = msg; return bare; } return null; } else |_| {} var lines = std.mem.splitScalar(u8, text, '\n'); while (lines.next()) |raw| { const line = std.mem.trim(u8, raw, " \t\r"); if (line.len > 0) { const found: ?[]const u8 = line[0..@min(line.len, 120)]; return found; } } return null;}
/// A CLI that wrote to stderr before failing has already explained itself, and that explanation is/// more useful than the exit code: "bl is not on PATH" beats "exit 126". The body is provider-visible/// text, so it is trimmed to one bounded line before it goes into a report.fn cliFailureReason(alloc: std.mem.Allocator, status: i64, resp0: std.json.ObjectMap) ![]const u8 { const body = usage.stringOf(resp0.get("body")); if (body) |text| { const first_line = std.mem.trim(u8, text[0..(std.mem.indexOfScalar(u8, text, '\n') orelse text.len)], " \t\r"); if (first_line.len > 0) { const capped = first_line[0..@min(first_line.len, 120)]; if (usage.stringOf(resp0.get("body"))) |text| { if (try cliFailureDetail(alloc, text)) |detail| { const capped = detail[0..@min(detail.len, 160)]; return std.fmt.allocPrint(alloc, "bailian CLI failed (exit {d}): {s}", .{ status, capped }); } } return std.fmt.allocPrint(alloc, "bailian CLI failed (exit {d})", .{status});135 unmodified linesExpand all