Files
ggbond268--mactools/Plugins/Translator/Tests/OpenAICompatibleClientTests.swift
wehub-resource-sync 1d1286fadb
Build / Build and test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:18:38 +08:00

244 lines
8.2 KiB
Swift

import Foundation
import XCTest
@testable import TranslatorPlugin
final class OpenAICompatibleClientTests: XCTestCase {
func testBuildsRequestAndParsesSuccessResult() async throws {
let recorder = RequestRecorder()
let httpClient = StubTranslatorHTTPClient(
recorder: recorder,
result: .success((
Self.responsePayload(content: " 你好 "),
Self.httpResponse(statusCode: 200)
))
)
let client = OpenAICompatibleClient(httpClient: httpClient)
let languageSelection = TranslatorLanguageSelection(source: .english, target: .simplifiedChinese)
let configuration = OpenAICompatibleConfiguration(
baseURL: "https://api.openai.com",
model: "gpt-test",
promptTemplate: "Translate from {{source_language}} to {{target_language}}:\n{{text}}"
)
let result = try await client.translate(
text: "Hello",
languageSelection: languageSelection,
configuration: configuration,
apiKey: "test-key"
)
XCTAssertEqual(
result,
TranslationResult(
providerTitle: "OpenAI 翻译",
text: "你好",
sourceText: "Hello",
languageSelection: languageSelection
)
)
let recordedRequests = await recorder.requests
let request = try XCTUnwrap(recordedRequests.first)
XCTAssertEqual(request.url?.absoluteString, "https://api.openai.com/v1/chat/completions")
XCTAssertEqual(request.httpMethod, "POST")
XCTAssertEqual(request.cachePolicy, .reloadIgnoringLocalCacheData)
XCTAssertEqual(request.timeoutInterval, 30)
XCTAssertEqual(request.value(forHTTPHeaderField: "Content-Type"), "application/json")
XCTAssertEqual(request.value(forHTTPHeaderField: "Authorization"), "Bearer test-key")
XCTAssertEqual(request.value(forHTTPHeaderField: "api-key"), "test-key")
let body = try Self.requestBody(from: request)
XCTAssertEqual(body.model, "gpt-test")
XCTAssertEqual(body.stream, false)
XCTAssertEqual(body.temperature, 0.2, accuracy: 0.0001)
XCTAssertEqual(body.messages.count, 1)
XCTAssertEqual(body.messages.first?.role, "user")
let prompt = try XCTUnwrap(body.messages.first?.content)
XCTAssertTrue(prompt.contains("English"))
XCTAssertTrue(prompt.contains("Simplified Chinese"))
XCTAssertTrue(prompt.contains("Hello"))
}
func testModelIsTrimmedInRequest() async throws {
let recorder = RequestRecorder()
let httpClient = StubTranslatorHTTPClient(
recorder: recorder,
result: .success((
Self.responsePayload(content: "Bonjour"),
Self.httpResponse(statusCode: 200)
))
)
let client = OpenAICompatibleClient(httpClient: httpClient)
let configuration = OpenAICompatibleConfiguration(model: " gpt-trimmed ")
_ = try await client.translate(
text: "Hello",
languageSelection: TranslatorLanguageSelection(source: .english, target: .french),
configuration: configuration,
apiKey: "test-key"
)
let recordedRequests = await recorder.requests
let request = try XCTUnwrap(recordedRequests.first)
let body = try Self.requestBody(from: request)
XCTAssertEqual(body.model, "gpt-trimmed")
}
func testSuccessResponseDoesNotRequireAssistantRole() async throws {
let client = OpenAICompatibleClient(
httpClient: StubTranslatorHTTPClient(
recorder: RequestRecorder(),
result: .success((
Self.responsePayload(content: "Hola"),
Self.httpResponse(statusCode: 200)
))
)
)
let result = try await client.translate(
text: "Hello",
languageSelection: TranslatorLanguageSelection(source: .english, target: .spanish),
configuration: OpenAICompatibleConfiguration(),
apiKey: "test-key"
)
XCTAssertEqual(result.text, "Hola")
}
func testHTTP500MapsToRequestFailure() async {
await assertTranslateError(
result: .success((Data("{}".utf8), Self.httpResponse(statusCode: 500))),
expectedDescription: "请求失败,请稍后重试"
)
}
func testHTTP401MapsToUnauthorized() async {
await assertTranslateError(
result: .success((Data("{}".utf8), Self.httpResponse(statusCode: 401))),
expectedDescription: "API Key 无效或无权限"
)
}
func testHTTP403MapsToUnauthorized() async {
await assertTranslateError(
result: .success((Data("{}".utf8), Self.httpResponse(statusCode: 403))),
expectedDescription: "API Key 无效或无权限"
)
}
func testEmptyResponseContentMapsToEmptyResponse() async {
await assertTranslateError(
result: .success((
Self.responsePayload(content: " \n\t "),
Self.httpResponse(statusCode: 200)
)),
expectedDescription: "响应为空"
)
}
func testMalformedPayloadMapsToParseFailure() async {
await assertTranslateError(
result: .success((Data(#"{"choices":[]}"#.utf8), Self.httpResponse(statusCode: 200))),
expectedDescription: "无法解析翻译结果"
)
}
func testTransportErrorMapsToRequestFailure() async {
await assertTranslateError(
result: .failure(URLError(.timedOut)),
expectedDescription: "请求失败,请稍后重试"
)
}
private static func responsePayload(content: String) -> Data {
Data(
"""
{
"choices": [
{
"message": {
"content": \(String(reflecting: content))
}
}
]
}
""".utf8
)
}
private static func httpResponse(statusCode: Int) -> HTTPURLResponse {
HTTPURLResponse(
url: URL(string: "https://api.openai.com/v1/chat/completions")!,
statusCode: statusCode,
httpVersion: nil,
headerFields: nil
)!
}
private static func requestBody(from request: URLRequest) throws -> RequestBody {
let data = try XCTUnwrap(request.httpBody)
return try JSONDecoder().decode(RequestBody.self, from: data)
}
private func assertTranslateError(
result: Result<(Data, HTTPURLResponse), Error>,
expectedDescription: String,
file: StaticString = #filePath,
line: UInt = #line
) async {
let client = OpenAICompatibleClient(
httpClient: StubTranslatorHTTPClient(
recorder: RequestRecorder(),
result: result
)
)
do {
_ = try await client.translate(
text: "Hello",
languageSelection: TranslatorLanguageSelection(source: .english, target: .simplifiedChinese),
configuration: OpenAICompatibleConfiguration(),
apiKey: "test-key"
)
XCTFail("Expected translate to throw", file: file, line: line)
} catch {
XCTAssertEqual(error.localizedDescription, expectedDescription, file: file, line: line)
}
}
}
private actor RequestRecorder {
private var storedRequests: [URLRequest] = []
var requests: [URLRequest] {
storedRequests
}
func record(_ request: URLRequest) {
storedRequests.append(request)
}
}
private struct StubTranslatorHTTPClient: TranslatorHTTPClient {
let recorder: RequestRecorder
let result: Result<(Data, HTTPURLResponse), Error>
func data(for request: URLRequest) async throws -> (Data, HTTPURLResponse) {
await recorder.record(request)
return try result.get()
}
}
private struct RequestBody: Decodable {
struct Message: Decodable {
let role: String
let content: String
}
let model: String
let messages: [Message]
let temperature: Double
let stream: Bool
}