From 9c5e9bce5b3c23345659dd3b9ae892fb3b337f68 Mon Sep 17 00:00:00 2001 From: Shane Maynard Date: Sat, 3 Oct 2026 12:17:35 -0400 Subject: [PATCH] test: add opt-in native chat transport qualification --- Tests/ChatLiveTests.swift | 67 +++++++++++++++++++++++++++++++++ Voice.xcodeproj/project.pbxproj | 4 ++ docs/PROJECT_STATUS.md | 18 +++++++++ 3 files changed, 89 insertions(+) create mode 100644 Tests/ChatLiveTests.swift diff --git a/Tests/ChatLiveTests.swift b/Tests/ChatLiveTests.swift new file mode 100644 index 0000000..14b0b4d --- /dev/null +++ b/Tests/ChatLiveTests.swift @@ -0,0 +1,67 @@ +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) } +} diff --git a/Voice.xcodeproj/project.pbxproj b/Voice.xcodeproj/project.pbxproj index 3428672..f6040f8 100644 --- a/Voice.xcodeproj/project.pbxproj +++ b/Voice.xcodeproj/project.pbxproj @@ -32,6 +32,7 @@ 943BCE971A01F220FAE9E250 /* TaskPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E85BACA267ADD462306C9920 /* TaskPanelView.swift */; }; 9461C0886FA294FDD9742E51 /* TranscriptListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD31F76E1880AC76ED31DD4D /* TranscriptListView.swift */; }; 98BA1A0F92DB7B98A8BA7814 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48F76881B4AE51DEE74511CC /* SettingsView.swift */; }; + A4A97EAE381B922B213128F3 /* ChatLiveTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2623CA741424CA9B1B6BDA0 /* ChatLiveTests.swift */; }; A5868238CF4958D6D0C5908E /* TaskStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0B60FBBB9C47BB9ADACA7CA /* TaskStore.swift */; }; A66BC894BEEC870D879F991F /* LightwellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87DD77AA383E1ECCEEEFB399 /* LightwellView.swift */; }; A6F527DF9D7F76FF5B4D71D7 /* HopeTheme.generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7C22C8FB52C3963186CC108 /* HopeTheme.generated.swift */; }; @@ -110,6 +111,7 @@ E85BACA267ADD462306C9920 /* TaskPanelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskPanelView.swift; sourceTree = ""; }; EA2918A7B29AB6EB033E707C /* TLSLiveTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TLSLiveTests.swift; sourceTree = ""; }; EB58EEF9490F81FB2E1CE31C /* VoiceSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceSession.swift; sourceTree = ""; }; + F2623CA741424CA9B1B6BDA0 /* ChatLiveTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatLiveTests.swift; sourceTree = ""; }; F5683993A1DE677121FFA1F3 /* ServerConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerConfig.swift; sourceTree = ""; }; F7C22C8FB52C3963186CC108 /* HopeTheme.generated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HopeTheme.generated.swift; sourceTree = ""; }; FAEF89B2DEEE68F7168A8014 /* TranscriptDedupeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranscriptDedupeTests.swift; sourceTree = ""; }; @@ -243,6 +245,7 @@ isa = PBXGroup; children = ( 262B5A712708421C35ED6AC0 /* AuthTests.swift */, + F2623CA741424CA9B1B6BDA0 /* ChatLiveTests.swift */, B5B373E4A57214C0D289536B /* ChatTests.swift */, C8F93A6F4B4354976953F69F /* ChatTransportTests.swift */, BDD0AA8B0E9B45A325D1E5E7 /* ConversationStateTests.swift */, @@ -446,6 +449,7 @@ buildActionMask = 2147483647; files = ( 2D1633BB6B8C3E7938A46A42 /* AuthTests.swift in Sources */, + A4A97EAE381B922B213128F3 /* ChatLiveTests.swift in Sources */, C28D28376A22BE3981908AC2 /* ChatTests.swift in Sources */, B0266A37FF81F7DF7F288B37 /* ChatTransportTests.swift in Sources */, FC696C6E74057D4C93E6DA67 /* ConversationStateTests.swift in Sources */, diff --git a/docs/PROJECT_STATUS.md b/docs/PROJECT_STATUS.md index b5c94d9..abfd634 100644 --- a/docs/PROJECT_STATUS.md +++ b/docs/PROJECT_STATUS.md @@ -70,6 +70,24 @@ the active keyboard; dark list capture shows full shell. Initial new dark UI tes failed the combined native picker label and was corrected; failed bundle retained `/tmp/hope-chat-polish-tests.xcresult`. No hardware installation or live request. +Physical follow-up — 2026-10-03: parent reported installation and ordinary launch +of the signed app succeeded around 12:04–12:05. Added `ChatLiveTests`, deliberately +opt-in with protected environment credentials; offline simulator compile/test +explicitly **skipped one live test**, zero failures, at +`/tmp/hope-chat-live-optin-compile.log`. One authorized physical HTTP attempt used +native pinned login and a disposable chat (no memory/preferences mutations). +It **failed**, not passed: `/tmp/hope-chat-physical-qualified.xcresult` reports +one failed test, zero passed, "test runner exited with code 0 before finishing"; +outer xcodebuild exited 65. Evidence `/tmp/hope-chat-physical-qualified.log` and +`/tmp/hope-chat-physical-summary.json`. App-host logs also show interactive voice +connections and microphone enable while the HTTP probe was running; the probe +itself calls no voice API. Concurrent user interaction is possible, not proven +causal. Do not claim no audio captured or successful physical chat qualification. +Cleanup after host exit is unconfirmed; parent should inspect only admin-owned +chat titled **Native qualification · disposable**, never unrelated user chats. +Further native live attempts stopped at parent's request while user uses voice. +No test-host UI guard was retained and no subsequent device action performed. + ## Approved Opal pink redesign — 2026-10-02 User approved the Opal HTML direction with the existing square reactive persona.