mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-16 23:52:40 -06:00
b7f99d0ffd
* fix(macos): harden Quick Chat capture — drop AX-role text, restore bar after area capture, guard overlays post-permission * chore(i18n): refresh native inventory line numbers after capture hardening
168 lines
6.2 KiB
Swift
168 lines
6.2 KiB
Swift
import Testing
|
|
@testable import OpenClaw
|
|
|
|
@Suite(.serialized)
|
|
struct QuickChatFocusedTextCollectorTests {
|
|
@Test func `collector collapses adjacent parent-child echoes`() {
|
|
let child = FakeTextNode(id: 2, stringValue: "same", computedName: "other")
|
|
let root = FakeTextNode(id: 1, stringValue: "same", computedName: "same", children: [child])
|
|
|
|
let result = QuickChatFocusedTextCollector.collect(root: root)
|
|
|
|
#expect(result.text == "same\nother")
|
|
#expect(result.textEntryCount == 2)
|
|
#expect(!result.wasTruncated)
|
|
}
|
|
|
|
@Test func `collector preserves equal text from distinct elements`() {
|
|
let cells = (2...4).map { FakeTextNode(id: UInt64($0), stringValue: "42") }
|
|
let root = FakeTextNode(id: 1, stringValue: "table", children: cells)
|
|
|
|
let result = QuickChatFocusedTextCollector.collect(root: root)
|
|
|
|
#expect(result.text == "table\n42\n42\n42")
|
|
#expect(result.textEntryCount == 4)
|
|
}
|
|
|
|
@Test func `collector suppresses echoes from any ancestor`() {
|
|
let grandchild = FakeTextNode(id: 3, stringValue: "same")
|
|
let child = FakeTextNode(id: 2, stringValue: "same", computedName: "middle", children: [grandchild])
|
|
let root = FakeTextNode(id: 1, stringValue: "same", children: [child])
|
|
|
|
let result = QuickChatFocusedTextCollector.collect(root: root)
|
|
|
|
#expect(result.text == "same\nmiddle")
|
|
#expect(result.textEntryCount == 2)
|
|
}
|
|
|
|
@Test func `collector honors cancellation and deadline`() {
|
|
let children = (2...40).map { FakeTextNode(id: UInt64($0), stringValue: "item \($0)") }
|
|
let root = FakeTextNode(id: 1, stringValue: "root", children: children)
|
|
|
|
var calls = 0
|
|
let cancelled = QuickChatFocusedTextCollector.collect(root: root, isCancelled: {
|
|
calls += 1
|
|
return calls > 3
|
|
})
|
|
#expect(cancelled.visitedElementCount < children.count)
|
|
#expect(cancelled.wasTruncated)
|
|
|
|
let expired = QuickChatFocusedTextCollector.collect(
|
|
root: root,
|
|
deadline: ContinuousClock.now.advanced(by: .seconds(-1)))
|
|
#expect(expired.visitedElementCount == 0)
|
|
#expect(expired.wasTruncated)
|
|
}
|
|
|
|
@Test func `collector enforces depth cap`() {
|
|
let deepest = FakeTextNode(id: 3, stringValue: "too deep")
|
|
let child = FakeTextNode(id: 2, stringValue: "child", children: [deepest])
|
|
let root = FakeTextNode(id: 1, stringValue: "root", children: [child])
|
|
|
|
let result = QuickChatFocusedTextCollector.collect(
|
|
root: root,
|
|
limits: QuickChatTextCollectionLimits(
|
|
maximumDepth: 1,
|
|
maximumElements: 20,
|
|
maximumCharacters: 100))
|
|
|
|
#expect(result.text.contains("root"))
|
|
#expect(result.text.contains("child"))
|
|
#expect(!result.text.contains("too deep"))
|
|
#expect(result.text.hasSuffix(QuickChatFocusedTextCollector.truncationMarker))
|
|
#expect(result.visitedElementCount == 2)
|
|
}
|
|
|
|
@Test func `collector enforces element cap`() {
|
|
let children = (2...12).map { FakeTextNode(id: UInt64($0), stringValue: "item \($0)") }
|
|
let root = FakeTextNode(id: 1, stringValue: "root", children: children)
|
|
|
|
let result = QuickChatFocusedTextCollector.collect(
|
|
root: root,
|
|
limits: QuickChatTextCollectionLimits(
|
|
maximumDepth: 12,
|
|
maximumElements: 3,
|
|
maximumCharacters: 100))
|
|
|
|
#expect(result.visitedElementCount == 3)
|
|
#expect(result.wasTruncated)
|
|
#expect(result.text.hasSuffix(QuickChatFocusedTextCollector.truncationMarker))
|
|
}
|
|
|
|
@Test func `collector enforces character cap with marker`() {
|
|
let root = FakeTextNode(id: 1, stringValue: String(repeating: "a", count: 200))
|
|
|
|
let result = QuickChatFocusedTextCollector.collect(
|
|
root: root,
|
|
limits: QuickChatTextCollectionLimits(
|
|
maximumDepth: 12,
|
|
maximumElements: 800,
|
|
maximumCharacters: 40))
|
|
|
|
#expect(result.text.count == 40)
|
|
#expect(result.text.hasSuffix(QuickChatFocusedTextCollector.truncationMarker))
|
|
#expect(result.wasTruncated)
|
|
}
|
|
|
|
@Test func `collector treats the exact sentinel character as truncation`() {
|
|
let root = FakeTextNode(id: 1, stringValue: String(repeating: "a", count: 41))
|
|
|
|
let result = QuickChatFocusedTextCollector.collect(
|
|
root: root,
|
|
limits: QuickChatTextCollectionLimits(
|
|
maximumDepth: 12,
|
|
maximumElements: 800,
|
|
maximumCharacters: 40))
|
|
|
|
#expect(result.text.count == 40)
|
|
#expect(result.text.hasSuffix(QuickChatFocusedTextCollector.truncationMarker))
|
|
#expect(result.wasTruncated)
|
|
}
|
|
|
|
@Test func `collector reports no text when every node is structure-only`() {
|
|
// Mirrors a canvas/image window: nodes expose no textual attributes (the AX
|
|
// adapter now returns nil instead of a role name). textEntryCount must stay 0
|
|
// so the caller surfaces "No readable text" rather than a chip of role words.
|
|
let child = FakeTextNode(id: 2)
|
|
let root = FakeTextNode(id: 1, children: [child])
|
|
|
|
let result = QuickChatFocusedTextCollector.collect(root: root)
|
|
|
|
#expect(result.text.isEmpty)
|
|
#expect(result.textEntryCount == 0)
|
|
}
|
|
}
|
|
|
|
private final class FakeTextNode: QuickChatTextTreeNode, Sendable {
|
|
let identity: UInt64
|
|
let storedStringValue: String?
|
|
let storedComputedName: String?
|
|
let childNodes: [FakeTextNode]
|
|
|
|
init(
|
|
id: UInt64,
|
|
stringValue: String? = nil,
|
|
computedName: String? = nil,
|
|
children: [FakeTextNode] = [])
|
|
{
|
|
self.identity = id
|
|
self.storedStringValue = stringValue
|
|
self.storedComputedName = computedName
|
|
self.childNodes = children
|
|
}
|
|
|
|
func stringValue() -> String? {
|
|
self.storedStringValue
|
|
}
|
|
|
|
func computedName() -> String? {
|
|
self.storedComputedName
|
|
}
|
|
|
|
func children(limit: Int) -> QuickChatTextTreeChildren {
|
|
QuickChatTextTreeChildren(
|
|
nodes: Array(self.childNodes.prefix(limit)),
|
|
wasTruncated: self.childNodes.count > limit)
|
|
}
|
|
}
|