Files
cmux/Packages/macOS/CmuxFoundation/Tests/CmuxFoundationTests/Process/CommandRunnerDescriptorLifecycleTests.swift
T
Lawrence Chenandaustinpower1258 80f40831da cmux-tui: render inline Kitty images through libghostty (#8811)
* test: cover kitty placement frame reuse

* fix: cache kitty placement frames

* test: cover pixel-accurate kitty clipping

* fix: clip kitty placements in pixel space

* test: cover pixel-accurate kitty replay clipping

* fix: clip kitty replay in pixel space

* test: cover number-only kitty image attach

* test: cover both numbered kitty image aliases

* fix: preserve kitty number aliases across attach

* test: cover inflight kitty replay across resize

* fix: preserve inflight kitty replay

* test: cover anonymous kitty replay collisions

* fix: preserve anonymous kitty placements in replay

* test: cover kitty object count limits

* fix: bound kitty graphics object counts

* test: cover Kitty graphics in web render mode

* test: cover host kitty scene invalidation

* fix: restore kitty graphics after host resize

* feat: render Kitty graphics in web terminal

* test: cover graphics writer shutdown quiescence

* fix: layer Kitty graphics above cell backgrounds

* fix: draw web graphics from callback ref

* fix: quiesce graphics before terminal restore

* test: cover kitty replay allocation order

* fix: preserve kitty replay allocation order

* test: cover incremental render graphics deltas

* test(tui): cover linear graphics state maintenance

* fix: send incremental render graphics deltas

* test: cover bounded kitty replay semantics

* test(tui): cover late Kitty image ordering

* test: cover render transport size boundaries

* fix(tui): maintain Kitty graphic IDs linearly

* test: cover atomic cell geometry updates

* test(tui): cover Kitty PNG compatibility

* test: cover full render metadata budget

* test: preserve measured cell pixels across resize

* test: bound kitty pixel cache lookups

* fix: make cell geometry updates atomic

* test: bound kitty placement grouping

* fix: align render transport size budgets

* test: bound render taps and resize replay

* fix: preserve kitty images in bounded vt replay

* fix: bound render taps and skip unused replay

* docs(tui): document inline Kitty image support

* style(tui): format merged changes

* test: cover UTF-8 before kitty replay

* test: cover large kitty resize replay

* fix: distinguish UTF-8 from C1 kitty APC

* fix: preserve kitty upload across resize

* test(tui-sdk): cover retained render metadata overflow

* fix(tui-sdk): bound retained render events

* test(tui-web): expose placement canvas memory blowup

* fix(tui-web): bound graphic canvas backing

* test: preserve hosted Kitty image aliases

* fix: preserve hosted Kitty image aliases

* test(tui): cover stale Kitty write after resize clear

* fix(tui): discard stale Kitty writes after resize clear

* test(browser): expose terminal host alias protocol gap

* fix(browser): support terminal host Kitty aliases

* test(cmux-tui): expect resize alias sidecars

* test(tui): preserve sparse viewport across replay

* fix(tui): preserve sparse rows in terminal replay

* fix(tui): align replayed scrollback rows

* test(tui): await terminal host process exit

* test(tui): cover Kitty alias history and sparse replay

* fix(tui): preserve Kitty alias and sparse row history

* fix(tui): address Kitty graphics review findings

* fix(tui): harden Kitty graphics integration

* fix(tui): resolve final Kitty autoreview findings

* fix(tui): close Kitty autoreview findings

* test(tui): cover final Kitty review regressions

* fix(tui): close final Kitty autoreview findings

* test(tui): reject overflowing PTY pixel geometry

* fix(tui): reject invalid PTY pixel geometry

* test(tui): cover remaining Kitty review regressions

* fix(tui): close remaining Kitty review findings

* fix(cmux-tui): close graphics review gaps

* fix(cmux-tui): preserve attach and startup progress

* fix(tui): reconcile image render geometry

* fix(tui): align attach wire progress

* fix(tui): bound inline image rendering resources

* fix(tui): close inline image review gaps

* fix(tui): bound inline graphics hot paths

* test(tui): cover graphics attachment memory regressions

* fix(tui): bound graphics attachment allocations

* test(tui): budget retained render capacity

* test(tui): cover graphics review regressions

* fix(tui): close graphics autoreview gaps

* test(tui): cover second graphics review regressions

* fix(tui): close remaining graphics review gaps

* test(tui): cover remaining graphics review regressions

* fix(tui): close graphics review findings

* test(tui): cover final graphics review regressions

* fix(tui): close final graphics review findings

* test(tui): cover graphics admission regressions

* fix(tui): make graphics admission lazy and refillable

* test(tui): cover final host lifecycle findings

* fix(tui): bound host lifecycle work

* test(tui): cover final protocol review findings

* fix(tui): close final protocol review gaps

* test(tui): cover bounded graphics writer failure

* fix(tui): bound graphics output failure lifecycle

* test(tui): cover final remote graphics review gaps

* fix(tui): validate and localize remote attach data

* test(tui): cover final graphics ownership findings

* fix(tui): scope graphics output ownership

* test(tui): cover graphics resource safety gaps

* fix(tui): bound graphics resource lifecycles

* test(tui): cover graphics budget scan fanout

* fix(tui): make graphics admission single-pass

* test(tui): cover review resource safety gaps

* fix(tui): bound graphics attachment resources

* test(tui): cover enhanced input adapter compatibility

* fix(tui): reconcile shortcut merge with graphics input

* test(tui): reconcile merged attach fixtures

* test(tui): bound inline surface state

* fix(tui): keep libghostty state out of line

* test(tui): cover cell pixel fanout retry gap

* fix(tui): reconcile skipped cell pixel fanout

* test(tui): cover aggregate graphics ownership gaps

* fix(tui): bound aggregate graphics ownership

* test(tui): cover graphics teardown ownership

* fix(tui): rebalance graphics ownership on teardown

* test(tui): cover aggregate graphics recovery

* fix(tui): recover aggregate graphics capacity

* test(tui): isolate graphics counters per thread

* test(tui): cover graphics resource ownership gaps

* fix(tui): close graphics resource ownership gaps

* test(tui): cover Kitty replay state divergence

* fix(tui): preserve Kitty replay state across mirrors

* test(tui): cover terminal resource lifecycle stalls

* fix(tui): decouple terminal resource lifecycle work

* test(tui): cover review lifecycle regressions

* fix(tui): close review lifecycle gaps

* test(tui): cover graphics review regressions

* fix(tui): reconcile graphics lifecycle under load

* test(tui): cover graphics baseline and quota exhaustion

* fix(tui): reconcile graphics baselines and quota overflow

* test(tui): make graphics backpressure deterministic

* test(tui): cover exited quota and reset replay ordering

* fix(tui): preserve graphics state across resets and exits

* test(tui): cover scrolled Kitty placement alignment

* fix(tui): align Kitty graphics with scrolled viewports

* test(tui): bound stalled renderer output

* fix(tui): preserve renderer output backpressure

* test(tui): cover relabel and retry bounds

* fix(tui): bound graphics recovery work

* fix(ci): isolate fork-agent singleton default

* test(tui): bound persistent graphics recovery

* fix(tui): bound persistent graphics recovery

* test(tui): drain stalled quota worker

* test(tui): cover panic and fanout lifecycles

* fix(tui): bound graphics worker lifecycles

* test(web): cover exhausted graphics decode queue

* fix(web): retire exhausted graphics decode jobs

* test(tui): cover final Kitty review findings

* fix(tui): close final Kitty replay gaps

* test(tui): cover encoded Kitty quota

* fix(tui): budget encoded Kitty uploads

* test(browser): sync Kitty replay ceilings

* fix(browser): match Kitty replay ceilings

* test(tui): cover unsupported Kitty grayscale

* fix(tui): bound Kitty snapshot formats

* test(tui): cover Kitty quota recovery

* fix(tui): reconcile Kitty quota recovery

* test(tui): cover reconnect completion retry

* fix(tui): retry failed host reconnect completion

* test(tui): cover attach priority and replay cursor state

* fix(tui): preserve attach priority and replay state

* test(tui): cover superseded attach resize failure

* fix(tui): settle the latest promoted resize

* chore(tui): satisfy strict attach lifecycle lint

* test(tui): cover numeric Kitty final chunks

* fix(tui): parse Kitty chunk flags numerically

* test(tui): cover Kitty images in web scrollback

* fix(tui): render Kitty images in web scrollback

* test(tui): admit terminals after graphics quota failure

* fix(tui): degrade graphics after quota failure

* test(tui): finish graphics probe at DA1 marker

* fix(tui): end graphics probe at DA1 marker

* test(tui): reject stale scrollback image epochs

* fix(tui): version scrollback image anchors

* test(tui): refresh active scrollback epochs

* fix(tui): refresh active scrollback epochs

* test(tui): ignore screen-only history epochs

* fix(tui): scope history epochs to retained rows

* test(tui): keep image frames out of history epochs

* fix(tui): keep image frames out of history epochs

* test(tui): assert stable screen-only history epochs

* test(tui): bound deferred work and reserve attaches

* fix(tui): bound deferred graphics coordination

* test(tui): exhaust saturated Kitty quota retries

* fix(tui): exhaust saturated Kitty quota retries

* test(tui): retain overlapping Kitty replay placements

* fix(tui): clip Kitty placements at replay boundaries

* fix(tui): preserve merged attach invariants

* test(ci): cover Ghostty path metadata

* fix(ci): inspect executable Ghostty consumers

* test(ios): replace wall-clock synchronization

* chore(xcode): normalize project ordering

* fix(ssh): simplify retry script assembly

* test(app): update detached transfer fixture

* test: update remote PTY lifecycle fake

* test: require explicit app-host test mode

* fix: declare app-host test launch mode

* fix: clear Xcode 26.3 warning gate

* test: detect embedded app-host test bundle

* fix: detect app-host tests from embedded bundle

* chore: drop unreliable app-host scheme marker

* test: avoid async ARC lifetime assertion

* test: require app-host build identity

* fix: stamp app-host test builds before launch

* test: require test-runner app-host marker

* fix: forward app-host test identity through xcodebuild

* fix: keep completed iroh dial single-flight through install

* test: re-report lifecycle after status clear

* Fix cmux-tui merge integration

* test(web): cover render attach WebSocket budget

* fix(web): admit full render attach frames

---------

Co-authored-by: austinpower1258 <[email protected]>
2026-08-01 07:54:34 -07:00

193 lines
6.9 KiB
Swift

import Darwin
import Foundation
import Testing
@testable import CmuxFoundation
@Suite("CommandRunner descriptor lifecycle", .serialized)
struct CommandRunnerDescriptorLifecycleTests {
private let tempDirectory = FileManager.default.temporaryDirectory
@Test("Capture pipes are close-on-exec and close before success returns")
func capturePipesAreCloseOnExecAndCloseAfterSuccess() async throws {
let execution = try makeExecution(executable: "/usr/bin/true")
let descriptors = try snapshotDescriptors(of: execution)
#expect(descriptors.count == 8)
for descriptor in descriptors {
let flags = fcntl(descriptor.fileDescriptor, F_GETFD)
#expect(flags != -1)
#expect(
flags & FD_CLOEXEC != 0,
"CommandRunner pipe descriptor \(descriptor.fileDescriptor) can leak into unrelated children"
)
}
let result = await execution.run(timeout: 5)
#expect(result.exitStatus == 0)
expectDescriptorsClosed(descriptors)
#expect(execution.process.terminationHandler == nil)
}
@Test("Launch failure closes capture pipes")
func launchFailureClosesPipes() async throws {
let execution = try makeExecution(
executable: "/usr/bin/true",
directory: URL(
fileURLWithPath: "/cmux-test-directory-that-does-not-exist-\(UUID().uuidString)"
)
)
let descriptors = try snapshotDescriptors(of: execution)
let result = await execution.run(timeout: 5)
#expect(result.executionError != nil)
expectDescriptorsClosed(descriptors)
}
@Test("Abandoning an execution before launch closes every pipe")
func abandonedExecutionClosesPipes() throws {
weak var abandonedExecution: CommandExecution?
var execution: CommandExecution? = try makeExecution(executable: "/usr/bin/true")
abandonedExecution = execution
let descriptors = try snapshotDescriptors(of: #require(execution))
execution = nil
#expect(abandonedExecution == nil)
expectDescriptorsClosed(descriptors)
}
@Test("Timeout terminates the child and closes capture pipes")
func timeoutTerminatesAndClosesPipes() async throws {
let execution = try makeExecution(
executable: "/bin/sh",
arguments: ["-c", "sleep 2 &"]
)
let descriptors = try snapshotDescriptors(of: execution)
let result = await execution.run(timeout: 0.1)
#expect(result.timedOut)
expectDescriptorsClosed(descriptors)
}
@Test("Cancellation before launch closes every pipe")
func cancellationBeforeLaunchClosesPipes() async throws {
let execution = try makeExecution(executable: "/usr/bin/true")
let descriptors = try snapshotDescriptors(of: execution)
let result = await Task {
withUnsafeCurrentTask { $0?.cancel() }
return await execution.run(timeout: 5)
}.value
#expect(result.timedOut == false)
#expect(result.executionError != nil)
expectDescriptorsClosed(descriptors)
}
@Test("Task cancellation terminates the child and closes capture pipes")
func cancellationTerminatesAndClosesPipes() async throws {
let pidFile = uniqueTemporaryFile(named: "pid")
defer { try? FileManager.default.removeItem(at: pidFile) }
let execution = try makeExecution(
executable: "/bin/sh",
arguments: ["-c", "printf %s $$ > \"$1\"; exec sleep 30", "cmux-test", pidFile.path]
)
let descriptors = try snapshotDescriptors(of: execution)
let command = Task {
await execution.run(timeout: 2)
}
try await waitForFile(at: pidFile)
let pidText = try String(contentsOf: pidFile, encoding: .utf8)
let pid = try #require(pid_t(pidText))
command.cancel()
let result = await command.value
#expect(result.timedOut == false)
#expect(result.executionError != nil)
#expect(kill(pid, 0) == -1 && errno == ESRCH)
expectDescriptorsClosed(descriptors)
}
private func makeExecution(
executable: String,
arguments: [String] = [],
directory: URL? = nil
) throws -> CommandExecution {
try CommandExecution(
executableURL: URL(fileURLWithPath: executable),
arguments: arguments,
currentDirectoryURL: directory ?? tempDirectory
)
}
private func snapshotDescriptors(
of execution: CommandExecution
) throws -> [DescriptorIdentity] {
let descriptors = [
execution.stdoutPipe.pipe.fileHandleForReading.fileDescriptor,
execution.stdoutPipe.pipe.fileHandleForWriting.fileDescriptor,
execution.stderrPipe.pipe.fileHandleForReading.fileDescriptor,
execution.stderrPipe.pipe.fileHandleForWriting.fileDescriptor,
execution.cancellationSignal.readDescriptor,
execution.cancellationSignal.writeDescriptor,
execution.stdoutReadDescriptor.rawValue,
execution.stderrReadDescriptor.rawValue,
]
#expect(Set(descriptors).count == descriptors.count)
return try descriptors.map { descriptor in
var metadata = stat()
guard fstat(descriptor, &metadata) == 0 else {
throw DescriptorLifecycleTestError.snapshotFailed(descriptor)
}
return DescriptorIdentity(
fileDescriptor: descriptor,
inode: UInt64(metadata.st_ino)
)
}
}
private func expectDescriptorsClosed(_ descriptors: [DescriptorIdentity]) {
for descriptor in descriptors {
var metadata = stat()
if fstat(descriptor.fileDescriptor, &metadata) == 0 {
#expect(
UInt64(metadata.st_ino) != descriptor.inode,
"CommandRunner retained pipe descriptor \(descriptor.fileDescriptor)"
)
} else {
#expect(errno == EBADF)
}
}
}
private func uniqueTemporaryFile(named suffix: String) -> URL {
tempDirectory.appendingPathComponent(
"cmux-command-runner-\(UUID().uuidString)-\(suffix)",
isDirectory: false
)
}
private func waitForFile(at url: URL) async throws {
let clock = ContinuousClock()
let deadline = clock.now.advanced(by: .seconds(5))
while !FileManager.default.fileExists(atPath: url.path) {
guard clock.now < deadline else {
throw DescriptorLifecycleTestError.markerTimedOut
}
try await clock.sleep(for: .milliseconds(10))
}
}
private struct DescriptorIdentity {
let fileDescriptor: Int32
let inode: UInt64
}
private enum DescriptorLifecycleTestError: Error {
case markerTimedOut
case snapshotFailed(Int32)
}
}