|
3 | 3 | import { |
4 | 4 | getErrorStatusCode, |
5 | 5 | getErrorMessage, |
| 6 | + extractMessageFromJsonPayload, |
6 | 7 | shouldReportApiErrorToTelemetry, |
7 | 8 | EXPECTED_API_ERROR_CODES, |
8 | 9 | ApiProviderError, |
@@ -48,17 +49,22 @@ describe("telemetry error utilities", () => { |
48 | 49 | }) |
49 | 50 |
|
50 | 51 | describe("getErrorMessage", () => { |
51 | | - it("should return undefined for non-OpenAI SDK errors", () => { |
| 52 | + it("should return undefined for null, undefined, or objects without message", () => { |
52 | 53 | expect(getErrorMessage(null)).toBeUndefined() |
53 | 54 | expect(getErrorMessage(undefined)).toBeUndefined() |
54 | | - expect(getErrorMessage({ message: "error" })).toBeUndefined() |
| 55 | + expect(getErrorMessage({})).toBeUndefined() |
| 56 | + expect(getErrorMessage({ code: 500 })).toBeUndefined() |
55 | 57 | }) |
56 | 58 |
|
57 | 59 | it("should return the primary message for simple OpenAI SDK errors", () => { |
58 | 60 | const error = { status: 400, message: "Bad request" } |
59 | 61 | expect(getErrorMessage(error)).toBe("Bad request") |
60 | 62 | }) |
61 | 63 |
|
| 64 | + it("should return message from plain objects with message property", () => { |
| 65 | + expect(getErrorMessage({ message: "error" })).toBe("error") |
| 66 | + }) |
| 67 | + |
62 | 68 | it("should prioritize nested error.message over primary message", () => { |
63 | 69 | const error = { |
64 | 70 | status: 500, |
@@ -100,6 +106,132 @@ describe("telemetry error utilities", () => { |
100 | 106 | } |
101 | 107 | expect(getErrorMessage(error)).toBe("Forbidden") |
102 | 108 | }) |
| 109 | + |
| 110 | + it("should extract message from JSON payload in error message", () => { |
| 111 | + const error = { |
| 112 | + status: 503, |
| 113 | + message: '503 {"error":{"code":"","message":"Model unavailable"}}', |
| 114 | + } |
| 115 | + expect(getErrorMessage(error)).toBe("Model unavailable") |
| 116 | + }) |
| 117 | + |
| 118 | + it("should extract message from JSON payload with status prefix", () => { |
| 119 | + const error = { |
| 120 | + status: 503, |
| 121 | + message: |
| 122 | + '503 {"error":{"code":"","message":"所有令牌分组 Tier 3 下对于模型 claude-sonnet-4-5 均无可用渠道,请更换分组尝试"}}', |
| 123 | + } |
| 124 | + expect(getErrorMessage(error)).toBe( |
| 125 | + "所有令牌分组 Tier 3 下对于模型 claude-sonnet-4-5 均无可用渠道,请更换分组尝试", |
| 126 | + ) |
| 127 | + }) |
| 128 | + |
| 129 | + it("should extract message from nested error.message containing JSON", () => { |
| 130 | + const error = { |
| 131 | + status: 500, |
| 132 | + message: "Request failed", |
| 133 | + error: { message: '{"error":{"message":"Upstream provider error"}}' }, |
| 134 | + } |
| 135 | + expect(getErrorMessage(error)).toBe("Upstream provider error") |
| 136 | + }) |
| 137 | + |
| 138 | + it("should return original message when JSON has no message field", () => { |
| 139 | + const error = { |
| 140 | + status: 500, |
| 141 | + message: '{"error":{"code":"123"}}', |
| 142 | + } |
| 143 | + expect(getErrorMessage(error)).toBe('{"error":{"code":"123"}}') |
| 144 | + }) |
| 145 | + |
| 146 | + it("should return original message when JSON is invalid", () => { |
| 147 | + const error = { |
| 148 | + status: 500, |
| 149 | + message: "503 {invalid json}", |
| 150 | + } |
| 151 | + expect(getErrorMessage(error)).toBe("503 {invalid json}") |
| 152 | + }) |
| 153 | + |
| 154 | + it("should extract message from standard Error object", () => { |
| 155 | + const error = new Error("Simple error message") |
| 156 | + expect(getErrorMessage(error)).toBe("Simple error message") |
| 157 | + }) |
| 158 | + |
| 159 | + it("should extract message from standard Error with JSON payload", () => { |
| 160 | + const error = new Error('503 {"error":{"code":"","message":"Model unavailable"}}') |
| 161 | + expect(getErrorMessage(error)).toBe("Model unavailable") |
| 162 | + }) |
| 163 | + |
| 164 | + it("should extract message from ApiProviderError", () => { |
| 165 | + const error = new ApiProviderError("Test error", "OpenRouter", "gpt-4", "createMessage") |
| 166 | + expect(getErrorMessage(error)).toBe("Test error") |
| 167 | + }) |
| 168 | + |
| 169 | + it("should extract message from ApiProviderError with JSON payload", () => { |
| 170 | + const jsonMessage = |
| 171 | + '503 {"error":{"code":"","message":"所有令牌分组 Tier 3 下对于模型 claude-sonnet-4-5 均无可用渠道"}}' |
| 172 | + const error = new ApiProviderError(jsonMessage, "Anthropic", "claude-sonnet-4-5", "createMessage") |
| 173 | + expect(getErrorMessage(error)).toBe("所有令牌分组 Tier 3 下对于模型 claude-sonnet-4-5 均无可用渠道") |
| 174 | + }) |
| 175 | + |
| 176 | + it("should handle ApiProviderError with errorCode but no status property", () => { |
| 177 | + const error = new ApiProviderError("Test error", "Anthropic", "claude-3-opus", "createMessage", 500) |
| 178 | + expect(getErrorMessage(error)).toBe("Test error") |
| 179 | + }) |
| 180 | + }) |
| 181 | + |
| 182 | + describe("extractMessageFromJsonPayload", () => { |
| 183 | + it("should return undefined for messages without JSON", () => { |
| 184 | + expect(extractMessageFromJsonPayload("Simple error message")).toBeUndefined() |
| 185 | + expect(extractMessageFromJsonPayload("Error: something went wrong")).toBeUndefined() |
| 186 | + expect(extractMessageFromJsonPayload("")).toBeUndefined() |
| 187 | + }) |
| 188 | + |
| 189 | + it("should extract message from error.message structure", () => { |
| 190 | + const json = '{"error":{"message":"Model unavailable"}}' |
| 191 | + expect(extractMessageFromJsonPayload(json)).toBe("Model unavailable") |
| 192 | + }) |
| 193 | + |
| 194 | + it("should extract message from error.message with code structure", () => { |
| 195 | + const json = '{"error":{"code":"","message":"Model unavailable"}}' |
| 196 | + expect(extractMessageFromJsonPayload(json)).toBe("Model unavailable") |
| 197 | + }) |
| 198 | + |
| 199 | + it("should extract message from status prefix followed by JSON", () => { |
| 200 | + const message = '503 {"error":{"code":"","message":"Model unavailable"}}' |
| 201 | + expect(extractMessageFromJsonPayload(message)).toBe("Model unavailable") |
| 202 | + }) |
| 203 | + |
| 204 | + it("should extract message from simple message structure", () => { |
| 205 | + const json = '{"message":"Simple error"}' |
| 206 | + expect(extractMessageFromJsonPayload(json)).toBe("Simple error") |
| 207 | + }) |
| 208 | + |
| 209 | + it("should return undefined for JSON without message field", () => { |
| 210 | + const json = '{"error":{"code":"500"}}' |
| 211 | + expect(extractMessageFromJsonPayload(json)).toBeUndefined() |
| 212 | + }) |
| 213 | + |
| 214 | + it("should return undefined for invalid JSON", () => { |
| 215 | + expect(extractMessageFromJsonPayload("{invalid json}")).toBeUndefined() |
| 216 | + expect(extractMessageFromJsonPayload("503 {not: valid: json}")).toBeUndefined() |
| 217 | + }) |
| 218 | + |
| 219 | + it("should handle nested error structure with empty code", () => { |
| 220 | + const json = '{"error":{"code":"","message":"Token quota exceeded"}}' |
| 221 | + expect(extractMessageFromJsonPayload(json)).toBe("Token quota exceeded") |
| 222 | + }) |
| 223 | + |
| 224 | + it("should handle Unicode messages correctly", () => { |
| 225 | + const json = '{"error":{"message":"所有令牌分组 Tier 3 下对于模型 claude-sonnet-4-5 均无可用渠道"}}' |
| 226 | + expect(extractMessageFromJsonPayload(json)).toBe( |
| 227 | + "所有令牌分组 Tier 3 下对于模型 claude-sonnet-4-5 均无可用渠道", |
| 228 | + ) |
| 229 | + }) |
| 230 | + |
| 231 | + it("should return undefined when message field is not a string", () => { |
| 232 | + const json = '{"error":{"message":123}}' |
| 233 | + expect(extractMessageFromJsonPayload(json)).toBeUndefined() |
| 234 | + }) |
103 | 235 | }) |
104 | 236 |
|
105 | 237 | describe("shouldReportApiErrorToTelemetry", () => { |
|
0 commit comments