mirror of
https://github.com/block/buzz.git
synced 2026-10-30 14:01:37 +00:00
evidence: PR #6368 native keyboard journey
Publish the sanitized exact-head receipt, MP4, AX snapshots, cleanup record, and causal native mutant needed for inspectable review. Signed-off-by: Thomas Petersen <thomasp@squareup.com>
This commit is contained in:
23
pr-6368-native-evidence/MANIFEST.sha256
Normal file
23
pr-6368-native-evidence/MANIFEST.sha256
Normal file
@@ -0,0 +1,23 @@
|
||||
23d329e23dfb7fc3a1710af8f29ae44a79896ea6958a5c212ad7e9201aaa5d38 README.md
|
||||
ebb0a53f87a1e71708510937462b1e927c8ce6d9256c24ef299ccdabea776901 cleanup.txt
|
||||
a60139a0b5918cec82e63d49c9377091b98c466a3fa56ee4a5eb90da189900e7 environment.txt
|
||||
36690b6554077cfea3f30aace8b7bd9dbc738a26dd6f9267f758f73379436591 exact-adjacent.json
|
||||
1e590f5113181a330d7a02e019f039fc18e001c952cd1cf70864c5d75937b39b exact-diagnostics.log
|
||||
12761900cf63596841eaf546778eeabea80573744e2fa2003221544e1ee07ac2 exact-focus-crop.png
|
||||
dc99f878a4802783d45b2ca2eeca3faeb71bce3420960d3c64228ea1778d04e4 exact-focused.json
|
||||
2c39cd7fff5a2c37f15fd451e780359ce1b9aeeda130d74cad0b3a11bd6c40bb exact-journey.mp4
|
||||
9e3934e6e67a1aa09ddb100f2365f5869bd317369f64b6185852e11f456a171f exact-receipt.json
|
||||
484533763f9650aa381aff7c51ddc8dd826eddb1df019690b51e084f6b2db072 exact-selected-crop.png
|
||||
1919e6d1ad9c769cd6c41fb4a225b53f01ad1e500e04ff76fc3e75d3d19bf802 exact-selected.json
|
||||
36690b6554077cfea3f30aace8b7bd9dbc738a26dd6f9267f758f73379436591 mutant-adjacent.json
|
||||
8bc29f598afe4a34d0902574b9863c8b4a2a2bf79a762a9721ecb47c7cbb0518 mutant-diagnostics.log
|
||||
fec3651b16e5abc0852d1ec04dcfb167a42887fbdfbfb713897818b529f1938d mutant-focus-crop.png
|
||||
dc99f878a4802783d45b2ca2eeca3faeb71bce3420960d3c64228ea1778d04e4 mutant-focused.json
|
||||
3f0a5d2fc4182d7e966e4ee62a8aacece16180f6a10f5bb0d7606a96dff9de10 mutant-receipt.json
|
||||
8070b9f873a0f49ccea41765b2dbca30df52a4ece47e6e2b29635b09094ee534 mutant.patch
|
||||
e3784c2a95178b30fe8580d89d4c164b0a11a66fc3db3b09fc018af22219469f native_journey.swift
|
||||
06d5865e68cb0085649934a4103df853e5fc305063d81f26485bf2acec0c7481 restored-adjacent.json
|
||||
005aeada458090833df23d95e86082cf321c0d0dfb28a31cfb5dc94cfbc8a013 restored-diagnostics.log
|
||||
dcd1a3efe372f5d5bb7ff412a4a59fa3b444c3d6ef850fa64660ec1b792f6794 restored-focused.json
|
||||
332105d66ccd3b5db46ca2fd54d25492abf71bb3b801603d95b396c4a5305b3d restored-receipt.json
|
||||
ddbb9a48ae499c31e571fcafde2d37e1b795203e2303a61708c1c3c7cf89d848 restored-selected.json
|
||||
17
pr-6368-native-evidence/README.md
Normal file
17
pr-6368-native-evidence/README.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# PR #6368 native keyboard evidence (sanitized public bundle)
|
||||
|
||||
Exact head: `ffb765e49e2db60fdb167fe58440f3c6d5efa17b`.
|
||||
|
||||
The native Tauri/WKWebView harness focused the adjacent row action, delivered macOS Tab (`key code 48`), asserted an entity-specific focused `AXCheckBox`, measured 732 bright focus-ring pixels, delivered Space (`key code 49`), and asserted checkbox value `1`, heading `1 task`, and `Clear selection`.
|
||||
|
||||
`exact-journey.mp4` is the complete 30-second interaction timeline cropped to the checkbox region so no workspace content is published on this public OSS PR. `exact-focus-crop.png` and `exact-selected-crop.png` show the focused and selected pixels. Semantic snapshots preserve the entity-specific `Open task` / `Select` prefixes while redacting the fixture title.
|
||||
|
||||
The feature-specific `mutant.patch` removes only `group-focus-within:opacity-100`. The same native harness then exited `1`: Tab still reached the named AX checkbox, but the visual measurement fell from 732 to 0 pixels. Restoring the class returned the harness to exit `0` and 732 pixels.
|
||||
|
||||
Artifacts:
|
||||
- exact, mutant, and restored receipts plus AX snapshots
|
||||
- exact, mutant, and restored step diagnostics
|
||||
- native harness source and feature-specific mutant patch
|
||||
- sanitized MP4 and focus/selected/mutant crops
|
||||
- environment/isolation and cleanup records
|
||||
- SHA-256 manifest
|
||||
10
pr-6368-native-evidence/cleanup.txt
Normal file
10
pr-6368-native-evidence/cleanup.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
Cleanup command: TERM process group 84227
|
||||
Native launcher completion: observed
|
||||
|
||||
PASS app process group exited
|
||||
PASS Vite port 30355 released
|
||||
PASS no worktree agent sidecars remain
|
||||
|
||||
Post-cleanup HEAD: ffb765e49e2db60fdb167fe58440f3c6d5efa17b
|
||||
Post-cleanup git status: clean
|
||||
Post-cleanup git diff --check: pass
|
||||
15
pr-6368-native-evidence/environment.txt
Normal file
15
pr-6368-native-evidence/environment.txt
Normal file
@@ -0,0 +1,15 @@
|
||||
PR: block/buzz#6368
|
||||
HEAD: ffb765e49e2db60fdb167fe58440f3c6d5efa17b
|
||||
Branch: projects-v6-pt2-selectable-workspaces
|
||||
Pre-run git status: clean
|
||||
Pre-run git diff --check: pass
|
||||
Application: Buzz Dev (projects-v6-pt2-selectable-workspaces)
|
||||
Application identifier: xyz.block.buzz.app.dev.projects-v6-pt2-selectable-workspaces
|
||||
Launch command: BUZZ_SHARE_IDENTITY=1 just production
|
||||
Vite port: 30355
|
||||
Binary SHA-256: 1600d8d63cfca8942e7d809879ccd3738b3fc04239839f87be27ee6fd4ec9f4f
|
||||
OS: macOS 26.6.1 (25G76)
|
||||
Architecture: arm64
|
||||
Display: 2560x1440 points, scale 2.0
|
||||
Isolation: worktree-specific app identifier and Vite port; mutant restored before cleanup.
|
||||
Publication note: full-screen artifacts remain local because this is a public OSS PR. Published crops, MP4, and semantic records redact workspace content without altering checkbox pixels or harness results.
|
||||
15
pr-6368-native-evidence/exact-adjacent.json
Normal file
15
pr-6368-native-evidence/exact-adjacent.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"description": "",
|
||||
"focused": true,
|
||||
"position": [
|
||||
317,
|
||||
224
|
||||
],
|
||||
"role": "AXButton",
|
||||
"size": [
|
||||
1921,
|
||||
40
|
||||
],
|
||||
"title": "Open task [redacted fixture title]",
|
||||
"value": ""
|
||||
}
|
||||
9
pr-6368-native-evidence/exact-diagnostics.log
Normal file
9
pr-6368-native-evidence/exact-diagnostics.log
Normal file
@@ -0,0 +1,9 @@
|
||||
[2026-08-20T11:58:25Z] application=Buzz Dev (projects-v6-pt2-selectable-workspaces) pid=84587 windowId=29941 mode=exact
|
||||
[2026-08-20T11:58:25Z] AXPress Projects
|
||||
[2026-08-20T11:58:26Z] AXPress Tasks
|
||||
[2026-08-20T11:58:26Z] AXPress List layout
|
||||
[2026-08-20T11:58:27Z] focused setup control Open task [redacted fixture title]
|
||||
[2026-08-20T11:58:28Z] posted macOS HID Tab
|
||||
[2026-08-20T11:58:30Z] focus-ring bright pixels=732
|
||||
[2026-08-20T11:58:30Z] posted macOS HID Space
|
||||
[2026-08-20T11:58:31Z] PASS native Tab → entity checkbox → visible focus ring → Space selection
|
||||
BIN
pr-6368-native-evidence/exact-focus-crop.png
Normal file
BIN
pr-6368-native-evidence/exact-focus-crop.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
15
pr-6368-native-evidence/exact-focused.json
Normal file
15
pr-6368-native-evidence/exact-focused.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"description": "Select [redacted fixture title]",
|
||||
"focused": true,
|
||||
"position": [
|
||||
335,
|
||||
238
|
||||
],
|
||||
"role": "AXCheckBox",
|
||||
"size": [
|
||||
12,
|
||||
12
|
||||
],
|
||||
"title": "Select [redacted fixture title]",
|
||||
"value": "0"
|
||||
}
|
||||
BIN
pr-6368-native-evidence/exact-journey.mp4
Normal file
BIN
pr-6368-native-evidence/exact-journey.mp4
Normal file
Binary file not shown.
55
pr-6368-native-evidence/exact-receipt.json
Normal file
55
pr-6368-native-evidence/exact-receipt.json
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"adjacentControl": {
|
||||
"description": "",
|
||||
"focused": true,
|
||||
"position": [
|
||||
317,
|
||||
224
|
||||
],
|
||||
"role": "AXButton",
|
||||
"size": [
|
||||
1921,
|
||||
40
|
||||
],
|
||||
"title": "Open task [redacted fixture title]",
|
||||
"value": ""
|
||||
},
|
||||
"applicationTitle": "Buzz Dev (projects-v6-pt2-selectable-workspaces)",
|
||||
"clearSelectionFound": true,
|
||||
"focusRingBrightPixels": 732,
|
||||
"focusedCheckbox": {
|
||||
"description": "Select [redacted fixture title]",
|
||||
"focused": true,
|
||||
"position": [
|
||||
335,
|
||||
238
|
||||
],
|
||||
"role": "AXCheckBox",
|
||||
"size": [
|
||||
12,
|
||||
12
|
||||
],
|
||||
"title": "Select [redacted fixture title]",
|
||||
"value": "0"
|
||||
},
|
||||
"mode": "exact",
|
||||
"pid": 84587,
|
||||
"selectedCheckbox": {
|
||||
"description": "Select [redacted fixture title]",
|
||||
"focused": true,
|
||||
"position": [
|
||||
335,
|
||||
238
|
||||
],
|
||||
"role": "AXCheckBox",
|
||||
"size": [
|
||||
12,
|
||||
12
|
||||
],
|
||||
"title": "Select [redacted fixture title]",
|
||||
"value": "1"
|
||||
},
|
||||
"selectedHeadingFound": true,
|
||||
"status": "passed",
|
||||
"windowId": 29941
|
||||
}
|
||||
BIN
pr-6368-native-evidence/exact-selected-crop.png
Normal file
BIN
pr-6368-native-evidence/exact-selected-crop.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
15
pr-6368-native-evidence/exact-selected.json
Normal file
15
pr-6368-native-evidence/exact-selected.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"description": "Select [redacted fixture title]",
|
||||
"focused": true,
|
||||
"position": [
|
||||
335,
|
||||
238
|
||||
],
|
||||
"role": "AXCheckBox",
|
||||
"size": [
|
||||
12,
|
||||
12
|
||||
],
|
||||
"title": "Select [redacted fixture title]",
|
||||
"value": "1"
|
||||
}
|
||||
15
pr-6368-native-evidence/mutant-adjacent.json
Normal file
15
pr-6368-native-evidence/mutant-adjacent.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"description": "",
|
||||
"focused": true,
|
||||
"position": [
|
||||
317,
|
||||
224
|
||||
],
|
||||
"role": "AXButton",
|
||||
"size": [
|
||||
1921,
|
||||
40
|
||||
],
|
||||
"title": "Open task [redacted fixture title]",
|
||||
"value": ""
|
||||
}
|
||||
9
pr-6368-native-evidence/mutant-diagnostics.log
Normal file
9
pr-6368-native-evidence/mutant-diagnostics.log
Normal file
@@ -0,0 +1,9 @@
|
||||
[2026-08-20T12:09:37Z] application=Buzz Dev (projects-v6-pt2-selectable-workspaces) pid=84587 windowId=29941 mode=mutant
|
||||
[2026-08-20T12:09:37Z] AXPress Projects
|
||||
[2026-08-20T12:09:38Z] AXPress Tasks
|
||||
[2026-08-20T12:09:39Z] AXPress List layout
|
||||
[2026-08-20T12:09:40Z] focused setup control Open task [redacted fixture title]
|
||||
[2026-08-20T12:09:41Z] posted macOS HID Tab
|
||||
[2026-08-20T12:09:43Z] focus-ring bright pixels=0
|
||||
[2026-08-20T12:09:43Z] FAIL visible focus-ring assertion failed: expected >=100 bright pixels, got 0
|
||||
Exit status: 1
|
||||
BIN
pr-6368-native-evidence/mutant-focus-crop.png
Normal file
BIN
pr-6368-native-evidence/mutant-focus-crop.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
15
pr-6368-native-evidence/mutant-focused.json
Normal file
15
pr-6368-native-evidence/mutant-focused.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"description": "Select [redacted fixture title]",
|
||||
"focused": true,
|
||||
"position": [
|
||||
335,
|
||||
238
|
||||
],
|
||||
"role": "AXCheckBox",
|
||||
"size": [
|
||||
12,
|
||||
12
|
||||
],
|
||||
"title": "Select [redacted fixture title]",
|
||||
"value": "0"
|
||||
}
|
||||
41
pr-6368-native-evidence/mutant-receipt.json
Normal file
41
pr-6368-native-evidence/mutant-receipt.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"adjacentControl": {
|
||||
"description": "",
|
||||
"focused": true,
|
||||
"position": [
|
||||
317,
|
||||
224
|
||||
],
|
||||
"role": "AXButton",
|
||||
"size": [
|
||||
1921,
|
||||
40
|
||||
],
|
||||
"title": "Open task [redacted fixture title]",
|
||||
"value": ""
|
||||
},
|
||||
"applicationTitle": "Buzz Dev (projects-v6-pt2-selectable-workspaces)",
|
||||
"clearSelectionFound": false,
|
||||
"failure": "visible focus-ring assertion failed: expected >=100 bright pixels, got 0",
|
||||
"focusRingBrightPixels": 0,
|
||||
"focusedCheckbox": {
|
||||
"description": "Select [redacted fixture title]",
|
||||
"focused": true,
|
||||
"position": [
|
||||
335,
|
||||
238
|
||||
],
|
||||
"role": "AXCheckBox",
|
||||
"size": [
|
||||
12,
|
||||
12
|
||||
],
|
||||
"title": "Select [redacted fixture title]",
|
||||
"value": "0"
|
||||
},
|
||||
"mode": "mutant",
|
||||
"pid": 84587,
|
||||
"selectedHeadingFound": false,
|
||||
"status": "failed",
|
||||
"windowId": 29941
|
||||
}
|
||||
6
pr-6368-native-evidence/mutant.patch
Normal file
6
pr-6368-native-evidence/mutant.patch
Normal file
@@ -0,0 +1,6 @@
|
||||
diff --git a/desktop/src/features/projects/ui/ProjectEntityListRow.tsx b/desktop/src/features/projects/ui/ProjectEntityListRow.tsx
|
||||
--- a/desktop/src/features/projects/ui/ProjectEntityListRow.tsx
|
||||
+++ b/desktop/src/features/projects/ui/ProjectEntityListRow.tsx
|
||||
@@
|
||||
- "absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 group-focus-within:opacity-100",
|
||||
+ "absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100",
|
||||
455
pr-6368-native-evidence/native_journey.swift
Normal file
455
pr-6368-native-evidence/native_journey.swift
Normal file
@@ -0,0 +1,455 @@
|
||||
import AppKit
|
||||
import ApplicationServices
|
||||
import CoreGraphics
|
||||
import Foundation
|
||||
import ImageIO
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
struct Snapshot: Codable {
|
||||
let role: String
|
||||
let title: String
|
||||
let description: String
|
||||
let value: String
|
||||
let focused: Bool
|
||||
let position: [Double]
|
||||
let size: [Double]
|
||||
}
|
||||
|
||||
struct Receipt: Codable {
|
||||
let status: String
|
||||
let mode: String
|
||||
let pid: Int32
|
||||
let applicationTitle: String
|
||||
let windowId: Int
|
||||
let adjacentControl: Snapshot
|
||||
let focusedCheckbox: Snapshot
|
||||
let focusRingBrightPixels: Int
|
||||
let selectedCheckbox: Snapshot?
|
||||
let selectedHeadingFound: Bool
|
||||
let clearSelectionFound: Bool
|
||||
let failure: String?
|
||||
}
|
||||
|
||||
enum HarnessError: Error, CustomStringConvertible {
|
||||
case message(String)
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case let .message(message):
|
||||
return message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let arguments = CommandLine.arguments
|
||||
guard arguments.count == 4, let pid = Int32(arguments[1]) else {
|
||||
fputs("usage: native_journey.swift <pid> <output-directory> <mode>\n", stderr)
|
||||
exit(2)
|
||||
}
|
||||
|
||||
let outputDirectory = URL(fileURLWithPath: arguments[2], isDirectory: true)
|
||||
let mode = arguments[3]
|
||||
let application = AXUIElementCreateApplication(pid)
|
||||
let encoder = JSONEncoder()
|
||||
encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]
|
||||
|
||||
func timestamp() -> String {
|
||||
ISO8601DateFormatter().string(from: Date())
|
||||
}
|
||||
|
||||
func diagnostic(_ message: String) {
|
||||
print("[\(timestamp())] \(message)")
|
||||
fflush(stdout)
|
||||
}
|
||||
|
||||
func attribute(_ element: AXUIElement, _ name: String) -> AnyObject? {
|
||||
var result: CFTypeRef?
|
||||
guard AXUIElementCopyAttributeValue(element, name as CFString, &result) == .success else {
|
||||
return nil
|
||||
}
|
||||
return result as AnyObject?
|
||||
}
|
||||
|
||||
func stringAttribute(_ element: AXUIElement, _ name: String) -> String {
|
||||
attribute(element, name) as? String ?? ""
|
||||
}
|
||||
|
||||
func boolAttribute(_ element: AXUIElement, _ name: String) -> Bool {
|
||||
attribute(element, name) as? Bool ?? false
|
||||
}
|
||||
|
||||
func pointAttribute(_ element: AXUIElement, _ name: String) -> CGPoint {
|
||||
guard let rawValue = attribute(element, name) else {
|
||||
return .zero
|
||||
}
|
||||
let value = rawValue as! AXValue
|
||||
var point = CGPoint.zero
|
||||
AXValueGetValue(value, .cgPoint, &point)
|
||||
return point
|
||||
}
|
||||
|
||||
func sizeAttribute(_ element: AXUIElement, _ name: String) -> CGSize {
|
||||
guard let rawValue = attribute(element, name) else {
|
||||
return .zero
|
||||
}
|
||||
let value = rawValue as! AXValue
|
||||
var size = CGSize.zero
|
||||
AXValueGetValue(value, .cgSize, &size)
|
||||
return size
|
||||
}
|
||||
|
||||
func snapshot(_ element: AXUIElement) -> Snapshot {
|
||||
let position = pointAttribute(element, kAXPositionAttribute)
|
||||
let size = sizeAttribute(element, kAXSizeAttribute)
|
||||
return Snapshot(
|
||||
role: stringAttribute(element, kAXRoleAttribute),
|
||||
title: stringAttribute(element, kAXTitleAttribute),
|
||||
description: stringAttribute(element, kAXDescriptionAttribute),
|
||||
value: String(describing: attribute(element, kAXValueAttribute) ?? "" as AnyObject),
|
||||
focused: boolAttribute(element, kAXFocusedAttribute),
|
||||
position: [position.x, position.y],
|
||||
size: [size.width, size.height]
|
||||
)
|
||||
}
|
||||
|
||||
func children(_ element: AXUIElement) -> [AXUIElement] {
|
||||
attribute(element, kAXChildrenAttribute) as? [AXUIElement] ?? []
|
||||
}
|
||||
|
||||
func find(
|
||||
_ element: AXUIElement,
|
||||
role: String? = nil,
|
||||
title: String? = nil,
|
||||
titlePrefix: String? = nil,
|
||||
focused: Bool? = nil
|
||||
) -> AXUIElement? {
|
||||
let elementRole = stringAttribute(element, kAXRoleAttribute)
|
||||
let elementTitle = stringAttribute(element, kAXTitleAttribute)
|
||||
let matches =
|
||||
(role == nil || elementRole == role) &&
|
||||
(title == nil || elementTitle == title) &&
|
||||
(titlePrefix == nil || elementTitle.hasPrefix(titlePrefix ?? "")) &&
|
||||
(focused == nil || boolAttribute(element, kAXFocusedAttribute) == focused)
|
||||
if matches {
|
||||
return element
|
||||
}
|
||||
for child in children(element) {
|
||||
if let match = find(
|
||||
child,
|
||||
role: role,
|
||||
title: title,
|
||||
titlePrefix: titlePrefix,
|
||||
focused: focused
|
||||
) {
|
||||
return match
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func waitFor(
|
||||
role: String? = nil,
|
||||
title: String? = nil,
|
||||
titlePrefix: String? = nil,
|
||||
focused: Bool? = nil,
|
||||
timeout: TimeInterval = 12
|
||||
) throws -> AXUIElement {
|
||||
let deadline = Date().addingTimeInterval(timeout)
|
||||
repeat {
|
||||
if let match = find(
|
||||
application,
|
||||
role: role,
|
||||
title: title,
|
||||
titlePrefix: titlePrefix,
|
||||
focused: focused
|
||||
) {
|
||||
return match
|
||||
}
|
||||
Thread.sleep(forTimeInterval: 0.2)
|
||||
} while Date() < deadline
|
||||
throw HarnessError.message(
|
||||
"timed out waiting for role=\(role ?? "*") title=\(title ?? titlePrefix ?? "*") focused=\(String(describing: focused))"
|
||||
)
|
||||
}
|
||||
|
||||
func press(_ element: AXUIElement, label: String) throws {
|
||||
let result = AXUIElementPerformAction(element, kAXPressAction as CFString)
|
||||
guard result == .success else {
|
||||
throw HarnessError.message("AXPress failed for \(label): \(result.rawValue)")
|
||||
}
|
||||
diagnostic("AXPress \(label)")
|
||||
Thread.sleep(forTimeInterval: 0.7)
|
||||
}
|
||||
|
||||
func focus(_ element: AXUIElement, label: String) throws {
|
||||
let result = AXUIElementSetAttributeValue(
|
||||
element,
|
||||
kAXFocusedAttribute as CFString,
|
||||
kCFBooleanTrue
|
||||
)
|
||||
guard result == .success else {
|
||||
throw HarnessError.message("AX focus failed for \(label): \(result.rawValue)")
|
||||
}
|
||||
diagnostic("focused setup control \(label)")
|
||||
Thread.sleep(forTimeInterval: 0.8)
|
||||
}
|
||||
|
||||
func key(_ virtualKey: CGKeyCode, label: String) throws {
|
||||
let process = Process()
|
||||
process.executableURL = URL(fileURLWithPath: "/usr/bin/osascript")
|
||||
process.arguments = [
|
||||
"-e", "tell application \"System Events\"",
|
||||
"-e", "set frontmost of first application process whose unix id is \(pid) to true",
|
||||
"-e", "key code \(virtualKey)",
|
||||
"-e", "end tell",
|
||||
]
|
||||
try process.run()
|
||||
process.waitUntilExit()
|
||||
guard process.terminationStatus == 0 else {
|
||||
throw HarnessError.message(
|
||||
"osascript could not deliver \(label): \(process.terminationStatus)"
|
||||
)
|
||||
}
|
||||
diagnostic("posted macOS HID \(label)")
|
||||
Thread.sleep(forTimeInterval: 1.2)
|
||||
}
|
||||
|
||||
func writeJSON<T: Encodable>(_ value: T, to name: String) throws {
|
||||
try encoder.encode(value).write(to: outputDirectory.appendingPathComponent(name))
|
||||
}
|
||||
|
||||
func windowId() throws -> Int {
|
||||
let windows =
|
||||
CGWindowListCopyWindowInfo([.optionOnScreenOnly, .excludeDesktopElements], kCGNullWindowID)
|
||||
as? [[String: Any]] ?? []
|
||||
for window in windows where window[kCGWindowOwnerPID as String] as? Int32 == pid {
|
||||
if let number = window[kCGWindowNumber as String] as? Int {
|
||||
return number
|
||||
}
|
||||
}
|
||||
throw HarnessError.message("no onscreen window found for pid \(pid)")
|
||||
}
|
||||
|
||||
func captureScreen(to url: URL) throws -> CGImage {
|
||||
let process = Process()
|
||||
process.executableURL = URL(fileURLWithPath: "/usr/sbin/screencapture")
|
||||
process.arguments = ["-x", "-D1", url.path]
|
||||
try process.run()
|
||||
process.waitUntilExit()
|
||||
guard process.terminationStatus == 0 else {
|
||||
throw HarnessError.message("screencapture failed with \(process.terminationStatus)")
|
||||
}
|
||||
guard
|
||||
let source = CGImageSourceCreateWithURL(url as CFURL, nil),
|
||||
let image = CGImageSourceCreateImageAtIndex(source, 0, nil)
|
||||
else {
|
||||
throw HarnessError.message("could not decode screenshot \(url.path)")
|
||||
}
|
||||
return image
|
||||
}
|
||||
|
||||
func writePNG(_ image: CGImage, to url: URL) throws {
|
||||
guard
|
||||
let destination = CGImageDestinationCreateWithURL(
|
||||
url as CFURL,
|
||||
UTType.png.identifier as CFString,
|
||||
1,
|
||||
nil
|
||||
)
|
||||
else {
|
||||
throw HarnessError.message("could not create PNG destination \(url.path)")
|
||||
}
|
||||
CGImageDestinationAddImage(destination, image, nil)
|
||||
guard CGImageDestinationFinalize(destination) else {
|
||||
throw HarnessError.message("could not write PNG \(url.path)")
|
||||
}
|
||||
}
|
||||
|
||||
func brightPixelCount(around element: AXUIElement, screenshot: CGImage) throws -> Int {
|
||||
let position = pointAttribute(element, kAXPositionAttribute)
|
||||
let size = sizeAttribute(element, kAXSizeAttribute)
|
||||
let displayBounds = CGDisplayBounds(CGMainDisplayID())
|
||||
let scale = Double(screenshot.width) / displayBounds.width
|
||||
let padding = 6.0
|
||||
let rect = CGRect(
|
||||
x: (position.x - displayBounds.origin.x - padding) * scale,
|
||||
y: (position.y - displayBounds.origin.y - padding) * scale,
|
||||
width: (size.width + padding * 2) * scale,
|
||||
height: (size.height + padding * 2) * scale
|
||||
).integral
|
||||
guard let crop = screenshot.cropping(to: rect) else {
|
||||
throw HarnessError.message("could not crop checkbox at \(rect)")
|
||||
}
|
||||
try writePNG(crop, to: outputDirectory.appendingPathComponent("\(mode)-focus-crop.png"))
|
||||
|
||||
let width = crop.width
|
||||
let height = crop.height
|
||||
var pixels = [UInt8](repeating: 0, count: width * height * 4)
|
||||
guard
|
||||
let context = CGContext(
|
||||
data: &pixels,
|
||||
width: width,
|
||||
height: height,
|
||||
bitsPerComponent: 8,
|
||||
bytesPerRow: width * 4,
|
||||
space: CGColorSpaceCreateDeviceRGB(),
|
||||
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
|
||||
)
|
||||
else {
|
||||
throw HarnessError.message("could not create pixel-analysis context")
|
||||
}
|
||||
context.draw(crop, in: CGRect(x: 0, y: 0, width: width, height: height))
|
||||
var count = 0
|
||||
for index in stride(from: 0, to: pixels.count, by: 4) {
|
||||
let red = Int(pixels[index])
|
||||
let green = Int(pixels[index + 1])
|
||||
let blue = Int(pixels[index + 2])
|
||||
if max(red, green, blue) >= 150 {
|
||||
count += 1
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func writeFailure(_ message: String, adjacent: Snapshot?, focused: Snapshot?, bright: Int?) {
|
||||
let receipt = Receipt(
|
||||
status: "failed",
|
||||
mode: mode,
|
||||
pid: pid,
|
||||
applicationTitle: stringAttribute(application, kAXTitleAttribute),
|
||||
windowId: (try? windowId()) ?? 0,
|
||||
adjacentControl: adjacent ?? Snapshot(
|
||||
role: "",
|
||||
title: "",
|
||||
description: "",
|
||||
value: "",
|
||||
focused: false,
|
||||
position: [],
|
||||
size: []
|
||||
),
|
||||
focusedCheckbox: focused ?? Snapshot(
|
||||
role: "",
|
||||
title: "",
|
||||
description: "",
|
||||
value: "",
|
||||
focused: false,
|
||||
position: [],
|
||||
size: []
|
||||
),
|
||||
focusRingBrightPixels: bright ?? 0,
|
||||
selectedCheckbox: nil,
|
||||
selectedHeadingFound: false,
|
||||
clearSelectionFound: false,
|
||||
failure: message
|
||||
)
|
||||
try? writeJSON(receipt, to: "\(mode)-receipt.json")
|
||||
}
|
||||
|
||||
var adjacentSnapshot: Snapshot?
|
||||
var focusedSnapshot: Snapshot?
|
||||
var measuredBrightPixels: Int?
|
||||
|
||||
do {
|
||||
try FileManager.default.createDirectory(
|
||||
at: outputDirectory,
|
||||
withIntermediateDirectories: true
|
||||
)
|
||||
let appTitle = stringAttribute(application, kAXTitleAttribute)
|
||||
guard appTitle == "Buzz Dev (projects-v6-pt2-selectable-workspaces)" else {
|
||||
throw HarnessError.message("unexpected application title: \(appTitle)")
|
||||
}
|
||||
let nativeWindowId = try windowId()
|
||||
diagnostic("application=\(appTitle) pid=\(pid) windowId=\(nativeWindowId) mode=\(mode)")
|
||||
|
||||
let projects = try waitFor(role: kAXButtonRole as String, title: "Projects")
|
||||
try press(projects, label: "Projects")
|
||||
let tasks = try waitFor(role: kAXCheckBoxRole as String, title: "Tasks")
|
||||
try press(tasks, label: "Tasks")
|
||||
let listLayout = try waitFor(role: kAXCheckBoxRole as String, title: "List layout")
|
||||
try press(listLayout, label: "List layout")
|
||||
|
||||
if let clearSelection = find(application, role: kAXButtonRole as String, title: "Clear selection") {
|
||||
try press(clearSelection, label: "Clear selection")
|
||||
}
|
||||
|
||||
let adjacent = try waitFor(role: kAXButtonRole as String, titlePrefix: "Open task ")
|
||||
try focus(adjacent, label: stringAttribute(adjacent, kAXTitleAttribute))
|
||||
adjacentSnapshot = snapshot(adjacent)
|
||||
try writeJSON(adjacentSnapshot, to: "\(mode)-adjacent.json")
|
||||
|
||||
try key(48, label: "Tab")
|
||||
let focused = try waitFor(role: kAXCheckBoxRole as String, focused: true)
|
||||
focusedSnapshot = snapshot(focused)
|
||||
try writeJSON(focusedSnapshot, to: "\(mode)-focused.json")
|
||||
guard focusedSnapshot?.title.hasPrefix("Select ") == true else {
|
||||
throw HarnessError.message("Tab did not reach an entity-specific checkbox")
|
||||
}
|
||||
guard focusedSnapshot?.value == "0" else {
|
||||
throw HarnessError.message("focused checkbox did not start unchecked")
|
||||
}
|
||||
|
||||
let focusedScreen = try captureScreen(
|
||||
to: outputDirectory.appendingPathComponent("\(mode)-focused-screen.png")
|
||||
)
|
||||
let brightPixels = try brightPixelCount(around: focused, screenshot: focusedScreen)
|
||||
measuredBrightPixels = brightPixels
|
||||
diagnostic("focus-ring bright pixels=\(brightPixels)")
|
||||
guard brightPixels >= 100 else {
|
||||
throw HarnessError.message(
|
||||
"visible focus-ring assertion failed: expected >=100 bright pixels, got \(brightPixels)"
|
||||
)
|
||||
}
|
||||
|
||||
try key(49, label: "Space")
|
||||
let selectionDeadline = Date().addingTimeInterval(6)
|
||||
var selected = snapshot(focused)
|
||||
var selectedHeadingFound = false
|
||||
var clearSelectionFound = false
|
||||
repeat {
|
||||
selected = snapshot(focused)
|
||||
selectedHeadingFound =
|
||||
find(application, role: kAXHeadingRole as String, title: "1 task") != nil
|
||||
clearSelectionFound =
|
||||
find(application, role: kAXButtonRole as String, title: "Clear selection") != nil
|
||||
if selected.value == "1", selectedHeadingFound, clearSelectionFound {
|
||||
break
|
||||
}
|
||||
Thread.sleep(forTimeInterval: 0.2)
|
||||
} while Date() < selectionDeadline
|
||||
try writeJSON(selected, to: "\(mode)-selected.json")
|
||||
guard selected.value == "1", selectedHeadingFound, clearSelectionFound else {
|
||||
throw HarnessError.message(
|
||||
"Space did not produce selected checkbox, 1 task heading, and Clear selection"
|
||||
)
|
||||
}
|
||||
_ = try captureScreen(
|
||||
to: outputDirectory.appendingPathComponent("\(mode)-selected-screen.png")
|
||||
)
|
||||
|
||||
let receipt = Receipt(
|
||||
status: "passed",
|
||||
mode: mode,
|
||||
pid: pid,
|
||||
applicationTitle: appTitle,
|
||||
windowId: nativeWindowId,
|
||||
adjacentControl: adjacentSnapshot ?? snapshot(adjacent),
|
||||
focusedCheckbox: focusedSnapshot ?? snapshot(focused),
|
||||
focusRingBrightPixels: brightPixels,
|
||||
selectedCheckbox: selected,
|
||||
selectedHeadingFound: selectedHeadingFound,
|
||||
clearSelectionFound: clearSelectionFound,
|
||||
failure: nil
|
||||
)
|
||||
try writeJSON(receipt, to: "\(mode)-receipt.json")
|
||||
diagnostic("PASS native Tab → entity checkbox → visible focus ring → Space selection")
|
||||
} catch {
|
||||
let message = String(describing: error)
|
||||
diagnostic("FAIL \(message)")
|
||||
writeFailure(
|
||||
message,
|
||||
adjacent: adjacentSnapshot,
|
||||
focused: focusedSnapshot,
|
||||
bright: measuredBrightPixels
|
||||
)
|
||||
exit(1)
|
||||
}
|
||||
15
pr-6368-native-evidence/restored-adjacent.json
Normal file
15
pr-6368-native-evidence/restored-adjacent.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"description": "",
|
||||
"focused": true,
|
||||
"position": [
|
||||
317,
|
||||
208
|
||||
],
|
||||
"role": "AXButton",
|
||||
"size": [
|
||||
1921,
|
||||
40
|
||||
],
|
||||
"title": "Open task [redacted fixture title]",
|
||||
"value": ""
|
||||
}
|
||||
10
pr-6368-native-evidence/restored-diagnostics.log
Normal file
10
pr-6368-native-evidence/restored-diagnostics.log
Normal file
@@ -0,0 +1,10 @@
|
||||
[2026-08-20T12:10:02Z] application=Buzz Dev (projects-v6-pt2-selectable-workspaces) pid=84587 windowId=29941 mode=restored
|
||||
[2026-08-20T12:10:02Z] AXPress Projects
|
||||
[2026-08-20T12:10:03Z] AXPress Tasks
|
||||
[2026-08-20T12:10:03Z] AXPress List layout
|
||||
[2026-08-20T12:10:04Z] focused setup control Open task [redacted fixture title]
|
||||
[2026-08-20T12:10:05Z] posted macOS HID Tab
|
||||
[2026-08-20T12:10:07Z] focus-ring bright pixels=732
|
||||
[2026-08-20T12:10:07Z] posted macOS HID Space
|
||||
[2026-08-20T12:10:08Z] PASS native Tab → entity checkbox → visible focus ring → Space selection
|
||||
Exit status: 0
|
||||
15
pr-6368-native-evidence/restored-focused.json
Normal file
15
pr-6368-native-evidence/restored-focused.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"description": "Select [redacted fixture title]",
|
||||
"focused": true,
|
||||
"position": [
|
||||
335,
|
||||
294
|
||||
],
|
||||
"role": "AXCheckBox",
|
||||
"size": [
|
||||
12,
|
||||
12
|
||||
],
|
||||
"title": "Select [redacted fixture title]",
|
||||
"value": "0"
|
||||
}
|
||||
55
pr-6368-native-evidence/restored-receipt.json
Normal file
55
pr-6368-native-evidence/restored-receipt.json
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"adjacentControl": {
|
||||
"description": "",
|
||||
"focused": true,
|
||||
"position": [
|
||||
317,
|
||||
208
|
||||
],
|
||||
"role": "AXButton",
|
||||
"size": [
|
||||
1921,
|
||||
40
|
||||
],
|
||||
"title": "Open task [redacted fixture title]",
|
||||
"value": ""
|
||||
},
|
||||
"applicationTitle": "Buzz Dev (projects-v6-pt2-selectable-workspaces)",
|
||||
"clearSelectionFound": true,
|
||||
"focusRingBrightPixels": 732,
|
||||
"focusedCheckbox": {
|
||||
"description": "Select [redacted fixture title]",
|
||||
"focused": true,
|
||||
"position": [
|
||||
335,
|
||||
294
|
||||
],
|
||||
"role": "AXCheckBox",
|
||||
"size": [
|
||||
12,
|
||||
12
|
||||
],
|
||||
"title": "Select [redacted fixture title]",
|
||||
"value": "0"
|
||||
},
|
||||
"mode": "restored",
|
||||
"pid": 84587,
|
||||
"selectedCheckbox": {
|
||||
"description": "Select [redacted fixture title]",
|
||||
"focused": false,
|
||||
"position": [
|
||||
335,
|
||||
294
|
||||
],
|
||||
"role": "AXCheckBox",
|
||||
"size": [
|
||||
12,
|
||||
12
|
||||
],
|
||||
"title": "Select [redacted fixture title]",
|
||||
"value": "1"
|
||||
},
|
||||
"selectedHeadingFound": true,
|
||||
"status": "passed",
|
||||
"windowId": 29941
|
||||
}
|
||||
15
pr-6368-native-evidence/restored-selected.json
Normal file
15
pr-6368-native-evidence/restored-selected.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"description": "Select [redacted fixture title]",
|
||||
"focused": false,
|
||||
"position": [
|
||||
335,
|
||||
294
|
||||
],
|
||||
"role": "AXCheckBox",
|
||||
"size": [
|
||||
12,
|
||||
12
|
||||
],
|
||||
"title": "Select [redacted fixture title]",
|
||||
"value": "1"
|
||||
}
|
||||
Reference in New Issue
Block a user