244 lines
8.2 KiB
Swift
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
|
|
}
|