mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-20 09:31:54 -06:00
231 lines
9.0 KiB
Swift
231 lines
9.0 KiB
Swift
import Foundation
|
|
import Testing
|
|
@testable import OpenClaw
|
|
|
|
struct LiveActivityPresentationArbiterTests {
|
|
@Test func `voice sample buffer quantizes clamps and stays bounded`() {
|
|
var buffer = LiveActivityVoiceSampleBuffer(capacity: 3)
|
|
|
|
for level in [-1.0, 0.25, 0.5, 2.0] {
|
|
if let sample = LiveActivityVoiceSampleBuffer.quantize(level) {
|
|
buffer.append(sample)
|
|
}
|
|
}
|
|
|
|
#expect(buffer.payload == [64, 128, 255])
|
|
#expect(LiveActivityVoiceSampleBuffer.quantize(.infinity) == nil)
|
|
buffer.reset()
|
|
#expect(buffer.payload == nil)
|
|
}
|
|
|
|
@Test
|
|
func `attention outranks tools voice and connection`() {
|
|
let now = Date()
|
|
var arbiter = LiveActivityPresentationArbiter()
|
|
arbiter.setConnection(Self.request(status: .connecting, detail: nil, startedAt: now))
|
|
arbiter.setVoice(Self.request(status: .voiceListening, detail: nil, startedAt: now))
|
|
arbiter.startTool(
|
|
id: "tool-1",
|
|
request: Self.request(status: .toolRunning, detail: "first", startedAt: now))
|
|
arbiter.setAttention(Self.request(status: .approvalNeeded, detail: nil, startedAt: now))
|
|
|
|
#expect(arbiter.current?.state.status == .approvalNeeded)
|
|
|
|
arbiter.setAttention(nil)
|
|
#expect(arbiter.current?.state.status == .toolRunning)
|
|
}
|
|
|
|
@Test
|
|
func `parallel tools resume the newest remaining tool then voice`() {
|
|
let now = Date()
|
|
var arbiter = LiveActivityPresentationArbiter()
|
|
arbiter.setVoice(Self.request(status: .voiceActive, detail: nil, startedAt: now))
|
|
arbiter.startTool(
|
|
id: "tool-1",
|
|
request: Self.request(status: .toolRunning, detail: "first", startedAt: now))
|
|
arbiter.startTool(
|
|
id: "tool-2",
|
|
request: Self.request(status: .toolRunning, detail: "second", startedAt: now))
|
|
|
|
#expect(arbiter.activeToolCount == 2)
|
|
#expect(arbiter.current?.state.verbatimDetail == "second")
|
|
|
|
arbiter.endTool(id: "tool-2", sessionKey: "main")
|
|
#expect(arbiter.current?.state.verbatimDetail == "first")
|
|
|
|
arbiter.endTool(id: "tool-1", sessionKey: "main")
|
|
#expect(arbiter.current?.state.status == .voiceActive)
|
|
}
|
|
|
|
@Test
|
|
func `tool identities are scoped to their owning session`() {
|
|
let now = Date()
|
|
var arbiter = LiveActivityPresentationArbiter()
|
|
arbiter.startTool(
|
|
id: "shared-id",
|
|
request: Self.request(
|
|
status: .toolRunning,
|
|
detail: "main",
|
|
startedAt: now,
|
|
sessionKey: "main"))
|
|
arbiter.startTool(
|
|
id: "shared-id",
|
|
request: Self.request(
|
|
status: .toolRunning,
|
|
detail: "other",
|
|
startedAt: now,
|
|
sessionKey: "other"))
|
|
|
|
#expect(arbiter.activeToolCount == 2)
|
|
arbiter.endTool(id: "shared-id", sessionKey: "main")
|
|
#expect(arbiter.activeToolCount == 1)
|
|
#expect(arbiter.current?.sessionKey == "other")
|
|
}
|
|
|
|
@Test
|
|
func `presentation lifecycle only carries across the same owner`() {
|
|
let startedAt = Date(timeIntervalSince1970: 100)
|
|
let now = Date(timeIntervalSince1970: 500)
|
|
let existing = Self.request(
|
|
status: .connecting,
|
|
detail: nil,
|
|
startedAt: startedAt,
|
|
sessionKey: "main")
|
|
|
|
#expect(LiveActivityManager.lifecycleStartedAt(
|
|
existing: existing,
|
|
agentName: "Aiden",
|
|
sessionKey: "main",
|
|
now: now) == startedAt)
|
|
#expect(LiveActivityManager.lifecycleStartedAt(
|
|
existing: existing,
|
|
agentName: "Aiden",
|
|
sessionKey: "other",
|
|
now: now) == now)
|
|
#expect(LiveActivityManager.lifecycleStartedAt(
|
|
existing: existing,
|
|
agentName: "Luna",
|
|
sessionKey: "main",
|
|
now: now) == now)
|
|
}
|
|
|
|
@Test
|
|
func `voice status distinguishes active listening and speaking`() {
|
|
#expect(LiveActivityPresentationArbiter.voiceStatus(isListening: false, isSpeaking: false) == .voiceActive)
|
|
#expect(LiveActivityPresentationArbiter.voiceStatus(isListening: true, isSpeaking: false) == .voiceListening)
|
|
#expect(LiveActivityPresentationArbiter.voiceStatus(isListening: true, isSpeaking: true) == .voiceSpeaking)
|
|
}
|
|
|
|
@Test
|
|
func `voice sample history survives active phase changes`() {
|
|
#expect(LiveActivityManager.shouldResetVoiceSamples(previousStatus: nil))
|
|
#expect(!LiveActivityManager.shouldResetVoiceSamples(previousStatus: .voiceActive))
|
|
#expect(!LiveActivityManager.shouldResetVoiceSamples(previousStatus: .voiceListening))
|
|
#expect(!LiveActivityManager.shouldResetVoiceSamples(previousStatus: .voiceSpeaking))
|
|
}
|
|
|
|
@Test
|
|
func `reconnect clears connection state without hiding active voice`() {
|
|
let now = Date()
|
|
var arbiter = LiveActivityPresentationArbiter()
|
|
arbiter.setConnection(Self.request(status: .reconnecting, detail: nil, startedAt: now))
|
|
arbiter.setAttention(Self.request(status: .actionRequired, detail: nil, startedAt: now))
|
|
arbiter.setVoice(Self.request(status: .voiceListening, detail: nil, startedAt: now))
|
|
|
|
arbiter.clearConnectionState()
|
|
|
|
#expect(arbiter.attention == nil)
|
|
#expect(arbiter.connection == nil)
|
|
#expect(arbiter.current?.state.status == .voiceListening)
|
|
}
|
|
|
|
@Test
|
|
func `live voice permanently replaces a hydrated tool fallback`() {
|
|
let now = Date()
|
|
var arbiter = LiveActivityPresentationArbiter()
|
|
arbiter.adoptInitialHydratedToolFallback(
|
|
Self.request(status: .toolRunning, detail: "restored", startedAt: now))
|
|
|
|
arbiter.setVoice(Self.request(status: .voiceListening, detail: nil, startedAt: now))
|
|
#expect(arbiter.hydratedToolFallback == nil)
|
|
#expect(arbiter.current?.state.status == .voiceListening)
|
|
|
|
arbiter.setVoice(nil)
|
|
#expect(arbiter.current == nil)
|
|
}
|
|
|
|
@Test
|
|
func `hydration trusts renewed voice and tool stale dates`() {
|
|
let now = Date()
|
|
let originalStart = now.addingTimeInterval(-600)
|
|
let renewedStaleDate = now.addingTimeInterval(240)
|
|
|
|
#expect(LiveActivityManager.shouldHydrate(
|
|
status: .voiceSpeaking,
|
|
startedAt: originalStart,
|
|
staleDate: renewedStaleDate,
|
|
now: now))
|
|
#expect(LiveActivityManager.shouldHydrate(
|
|
status: .toolRunning,
|
|
startedAt: originalStart,
|
|
staleDate: renewedStaleDate,
|
|
now: now))
|
|
}
|
|
|
|
@Test
|
|
func `hydration rejects expired and old non-heartbeat presentations`() {
|
|
let now = Date()
|
|
let originalStart = now.addingTimeInterval(-600)
|
|
|
|
#expect(!LiveActivityManager.shouldHydrate(
|
|
status: .voiceListening,
|
|
startedAt: originalStart,
|
|
staleDate: now.addingTimeInterval(-1),
|
|
now: now))
|
|
#expect(!LiveActivityManager.shouldHydrate(
|
|
status: .reconnecting,
|
|
startedAt: originalStart,
|
|
staleDate: now.addingTimeInterval(240),
|
|
now: now))
|
|
}
|
|
|
|
@Test
|
|
func `voice live activity lifecycle is app owned`() throws {
|
|
let testsDirectory = URL(fileURLWithPath: #filePath).deletingLastPathComponent()
|
|
let sourcesDirectory = testsDirectory.deletingLastPathComponent().appendingPathComponent("Sources")
|
|
let appSource = try String(
|
|
contentsOf: sourcesDirectory.appendingPathComponent("OpenClawApp.swift"),
|
|
encoding: .utf8)
|
|
let chatSource = try String(
|
|
contentsOf: sourcesDirectory.appendingPathComponent("Design/ChatProTab.swift"),
|
|
encoding: .utf8)
|
|
let coordinatorSource = try String(
|
|
contentsOf: sourcesDirectory.appendingPathComponent(
|
|
"LiveActivity/VoiceLiveActivityCoordinator.swift"),
|
|
encoding: .utf8)
|
|
|
|
#expect(appSource.contains("if !Self.screenshotModeEnabled {"))
|
|
#expect(appSource.contains("self.voiceLiveActivityCoordinator.start(appModel: self.appModel)"))
|
|
#expect(coordinatorSource.contains("LiveActivityManager.shared.showVoice("))
|
|
#expect(coordinatorSource.contains("LiveActivityManager.shared.updateVoiceLevel("))
|
|
#expect(!chatSource.contains("syncVoiceLiveActivity"))
|
|
#expect(!chatSource.contains("LiveActivityManager.shared.showVoice("))
|
|
}
|
|
|
|
private static func request(
|
|
status: OpenClawActivityAttributes.ContentState.Status,
|
|
detail: String?,
|
|
startedAt: Date,
|
|
sessionKey: String = "main") -> LiveActivityPresentationRequest
|
|
{
|
|
LiveActivityPresentationRequest(
|
|
state: OpenClawActivityAttributes.ContentState(
|
|
status: status,
|
|
verbatimDetail: detail,
|
|
startedAt: startedAt),
|
|
staleDate: startedAt.addingTimeInterval(300),
|
|
agentName: "Aiden",
|
|
sessionKey: sessionKey)
|
|
}
|
|
}
|