openclaw/apps/macos/Tests/MoltbotIPCTests/VoiceWakeRuntimeTests.swift
Guillaume Nodet 479fc7450e macOS: fix voice wake crash in trimmedAfterTrigger
Fixed an index out of bounds crash in VoiceWakeRuntime.trimmedAfterTrigger that occurred when processing voice transcripts. The issue was caused by attempting to subscript a string with an index that could exceed the string's endIndex when using indices from a lowercased version of the string.

Added a guard statement to check that the index is within bounds before attempting to subscript the string. If the index is out of bounds, the function continues to the next trigger instead of crashing.

Fixes the crash reported in crash.txt at line 743.
2026-01-28 15:15:36 +01:00

93 lines
3.5 KiB
Swift

import Foundation
import SwabbleKit
import Testing
@testable import Moltbot
@Suite struct VoiceWakeRuntimeTests {
@Test func trimsAfterTriggerKeepsPostSpeech() {
let triggers = ["claude", "clawd"]
let text = "hey Claude how are you"
#expect(VoiceWakeRuntime._testTrimmedAfterTrigger(text, triggers: triggers) == "how are you")
}
@Test func trimsAfterTriggerReturnsOriginalWhenNoTrigger() {
let triggers = ["claude"]
let text = "good morning friend"
#expect(VoiceWakeRuntime._testTrimmedAfterTrigger(text, triggers: triggers) == text)
}
@Test func trimsAfterFirstMatchingTrigger() {
let triggers = ["buddy", "claude"]
let text = "hello buddy this is after trigger claude also here"
#expect(VoiceWakeRuntime
._testTrimmedAfterTrigger(text, triggers: triggers) == "this is after trigger claude also here")
}
@Test func hasContentAfterTriggerFalseWhenOnlyTrigger() {
let triggers = ["clawd"]
let text = "hey clawd"
#expect(!VoiceWakeRuntime._testHasContentAfterTrigger(text, triggers: triggers))
}
@Test func hasContentAfterTriggerTrueWhenSpeechContinues() {
let triggers = ["claude"]
let text = "claude write a note"
#expect(VoiceWakeRuntime._testHasContentAfterTrigger(text, triggers: triggers))
}
@Test func trimsAfterTriggerHandlesTriggerAtEnd() {
let triggers = ["clawd"]
let text = "hey clawd"
#expect(VoiceWakeRuntime._testTrimmedAfterTrigger(text, triggers: triggers) == "")
}
@Test func trimsAfterTriggerHandlesEdgeCaseIndexBounds() {
// Regression test for crash when trigger range upperBound exceeds text.endIndex
let triggers = ["claude"]
let text = "claude"
#expect(VoiceWakeRuntime._testTrimmedAfterTrigger(text, triggers: triggers) == "")
}
@Test func gateRequiresGapBetweenTriggerAndCommand() {
let transcript = "hey clawd do thing"
let segments = makeSegments(
transcript: transcript,
words: [
("hey", 0.0, 0.1),
("clawd", 0.2, 0.1),
("do", 0.35, 0.1),
("thing", 0.5, 0.1),
])
let config = WakeWordGateConfig(triggers: ["clawd"], minPostTriggerGap: 0.3)
#expect(WakeWordGate.match(transcript: transcript, segments: segments, config: config) == nil)
}
@Test func gateAcceptsGapAndExtractsCommand() {
let transcript = "hey clawd do thing"
let segments = makeSegments(
transcript: transcript,
words: [
("hey", 0.0, 0.1),
("clawd", 0.2, 0.1),
("do", 0.9, 0.1),
("thing", 1.1, 0.1),
])
let config = WakeWordGateConfig(triggers: ["clawd"], minPostTriggerGap: 0.3)
#expect(WakeWordGate.match(transcript: transcript, segments: segments, config: config)?.command == "do thing")
}
}
private func makeSegments(
transcript: String,
words: [(String, TimeInterval, TimeInterval)])
-> [WakeWordSegment] {
var searchStart = transcript.startIndex
var output: [WakeWordSegment] = []
for (word, start, duration) in words {
let range = transcript.range(of: word, range: searchStart..<transcript.endIndex)
output.append(WakeWordSegment(text: word, start: start, duration: duration, range: range))
if let range { searchStart = range.upperBound }
}
return output
}