mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-15 10:26:07 +00:00
* fix(ai): preserve streamed tool-call identity * fix(computer): bind actions to current tool authority * fix(macos): serialize computer control lifecycle * docs(computer): document hardened control contract * chore: follow release-owned changelog policy * test(agents): cover node list cancellation
274 lines
12 KiB
Swift
274 lines
12 KiB
Swift
import Foundation
|
|
import OpenClawKit
|
|
import Testing
|
|
|
|
struct ComputerInputGeometryTests {
|
|
private func display(
|
|
originX: Double = 0,
|
|
originY: Double = 0,
|
|
width: Double,
|
|
height: Double) -> OpenClawComputerDisplayGeometry
|
|
{
|
|
OpenClawComputerDisplayGeometry(
|
|
originX: originX,
|
|
originY: originY,
|
|
widthPoints: width,
|
|
heightPoints: height)
|
|
}
|
|
|
|
private func capturedWidth(refWidth: Int?, sourceWidth: Double, sourceHeight: Double = 0) -> Double {
|
|
OpenClawComputerInputGeometry.capturedWidth(
|
|
refWidth: refWidth,
|
|
sourceWidth: sourceWidth,
|
|
sourceHeight: sourceHeight)
|
|
}
|
|
|
|
@Test func `display frame identity changes with physical display geometry`() {
|
|
let display = self.display(originX: -1280, originY: 0, width: 1280, height: 800)
|
|
let original = OpenClawComputerInputGeometry.displayFrameId(
|
|
displayID: 42,
|
|
sourceWidth: 2560,
|
|
sourceHeight: 1600,
|
|
referenceWidth: 1280,
|
|
display: display)
|
|
let repeated = OpenClawComputerInputGeometry.displayFrameId(
|
|
displayID: 42,
|
|
sourceWidth: 2560,
|
|
sourceHeight: 1600,
|
|
referenceWidth: 1280,
|
|
display: display)
|
|
let reindexed = OpenClawComputerInputGeometry.displayFrameId(
|
|
displayID: 43,
|
|
sourceWidth: 2560,
|
|
sourceHeight: 1600,
|
|
referenceWidth: 1280,
|
|
display: display)
|
|
let resized = OpenClawComputerInputGeometry.displayFrameId(
|
|
displayID: 42,
|
|
sourceWidth: 1920,
|
|
sourceHeight: 1200,
|
|
referenceWidth: 1280,
|
|
display: display)
|
|
let moved = OpenClawComputerInputGeometry.displayFrameId(
|
|
displayID: 42,
|
|
sourceWidth: 2560,
|
|
sourceHeight: 1600,
|
|
referenceWidth: 1280,
|
|
display: self.display(originX: 0, originY: 0, width: 1280, height: 800))
|
|
|
|
let rescaled = OpenClawComputerInputGeometry.displayFrameId(
|
|
displayID: 42,
|
|
sourceWidth: 2560,
|
|
sourceHeight: 1600,
|
|
referenceWidth: 640,
|
|
display: display)
|
|
|
|
#expect(original.hasPrefix("display-frame:v1:"))
|
|
#expect(original == repeated)
|
|
#expect(Set([original, reindexed, resized, moved, rescaled]).count == 5)
|
|
}
|
|
|
|
@Test func `maps one to one when capture matches display`() {
|
|
// Source width equals display points and refWidth: captured 1280px over
|
|
// a 1280pt display → 1:1.
|
|
let captured = self.capturedWidth(refWidth: 1280, sourceWidth: 1280)
|
|
#expect(captured == 1280)
|
|
let mapped = OpenClawComputerInputGeometry.mapReferencePointToGlobal(
|
|
x: 100,
|
|
y: 200,
|
|
capturedWidthPixels: captured,
|
|
display: self.display(width: 1280, height: 800))
|
|
#expect(mapped.x == 100)
|
|
#expect(mapped.y == 200)
|
|
}
|
|
|
|
@Test func `never upscales when ref width exceeds source`() {
|
|
// refWidth larger than the capture source means the screenshot is the
|
|
// full source width, so mapping over a matching-point display stays 1:1.
|
|
let captured = self.capturedWidth(refWidth: 4000, sourceWidth: 1512)
|
|
#expect(captured == 1512)
|
|
let mapped = OpenClawComputerInputGeometry.mapReferencePointToGlobal(
|
|
x: 300,
|
|
y: 150,
|
|
capturedWidthPixels: captured,
|
|
display: self.display(width: 1512, height: 982))
|
|
#expect(mapped.x == 300)
|
|
#expect(mapped.y == 150)
|
|
}
|
|
|
|
@Test func `scales up from downscaled screenshot`() {
|
|
// Display is 2560pt wide, capture source 2560; screenshot captured at
|
|
// refWidth 1280 → captured 1280px, scale 2x.
|
|
let captured = self.capturedWidth(refWidth: 1280, sourceWidth: 2560)
|
|
#expect(captured == 1280)
|
|
let mapped = OpenClawComputerInputGeometry.mapReferencePointToGlobal(
|
|
x: 100,
|
|
y: 50,
|
|
capturedWidthPixels: captured,
|
|
display: self.display(width: 2560, height: 1600))
|
|
#expect(mapped.x == 200)
|
|
#expect(mapped.y == 100)
|
|
}
|
|
|
|
@Test func `scales down on retina when pixels exceed points`() {
|
|
// Retina display set to 1024 logical points but 2048 physical pixels:
|
|
// capture source (pixels) 2048 downscales to refWidth 1280, and the
|
|
// 1280px screenshot must map back onto the 1024pt display (scale 0.8).
|
|
let captured = self.capturedWidth(refWidth: 1280, sourceWidth: 2048)
|
|
#expect(captured == 1280)
|
|
let mapped = OpenClawComputerInputGeometry.mapReferencePointToGlobal(
|
|
x: 1280,
|
|
y: 0,
|
|
capturedWidthPixels: captured,
|
|
display: self.display(width: 1024, height: 640))
|
|
#expect(mapped.x == 1024)
|
|
#expect(mapped.y == 0)
|
|
}
|
|
|
|
@Test func `adds display origin after scaling`() {
|
|
let captured = self.capturedWidth(refWidth: 1280, sourceWidth: 2560)
|
|
let mapped = OpenClawComputerInputGeometry.mapReferencePointToGlobal(
|
|
x: 100,
|
|
y: 0,
|
|
capturedWidthPixels: captured,
|
|
display: self.display(originX: 1512, originY: 0, width: 2560, height: 1600))
|
|
// 100 * (2560/1280) + 1512 origin.
|
|
#expect(mapped.x == 1712)
|
|
#expect(mapped.y == 0)
|
|
}
|
|
|
|
@Test func `scales down portrait capture whose height exceeds reference width`() {
|
|
// Portrait source 1080x1920: width cap keeps 1080, but the capture's
|
|
// longest edge (1920) exceeds the 1280 reference width, so the agent
|
|
// scales the whole frame down. Delivered width = 1080 * 1280 / 1920 = 720.
|
|
let captured = self.capturedWidth(refWidth: 1280, sourceWidth: 1080, sourceHeight: 1920)
|
|
#expect(captured == 720)
|
|
// The 720px-wide delivered frame maps back onto the 1080pt portrait
|
|
// display (scale 1.5), so replayed coordinates land on target.
|
|
let mapped = OpenClawComputerInputGeometry.mapReferencePointToGlobal(
|
|
x: 360,
|
|
y: 0,
|
|
capturedWidthPixels: captured,
|
|
display: self.display(width: 1080, height: 1920))
|
|
#expect(mapped.x == 540)
|
|
#expect(mapped.y == 0)
|
|
}
|
|
|
|
@Test func `keeps landscape width when the width is the longest edge`() {
|
|
// Landscape source 2560x1440: the width cap (1280) is already the longest
|
|
// edge, so longest-edge scaling is a no-op and the width cap stands.
|
|
let captured = self.capturedWidth(refWidth: 1280, sourceWidth: 2560, sourceHeight: 1440)
|
|
#expect(captured == 1280)
|
|
}
|
|
|
|
@Test func `accepts finite positive mapping geometry with negative display origins`() {
|
|
let display = self.display(originX: -2560, originY: -1440, width: 2560, height: 1440)
|
|
#expect(OpenClawComputerInputGeometry.isValidMappingGeometry(
|
|
sourceWidth: 2560,
|
|
sourceHeight: 1440,
|
|
display: display))
|
|
}
|
|
|
|
@Test func `rejects nonpositive or nonfinite mapping geometry`() {
|
|
let validDisplay = self.display(originX: -100, originY: 200, width: 1280, height: 800)
|
|
for invalidSourceWidth in [0, -1, Double.nan, .infinity, -.infinity] {
|
|
#expect(!OpenClawComputerInputGeometry.isValidMappingGeometry(
|
|
sourceWidth: invalidSourceWidth,
|
|
sourceHeight: 800,
|
|
display: validDisplay))
|
|
}
|
|
for invalidSourceHeight in [0, -1, Double.nan, .infinity, -.infinity] {
|
|
#expect(!OpenClawComputerInputGeometry.isValidMappingGeometry(
|
|
sourceWidth: 1280,
|
|
sourceHeight: invalidSourceHeight,
|
|
display: validDisplay))
|
|
}
|
|
|
|
let invalidDisplays = [
|
|
display(originX: .nan, originY: 0, width: 1280, height: 800),
|
|
display(originX: 0, originY: .infinity, width: 1280, height: 800),
|
|
display(width: 0, height: 800),
|
|
display(width: -1, height: 800),
|
|
display(width: .nan, height: 800),
|
|
display(width: .infinity, height: 800),
|
|
display(width: 1280, height: 0),
|
|
display(width: 1280, height: -1),
|
|
display(width: 1280, height: .nan),
|
|
display(width: 1280, height: .infinity),
|
|
]
|
|
for display in invalidDisplays {
|
|
#expect(!OpenClawComputerInputGeometry.isValidMappingGeometry(
|
|
sourceWidth: 1280,
|
|
sourceHeight: 800,
|
|
display: display))
|
|
}
|
|
}
|
|
|
|
@Test func `clamps far-edge and out-of-bounds points strictly inside the display`() {
|
|
let display = self.display(originX: 100, originY: 200, width: 1280, height: 800)
|
|
// The far right/bottom edge maps to origin + size, which belongs to the
|
|
// adjacent display; clamp keeps it on the last in-bounds point.
|
|
let farEdge = OpenClawComputerInputGeometry.clampToDisplay(
|
|
x: 100 + 1280,
|
|
y: 200 + 800,
|
|
display: display)
|
|
#expect(farEdge.x == 100 + 1279)
|
|
#expect(farEdge.y == 200 + 799)
|
|
// A slightly-negative epsilon overrun clamps back to the origin.
|
|
let negative = OpenClawComputerInputGeometry.clampToDisplay(x: 99, y: 199, display: display)
|
|
#expect(negative.x == 100)
|
|
#expect(negative.y == 200)
|
|
// A point already inside is unchanged.
|
|
let inside = OpenClawComputerInputGeometry.clampToDisplay(x: 640, y: 400, display: display)
|
|
#expect(inside.x == 640)
|
|
#expect(inside.y == 400)
|
|
}
|
|
|
|
@Test func `captured width is non-positive for a non-positive reference width`() {
|
|
// Justifies the executor rejecting refWidth <= 0 before mapping: geometry
|
|
// would otherwise yield a non-positive width that collapses every
|
|
// coordinate onto the display origin.
|
|
#expect(self.capturedWidth(refWidth: 0, sourceWidth: 1280, sourceHeight: 800) <= 0)
|
|
#expect(self.capturedWidth(refWidth: -5, sourceWidth: 1280, sourceHeight: 800) <= 0)
|
|
}
|
|
|
|
@Test func `falls back to origin for degenerate display`() {
|
|
let mapped = OpenClawComputerInputGeometry.mapReferencePointToGlobal(
|
|
x: 42,
|
|
y: 42,
|
|
capturedWidthPixels: 0,
|
|
display: self.display(originX: 10, originY: 20, width: 0, height: 0))
|
|
#expect(mapped.x == 10)
|
|
#expect(mapped.y == 20)
|
|
}
|
|
|
|
@Test func `decodes partial act params`() throws {
|
|
let json = """
|
|
{"action":"left_click","displayFrameId":"display-frame:v1:test","x":12,"y":34,"modifiers":"shift","screenIndex":0,"refWidth":1280}
|
|
"""
|
|
let data = try #require(json.data(using: .utf8))
|
|
let params = try JSONDecoder().decode(OpenClawComputerActParams.self, from: data)
|
|
#expect(params.action == .leftClick)
|
|
#expect(params.displayFrameId == "display-frame:v1:test")
|
|
#expect(params.x == 12)
|
|
#expect(params.modifiers == "shift")
|
|
#expect(params.refWidth == 1280)
|
|
}
|
|
|
|
@Test func `decodes scroll and hold actions`() throws {
|
|
let scroll = try #require(
|
|
"{\"action\":\"scroll\",\"scrollDirection\":\"down\",\"scrollAmount\":3}".data(using: .utf8))
|
|
let scrollParams = try JSONDecoder().decode(OpenClawComputerActParams.self, from: scroll)
|
|
#expect(scrollParams.action == .scroll)
|
|
#expect(scrollParams.scrollDirection == .down)
|
|
#expect(scrollParams.scrollAmount == 3)
|
|
|
|
let hold = try #require(
|
|
"{\"action\":\"hold_key\",\"keys\":\"space\",\"durationMs\":2000}".data(using: .utf8))
|
|
let holdParams = try JSONDecoder().decode(OpenClawComputerActParams.self, from: hold)
|
|
#expect(holdParams.action == .holdKey)
|
|
#expect(holdParams.keys == "space")
|
|
#expect(holdParams.durationMs == 2000)
|
|
}
|
|
}
|