test: add opt-in native chat transport qualification

This commit is contained in:
snowspeeder committed 2026-10-03 12:17:35 -04:00
1 parent ec510b938b
commit 9c5e9bce5b
3 files changed
+89

No files matched your search

+67
View File
@@ -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) }
}
+4
View File
@@ -32,6 +32,7 @@
943BCE971A01F220FAE9E250 /* TaskPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E85BACA267ADD462306C9920 /* TaskPanelView.swift */; }; 943BCE971A01F220FAE9E250 /* TaskPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E85BACA267ADD462306C9920 /* TaskPanelView.swift */; };
9461C0886FA294FDD9742E51 /* TranscriptListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD31F76E1880AC76ED31DD4D /* TranscriptListView.swift */; }; 9461C0886FA294FDD9742E51 /* TranscriptListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD31F76E1880AC76ED31DD4D /* TranscriptListView.swift */; };
98BA1A0F92DB7B98A8BA7814 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48F76881B4AE51DEE74511CC /* SettingsView.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 */; }; A5868238CF4958D6D0C5908E /* TaskStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0B60FBBB9C47BB9ADACA7CA /* TaskStore.swift */; };
A66BC894BEEC870D879F991F /* LightwellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87DD77AA383E1ECCEEEFB399 /* LightwellView.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 */; }; 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 = "<group>"; }; E85BACA267ADD462306C9920 /* TaskPanelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskPanelView.swift; sourceTree = "<group>"; };
EA2918A7B29AB6EB033E707C /* TLSLiveTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TLSLiveTests.swift; sourceTree = "<group>"; }; EA2918A7B29AB6EB033E707C /* TLSLiveTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TLSLiveTests.swift; sourceTree = "<group>"; };
EB58EEF9490F81FB2E1CE31C /* VoiceSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceSession.swift; sourceTree = "<group>"; }; EB58EEF9490F81FB2E1CE31C /* VoiceSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceSession.swift; sourceTree = "<group>"; };
F2623CA741424CA9B1B6BDA0 /* ChatLiveTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatLiveTests.swift; sourceTree = "<group>"; };
F5683993A1DE677121FFA1F3 /* ServerConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerConfig.swift; sourceTree = "<group>"; }; F5683993A1DE677121FFA1F3 /* ServerConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerConfig.swift; sourceTree = "<group>"; };
F7C22C8FB52C3963186CC108 /* HopeTheme.generated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HopeTheme.generated.swift; sourceTree = "<group>"; }; F7C22C8FB52C3963186CC108 /* HopeTheme.generated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HopeTheme.generated.swift; sourceTree = "<group>"; };
FAEF89B2DEEE68F7168A8014 /* TranscriptDedupeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranscriptDedupeTests.swift; sourceTree = "<group>"; }; FAEF89B2DEEE68F7168A8014 /* TranscriptDedupeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranscriptDedupeTests.swift; sourceTree = "<group>"; };
@@ -243,6 +245,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
262B5A712708421C35ED6AC0 /* AuthTests.swift */, 262B5A712708421C35ED6AC0 /* AuthTests.swift */,
F2623CA741424CA9B1B6BDA0 /* ChatLiveTests.swift */,
B5B373E4A57214C0D289536B /* ChatTests.swift */, B5B373E4A57214C0D289536B /* ChatTests.swift */,
C8F93A6F4B4354976953F69F /* ChatTransportTests.swift */, C8F93A6F4B4354976953F69F /* ChatTransportTests.swift */,
BDD0AA8B0E9B45A325D1E5E7 /* ConversationStateTests.swift */, BDD0AA8B0E9B45A325D1E5E7 /* ConversationStateTests.swift */,
@@ -446,6 +449,7 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
2D1633BB6B8C3E7938A46A42 /* AuthTests.swift in Sources */, 2D1633BB6B8C3E7938A46A42 /* AuthTests.swift in Sources */,
A4A97EAE381B922B213128F3 /* ChatLiveTests.swift in Sources */,
C28D28376A22BE3981908AC2 /* ChatTests.swift in Sources */, C28D28376A22BE3981908AC2 /* ChatTests.swift in Sources */,
B0266A37FF81F7DF7F288B37 /* ChatTransportTests.swift in Sources */, B0266A37FF81F7DF7F288B37 /* ChatTransportTests.swift in Sources */,
FC696C6E74057D4C93E6DA67 /* ConversationStateTests.swift in Sources */, FC696C6E74057D4C93E6DA67 /* ConversationStateTests.swift in Sources */,
+18
View File
@@ -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 failed the combined native picker label and was corrected; failed bundle retained
`/tmp/hope-chat-polish-tests.xcresult`. No hardware installation or live request. `/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 ## Approved Opal pink redesign — 2026-10-02
User approved the Opal HTML direction with the existing square reactive persona. User approved the Opal HTML direction with the existing square reactive persona.