From 7737a86121ce99a6ede5bc97ba4eb8194cc4c02a Mon Sep 17 00:00:00 2001 From: Igor Costa Date: Tue, 10 Nov 2026 16:29:58 +1300 Subject: [PATCH 1/3] Track native VS Code windows while reading current terminal rows Read one unambiguous source-defined screen row list from a fresh application snapshot, preserve raw duplicates, and guard the process and native window identity. Refresh setup geometry around physical gestures before permitting a move. Co-authored-by: Autohand Evolve --- docs/terminal-compatibility.md | 6 +- .../TerminalCompatibilityUITests.swift | 261 +++++++++++++++++- 2 files changed, 251 insertions(+), 16 deletions(-) diff --git a/docs/terminal-compatibility.md b/docs/terminal-compatibility.md index 6b78dfdd..dff512d2 100644 --- a/docs/terminal-compatibility.md +++ b/docs/terminal-compatibility.md @@ -41,11 +41,11 @@ First-run dialogs are handled before fixture input. The offline fixture denies o VS Code uses separate disposable profiles for idle and streaming cases under a short `/private/tmp/ah-vsc-*` directory. Its [main IPC socket](https://github.com/microsoft/vscode/blob/main/src/vs/base/parts/ipc/node/ipc.net.ts) lives under the user-data directory, so putting profiles below a long checkout path can exceed macOS's Unix socket limit and make Code exit before creating a window. Settings evidence stays under the checkout's `.artifacts/native-terminal/` directory, and bounded main-log tails are attached to XCTest results. The profiles enable [`chat.disableAIFeatures`](https://code.visualstudio.com/docs/agents/reference/ai-settings) to suppress built-in Copilot onboarding. VS Code also launches with [`--force-renderer-accessibility`](https://www.chromium.org/developers/design-documents/accessibility/) to expose Chromium's native accessibility tree; the editor's screen-reader setting alone does not ensure that tree is available to XCTest. The scenario opens the terminal through its native Terminal > New Terminal menu. If the specific Copilot sign-in dialog still appears, the scenario closes that dialog and opens the integrated terminal. It does not sign in or grant network access. Window, workbench, and terminal-view failures preserve bounded accessibility diagnostics and screenshots; the workflow also retains the Xcode output and generated VS Code settings. -Native scenarios first size and position their fixture window within the reported display's visible bounds, leaving room for all four physical resize gestures. Setup shrinks oversized dimensions before moving the window, then grows it at the target position, so a full-width window's resize handle never moves off-screen. VS Code's input readiness uses the native Split Terminal command becoming enabled after New Terminal, which establishes terminal focus; it does not require xterm's input to expose the same TextView role as standalone terminals. The fixture's rendered output remains the mandatory proof that paste, Enter, and terminal rendering worked. All scenarios retain the exact transcript, streaming, draft, cursor, resize, and exit assertions. +Native scenarios first size and position their fixture window within the reported display's visible bounds, leaving room for all four physical resize gestures. Setup shrinks oversized dimensions before moving the window, then grows it at the target position, so a full-width window's resize handle never moves off-screen. VS Code setup starts after terminal readiness and verifies the actual width and height accepted by each gesture; a partially accepted shrink cannot authorize an off-screen move. VS Code's input readiness uses the native Split Terminal command becoming enabled after New Terminal, which establishes terminal focus; it does not require xterm's input to expose the same TextView role as standalone terminals. The fixture's rendered output remains the mandatory proof that paste, Enter, and terminal rendering worked. All scenarios retain the exact transcript, streaming, draft, cursor, resize, and exit assertions. -VS Code's readout preflight captures one immutable native window snapshot and searches at most 2,048 nodes for a TextView buffer containing the real fixture-history marker. It does not concatenate workbench StaticText nodes: xterm exposes both individual screen rows and a live announcement region, which can repeat hundreds of full-screen redraw announcements. Those announcements are not the terminal buffer. The native snapshot avoids separate remote accessibility calls for every node and attribute; exceeding its traversal budget fails closed to the buffer-copy preflight. If accessibility lacks it, a disposable-profile shortcut uses [`runCommands`](https://code.visualstudio.com/docs/configure/keybindings#_running-multiple-commands) to select the full terminal buffer and [copy and clear its selection](https://github.com/microsoft/vscode/blob/main/src/vs/workbench/contrib/terminalContrib/clipboard/browser/terminal.clipboard.contribution.ts). The preflight uses that raw copied buffer only if it contains the actual fixture marker and a second accessibility read still lacks it. Each subsequent copy clears the pasteboard and requires a new pasteboard generation; old frames cannot satisfy later resize checks. A temporarily unavailable copy produces no frame; the original startup, resize, cursor, or exit predicate retains its deadline. Once resize readiness accepts a fresh raw frame, all exact counts and streaming assertions inspect that same frame without issuing a second copy. Readiness checks only history and draft presence, so duplicated accepted frames still fail immediately. The command clears terminal selection while preserving focus and the PTY cursor, which the existing cursor-edit assertion verifies. No terminal output is filtered or deduplicated. Readout samples, selected backend, settings, and keybindings are retained with native results. +VS Code's readout preflight captures one immutable app snapshot, scopes it to the fixture's current native window frame, and searches at most 2,048 nodes. Its native process ID, Core Graphics window ID, and single-window count remain fixed through all gestures and reads, while accessibility window references are refreshed after resize. [xterm's accessibility tree](https://github.com/xtermjs/xterm.js/blob/master/src/browser/AccessibilityManager.ts) exposes a screen-row list separately from its live announcement region. In the recorded macOS tree this is an Other node containing consecutive, equal-height Group rows, each with one StaticText value. The adapter requires one unambiguous, complete row list in the native window; it preserves row order, blank rows, and repeated values. It never selects rows based on fixture content or concatenates announcement siblings. Native TextView buffers remain supported. Ambiguous, oversized, malformed, or stale snapshots fail closed to the buffer-copy preflight. If accessibility lacks it, a disposable-profile shortcut uses [`runCommands`](https://code.visualstudio.com/docs/configure/keybindings#_running-multiple-commands) to select the full terminal buffer and [copy and clear its selection](https://github.com/microsoft/vscode/blob/main/src/vs/workbench/contrib/terminalContrib/clipboard/browser/terminal.clipboard.contribution.ts). The preflight uses that raw copied buffer only if it contains the actual fixture marker and a second accessibility read still lacks it. Each subsequent copy clears the pasteboard and requires a new pasteboard generation; old frames cannot satisfy later resize checks. A temporarily unavailable copy produces no frame; the original startup, resize, cursor, or exit predicate retains its deadline. Once resize readiness accepts a fresh raw frame, all exact counts and streaming assertions inspect that same frame without issuing a second copy. Readiness checks only history and draft presence, so duplicated accepted frames still fail immediately. The command clears terminal selection while preserving focus and the PTY cursor, which the existing cursor-edit assertion verifies. No terminal output is filtered or deduplicated. Readout samples, selected backend, settings, and keybindings are retained with native results. -The strict visible-history assertions require room for both transcript exchanges and the multiline composer. At a compact 57×19 terminal, the full-screen transcript correctly follows its newest exchange and clips older rows; a separate PTY case checks that the history reappears on growth, the draft and cursor survive, and Ctrl+C exits cleanly. This distinguishes viewport clipping from lost transcript data. Native window references bind to their accessibility identities so later queries continue inspecting the resized window. +The strict visible-history assertions require room for both transcript exchanges and the multiline composer. At a compact 57×19 terminal, the full-screen transcript correctly follows its newest exchange and clips older rows; a separate PTY case checks that the history reappears on growth, the draft and cursor survive, and Ctrl+C exits cleanly. This distinguishes viewport clipping from lost transcript data. Standalone terminal references bind to their accessibility identities. VS Code refreshes its accessibility window before setup steps and after every setup or scenario gesture, guarded by the same native process ID, window ID, single-window count, and current frame. Native CI availability and per-application results must be assessed from that workflow's completed jobs. A passing PTY suite or a successful XCTest build alone does not prove a native application passed. Failures in app launch, accessibility access, or window resizing fail the native job and preserve its XCTest evidence; they are not silently converted to headless passes. diff --git a/src/testing/drivers/native-terminal-lab/TerminalCompatibilityUITests.swift b/src/testing/drivers/native-terminal-lab/TerminalCompatibilityUITests.swift index 01bb9236..752db945 100644 --- a/src/testing/drivers/native-terminal-lab/TerminalCompatibilityUITests.swift +++ b/src/testing/drivers/native-terminal-lab/TerminalCompatibilityUITests.swift @@ -12,6 +12,8 @@ private struct NativeAccessibilityText { private struct NativeReadoutNode { var isTextView = false + var elementType: XCUIElement.ElementType = .other + var frame = CGRect.zero var text = NativeAccessibilityText() var children: [NativeReadoutNode] = [] } @@ -19,22 +21,87 @@ private struct NativeReadoutNode { private func nativeCodeTerminalBuffer(_ capture: () throws -> Snapshot, children: (Snapshot) -> [Snapshot], textViewValue: (Snapshot) -> String?, + screenBuffer: (Snapshot) -> String? = { _ in nil }, maximumNodes: Int = 2_048) rethrows -> String { var pending = [try capture()] var visited = 0 var values: [String] = [] + var screens: [String] = [] while let node = pending.popLast() { guard visited < maximumNodes else { return "" } visited += 1 if let value = textViewValue(node) { values.append(value) } + if let screen = screenBuffer(node) { screens.append(screen) } + guard screens.count <= 1 else { return "" } let descendants = children(node) guard descendants.count <= maximumNodes - visited - pending.count else { return "" } pending.append(contentsOf: descendants.reversed()) } + if let screen = screens.first { return screen } return values.filter { $0.contains("RESIZE_HISTORY retained text") }.max { $0.count < $1.count } ?? values.max { $0.count < $1.count } ?? "" } +private struct NativeWindowFrame: Equatable { + let processIdentifier: Int32 + let windowIdentifier: UInt32 + let frame: CGRect +} + +private func nativeFixtureWindow(_ windows: [NativeWindowFrame], identity: NativeWindowFrame) -> NativeWindowFrame? { + let owned = windows.filter { $0.processIdentifier == identity.processIdentifier } + guard owned.count == 1, owned[0].windowIdentifier == identity.windowIdentifier else { return nil } + return owned[0] +} + +private func nativeFixtureWindowReference(_ retained: Window, refresh: (() -> Window?)?) -> Window? { + if let refresh { return refresh() } + return retained +} + +private func nativeFixtureMoveFrame(current: CGRect, target: CGRect, bounds: CGRect) -> CGRect? { + let proposed = CGRect(origin: target.origin, size: current.size) + return bounds.contains(proposed) ? proposed : nil +} + +private func nativeCodeScreenRows(_ capture: () throws -> Snapshot, windowFrame: CGRect, + children: (Snapshot) -> [Snapshot], + elementType: (Snapshot) -> XCUIElement.ElementType, + frame: (Snapshot) -> CGRect, value: (Snapshot) -> String?, + maximumNodes: Int = 2_048) rethrows -> String { + let snapshot = try capture() + let windows = children(snapshot).filter { elementType($0) == .window && frame($0) == windowFrame } + guard windows.count == 1 else { return "" } + return nativeCodeTerminalBuffer({ windows[0] }, children: children, + textViewValue: { elementType($0) == .textView ? value($0) : nil }, + screenBuffer: { node in + guard elementType(node) == .other else { return nil } + let rows = children(node) + let viewport = frame(node) + guard rows.count >= 2, rows.count <= maximumNodes / 2, + viewport.width > 0, viewport.height > 0, windowFrame.contains(viewport) else { return nil } + var previous: CGRect? + var values: [String] = [] + for row in rows { + let position = frame(row) + let contents = children(row) + guard elementType(row) == .group, contents.count == 1, + elementType(contents[0]) == .staticText, let text = value(contents[0]), + position.width > 0, position.height > 0, + abs(position.minX - viewport.minX) <= 1, + position.maxX <= viewport.maxX + 1, position.maxY <= viewport.maxY + 1 else { return nil } + if let previous { + guard abs(previous.maxY - position.minY) <= 1, + abs(previous.height - position.height) <= 1 else { return nil } + } else if abs(position.minY - viewport.minY) > 1 { return nil } + values.append(text) + previous = position + } + guard let last = previous, viewport.maxY - last.maxY <= last.height + 1 else { return nil } + return values.joined(separator: "\n") + }, maximumNodes: maximumNodes) +} + private struct NativeTerminalReadout { let read: () -> String? private(set) var frame: String? @@ -91,6 +158,85 @@ final class TerminalCompatibilityUITests: XCTestCase { @MainActor func testIdleResize() throws { try resizeScenario(streaming: false) } @MainActor func testStreamingResize() throws { try resizeScenario(streaming: true) } + private func screenRows(_ values: [String], origin: CGPoint = CGPoint(x: 73, y: 101)) -> NativeReadoutNode { + NativeReadoutNode(elementType: .other, + frame: CGRect(origin: origin, size: CGSize(width: 731, height: CGFloat(values.count) * 14 + 6)), + children: values.enumerated().map { index, value in + let row = CGRect(x: origin.x, y: origin.y + CGFloat(index) * 14, width: 731, height: 14) + return NativeReadoutNode(elementType: .group, frame: row, children: [ + NativeReadoutNode(elementType: .staticText, frame: row.offsetBy(dx: 0, dy: 2), + text: NativeAccessibilityText(value: value)) + ]) + }) + } + + private func screenReadout(_ snapshot: NativeReadoutNode, maximumNodes: Int = 2_048) -> String { + nativeCodeScreenRows({ snapshot }, windowFrame: CGRect(x: 20, y: 33, width: 784, height: 550), + children: { $0.children }, elementType: { $0.elementType }, frame: { $0.frame }, + value: { $0.text.value }, maximumNodes: maximumNodes) + } + + func testNativeScreenRowsReadTheFreshAppWindowAndExcludeAnnouncementSiblingsByStructure() { + let values = ["Autohand Code", "\u{00a0}", "RESIZE_HISTORY retained text", "preserved draft", "RESIZE_STREAM 365"] + let current = NativeReadoutNode(elementType: .window, frame: CGRect(x: 20, y: 33, width: 784, height: 550), children: [ + NativeReadoutNode(elementType: .staticText, text: NativeAccessibilityText(value: values.joined() + values.joined())), + screenRows(values) + ]) + var captures = 0 + let value = nativeCodeScreenRows({ captures += 1; return NativeReadoutNode(children: [current]) }, windowFrame: current.frame, + children: { $0.children }, elementType: { $0.elementType }, frame: { $0.frame }, value: { $0.text.value }) + XCTAssertEqual(value, values.joined(separator: "\n")) + XCTAssertEqual(captures, 1) + let sparseOldWindow = NativeReadoutNode(elementType: .window, frame: current.frame, + children: [NativeReadoutNode(elementType: .button)]) + XCTAssertEqual(screenReadout(NativeReadoutNode(children: [sparseOldWindow])), "") + } + + func testNativeScreenRowsPreserveDuplicateAndBlankRowsWithoutChoosingACleanCandidate() { + let values = ["RESIZE_HISTORY retained text", "", "RESIZE_HISTORY retained text", "preserved draft"] + var window = NativeReadoutNode(elementType: .window, frame: CGRect(x: 20, y: 33, width: 784, height: 550), children: [screenRows(values)]) + XCTAssertEqual(screenReadout(NativeReadoutNode(children: [window])), values.joined(separator: "\n")) + window.children.append(screenRows(["RESIZE_HISTORY retained text", "preserved draft"])) + XCTAssertEqual(screenReadout(NativeReadoutNode(children: [window])), "") + } + + func testNativeScreenRowsRejectStaleGeometryBudgetsAndNonConsecutiveRows() { + var window = NativeReadoutNode(elementType: .window, frame: CGRect(x: 20, y: 33, width: 784, height: 550), + children: [screenRows(["RESIZE_HISTORY retained text", "preserved draft"])]) + XCTAssertEqual(screenReadout(NativeReadoutNode(children: [window]), maximumNodes: 3), "") + window.frame.size.width += 1 + XCTAssertEqual(screenReadout(NativeReadoutNode(children: [window])), "") + window.frame.size.width -= 1 + window.children[0].children.reverse() + XCTAssertEqual(screenReadout(NativeReadoutNode(children: [window])), "") + } + + func testNativeViewportDoesNotMoveAWindowWhoseWidthIgnoredTheShrink() { + let bounds = CGRect(x: 0, y: 0, width: 1024, height: 768) + let target = CGRect(x: 20, y: 33, width: 904, height: 610) + XCTAssertNil(nativeFixtureMoveFrame(current: CGRect(x: 0, y: 31, width: 1024, height: 610), target: target, bounds: bounds)) + XCTAssertEqual(nativeFixtureMoveFrame(current: CGRect(x: 0, y: 31, width: 904, height: 610), target: target, bounds: bounds), target) + } + + func testNativeViewportRefreshesSetupGeometryAndDoesNotReuseAStaleReferenceOnFailure() { + let retained = NativeWindowFrame(processIdentifier: 6736, windowIdentifier: 101, frame: CGRect(x: 0, y: 31, width: 1024, height: 610)) + let current = NativeWindowFrame(processIdentifier: 6736, windowIdentifier: 101, frame: CGRect(x: 0, y: 31, width: 904, height: 610)) + let refreshed = nativeFixtureWindowReference(retained, refresh: { nativeFixtureWindow([current], identity: retained) }) + XCTAssertEqual(refreshed?.frame.width, 904) + let replaced = NativeWindowFrame(processIdentifier: 6736, windowIdentifier: 102, frame: current.frame) + XCTAssertNil(nativeFixtureWindowReference(retained, refresh: { nativeFixtureWindow([replaced], identity: retained) })) + XCTAssertEqual(nativeFixtureWindowReference(retained, refresh: nil), retained) + } + + func testNativeViewportKeepsTheNativeWindowIdentityWhileAccessibilityNodesChange() { + let identity = NativeWindowFrame(processIdentifier: 6736, windowIdentifier: 101, frame: CGRect(x: 0, y: 31, width: 904, height: 610)) + let resized = NativeWindowFrame(processIdentifier: 6736, windowIdentifier: 101, frame: CGRect(x: 20, y: 33, width: 784, height: 550)) + XCTAssertEqual(nativeFixtureWindow([resized], identity: identity), resized) + XCTAssertNil(nativeFixtureWindow([NativeWindowFrame(processIdentifier: 6736, windowIdentifier: 102, frame: resized.frame)], identity: identity)) + XCTAssertNil(nativeFixtureWindow([resized, identity], identity: identity)) + XCTAssertNil(nativeFixtureWindow([NativeWindowFrame(processIdentifier: 6737, windowIdentifier: 101, frame: resized.frame)], identity: identity)) + } + func testNativeReadoutAssertsAgainstTheAcceptedFreshFrameWithoutCopyingAgain() { let raw = "Autohand Code\nRESIZE_HISTORY retained text\npreserved draft\nRESIZE_STREAM 308" var reads = 0 @@ -284,7 +430,9 @@ final class TerminalCompatibilityUITests: XCTestCase { configuration.environment = app.launchEnvironment configuration.createsNewApplicationInstance = true var launchError: Error? - NSWorkspace.shared.openApplication(at: URL(fileURLWithPath: application), configuration: configuration) { _, error in + var launchedProcessIdentifier: Int32? + NSWorkspace.shared.openApplication(at: URL(fileURLWithPath: application), configuration: configuration) { running, error in + launchedProcessIdentifier = running?.processIdentifier launchError = error launch.fulfill() } @@ -318,7 +466,7 @@ final class TerminalCompatibilityUITests: XCTestCase { return } window = fixtureWindow - let displayBounds = normalizeViewport(window, terminal: terminal) + let initialBounds = terminal == "vscode" ? nil : normalizeViewport(window, terminal: terminal) if terminal == "vscode" { guard app.menuBars.menuBarItems["Terminal"].waitForExistence(timeout: 30) else { attachStartupFailure(app, terminal: terminal, phase: "workbench") @@ -343,6 +491,31 @@ final class TerminalCompatibilityUITests: XCTestCase { XCTFail("The native terminal did not expose a ready input target") return } + let nativeIdentity: NativeWindowFrame? + if terminal == "vscode" { + guard let processIdentifier = launchedProcessIdentifier else { + XCTFail("macOS did not identify the launched VS Code process") + return + } + let native = visibleNativeWindows(processIdentifier: processIdentifier) + guard native.count == 1, native[0].frame == window.frame else { + XCTFail("VS Code must have one identifiable native fixture window") + return + } + nativeIdentity = native[0] + print("NATIVE_TERMINAL_WINDOW vscode pid=\(native[0].processIdentifier) id=\(native[0].windowIdentifier)") + } else { nativeIdentity = nil } + let displayBounds = initialBounds ?? normalizeViewport(window, terminal: terminal, verifyWindow: { + guard let identity = nativeIdentity else { return false } + return nativeFixtureWindow(self.visibleNativeWindows(processIdentifier: identity.processIdentifier), identity: identity) != nil + }, refreshWindow: { + guard let identity = nativeIdentity else { return nil } + return self.currentCodeWindow(app, identity: identity) + }) + let readTerminal: (Bool) -> String? = { copied in + if let identity = nativeIdentity { return self.codeTerminalValue(app, identity: identity, useCopiedBuffer: copied) } + return self.terminalValue(window) + } if terminal != "vscode" { terminalView.click() } NSPasteboard.general.clearContents() XCTAssertTrue(NSPasteboard.general.writeObjects([command as NSString])) @@ -353,11 +526,11 @@ final class TerminalCompatibilityUITests: XCTestCase { var copiedStartup = "" var useCopiedBuffer = false let ready = NSPredicate { _, _ in - accessibilityStartup = self.terminalValue(window) ?? "" + accessibilityStartup = readTerminal(false) ?? "" startupValue = accessibilityStartup if !startupValue.contains("RESIZE_HISTORY retained text"), terminal == "vscode" { - copiedStartup = self.copiedTerminalValue(window) ?? "" - accessibilityStartup = self.terminalValue(window) ?? "" + copiedStartup = readTerminal(true) ?? "" + accessibilityStartup = readTerminal(false) ?? "" useCopiedBuffer = nativeReadoutUsesCopiedBuffer(terminal: terminal, accessibilityHasFixture: accessibilityStartup.contains("RESIZE_HISTORY retained text"), copiedBufferHasFixture: copiedStartup.contains("RESIZE_HISTORY retained text")) @@ -386,12 +559,20 @@ final class TerminalCompatibilityUITests: XCTestCase { add(evidence) } for (index, delta) in nativeResizeDeltas.enumerated() { + if let identity = nativeIdentity { + guard let current = currentCodeWindow(app, identity: identity) else { return } + window = current + } let frame = window.frame let corner = window.coordinate(withNormalizedOffset: CGVector(dx: 1, dy: 1)).withOffset(CGVector(dx: -2, dy: -2)) corner.press(forDuration: 0.1, thenDragTo: corner.withOffset(delta)) + if let identity = nativeIdentity { + guard let current = currentCodeWindow(app, identity: identity) else { return } + window = current + } XCTAssertNotEqual(window.frame.size, frame.size, "The native window did not resize") XCTAssertTrue(displayBounds.contains(window.frame), "The resized fixture window left the display bounds") - var readout = NativeTerminalReadout(read: { self.terminalValue(window, useCopiedBuffer: useCopiedBuffer) }) + var readout = NativeTerminalReadout(read: { readTerminal(useCopiedBuffer) }) let settled = NSPredicate { _, _ in readout.capture() } guard XCTWaiter.wait(for: [XCTNSPredicateExpectation(predicate: settled, object: window)], timeout: 10) == .completed, let content = readout.frame else { @@ -415,21 +596,23 @@ final class TerminalCompatibilityUITests: XCTestCase { app.typeKey(.leftArrow, modifierFlags: []) app.typeKey(.leftArrow, modifierFlags: []) app.typeKey("x", modifierFlags: .shift) - let edited = NSPredicate { _, _ in self.terminalValue(window, useCopiedBuffer: useCopiedBuffer)?.contains("taXil") == true } + let edited = NSPredicate { _, _ in readTerminal(useCopiedBuffer)?.contains("taXil") == true } expectation(for: edited, evaluatedWith: app) waitForExpectations(timeout: 10) app.typeKey(.escape, modifierFlags: []) app.typeKey("c", modifierFlags: .control) app.typeKey("c", modifierFlags: .control) app.typeKey("c", modifierFlags: .control) - let exit = NSPredicate { _, _ in app.state == .notRunning || self.terminalValue(window, useCopiedBuffer: useCopiedBuffer)?.contains("RESIZE_PTY_EXIT") == true } + let exit = NSPredicate { _, _ in app.state == .notRunning || readTerminal(useCopiedBuffer)?.contains("RESIZE_PTY_EXIT") == true } expectation(for: exit, evaluatedWith: app) waitForExpectations(timeout: 15) print(streaming ? "NATIVE_TERMINAL_STREAMING_PASS" : "NATIVE_TERMINAL_IDLE_PASS") } @MainActor - private func normalizeViewport(_ window: XCUIElement, terminal: String) -> CGRect { + private func normalizeViewport(_ retainedWindow: XCUIElement, terminal: String, verifyWindow: () -> Bool = { true }, + refreshWindow: (() -> XCUIElement?)? = nil) -> CGRect { + guard var window = nativeFixtureWindowReference(retainedWindow, refresh: refreshWindow) else { return .zero } guard let screen = NSScreen.main else { XCTFail("The native terminal runner has no display") return .zero @@ -440,17 +623,33 @@ final class TerminalCompatibilityUITests: XCTestCase { let available = CGRect(x: visible.minX, y: desktopTop - visible.maxY, width: visible.width, height: visible.height) let target = nativeFixtureBaseline(current: window.frame, available: available) for step in nativeFixturePreparationFrames(current: window.frame, available: available) { + guard let current = nativeFixtureWindowReference(window, refresh: refreshWindow) else { return bounds } + window = current + XCTAssertTrue(verifyWindow(), "The native fixture window identity changed before setup") let frame = window.frame if abs(step.minX - frame.minX) > 0.5 || abs(step.minY - frame.minY) > 0.5 { + guard nativeFixtureMoveFrame(current: frame, target: step, bounds: bounds) != nil else { + XCTFail("The native window did not shrink enough for an on-screen move") + return bounds + } let grip = window.coordinate(withNormalizedOffset: .zero).withOffset(CGVector(dx: terminal == "vscode" ? 110 : frame.width * 0.75, dy: 12)) grip.press(forDuration: 0.1, thenDragTo: grip.withOffset(CGVector(dx: step.minX - frame.minX, dy: step.minY - frame.minY))) + guard let current = nativeFixtureWindowReference(window, refresh: refreshWindow) else { return bounds } + window = current } let moved = window.frame if abs(step.width - moved.width) > 0.5 || abs(step.height - moved.height) > 0.5 { let corner = window.coordinate(withNormalizedOffset: CGVector(dx: 1, dy: 1)).withOffset(CGVector(dx: -2, dy: -2)) corner.press(forDuration: 0.1, thenDragTo: corner.withOffset(CGVector(dx: step.width - moved.width, dy: step.height - moved.height))) + guard let current = nativeFixtureWindowReference(window, refresh: refreshWindow) else { return bounds } + window = current } print("NATIVE_TERMINAL_PREPARE \(terminal) \(window.frame) target=\(step) display=\(bounds)") + XCTAssertTrue(verifyWindow(), "The native fixture window identity changed during setup") + if terminal == "vscode" { + XCTAssertEqual(window.frame.width, step.width, accuracy: 1, "The native window did not accept the requested width") + XCTAssertEqual(window.frame.height, step.height, accuracy: 1, "The native window did not accept the requested height") + } XCTAssertTrue(bounds.contains(window.frame), "Every fixture setup gesture must stay within the display") } XCTAssertGreaterThanOrEqual(window.frame.height, target.height - 20, "The native fixture needs room for both transcript exchanges") @@ -513,6 +712,46 @@ final class TerminalCompatibilityUITests: XCTestCase { NativeAccessibilityText(label: element.label, title: element.title, value: element.value as? String ?? "") } + private func visibleNativeWindows(processIdentifier: Int32) -> [NativeWindowFrame] { + guard let windows = CGWindowListCopyWindowInfo([.optionOnScreenOnly, .excludeDesktopElements], kCGNullWindowID) as? [[String: Any]] else { return [] } + return windows.compactMap { window in + guard let pid = window[kCGWindowOwnerPID as String] as? NSNumber, pid.int32Value == processIdentifier, + let layer = window[kCGWindowLayer as String] as? NSNumber, layer.intValue == 0, + let identifier = window[kCGWindowNumber as String] as? NSNumber, + let bounds = window[kCGWindowBounds as String] as? NSDictionary, + let frame = CGRect(dictionaryRepresentation: bounds) else { return nil } + return NativeWindowFrame(processIdentifier: pid.int32Value, windowIdentifier: identifier.uint32Value, frame: frame) + } + } + + private func currentCodeWindow(_ app: XCUIApplication, identity: NativeWindowFrame) -> XCUIElement? { + guard let native = nativeFixtureWindow(visibleNativeWindows(processIdentifier: identity.processIdentifier), identity: identity) else { + XCTFail("VS Code changed the fixture's native window ID, process, or window count") + return nil + } + let windows = app.windows.allElementsBoundByAccessibilityElement.filter { $0.frame == native.frame } + guard windows.count == 1 else { + XCTFail("VS Code did not expose exactly one accessibility window for the native fixture frame") + return nil + } + return windows[0] + } + + private func codeTerminalValue(_ app: XCUIApplication, identity: NativeWindowFrame, useCopiedBuffer: Bool) -> String? { + guard let native = nativeFixtureWindow(visibleNativeWindows(processIdentifier: identity.processIdentifier), identity: identity) else { + XCTFail("VS Code changed the fixture's native window ID, process, or window count") + return nil + } + let value: String? + if useCopiedBuffer { value = copiedTerminalValue(app) } + else { + value = try? nativeCodeScreenRows({ try app.snapshot() }, windowFrame: native.frame, + children: { $0.children }, elementType: { $0.elementType }, frame: { $0.frame }, value: { $0.value as? String }) + } + guard nativeFixtureWindow(visibleNativeWindows(processIdentifier: identity.processIdentifier), identity: identity) == native else { return nil } + return value + } + private func copiedTerminalValue(_ window: XCUIElement) -> String? { let pasteboard = NSPasteboard.general pasteboard.clearContents() @@ -529,10 +768,6 @@ final class TerminalCompatibilityUITests: XCTestCase { private func terminalValue(_ window: XCUIElement, useCopiedBuffer: Bool = false) -> String? { if useCopiedBuffer { return copiedTerminalValue(window) } - if environment["AUTOHAND_NATIVE_TERMINAL"] == "vscode" { - return (try? nativeCodeTerminalBuffer({ try window.snapshot() }, children: { $0.children }, - textViewValue: { $0.elementType == .textView ? $0.value as? String : nil })) ?? "" - } let values = window.textViews.allElementsBoundByIndex.compactMap { $0.value as? String } if let terminal = values.filter({ $0.contains("RESIZE_HISTORY retained text") }).max(by: { $0.count < $1.count }) { return terminal From f1106145bd555216dca5ab1ebd1ddc33e0a82f77 Mon Sep 17 00:00:00 2001 From: Igor Costa Date: Tue, 10 Nov 2026 16:42:36 +1300 Subject: [PATCH 2/3] Match native terminal row geometry to xterm text scaling Accept text-scaled accessibility rows that extend beyond their list width while retaining vertical, structural, window, and content checks. Reproduce the captured resize geometry without removing duplicate rows. Co-authored-by: Autohand Evolve --- docs/terminal-compatibility.md | 2 +- .../TerminalCompatibilityUITests.swift | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/docs/terminal-compatibility.md b/docs/terminal-compatibility.md index dff512d2..de282d52 100644 --- a/docs/terminal-compatibility.md +++ b/docs/terminal-compatibility.md @@ -43,7 +43,7 @@ VS Code uses separate disposable profiles for idle and streaming cases under a s Native scenarios first size and position their fixture window within the reported display's visible bounds, leaving room for all four physical resize gestures. Setup shrinks oversized dimensions before moving the window, then grows it at the target position, so a full-width window's resize handle never moves off-screen. VS Code setup starts after terminal readiness and verifies the actual width and height accepted by each gesture; a partially accepted shrink cannot authorize an off-screen move. VS Code's input readiness uses the native Split Terminal command becoming enabled after New Terminal, which establishes terminal focus; it does not require xterm's input to expose the same TextView role as standalone terminals. The fixture's rendered output remains the mandatory proof that paste, Enter, and terminal rendering worked. All scenarios retain the exact transcript, streaming, draft, cursor, resize, and exit assertions. -VS Code's readout preflight captures one immutable app snapshot, scopes it to the fixture's current native window frame, and searches at most 2,048 nodes. Its native process ID, Core Graphics window ID, and single-window count remain fixed through all gestures and reads, while accessibility window references are refreshed after resize. [xterm's accessibility tree](https://github.com/xtermjs/xterm.js/blob/master/src/browser/AccessibilityManager.ts) exposes a screen-row list separately from its live announcement region. In the recorded macOS tree this is an Other node containing consecutive, equal-height Group rows, each with one StaticText value. The adapter requires one unambiguous, complete row list in the native window; it preserves row order, blank rows, and repeated values. It never selects rows based on fixture content or concatenates announcement siblings. Native TextView buffers remain supported. Ambiguous, oversized, malformed, or stale snapshots fail closed to the buffer-copy preflight. If accessibility lacks it, a disposable-profile shortcut uses [`runCommands`](https://code.visualstudio.com/docs/configure/keybindings#_running-multiple-commands) to select the full terminal buffer and [copy and clear its selection](https://github.com/microsoft/vscode/blob/main/src/vs/workbench/contrib/terminalContrib/clipboard/browser/terminal.clipboard.contribution.ts). The preflight uses that raw copied buffer only if it contains the actual fixture marker and a second accessibility read still lacks it. Each subsequent copy clears the pasteboard and requires a new pasteboard generation; old frames cannot satisfy later resize checks. A temporarily unavailable copy produces no frame; the original startup, resize, cursor, or exit predicate retains its deadline. Once resize readiness accepts a fresh raw frame, all exact counts and streaming assertions inspect that same frame without issuing a second copy. Readiness checks only history and draft presence, so duplicated accepted frames still fail immediately. The command clears terminal selection while preserving focus and the PTY cursor, which the existing cursor-edit assertion verifies. No terminal output is filtered or deduplicated. Readout samples, selected backend, settings, and keybindings are retained with native results. +VS Code's readout preflight captures one immutable app snapshot, scopes it to the fixture's current native window frame, and searches at most 2,048 nodes. Its native process ID, Core Graphics window ID, and single-window count remain fixed through all gestures and reads, while accessibility window references are refreshed after resize. [xterm's accessibility tree](https://github.com/xtermjs/xterm.js/blob/master/src/browser/AccessibilityManager.ts) exposes a screen-row list separately from its live announcement region. In the recorded macOS tree this is an Other node containing consecutive, equal-height Group rows, each with one StaticText value. The adapter requires one unambiguous, complete row list in the native window; it preserves row order, blank rows, and repeated values. xterm scales each accessibility row to its text and cell width, so rows can be wider than the list container after resize; the adapter validates their vertical grid without requiring horizontal containment. It never selects rows based on fixture content or concatenates announcement siblings. Native TextView buffers remain supported. Ambiguous, oversized, malformed, or stale snapshots fail closed to the buffer-copy preflight. If accessibility lacks it, a disposable-profile shortcut uses [`runCommands`](https://code.visualstudio.com/docs/configure/keybindings#_running-multiple-commands) to select the full terminal buffer and [copy and clear its selection](https://github.com/microsoft/vscode/blob/main/src/vs/workbench/contrib/terminalContrib/clipboard/browser/terminal.clipboard.contribution.ts). The preflight uses that raw copied buffer only if it contains the actual fixture marker and a second accessibility read still lacks it. Each subsequent copy clears the pasteboard and requires a new pasteboard generation; old frames cannot satisfy later resize checks. A temporarily unavailable copy produces no frame; the original startup, resize, cursor, or exit predicate retains its deadline. Once resize readiness accepts a fresh raw frame, all exact counts and streaming assertions inspect that same frame without issuing a second copy. Readiness checks only history and draft presence, so duplicated accepted frames still fail immediately. The command clears terminal selection while preserving focus and the PTY cursor, which the existing cursor-edit assertion verifies. No terminal output is filtered or deduplicated. Readout samples, selected backend, settings, and keybindings are retained with native results. The strict visible-history assertions require room for both transcript exchanges and the multiline composer. At a compact 57×19 terminal, the full-screen transcript correctly follows its newest exchange and clips older rows; a separate PTY case checks that the history reappears on growth, the draft and cursor survive, and Ctrl+C exits cleanly. This distinguishes viewport clipping from lost transcript data. Standalone terminal references bind to their accessibility identities. VS Code refreshes its accessibility window before setup steps and after every setup or scenario gesture, guarded by the same native process ID, window ID, single-window count, and current frame. diff --git a/src/testing/drivers/native-terminal-lab/TerminalCompatibilityUITests.swift b/src/testing/drivers/native-terminal-lab/TerminalCompatibilityUITests.swift index 752db945..e8bbc5a5 100644 --- a/src/testing/drivers/native-terminal-lab/TerminalCompatibilityUITests.swift +++ b/src/testing/drivers/native-terminal-lab/TerminalCompatibilityUITests.swift @@ -89,7 +89,7 @@ private func nativeCodeScreenRows(_ capture: () throws -> Snapshot, wi elementType(contents[0]) == .staticText, let text = value(contents[0]), position.width > 0, position.height > 0, abs(position.minX - viewport.minX) <= 1, - position.maxX <= viewport.maxX + 1, position.maxY <= viewport.maxY + 1 else { return nil } + position.maxY <= viewport.maxY + 1 else { return nil } if let previous { guard abs(previous.maxY - position.minY) <= 1, abs(previous.height - position.height) <= 1 else { return nil } @@ -200,6 +200,23 @@ final class TerminalCompatibilityUITests: XCTestCase { XCTAssertEqual(screenReadout(NativeReadoutNode(children: [window])), "") } + func testNativeScreenRowsPreserveTextScaledBeyondTheCapturedListWidth() { + let values = ["Autohand Code", "RESIZE_HISTORY retained text", "", "RESIZE_HISTORY retained text", "preserved draft"] + + Array(repeating: "\u{00a0}", count: 22) + var rows = screenRows(values) + rows.frame.size = CGSize(width: 651, height: 378) + for index in rows.children.indices { + rows.children[index].frame.size.width = index == 0 ? 182 : 686 + rows.children[index].children[0].frame = rows.children[index].frame.offsetBy(dx: 0, dy: 2) + } + let nativeFrame = CGRect(x: 20, y: 33, width: 744, height: 510) + let window = NativeReadoutNode(elementType: .window, frame: nativeFrame, children: [rows]) + let content = nativeCodeScreenRows({ NativeReadoutNode(children: [window]) }, windowFrame: nativeFrame, + children: { $0.children }, elementType: { $0.elementType }, frame: { $0.frame }, value: { $0.text.value }) + XCTAssertEqual(content, values.joined(separator: "\n")) + XCTAssertEqual(content.components(separatedBy: "RESIZE_HISTORY retained text").count - 1, 2) + } + func testNativeScreenRowsRejectStaleGeometryBudgetsAndNonConsecutiveRows() { var window = NativeReadoutNode(elementType: .window, frame: CGRect(x: 20, y: 33, width: 784, height: 550), children: [screenRows(["RESIZE_HISTORY retained text", "preserved draft"])]) From 5d6f6373c0c2a8a89f73a1a879fb66e001dd6c01 Mon Sep 17 00:00:00 2001 From: Igor Costa Date: Tue, 10 Nov 2026 17:00:25 +1300 Subject: [PATCH 3/3] Wait for complete native terminal frames before asserting content counts Require header, history, draft, and streaming marker presence within the existing resize deadline. Retain exact raw-frame count assertions so duplicated content and permanent marker loss remain failures. Co-authored-by: Autohand Evolve --- docs/terminal-compatibility.md | 2 +- .../TerminalCompatibilityUITests.swift | 46 +++++++++++++++++-- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/docs/terminal-compatibility.md b/docs/terminal-compatibility.md index de282d52..8ae1b4b1 100644 --- a/docs/terminal-compatibility.md +++ b/docs/terminal-compatibility.md @@ -43,7 +43,7 @@ VS Code uses separate disposable profiles for idle and streaming cases under a s Native scenarios first size and position their fixture window within the reported display's visible bounds, leaving room for all four physical resize gestures. Setup shrinks oversized dimensions before moving the window, then grows it at the target position, so a full-width window's resize handle never moves off-screen. VS Code setup starts after terminal readiness and verifies the actual width and height accepted by each gesture; a partially accepted shrink cannot authorize an off-screen move. VS Code's input readiness uses the native Split Terminal command becoming enabled after New Terminal, which establishes terminal focus; it does not require xterm's input to expose the same TextView role as standalone terminals. The fixture's rendered output remains the mandatory proof that paste, Enter, and terminal rendering worked. All scenarios retain the exact transcript, streaming, draft, cursor, resize, and exit assertions. -VS Code's readout preflight captures one immutable app snapshot, scopes it to the fixture's current native window frame, and searches at most 2,048 nodes. Its native process ID, Core Graphics window ID, and single-window count remain fixed through all gestures and reads, while accessibility window references are refreshed after resize. [xterm's accessibility tree](https://github.com/xtermjs/xterm.js/blob/master/src/browser/AccessibilityManager.ts) exposes a screen-row list separately from its live announcement region. In the recorded macOS tree this is an Other node containing consecutive, equal-height Group rows, each with one StaticText value. The adapter requires one unambiguous, complete row list in the native window; it preserves row order, blank rows, and repeated values. xterm scales each accessibility row to its text and cell width, so rows can be wider than the list container after resize; the adapter validates their vertical grid without requiring horizontal containment. It never selects rows based on fixture content or concatenates announcement siblings. Native TextView buffers remain supported. Ambiguous, oversized, malformed, or stale snapshots fail closed to the buffer-copy preflight. If accessibility lacks it, a disposable-profile shortcut uses [`runCommands`](https://code.visualstudio.com/docs/configure/keybindings#_running-multiple-commands) to select the full terminal buffer and [copy and clear its selection](https://github.com/microsoft/vscode/blob/main/src/vs/workbench/contrib/terminalContrib/clipboard/browser/terminal.clipboard.contribution.ts). The preflight uses that raw copied buffer only if it contains the actual fixture marker and a second accessibility read still lacks it. Each subsequent copy clears the pasteboard and requires a new pasteboard generation; old frames cannot satisfy later resize checks. A temporarily unavailable copy produces no frame; the original startup, resize, cursor, or exit predicate retains its deadline. Once resize readiness accepts a fresh raw frame, all exact counts and streaming assertions inspect that same frame without issuing a second copy. Readiness checks only history and draft presence, so duplicated accepted frames still fail immediately. The command clears terminal selection while preserving focus and the PTY cursor, which the existing cursor-edit assertion verifies. No terminal output is filtered or deduplicated. Readout samples, selected backend, settings, and keybindings are retained with native results. +VS Code's readout preflight captures one immutable app snapshot, scopes it to the fixture's current native window frame, and searches at most 2,048 nodes. Its native process ID, Core Graphics window ID, and single-window count remain fixed through all gestures and reads, while accessibility window references are refreshed after resize. [xterm's accessibility tree](https://github.com/xtermjs/xterm.js/blob/master/src/browser/AccessibilityManager.ts) exposes a screen-row list separately from its live announcement region. In the recorded macOS tree this is an Other node containing consecutive, equal-height Group rows, each with one StaticText value. The adapter requires one unambiguous, complete row list in the native window; it preserves row order, blank rows, and repeated values. xterm scales each accessibility row to its text and cell width, so rows can be wider than the list container after resize; the adapter validates their vertical grid without requiring horizontal containment. It never selects rows based on fixture content or concatenates announcement siblings. Native TextView buffers remain supported. Ambiguous, oversized, malformed, or stale snapshots fail closed to the buffer-copy preflight. If accessibility lacks it, a disposable-profile shortcut uses [`runCommands`](https://code.visualstudio.com/docs/configure/keybindings#_running-multiple-commands) to select the full terminal buffer and [copy and clear its selection](https://github.com/microsoft/vscode/blob/main/src/vs/workbench/contrib/terminalContrib/clipboard/browser/terminal.clipboard.contribution.ts). The preflight uses that raw copied buffer only if it contains the actual fixture marker and a second accessibility read still lacks it. Each subsequent copy clears the pasteboard and requires a new pasteboard generation; old frames cannot satisfy later resize checks. A temporarily unavailable copy produces no frame; the original startup, resize, cursor, or exit predicate retains its deadline. Once resize readiness accepts a fresh raw frame, all exact counts and streaming assertions inspect that same frame without issuing a second copy. Resize readiness requires the header, history, draft, and the stream marker for streaming scenarios, without examining their counts. This avoids accepting a partial accessibility update before the header returns; duplicated accepted frames still fail immediately against the exact-count assertions. Permanent marker loss fails the original settling deadline. The command clears terminal selection while preserving focus and the PTY cursor, which the existing cursor-edit assertion verifies. No terminal output is filtered or deduplicated. Readout samples, selected backend, settings, and keybindings are retained with native results. The strict visible-history assertions require room for both transcript exchanges and the multiline composer. At a compact 57×19 terminal, the full-screen transcript correctly follows its newest exchange and clips older rows; a separate PTY case checks that the history reappears on growth, the draft and cursor survive, and Ctrl+C exits cleanly. This distinguishes viewport clipping from lost transcript data. Standalone terminal references bind to their accessibility identities. VS Code refreshes its accessibility window before setup steps and after every setup or scenario gesture, guarded by the same native process ID, window ID, single-window count, and current frame. diff --git a/src/testing/drivers/native-terminal-lab/TerminalCompatibilityUITests.swift b/src/testing/drivers/native-terminal-lab/TerminalCompatibilityUITests.swift index e8bbc5a5..5cfc0909 100644 --- a/src/testing/drivers/native-terminal-lab/TerminalCompatibilityUITests.swift +++ b/src/testing/drivers/native-terminal-lab/TerminalCompatibilityUITests.swift @@ -104,12 +104,19 @@ private func nativeCodeScreenRows(_ capture: () throws -> Snapshot, wi private struct NativeTerminalReadout { let read: () -> String? + let requiresStreaming: Bool private(set) var frame: String? + init(read: @escaping () -> String?, requiresStreaming: Bool = false) { + self.read = read + self.requiresStreaming = requiresStreaming + } + mutating func capture() -> Bool { frame = read() guard let value = frame else { return false } - return value.contains("RESIZE_HISTORY retained text") && value.contains("preserved draft") + return value.contains("Autohand Code") && value.contains("RESIZE_HISTORY retained text") + && value.contains("preserved draft") && (!requiresStreaming || value.contains("RESIZE_STREAM ")) } } @@ -254,6 +261,39 @@ final class TerminalCompatibilityUITests: XCTestCase { XCTAssertNil(nativeFixtureWindow([NativeWindowFrame(processIdentifier: 6737, windowIdentifier: 101, frame: resized.frame)], identity: identity)) } + func testNativeReadoutWaitsForTheHeaderBeforeAcceptingACompleteFrame() { + let partial = "RESIZE_HISTORY retained text\npreserved draft\nRESIZE_STREAM 293" + let complete = "Autohand Code\n" + partial + var reads = 0 + var readout = NativeTerminalReadout(read: { reads += 1; return reads == 1 ? partial : complete }, requiresStreaming: true) + XCTAssertFalse(readout.capture()) + XCTAssertTrue(readout.capture()) + XCTAssertEqual(readout.frame, complete) + XCTAssertEqual(reads, 2) + var duplicate = NativeTerminalReadout(read: { "Autohand Code\n" + complete }, requiresStreaming: true) + XCTAssertTrue(duplicate.capture()) + XCTAssertEqual(duplicate.frame?.components(separatedBy: "Autohand Code").count, 3, + "Presence readiness must leave duplicate headers for the exact-count assertion") + } + + func testNativeReadoutPermanentlyMissingHeaderFailsTheUnchangedReadinessDeadline() { + var readout = NativeTerminalReadout(read: { "RESIZE_HISTORY retained text\npreserved draft" }) + XCTAssertFalse(readout.capture()) + let ready = NSPredicate { _, _ in readout.capture() } + XCTAssertEqual(XCTWaiter.wait(for: [XCTNSPredicateExpectation(predicate: ready, object: nil)], timeout: 0.01), .timedOut) + } + + func testNativeReadoutRequiresTheStreamMarkerOnlyForStreamingScenarios() { + var value = "Autohand Code\nRESIZE_HISTORY retained text\npreserved draft" + var idle = NativeTerminalReadout(read: { value }) + XCTAssertTrue(idle.capture()) + var streaming = NativeTerminalReadout(read: { value }, requiresStreaming: true) + XCTAssertFalse(streaming.capture()) + value += "\nRESIZE_STREAM 341" + XCTAssertTrue(streaming.capture()) + XCTAssertEqual(streaming.frame, value) + } + func testNativeReadoutAssertsAgainstTheAcceptedFreshFrameWithoutCopyingAgain() { let raw = "Autohand Code\nRESIZE_HISTORY retained text\npreserved draft\nRESIZE_STREAM 308" var reads = 0 @@ -278,7 +318,7 @@ final class TerminalCompatibilityUITests: XCTestCase { var reads = 0 var readout = NativeTerminalReadout(read: { reads += 1 - return reads == 1 ? "RESIZE_HISTORY retained text\npreserved draft" : nil + return reads == 1 ? "Autohand Code\nRESIZE_HISTORY retained text\npreserved draft" : nil }) XCTAssertTrue(readout.capture()) XCTAssertFalse(readout.capture()) @@ -589,7 +629,7 @@ final class TerminalCompatibilityUITests: XCTestCase { } XCTAssertNotEqual(window.frame.size, frame.size, "The native window did not resize") XCTAssertTrue(displayBounds.contains(window.frame), "The resized fixture window left the display bounds") - var readout = NativeTerminalReadout(read: { readTerminal(useCopiedBuffer) }) + var readout = NativeTerminalReadout(read: { readTerminal(useCopiedBuffer) }, requiresStreaming: streaming) let settled = NSPredicate { _, _ in readout.capture() } guard XCTWaiter.wait(for: [XCTNSPredicateExpectation(predicate: settled, object: window)], timeout: 10) == .completed, let content = readout.frame else {