diff --git a/pr-6368-native-evidence/MANIFEST.sha256 b/pr-6368-native-evidence/MANIFEST.sha256 new file mode 100644 index 0000000000..fdfa161a33 --- /dev/null +++ b/pr-6368-native-evidence/MANIFEST.sha256 @@ -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 diff --git a/pr-6368-native-evidence/README.md b/pr-6368-native-evidence/README.md new file mode 100644 index 0000000000..cf9fecb724 --- /dev/null +++ b/pr-6368-native-evidence/README.md @@ -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 diff --git a/pr-6368-native-evidence/cleanup.txt b/pr-6368-native-evidence/cleanup.txt new file mode 100644 index 0000000000..f3846697be --- /dev/null +++ b/pr-6368-native-evidence/cleanup.txt @@ -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 diff --git a/pr-6368-native-evidence/environment.txt b/pr-6368-native-evidence/environment.txt new file mode 100644 index 0000000000..2089c2e720 --- /dev/null +++ b/pr-6368-native-evidence/environment.txt @@ -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. diff --git a/pr-6368-native-evidence/exact-adjacent.json b/pr-6368-native-evidence/exact-adjacent.json new file mode 100644 index 0000000000..e2e8ab9f91 --- /dev/null +++ b/pr-6368-native-evidence/exact-adjacent.json @@ -0,0 +1,15 @@ +{ + "description": "", + "focused": true, + "position": [ + 317, + 224 + ], + "role": "AXButton", + "size": [ + 1921, + 40 + ], + "title": "Open task [redacted fixture title]", + "value": "" +} diff --git a/pr-6368-native-evidence/exact-diagnostics.log b/pr-6368-native-evidence/exact-diagnostics.log new file mode 100644 index 0000000000..72f775b835 --- /dev/null +++ b/pr-6368-native-evidence/exact-diagnostics.log @@ -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 diff --git a/pr-6368-native-evidence/exact-focus-crop.png b/pr-6368-native-evidence/exact-focus-crop.png new file mode 100644 index 0000000000..0671300feb Binary files /dev/null and b/pr-6368-native-evidence/exact-focus-crop.png differ diff --git a/pr-6368-native-evidence/exact-focused.json b/pr-6368-native-evidence/exact-focused.json new file mode 100644 index 0000000000..b8de370033 --- /dev/null +++ b/pr-6368-native-evidence/exact-focused.json @@ -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" +} diff --git a/pr-6368-native-evidence/exact-journey.mp4 b/pr-6368-native-evidence/exact-journey.mp4 new file mode 100644 index 0000000000..3dbffffeed Binary files /dev/null and b/pr-6368-native-evidence/exact-journey.mp4 differ diff --git a/pr-6368-native-evidence/exact-receipt.json b/pr-6368-native-evidence/exact-receipt.json new file mode 100644 index 0000000000..edd71cb52f --- /dev/null +++ b/pr-6368-native-evidence/exact-receipt.json @@ -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 +} diff --git a/pr-6368-native-evidence/exact-selected-crop.png b/pr-6368-native-evidence/exact-selected-crop.png new file mode 100644 index 0000000000..1bdb137ccf Binary files /dev/null and b/pr-6368-native-evidence/exact-selected-crop.png differ diff --git a/pr-6368-native-evidence/exact-selected.json b/pr-6368-native-evidence/exact-selected.json new file mode 100644 index 0000000000..2a9ffdcf0a --- /dev/null +++ b/pr-6368-native-evidence/exact-selected.json @@ -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" +} diff --git a/pr-6368-native-evidence/mutant-adjacent.json b/pr-6368-native-evidence/mutant-adjacent.json new file mode 100644 index 0000000000..e2e8ab9f91 --- /dev/null +++ b/pr-6368-native-evidence/mutant-adjacent.json @@ -0,0 +1,15 @@ +{ + "description": "", + "focused": true, + "position": [ + 317, + 224 + ], + "role": "AXButton", + "size": [ + 1921, + 40 + ], + "title": "Open task [redacted fixture title]", + "value": "" +} diff --git a/pr-6368-native-evidence/mutant-diagnostics.log b/pr-6368-native-evidence/mutant-diagnostics.log new file mode 100644 index 0000000000..15349390c8 --- /dev/null +++ b/pr-6368-native-evidence/mutant-diagnostics.log @@ -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 diff --git a/pr-6368-native-evidence/mutant-focus-crop.png b/pr-6368-native-evidence/mutant-focus-crop.png new file mode 100644 index 0000000000..79a2e875db Binary files /dev/null and b/pr-6368-native-evidence/mutant-focus-crop.png differ diff --git a/pr-6368-native-evidence/mutant-focused.json b/pr-6368-native-evidence/mutant-focused.json new file mode 100644 index 0000000000..b8de370033 --- /dev/null +++ b/pr-6368-native-evidence/mutant-focused.json @@ -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" +} diff --git a/pr-6368-native-evidence/mutant-receipt.json b/pr-6368-native-evidence/mutant-receipt.json new file mode 100644 index 0000000000..10b2fcac62 --- /dev/null +++ b/pr-6368-native-evidence/mutant-receipt.json @@ -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 +} diff --git a/pr-6368-native-evidence/mutant.patch b/pr-6368-native-evidence/mutant.patch new file mode 100644 index 0000000000..94f2c85596 --- /dev/null +++ b/pr-6368-native-evidence/mutant.patch @@ -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", diff --git a/pr-6368-native-evidence/native_journey.swift b/pr-6368-native-evidence/native_journey.swift new file mode 100644 index 0000000000..59f122158c --- /dev/null +++ b/pr-6368-native-evidence/native_journey.swift @@ -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 \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(_ 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) +} diff --git a/pr-6368-native-evidence/restored-adjacent.json b/pr-6368-native-evidence/restored-adjacent.json new file mode 100644 index 0000000000..2af810442a --- /dev/null +++ b/pr-6368-native-evidence/restored-adjacent.json @@ -0,0 +1,15 @@ +{ + "description": "", + "focused": true, + "position": [ + 317, + 208 + ], + "role": "AXButton", + "size": [ + 1921, + 40 + ], + "title": "Open task [redacted fixture title]", + "value": "" +} diff --git a/pr-6368-native-evidence/restored-diagnostics.log b/pr-6368-native-evidence/restored-diagnostics.log new file mode 100644 index 0000000000..dd51cd4bbd --- /dev/null +++ b/pr-6368-native-evidence/restored-diagnostics.log @@ -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 diff --git a/pr-6368-native-evidence/restored-focused.json b/pr-6368-native-evidence/restored-focused.json new file mode 100644 index 0000000000..4f3fc22b40 --- /dev/null +++ b/pr-6368-native-evidence/restored-focused.json @@ -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" +} diff --git a/pr-6368-native-evidence/restored-receipt.json b/pr-6368-native-evidence/restored-receipt.json new file mode 100644 index 0000000000..dbadc8dc15 --- /dev/null +++ b/pr-6368-native-evidence/restored-receipt.json @@ -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 +} diff --git a/pr-6368-native-evidence/restored-selected.json b/pr-6368-native-evidence/restored-selected.json new file mode 100644 index 0000000000..9686d4c855 --- /dev/null +++ b/pr-6368-native-evidence/restored-selected.json @@ -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" +}