mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-16 23:52:40 -06:00
bf754b8a34
* feat(macos): wire sessions manager, assistant trace, talk, voice notes, and Listen into native chat * feat(clients): add input history, reply quoting, and per-session drafts to native chat composer * fix(clients): capture TTS server lease before gateway check and dedupe restored input-history entries * docs(clients): document reply-chip consume parity with web accepted-send semantics * fix(clients): retire sent-text recall stash and keep recall markers stable under prepends * test(clients): wait for outbox restore before asserting live-send paths * docs(clients): note cross-session outbox flush scheduling contract * chore(clients): split oversized chat files to satisfy SwiftLint length caps * chore(i18n): sync native app i18n inventory for new chat strings * chore: retrigger ci * chore(i18n): translate new chat strings and regenerate Apple catalogs * chore(i18n): regenerate Android localization resources for new chat strings * test(clients): await lifecycle cleanup before modeling replacement gateway traffic * chore(clients): split invoke timeout helper to satisfy SwiftLint length cap * chore(clients): drop unused OpenClawProtocol import from invoke timeout split
209 lines
7.2 KiB
Swift
209 lines
7.2 KiB
Swift
import Foundation
|
|
import Testing
|
|
@testable import OpenClawChatUI
|
|
#if os(macOS)
|
|
import AppKit
|
|
#endif
|
|
|
|
struct ChatInputHistoryTests {
|
|
@Test func `up walks newest to oldest and down restores draft`() {
|
|
var history = ChatInputHistory()
|
|
history.record("first")
|
|
history.record("second")
|
|
|
|
#expect(history.previous(draft: "working draft") == "second")
|
|
#expect(history.previous(draft: "ignored while recalling") == "first")
|
|
#expect(history.previous(draft: "ignored") == nil)
|
|
#expect(history.next() == "second")
|
|
#expect(history.next() == "working draft")
|
|
#expect(history.next() == nil)
|
|
#expect(!history.isRecalling)
|
|
}
|
|
|
|
@Test func `escape restores draft and boundaries do not move`() {
|
|
var history = ChatInputHistory()
|
|
#expect(history.previous(draft: "draft") == nil)
|
|
history.record("sent")
|
|
#expect(history.next() == nil)
|
|
#expect(history.previous(draft: "draft") == "sent")
|
|
#expect(history.previous(draft: "draft") == nil)
|
|
#expect(history.cancel() == "draft")
|
|
#expect(history.cancel() == nil)
|
|
}
|
|
|
|
@Test func `consecutive duplicates collapse but separated duplicates remain`() {
|
|
var history = ChatInputHistory()
|
|
history.record("same")
|
|
history.record(" same \n")
|
|
history.record("different")
|
|
history.record("same")
|
|
|
|
#expect(history.entries == ["same", "different", "same"])
|
|
}
|
|
|
|
@Test func `history is capped at one hundred entries`() {
|
|
var history = ChatInputHistory()
|
|
for index in 0...100 {
|
|
history.record("entry-\(index)")
|
|
}
|
|
|
|
#expect(history.entries.count == 100)
|
|
#expect(history.entries.first == "entry-100")
|
|
#expect(history.entries.last == "entry-1")
|
|
}
|
|
|
|
@Test func `transcript seed retains non transcript command capture`() {
|
|
var history = ChatInputHistory()
|
|
history.seed(transcriptInputs: ["hello", "answer this"])
|
|
history.record("/compact")
|
|
history.seed(transcriptInputs: ["hello", "answer this"])
|
|
|
|
#expect(history.entries == ["/compact", "answer this", "hello"])
|
|
}
|
|
|
|
@Test func `transcript refresh preserves recall draft and cursor`() {
|
|
var history = ChatInputHistory()
|
|
history.seed(transcriptInputs: ["older"])
|
|
#expect(history.previous(draft: "working draft") == "older")
|
|
|
|
history.seed(transcriptInputs: ["older", "new from elsewhere"])
|
|
|
|
#expect(history.cancel() == "working draft")
|
|
}
|
|
|
|
@Test func `late successful input preserves active recall draft`() {
|
|
var history = ChatInputHistory()
|
|
history.record("older")
|
|
#expect(history.previous(draft: "working draft") == "older")
|
|
|
|
history.record("newly accepted")
|
|
|
|
#expect(history.cancel() == "working draft")
|
|
#expect(history.entries == ["newly accepted", "older"])
|
|
}
|
|
|
|
@Test func `transcript echoes do not displace local command chronology`() {
|
|
var history = ChatInputHistory()
|
|
history.seed(transcriptInputs: ["older"])
|
|
history.record("/compact")
|
|
history.record("hello", transcriptEcho: "hello")
|
|
|
|
history.seed(transcriptInputs: ["older", "hello"])
|
|
|
|
#expect(history.entries == ["hello", "/compact", "older"])
|
|
}
|
|
|
|
@Test func `generated transport text stays out of history`() {
|
|
var history = ChatInputHistory()
|
|
history.seed(transcriptInputs: ["older"])
|
|
history.record("continue", transcriptEcho: "> **Assistant:** quote\n\ncontinue")
|
|
history.record("", transcriptEcho: "See attached.")
|
|
|
|
history.seed(transcriptInputs: [
|
|
"older",
|
|
"> **Assistant:** quote\n\ncontinue",
|
|
"See attached.",
|
|
])
|
|
|
|
#expect(history.entries == ["continue", "older"])
|
|
}
|
|
|
|
@Test func `generated transcript echo arriving before acceptance is reconciled`() {
|
|
var history = ChatInputHistory()
|
|
let generated = "> **Assistant:** quote\n\ncontinue"
|
|
history.seed(transcriptInputs: ["older", generated])
|
|
|
|
history.record("continue", transcriptEcho: generated)
|
|
|
|
#expect(history.entries == ["continue", "older"])
|
|
}
|
|
|
|
@Test func `transcript rollback and prepend do not duplicate entries`() {
|
|
var history = ChatInputHistory()
|
|
history.seed(transcriptInputs: ["one", "two"])
|
|
history.seed(transcriptInputs: ["one"])
|
|
#expect(history.entries == ["two", "one"])
|
|
|
|
history.seed(transcriptInputs: ["zero", "one"])
|
|
#expect(history.entries == ["two", "one", "zero"])
|
|
}
|
|
|
|
@Test func `rollback restore after local command does not duplicate entries`() {
|
|
var history = ChatInputHistory()
|
|
history.seed(transcriptInputs: ["one", "two"])
|
|
history.record("/compact")
|
|
history.seed(transcriptInputs: ["one"])
|
|
history.seed(transcriptInputs: ["one", "two"])
|
|
|
|
#expect(history.entries == ["two", "/compact", "one"])
|
|
}
|
|
|
|
@Test func `late acceptance retires a stash holding the sent text`() {
|
|
var history = ChatInputHistory()
|
|
history.record("older")
|
|
#expect(history.previous(draft: "sent me") == "older")
|
|
|
|
history.record("sent me")
|
|
|
|
#expect(history.cancel() == "")
|
|
}
|
|
|
|
@Test func `late acceptance keeps a genuinely newer stashed draft`() {
|
|
var history = ChatInputHistory()
|
|
history.record("older")
|
|
#expect(history.previous(draft: "newer unsent draft") == "older")
|
|
|
|
history.record("sent me")
|
|
|
|
#expect(history.cancel() == "newer unsent draft")
|
|
}
|
|
|
|
@Test func `recall marker stays on the same duplicate after an identical prepend`() {
|
|
var history = ChatInputHistory()
|
|
history.record("dup")
|
|
history.record("x")
|
|
#expect(history.previous(draft: "") == "x")
|
|
#expect(history.previous(draft: "") == "dup")
|
|
|
|
history.record("dup")
|
|
|
|
#expect(history.entries == ["dup", "x", "dup"])
|
|
// Cursor still points at the oldest "dup": one step newer is "x".
|
|
#expect(history.next() == "x")
|
|
}
|
|
|
|
@Test func `manual edit exits recall without overwriting stashed draft`() {
|
|
var history = ChatInputHistory()
|
|
history.record("sent")
|
|
#expect(history.previous(draft: "draft") == "sent")
|
|
|
|
history.draftChanged(to: "edited recalled value")
|
|
|
|
#expect(!history.isRecalling)
|
|
#expect(history.stashedDraft == nil)
|
|
}
|
|
}
|
|
|
|
#if os(macOS)
|
|
struct ChatInputHistoryKeyContextTests {
|
|
@Test func `first line requires insertion caret before newline`() {
|
|
#expect(ChatComposerKeyCommandContext.resolve(
|
|
text: "first\nsecond",
|
|
selectedRange: NSRange(location: 3, length: 0)).caretOnFirstLine)
|
|
#expect(!ChatComposerKeyCommandContext.resolve(
|
|
text: "first\nsecond",
|
|
selectedRange: NSRange(location: 8, length: 0)).caretOnFirstLine)
|
|
#expect(!ChatComposerKeyCommandContext.resolve(
|
|
text: "first",
|
|
selectedRange: NSRange(location: 0, length: 2)).caretOnFirstLine)
|
|
}
|
|
|
|
@Test func `shift up remains native text selection`() {
|
|
#expect(ChatComposerKeyRouting.command(
|
|
keyCode: 126,
|
|
modifierFlags: [.shift],
|
|
hasMarkedText: false) == nil)
|
|
}
|
|
}
|
|
#endif
|