Files
openclaw/apps/macos/Tests/OpenClawIPCTests/QuickChatFocusedTextCollectorTests.swift
Peter Steinberger b7f99d0ffd fix(macos): harden Quick Chat capture (area-send visibility, AX text quality, overlay race) (#110830)
* 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
2026-07-18 19:08:23 +01:00

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)
}
}