Archived
68 lines
4.5 KiB
Swift
68 lines
4.5 KiB
Swift
import XCTest
|
|
@testable import Voice
|
|
|
|
/// Deliberate physical/native transport qualification. No stored credentials,
|
|
/// microphones, account memory edits, preference mutation or payload logging.
|
|
final class ChatLiveTests: XCTestCase {
|
|
func testAuthenticatedNativeDurableChat() async throws {
|
|
let env = ProcessInfo.processInfo.environment
|
|
guard env["HOPE_CHAT_LIVE"] == "1" else { throw XCTSkip("Native live chat requires explicit HOPE_CHAT_LIVE=1") }
|
|
guard let username = env["HOPE_TEST_USERNAME"], let password = env["HOPE_TEST_PASSWORD"], !password.isEmpty else { throw XCTSkip("Protected live credentials were not supplied") }
|
|
let config = try ServerConfig.parse(env["HOPE_TEST_ORIGIN"] ?? "https://192.168.86.2:9178").get()
|
|
let auth = AuthClient(config: config)
|
|
print("HOPE_CHAT_PROBE native authenticated HTTP started; microphone remains off")
|
|
let login = try await auth.login(username: username, password: password)
|
|
let api = ChatAPI(config: config, token: login.token)
|
|
var createdID: String?
|
|
do {
|
|
let verified = try await auth.me(token: login.token)
|
|
XCTAssertEqual(verified.id, login.user.id)
|
|
let creation = try await api.request("api/conversations", method: "POST", body: ["title": "Native qualification · disposable"])
|
|
let conversation = try ChatCoding.decoder.decode(ConversationEnvelope.self, from: creation).conversation
|
|
createdID = conversation.id
|
|
let sent = try await api.request("api/conversations/\(conversation.id)/messages", method: "POST", body: ["content": "Reply exactly: native chat transport verified", "idempotency_key": UUID().uuidString, "mode": "normal"])
|
|
let operation = try ChatCoding.decoder.decode(SendEnvelope.self, from: sent)
|
|
var completed: RunDetail?
|
|
for _ in 0..<90 {
|
|
let bytes = try await api.request("api/runs/\(operation.run.id)")
|
|
let detail = try ChatCoding.decoder.decode(RunDetail.self, from: bytes)
|
|
if !detail.run.active { completed = detail; break }
|
|
try await Task.sleep(for: .seconds(2))
|
|
}
|
|
let result = try XCTUnwrap(completed, "Run did not terminate within qualification deadline")
|
|
XCTAssertEqual(result.run.status, "completed", "Server run failed; body intentionally not logged")
|
|
XCTAssertTrue(result.messages.contains { $0.role == "assistant" && !$0.content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty })
|
|
let recorder = await MainActor.run { LiveEventRecorder() }
|
|
try await api.events(run: operation.run.id, after: 0) { event in recorder.accept(event) }
|
|
let replay = await MainActor.run { recorder.ids }
|
|
XCTAssertFalse(replay.isEmpty)
|
|
XCTAssertEqual(replay, replay.sorted())
|
|
XCTAssertEqual(Set(replay).count, replay.count)
|
|
let detailBytes = try await api.request("api/conversations/\(conversation.id)")
|
|
let detail = try ChatCoding.decoder.decode(ConversationDetail.self, from: detailBytes)
|
|
XCTAssertTrue(detail.messages.contains { $0.role == "user" })
|
|
XCTAssertTrue(detail.messages.contains { $0.role == "assistant" })
|
|
let pinnedBytes = try await api.request("api/conversations/\(conversation.id)", method: "PATCH", body: ["revision": detail.conversation.revision, "pinned": true])
|
|
let pinned = try ChatCoding.decoder.decode(ConversationEnvelope.self, from: pinnedBytes).conversation
|
|
XCTAssertTrue(pinned.pinned)
|
|
_ = try await api.request("api/conversations/\(conversation.id)", method: "PATCH", body: ["revision": pinned.revision, "archived": true])
|
|
_ = try await api.request("api/conversations/\(conversation.id)", method: "DELETE")
|
|
createdID = nil
|
|
try await auth.logout(token: login.token)
|
|
api.cancel()
|
|
print("HOPE_CHAT_PROBE native login/me/create/send/completion/replay/pin/archive/delete/logout completed; no audio captured")
|
|
} catch {
|
|
if let createdID { _ = try? await api.request("api/conversations/\(createdID)", method: "DELETE") }
|
|
try? await auth.logout(token: login.token)
|
|
api.cancel()
|
|
print("HOPE_CHAT_PROBE cleanup attempted after failure; bodies/credentials withheld")
|
|
throw error
|
|
}
|
|
}
|
|
}
|
|
@MainActor
|
|
private final class LiveEventRecorder {
|
|
var ids: [Int] = []
|
|
func accept(_ event: ChatEvent) { ids.append(event.id) }
|
|
}
|