Files
openclaw/apps/shared/OpenClawKit/Tests/OpenClawKitTests/ChatInputHistoryTests.swift
Peter Steinberger bf754b8a34 feat: align native macOS chat with the web chat capability set (#109712)
* 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
2026-07-17 13:44:48 +01:00

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