import XCTest /// Opt-in tests against the actual NAS deployment. No injected replies or media. final class VoiceLiveTests: XCTestCase { private var app: XCUIApplication! private var startedMusic = false override func record(_ issue: XCTIssue) { if let app, app.state == .runningForeground { capture("Failure state") let hierarchy = XCTAttachment(string: app.debugDescription) hierarchy.name = "Failure UI hierarchy" hierarchy.lifetime = .keepAlways add(hierarchy) } super.record(issue) } override func tearDownWithError() throws { defer { app?.terminate() } guard let app, app.state == .runningForeground else { return } // Stop only playback initiated by this test, including assertion failures. // Do not leave the shared default room playing between test runs. if startedMusic, app.buttons["Stop playback"].exists { app.buttons["Stop playback"].tap() } } override func setUpWithError() throws { try XCTSkipUnless(ProcessInfo.processInfo.environment["HOPE_RUN_LIVE_TESTS"] == "1", "Requires the live Hope server") continueAfterFailure = false app = XCUIApplication() startedMusic = false app.launchArguments = ["--ui-testing", "--reset-ui-testing", "--live-testing"] app.launch() } func testLiveConversationAndBackgroundTask() { app.buttons["Start chatting"].tap() let composer = app.descendants(matching: .any).matching(identifier: "Message Hope").firstMatch XCTAssertTrue(composer.waitForExistence(timeout: 60), "Agent must join the real LiveKit room") composer.tap() composer.typeText("What is six times seven? Reply in one short sentence.") app.buttons["Send message"].tap() let reply = app.staticTexts.matching(NSPredicate(format: "label CONTAINS[c] 'forty-two' OR label CONTAINS[c] 'forty two' OR label CONTAINS '42'")).firstMatch XCTAssertTrue(reply.waitForExistence(timeout: 60), "Qwen must produce a real response") capture("Live Qwen reply") composer.tap() composer.typeText("Compare espresso and filter coffee in a short background task. Use your existing knowledge; no web search needed.") app.buttons["Send message"].tap() app.tabBars.buttons["Activity"].tap() let complete = app.staticTexts["Complete"].firstMatch XCTAssertTrue(complete.waitForExistence(timeout: 90), "Background task must finish and deliver its result") capture("Live background result") app.tabBars.buttons["Talk"].tap() app.buttons["End conversation"].tap() XCTAssertTrue(app.buttons["Start chatting"].waitForExistence(timeout: 20)) // A second room must connect cleanly after the first was torn down. app.buttons["Start chatting"].tap() XCTAssertTrue(composer.waitForExistence(timeout: 60)) app.buttons["End conversation"].tap() XCTAssertTrue(app.buttons["Start chatting"].waitForExistence(timeout: 20)) } func testLiveLibraryPlayback() { app.tabBars.buttons["Music"].tap() let play = app.buttons.matching(NSPredicate(format: "label BEGINSWITH 'Play Sheryl Crow'")).firstMatch XCTAssertTrue(play.waitForExistence(timeout: 20), "Existing library must load from NAS") startedMusic = true play.tap() XCTAssertTrue(app.buttons["Pause"].waitForExistence(timeout: 30)) // Elapsed media time proves AVPlayer decoded and advanced, beyond a server desired-state label. let position = app.sliders["Playback position"] XCTAssertTrue(position.waitForExistence(timeout: 10)) let advanced = NSPredicate { _, _ in let value = position.value as? String ?? "0%" return value != "0%" && value != "0" && !value.isEmpty } expectation(for: advanced, evaluatedWith: position) waitForExpectations(timeout: 35) app.buttons["Pause"].tap() XCTAssertTrue(app.buttons["Play"].waitForExistence(timeout: 10)) app.buttons["Seek forward 10 seconds"].tap() capture("Live music playback") app.buttons["Stop playback"].tap() XCTAssertFalse(app.buttons["Pause"].exists) startedMusic = false } private func capture(_ name: String) { let attachment = XCTAttachment(screenshot: app.screenshot()) attachment.name = name attachment.lifetime = .keepAlways add(attachment) } }