test: add opt-in native chat transport qualification
This commit is contained in:
1 parent
ec510b938b
commit
9c5e9bce5b
3 files changed
+89
No files matched your search
@@ -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) }
|
||||||
|
}
|
||||||
@@ -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 */,
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in new issue
Block a user