Files
Termind/TermindTests/RemoteWindowTitleTests.swift
T
Kaede Akino 41404ca48d Fix flaky and never-green tests
The suite never finished. A SIGSEGV around 272 of 975 tests killed the
runner, so xcodebuild only ever reported one shard and roughly seventy
percent of the tests were not being executed at all. Underneath it sat
four failures, two of which had been red since the day they were
written, and three tests whose outcome was decided by state they did not
own.

The crash: `RemoteWindowTitleTests` closed a window whose
`isReleasedWhenClosed` still defaults to true, releasing one ARC also
owned. The second release landed on freed memory during an autorelease
pool drain, surfacing as `-[_NSWindowTransformAnimation dealloc]` and
charged to whichever test happened to be running. Closing it also risked
terminating the test host, which is the real application and whose
delegate quits once the last window closes. Ordering the window out
instead settles both.

Never green since written:

- `conversationOwnedImportHonorsAttachmentLimitBeforeStartingTasks`
  staged twenty chips that differed only by label, and `attach`
  deduplicates on kind and content, so one went in and the limit under
  test was never reached.
- `failedCompactionSaveDoesNotChangeTheInMemoryContext` asked for a cut
  keeping two tokens after a last turn worth one, so the search walked
  past it and `compactionSpan` returned nil. Both turns are now sized in
  the unit the cut is chosen in, sharing `filler(tokens:)`.

Stale or ambient:

- `ColorSystemTests` read the domain value `colorTag: .blue` as a
  display colour. The scan predates tag colours by a day.
- The Chinese text for a finished run changed in 0b67497; the assertion
  did not follow.
- `conversationOwnedFileImportFinishesAndKeepsFilename` yielded rather
  than slept, which does not wait for a read off the main actor.
- `shiftedPunctuationMatchesItsUnshiftedShortcutKey` rests on AppKit
  re-deriving a character through the current input source, which it
  declines for a synthesized event about a third of the time. It now
  asserts when AppKit answers and stands down when it does not.
- `FileURLDropTargetTests` reached `hitTest`, which reads
  `NSApp.currentEvent` and the machine-wide drag pasteboard for itself.
  That decision moves to `participates(at:currentEventType:pasteboard:)`
  with both inputs passed in; `hitTest` keeps its behaviour and the
  tests keep their own pasteboard.

Verified with eight consecutive full runs at 975 of 975.
2026-09-02 18:07:34 +08:00

114 lines
4.4 KiB
Swift

import AppKit
import Foundation
import GhosttyTerminal
import Testing
@testable import Termind
@Suite(.serialized)
@MainActor
struct RemoteWindowTitleTests {
@Test
func sanitizerKeepsOnePrintableLineAndRejectsEmptyTitles() {
#expect(RemoteWindowTitle.sanitized(" host: /srv ") == "host: /srv")
#expect(RemoteWindowTitle.sanitized("host\r\n/srv\u{7}") == "host /srv")
#expect(RemoteWindowTitle.sanitized("\u{1B}[1;32mgreen\u{1B}[0m") == "green")
#expect(RemoteWindowTitle.sanitized("\n\u{7}\u{1B}[31m") == nil)
}
@Test
func menuFollowsOnlyTheKeyWindowAndUsesKittyTitleFormat() throws {
let mainMenu = NSMenu()
let help = NSMenuItem(title: "Help", action: nil, keyEquivalent: "")
help.submenu = NSMenu(title: "Help")
mainMenu.addItem(help)
let firstWindow = NSWindow()
let secondWindow = NSWindow()
var keyWindow: NSWindow? = firstWindow
let coordinator = RemoteWindowTitleMenuCoordinator(
mainMenuProvider: { mainMenu }, keyWindowProvider: { keyWindow },
observesKeyWindowChanges: false
)
let firstToken = UUID()
let secondToken = UUID()
coordinator.report(
title: "kaede@lima-default:/Users/kaede",
for: firstWindow, token: firstToken
)
#expect(mainMenu.items.count == 2)
#expect(mainMenu.items[0] === help)
#expect(mainMenu.items[1].title.isEmpty)
#expect(
try #require(mainMenu.items[1].submenu).title
== " :: kaede@lima-default:/Users/kaede"
)
#expect(mainMenu.items[1].submenu?.items.isEmpty == true)
coordinator.report(title: "second:/tmp", for: secondWindow, token: secondToken)
#expect(mainMenu.items[1].submenu?.title == " :: kaede@lima-default:/Users/kaede")
keyWindow = secondWindow
coordinator.refresh()
#expect(mainMenu.items[1].submenu?.title == " :: second:/tmp")
coordinator.withdraw(token: secondToken)
#expect(mainMenu.items == [help])
}
@Test(
.disabled(
if: !ghosttyHasActiveDisplay,
"Ghostty's Metal surface requires an active display"
))
func hostManagedGhosttySurfaceParsesOSCZeroAndTwo() async throws {
let backend = InMemoryTerminalSession(write: { _ in }, resize: { _ in })
let state = TerminalViewState(configSource: .none)
state.configuration = TerminalSurfaceOptions(backend: .inMemory(backend))
let terminal = TerminalView(frame: NSRect(x: 0, y: 0, width: 640, height: 400))
terminal.delegate = state
terminal.configuration = state.configuration
terminal.controller = state.controller
let window = NSWindow(
contentRect: terminal.frame, styleMask: [.titled],
backing: .buffered, defer: false
)
window.contentView = terminal
window.makeKeyAndOrderFront(nil)
// Ordered out rather than closed, for two reasons. `close()` on a window
// whose `isReleasedWhenClosed` still defaults to true releases one ARC
// also owns, and the second release lands on freed memory — a SIGSEGV in
// `-[_NSWindowTransformAnimation dealloc]`, charged to whichever test is
// running when the pool drains. And the test host is the real app, whose
// delegate terminates it once the last window closes, which ends the run
// wherever it has got to. Letting ARC take the window is enough.
defer { window.orderOut(nil) }
backend.receive(Data("\u{1B}]0;zero-title\u{7}".utf8))
try await waitUntil { state.title == "zero-title" }
backend.receive(Data("\u{1B}]2;two-title\u{7}".utf8))
try await waitUntil { state.title == "two-title" }
}
private func waitUntil(
timeout: Duration = .seconds(2), condition: @MainActor () -> Bool
) async throws {
let clock = ContinuousClock()
let deadline = clock.now.advanced(by: timeout)
while !condition() {
guard clock.now < deadline else {
Issue.record("Timed out waiting for terminal state")
return
}
try await Task.sleep(for: .milliseconds(10))
}
}
}
private nonisolated let ghosttyHasActiveDisplay: Bool = {
var count: UInt32 = 0
guard CGGetActiveDisplayList(0, nil, &count) == .success else { return false }
return count > 0
}()