mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-16 16:01:38 +00:00
137 lines
4.8 KiB
Swift
137 lines
4.8 KiB
Swift
import Foundation
|
|
import OpenClawKit
|
|
import Testing
|
|
@testable import OpenClawChatUI
|
|
|
|
private final class HapticRecorder: @unchecked Sendable {
|
|
private let lock = NSLock()
|
|
private var recordedEvents: [OpenClawChatHaptics.Event] = []
|
|
|
|
var events: [OpenClawChatHaptics.Event] {
|
|
self.lock.lock()
|
|
defer { self.lock.unlock() }
|
|
return self.recordedEvents
|
|
}
|
|
|
|
func record(_ event: OpenClawChatHaptics.Event) {
|
|
self.lock.lock()
|
|
defer { self.lock.unlock() }
|
|
self.recordedEvents.append(event)
|
|
}
|
|
}
|
|
|
|
private final class HapticsTestTransport: @unchecked Sendable, OpenClawChatTransport {
|
|
private let response: OpenClawChatSendResponse
|
|
private let historyMessages: [AnyCodable]
|
|
private let stream: AsyncStream<OpenClawChatTransportEvent>
|
|
private let continuation: AsyncStream<OpenClawChatTransportEvent>.Continuation
|
|
|
|
init(status: String, historyMessages: [AnyCodable] = []) {
|
|
self.response = OpenClawChatSendResponse(runId: "run-1", status: status)
|
|
self.historyMessages = historyMessages
|
|
var continuation: AsyncStream<OpenClawChatTransportEvent>.Continuation!
|
|
self.stream = AsyncStream { continuation = $0 }
|
|
self.continuation = continuation
|
|
}
|
|
|
|
func requestHistory(sessionKey: String) async throws -> OpenClawChatHistoryPayload {
|
|
OpenClawChatHistoryPayload(
|
|
sessionKey: sessionKey,
|
|
sessionId: "session-1",
|
|
messages: self.historyMessages,
|
|
thinkingLevel: "off")
|
|
}
|
|
|
|
func sendMessage(
|
|
sessionKey _: String,
|
|
message _: String,
|
|
thinking _: String,
|
|
idempotencyKey _: String,
|
|
attachments _: [OpenClawChatAttachmentPayload]) async throws -> OpenClawChatSendResponse
|
|
{
|
|
self.response
|
|
}
|
|
|
|
func requestHealth(timeoutMs _: Int) async throws -> Bool {
|
|
true
|
|
}
|
|
|
|
func events() -> AsyncStream<OpenClawChatTransportEvent> {
|
|
self.stream
|
|
}
|
|
|
|
func emit(_ event: OpenClawChatTransportEvent) {
|
|
self.continuation.yield(event)
|
|
}
|
|
}
|
|
|
|
private func makeHapticsViewModel(status: String, historyMessages: [AnyCodable] = []) async -> (
|
|
HapticsTestTransport,
|
|
OpenClawChatViewModel,
|
|
HapticRecorder)
|
|
{
|
|
let transport = HapticsTestTransport(status: status, historyMessages: historyMessages)
|
|
let recorder = HapticRecorder()
|
|
let haptics = OpenClawChatHaptics(performer: recorder.record)
|
|
let viewModel = await MainActor.run {
|
|
OpenClawChatViewModel(sessionKey: "main", transport: transport, haptics: haptics)
|
|
}
|
|
return (transport, viewModel, recorder)
|
|
}
|
|
|
|
private func sendHapticsTestMessage(_ viewModel: OpenClawChatViewModel) async {
|
|
await MainActor.run {
|
|
viewModel.input = "hello"
|
|
viewModel.send()
|
|
}
|
|
}
|
|
|
|
struct ChatHapticsTests {
|
|
@Test func `send acceptance fires message sent exactly once`() async throws {
|
|
let (_, viewModel, recorder) = await makeHapticsViewModel(status: "started")
|
|
await sendHapticsTestMessage(viewModel)
|
|
try await waitUntil("message sent haptic") { recorder.events == [.messageSent] }
|
|
try await Task.sleep(for: .milliseconds(20))
|
|
#expect(recorder.events == [.messageSent])
|
|
}
|
|
|
|
@Test func `completion fires once for duplicate terminal events`() async throws {
|
|
let (transport, viewModel, recorder) = await makeHapticsViewModel(status: "started")
|
|
await sendHapticsTestMessage(viewModel)
|
|
try await waitUntil("message accepted") { recorder.events == [.messageSent] }
|
|
|
|
let final = OpenClawChatTransportEvent.chat(OpenClawChatEventPayload(
|
|
runId: "run-1",
|
|
sessionKey: "main",
|
|
state: "final",
|
|
message: nil,
|
|
errorMessage: nil))
|
|
transport.emit(final)
|
|
transport.emit(final)
|
|
try await waitUntil("completion haptic") {
|
|
recorder.events == [.messageSent, .runCompleted]
|
|
}
|
|
try await Task.sleep(for: .milliseconds(20))
|
|
#expect(recorder.events == [.messageSent, .runCompleted])
|
|
}
|
|
|
|
@Test(arguments: ["error", "aborted"])
|
|
func `durable assistant failure fires run failed`(stopReason: String) async throws {
|
|
let error = AnyCodable([
|
|
"role": "assistant",
|
|
"content": [],
|
|
"timestamp": Date().timeIntervalSince1970 * 1000 + 1000,
|
|
"stopReason": stopReason,
|
|
"errorMessage": "provider failed",
|
|
] as [String: Any])
|
|
let (_, viewModel, recorder) = await makeHapticsViewModel(
|
|
status: "started",
|
|
historyMessages: [error])
|
|
await sendHapticsTestMessage(viewModel)
|
|
try await waitUntil("run failed haptic") {
|
|
recorder.events == [.messageSent, .runFailed]
|
|
}
|
|
#expect(recorder.events == [.messageSent, .runFailed])
|
|
}
|
|
}
|