Files
hope-voice-api/ios/Tests/ChatLiveTests.swift
T
2026-10-04 07:23:49 -04:00

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) }
}