cmux/cmuxTests/SessionPersistenceTests.swift
Lawrence Chen 0a490b0e03
Fix Claude wrapper hook errors when cmux socket is stale (#868)
* Fix Claude wrapper hook injection when cmux socket is stale

* Harden socket listener lifecycle and rearm policy

* Unset CLAUDECODE in stale-socket passthrough

* Harden listener cleanup and bound claude ping probe

* Guard socket unlink during listener startup window
2026-03-04 18:52:57 -08:00

873 lines
35 KiB
Swift

import XCTest
#if canImport(cmux_DEV)
@testable import cmux_DEV
#elseif canImport(cmux)
@testable import cmux
#endif
final class SessionPersistenceTests: XCTestCase {
func testSaveAndLoadRoundTripWithCustomSnapshotPath() throws {
let tempDir = FileManager.default.temporaryDirectory
.appendingPathComponent("cmux-session-tests-\(UUID().uuidString)", isDirectory: true)
try? FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: tempDir) }
let snapshotURL = tempDir.appendingPathComponent("session.json", isDirectory: false)
let snapshot = makeSnapshot(version: SessionSnapshotSchema.currentVersion)
XCTAssertTrue(SessionPersistenceStore.save(snapshot, fileURL: snapshotURL))
let loaded = SessionPersistenceStore.load(fileURL: snapshotURL)
XCTAssertNotNil(loaded)
XCTAssertEqual(loaded?.version, SessionSnapshotSchema.currentVersion)
XCTAssertEqual(loaded?.windows.count, 1)
XCTAssertEqual(loaded?.windows.first?.sidebar.selection, .tabs)
let frame = try XCTUnwrap(loaded?.windows.first?.frame)
XCTAssertEqual(frame.x, 10, accuracy: 0.001)
XCTAssertEqual(frame.y, 20, accuracy: 0.001)
XCTAssertEqual(frame.width, 900, accuracy: 0.001)
XCTAssertEqual(frame.height, 700, accuracy: 0.001)
XCTAssertEqual(loaded?.windows.first?.display?.displayID, 42)
let visibleFrame = try XCTUnwrap(loaded?.windows.first?.display?.visibleFrame)
XCTAssertEqual(visibleFrame.y, 25, accuracy: 0.001)
}
func testSaveAndLoadRoundTripPreservesWorkspaceCustomColor() {
let tempDir = FileManager.default.temporaryDirectory
.appendingPathComponent("cmux-session-tests-\(UUID().uuidString)", isDirectory: true)
try? FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: tempDir) }
let snapshotURL = tempDir.appendingPathComponent("session.json", isDirectory: false)
var snapshot = makeSnapshot(version: SessionSnapshotSchema.currentVersion)
snapshot.windows[0].tabManager.workspaces[0].customColor = "#C0392B"
XCTAssertTrue(SessionPersistenceStore.save(snapshot, fileURL: snapshotURL))
let loaded = SessionPersistenceStore.load(fileURL: snapshotURL)
XCTAssertEqual(
loaded?.windows.first?.tabManager.workspaces.first?.customColor,
"#C0392B"
)
}
func testWorkspaceCustomColorDecodeSupportsMissingLegacyField() throws {
var snapshot = makeSnapshot(version: SessionSnapshotSchema.currentVersion)
snapshot.windows[0].tabManager.workspaces[0].customColor = nil
let encoder = JSONEncoder()
let data = try encoder.encode(snapshot)
let json = try XCTUnwrap(String(data: data, encoding: .utf8))
XCTAssertFalse(json.contains("\"customColor\""))
let decoded = try JSONDecoder().decode(AppSessionSnapshot.self, from: data)
XCTAssertNil(decoded.windows.first?.tabManager.workspaces.first?.customColor)
}
func testLoadRejectsSchemaVersionMismatch() {
let tempDir = FileManager.default.temporaryDirectory
.appendingPathComponent("cmux-session-tests-\(UUID().uuidString)", isDirectory: true)
try? FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: tempDir) }
let snapshotURL = tempDir.appendingPathComponent("session.json", isDirectory: false)
XCTAssertTrue(SessionPersistenceStore.save(makeSnapshot(version: SessionSnapshotSchema.currentVersion + 1), fileURL: snapshotURL))
XCTAssertNil(SessionPersistenceStore.load(fileURL: snapshotURL))
}
func testDefaultSnapshotPathSanitizesBundleIdentifier() {
let tempDir = FileManager.default.temporaryDirectory
.appendingPathComponent("cmux-session-tests-\(UUID().uuidString)", isDirectory: true)
try? FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: tempDir) }
let path = SessionPersistenceStore.defaultSnapshotFileURL(
bundleIdentifier: "com.example/unsafe id",
appSupportDirectory: tempDir
)
XCTAssertNotNil(path)
XCTAssertTrue(path?.path.contains("com.example_unsafe_id") == true)
}
func testRestorePolicySkipsWhenLaunchHasExplicitArguments() {
let shouldRestore = SessionRestorePolicy.shouldAttemptRestore(
arguments: ["/Applications/cmux.app/Contents/MacOS/cmux", "--window", "window:1"],
environment: [:]
)
XCTAssertFalse(shouldRestore)
}
func testRestorePolicyAllowsFinderStyleLaunchArgumentsOnly() {
let shouldRestore = SessionRestorePolicy.shouldAttemptRestore(
arguments: ["/Applications/cmux.app/Contents/MacOS/cmux", "-psn_0_12345"],
environment: [:]
)
XCTAssertTrue(shouldRestore)
}
func testRestorePolicySkipsWhenRunningUnderXCTest() {
let shouldRestore = SessionRestorePolicy.shouldAttemptRestore(
arguments: ["/Applications/cmux.app/Contents/MacOS/cmux"],
environment: ["XCTestConfigurationFilePath": "/tmp/xctest.xctestconfiguration"]
)
XCTAssertFalse(shouldRestore)
}
func testSidebarWidthSanitizationClampsToPolicyRange() {
XCTAssertEqual(
SessionPersistencePolicy.sanitizedSidebarWidth(-20),
SessionPersistencePolicy.minimumSidebarWidth,
accuracy: 0.001
)
XCTAssertEqual(
SessionPersistencePolicy.sanitizedSidebarWidth(10_000),
SessionPersistencePolicy.maximumSidebarWidth,
accuracy: 0.001
)
XCTAssertEqual(
SessionPersistencePolicy.sanitizedSidebarWidth(nil),
SessionPersistencePolicy.defaultSidebarWidth,
accuracy: 0.001
)
}
func testSessionRectSnapshotEncodesXYWidthHeightKeys() throws {
let snapshot = SessionRectSnapshot(x: 101.25, y: 202.5, width: 903.75, height: 704.5)
let data = try JSONEncoder().encode(snapshot)
let object = try XCTUnwrap(try JSONSerialization.jsonObject(with: data) as? [String: Double])
XCTAssertEqual(Set(object.keys), Set(["x", "y", "width", "height"]))
XCTAssertEqual(try XCTUnwrap(object["x"]), 101.25, accuracy: 0.001)
XCTAssertEqual(try XCTUnwrap(object["y"]), 202.5, accuracy: 0.001)
XCTAssertEqual(try XCTUnwrap(object["width"]), 903.75, accuracy: 0.001)
XCTAssertEqual(try XCTUnwrap(object["height"]), 704.5, accuracy: 0.001)
}
func testSessionBrowserPanelSnapshotHistoryRoundTrip() throws {
let source = SessionBrowserPanelSnapshot(
urlString: "https://example.com/current",
shouldRenderWebView: true,
pageZoom: 1.2,
developerToolsVisible: true,
backHistoryURLStrings: [
"https://example.com/a",
"https://example.com/b"
],
forwardHistoryURLStrings: [
"https://example.com/d"
]
)
let data = try JSONEncoder().encode(source)
let decoded = try JSONDecoder().decode(SessionBrowserPanelSnapshot.self, from: data)
XCTAssertEqual(decoded.urlString, source.urlString)
XCTAssertEqual(decoded.backHistoryURLStrings, source.backHistoryURLStrings)
XCTAssertEqual(decoded.forwardHistoryURLStrings, source.forwardHistoryURLStrings)
}
func testSessionBrowserPanelSnapshotHistoryDecodesWhenKeysAreMissing() throws {
let json = """
{
"urlString": "https://example.com/current",
"shouldRenderWebView": true,
"pageZoom": 1.0,
"developerToolsVisible": false
}
""".data(using: .utf8)!
let decoded = try JSONDecoder().decode(SessionBrowserPanelSnapshot.self, from: json)
XCTAssertEqual(decoded.urlString, "https://example.com/current")
XCTAssertNil(decoded.backHistoryURLStrings)
XCTAssertNil(decoded.forwardHistoryURLStrings)
}
func testScrollbackReplayEnvironmentWritesReplayFile() {
let tempDir = FileManager.default.temporaryDirectory
.appendingPathComponent("cmux-scrollback-replay-\(UUID().uuidString)", isDirectory: true)
try? FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: tempDir) }
let environment = SessionScrollbackReplayStore.replayEnvironment(
for: "line one\nline two\n",
tempDirectory: tempDir
)
let path = environment[SessionScrollbackReplayStore.environmentKey]
XCTAssertNotNil(path)
XCTAssertTrue(path?.hasPrefix(tempDir.path) == true)
guard let path else { return }
let contents = try? String(contentsOfFile: path, encoding: .utf8)
XCTAssertEqual(contents, "line one\nline two\n")
}
func testScrollbackReplayEnvironmentSkipsWhitespaceOnlyContent() {
let tempDir = FileManager.default.temporaryDirectory
.appendingPathComponent("cmux-scrollback-replay-\(UUID().uuidString)", isDirectory: true)
try? FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: tempDir) }
let environment = SessionScrollbackReplayStore.replayEnvironment(
for: " \n\t ",
tempDirectory: tempDir
)
XCTAssertTrue(environment.isEmpty)
}
func testScrollbackReplayEnvironmentPreservesANSIColorSequences() {
let tempDir = FileManager.default.temporaryDirectory
.appendingPathComponent("cmux-scrollback-replay-\(UUID().uuidString)", isDirectory: true)
try? FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: tempDir) }
let red = "\u{001B}[31m"
let reset = "\u{001B}[0m"
let source = "\(red)RED\(reset)\n"
let environment = SessionScrollbackReplayStore.replayEnvironment(
for: source,
tempDirectory: tempDir
)
guard let path = environment[SessionScrollbackReplayStore.environmentKey] else {
XCTFail("Expected replay file path")
return
}
guard let contents = try? String(contentsOfFile: path, encoding: .utf8) else {
XCTFail("Expected replay file contents")
return
}
XCTAssertTrue(contents.contains("\(red)RED\(reset)"))
XCTAssertTrue(contents.hasPrefix(reset))
XCTAssertTrue(contents.hasSuffix(reset))
}
func testTruncatedScrollbackAvoidsLeadingPartialANSICSISequence() {
let maxChars = SessionPersistencePolicy.maxScrollbackCharactersPerTerminal
let source = "\u{001B}[31m"
+ String(repeating: "X", count: maxChars - 7)
+ "\u{001B}[0m"
guard let truncated = SessionPersistencePolicy.truncatedScrollback(source) else {
XCTFail("Expected truncated scrollback")
return
}
XCTAssertFalse(truncated.hasPrefix("31m"))
XCTAssertFalse(truncated.hasPrefix("[31m"))
XCTAssertFalse(truncated.hasPrefix("m"))
}
func testNormalizedExportedScreenPathAcceptsAbsoluteAndFileURL() {
XCTAssertEqual(
TerminalController.normalizedExportedScreenPath("/tmp/cmux-screen.txt"),
"/tmp/cmux-screen.txt"
)
XCTAssertEqual(
TerminalController.normalizedExportedScreenPath(" file:///tmp/cmux-screen.txt "),
"/tmp/cmux-screen.txt"
)
}
func testNormalizedExportedScreenPathRejectsRelativeAndWhitespace() {
XCTAssertNil(TerminalController.normalizedExportedScreenPath("relative/path.txt"))
XCTAssertNil(TerminalController.normalizedExportedScreenPath(" "))
XCTAssertNil(TerminalController.normalizedExportedScreenPath(nil))
}
func testShouldRemoveExportedScreenDirectoryOnlyWithinTemporaryRoot() {
let tempRoot = URL(fileURLWithPath: "/tmp")
.appendingPathComponent("cmux-export-tests-\(UUID().uuidString)", isDirectory: true)
let tempFile = tempRoot
.appendingPathComponent(UUID().uuidString, isDirectory: true)
.appendingPathComponent("screen.txt", isDirectory: false)
let outsideFile = URL(fileURLWithPath: "/Users/example/screen.txt")
XCTAssertTrue(
TerminalController.shouldRemoveExportedScreenDirectory(
fileURL: tempFile,
temporaryDirectory: tempRoot
)
)
XCTAssertFalse(
TerminalController.shouldRemoveExportedScreenDirectory(
fileURL: outsideFile,
temporaryDirectory: tempRoot
)
)
}
func testShouldRemoveExportedScreenFileOnlyWithinTemporaryRoot() {
let tempRoot = URL(fileURLWithPath: "/tmp")
.appendingPathComponent("cmux-export-tests-\(UUID().uuidString)", isDirectory: true)
let tempFile = tempRoot
.appendingPathComponent(UUID().uuidString, isDirectory: true)
.appendingPathComponent("screen.txt", isDirectory: false)
let outsideFile = URL(fileURLWithPath: "/Users/example/screen.txt")
XCTAssertTrue(
TerminalController.shouldRemoveExportedScreenFile(
fileURL: tempFile,
temporaryDirectory: tempRoot
)
)
XCTAssertFalse(
TerminalController.shouldRemoveExportedScreenFile(
fileURL: outsideFile,
temporaryDirectory: tempRoot
)
)
}
func testWindowUnregisterSnapshotPersistencePolicy() {
XCTAssertTrue(
AppDelegate.shouldPersistSnapshotOnWindowUnregister(isTerminatingApp: false)
)
XCTAssertFalse(
AppDelegate.shouldPersistSnapshotOnWindowUnregister(isTerminatingApp: true)
)
XCTAssertTrue(
AppDelegate.shouldRemoveSnapshotWhenNoWindowsRemainOnWindowUnregister(isTerminatingApp: false)
)
XCTAssertFalse(
AppDelegate.shouldRemoveSnapshotWhenNoWindowsRemainOnWindowUnregister(isTerminatingApp: true)
)
}
func testShouldSkipSessionSaveDuringStartupRestorePolicy() {
XCTAssertTrue(
AppDelegate.shouldSkipSessionSaveDuringStartupRestore(
isApplyingStartupSessionRestore: true,
includeScrollback: false
)
)
XCTAssertFalse(
AppDelegate.shouldSkipSessionSaveDuringStartupRestore(
isApplyingStartupSessionRestore: true,
includeScrollback: true
)
)
XCTAssertFalse(
AppDelegate.shouldSkipSessionSaveDuringStartupRestore(
isApplyingStartupSessionRestore: false,
includeScrollback: false
)
)
}
func testSessionAutosaveTickPolicySkipsWhenTerminating() {
XCTAssertTrue(
AppDelegate.shouldRunSessionAutosaveTick(isTerminatingApp: false)
)
XCTAssertFalse(
AppDelegate.shouldRunSessionAutosaveTick(isTerminatingApp: true)
)
}
func testSessionSnapshotSynchronousWritePolicy() {
XCTAssertFalse(
AppDelegate.shouldWriteSessionSnapshotSynchronously(
isTerminatingApp: false,
includeScrollback: false
)
)
XCTAssertFalse(
AppDelegate.shouldWriteSessionSnapshotSynchronously(
isTerminatingApp: false,
includeScrollback: true
)
)
XCTAssertFalse(
AppDelegate.shouldWriteSessionSnapshotSynchronously(
isTerminatingApp: true,
includeScrollback: false
)
)
XCTAssertTrue(
AppDelegate.shouldWriteSessionSnapshotSynchronously(
isTerminatingApp: true,
includeScrollback: true
)
)
}
func testUnchangedAutosaveFingerprintSkipsWithinStalenessWindow() {
let now = Date()
XCTAssertTrue(
AppDelegate.shouldSkipSessionAutosaveForUnchangedFingerprint(
isTerminatingApp: false,
includeScrollback: false,
previousFingerprint: 1234,
currentFingerprint: 1234,
lastPersistedAt: now.addingTimeInterval(-5),
now: now,
maximumAutosaveSkippableInterval: 60
)
)
}
func testUnchangedAutosaveFingerprintDoesNotSkipAfterStalenessWindow() {
let now = Date()
XCTAssertFalse(
AppDelegate.shouldSkipSessionAutosaveForUnchangedFingerprint(
isTerminatingApp: false,
includeScrollback: false,
previousFingerprint: 1234,
currentFingerprint: 1234,
lastPersistedAt: now.addingTimeInterval(-120),
now: now,
maximumAutosaveSkippableInterval: 60
)
)
}
func testUnchangedAutosaveFingerprintNeverSkipsTerminatingOrScrollbackWrites() {
let now = Date()
XCTAssertFalse(
AppDelegate.shouldSkipSessionAutosaveForUnchangedFingerprint(
isTerminatingApp: true,
includeScrollback: false,
previousFingerprint: 1234,
currentFingerprint: 1234,
lastPersistedAt: now.addingTimeInterval(-1),
now: now
)
)
XCTAssertFalse(
AppDelegate.shouldSkipSessionAutosaveForUnchangedFingerprint(
isTerminatingApp: false,
includeScrollback: true,
previousFingerprint: 1234,
currentFingerprint: 1234,
lastPersistedAt: now.addingTimeInterval(-1),
now: now
)
)
}
func testResolvedWindowFramePrefersSavedDisplayIdentity() {
let savedFrame = SessionRectSnapshot(x: 1_200, y: 100, width: 600, height: 400)
let savedDisplay = SessionDisplaySnapshot(
displayID: 2,
frame: SessionRectSnapshot(x: 1_000, y: 0, width: 1_000, height: 800),
visibleFrame: SessionRectSnapshot(x: 1_000, y: 0, width: 1_000, height: 800)
)
// Display 1 and 2 swapped horizontal positions between snapshot and restore.
let display1 = AppDelegate.SessionDisplayGeometry(
displayID: 1,
frame: CGRect(x: 1_000, y: 0, width: 1_000, height: 800),
visibleFrame: CGRect(x: 1_000, y: 0, width: 1_000, height: 800)
)
let display2 = AppDelegate.SessionDisplayGeometry(
displayID: 2,
frame: CGRect(x: 0, y: 0, width: 1_000, height: 800),
visibleFrame: CGRect(x: 0, y: 0, width: 1_000, height: 800)
)
let restored = AppDelegate.resolvedWindowFrame(
from: savedFrame,
display: savedDisplay,
availableDisplays: [display1, display2],
fallbackDisplay: display1
)
XCTAssertNotNil(restored)
guard let restored else { return }
XCTAssertTrue(display2.visibleFrame.intersects(restored))
XCTAssertFalse(display1.visibleFrame.intersects(restored))
XCTAssertEqual(restored.width, 600, accuracy: 0.001)
XCTAssertEqual(restored.height, 400, accuracy: 0.001)
XCTAssertEqual(restored.minX, 200, accuracy: 0.001)
XCTAssertEqual(restored.minY, 100, accuracy: 0.001)
}
func testResolvedWindowFrameKeepsIntersectingFrameWithoutDisplayMetadata() {
let savedFrame = SessionRectSnapshot(x: 120, y: 80, width: 500, height: 350)
let display = AppDelegate.SessionDisplayGeometry(
displayID: 1,
frame: CGRect(x: 0, y: 0, width: 1_000, height: 800),
visibleFrame: CGRect(x: 0, y: 0, width: 1_000, height: 800)
)
let restored = AppDelegate.resolvedWindowFrame(
from: savedFrame,
display: nil,
availableDisplays: [display],
fallbackDisplay: display
)
XCTAssertNotNil(restored)
guard let restored else { return }
XCTAssertEqual(restored.minX, 120, accuracy: 0.001)
XCTAssertEqual(restored.minY, 80, accuracy: 0.001)
XCTAssertEqual(restored.width, 500, accuracy: 0.001)
XCTAssertEqual(restored.height, 350, accuracy: 0.001)
}
func testResolvedStartupPrimaryWindowFrameFallsBackToPersistedGeometryWhenPrimaryMissing() {
let fallbackFrame = SessionRectSnapshot(x: 180, y: 140, width: 900, height: 640)
let fallbackDisplay = SessionDisplaySnapshot(
displayID: 1,
frame: SessionRectSnapshot(x: 0, y: 0, width: 1_600, height: 1_000),
visibleFrame: SessionRectSnapshot(x: 0, y: 0, width: 1_600, height: 1_000)
)
let display = AppDelegate.SessionDisplayGeometry(
displayID: 1,
frame: CGRect(x: 0, y: 0, width: 1_600, height: 1_000),
visibleFrame: CGRect(x: 0, y: 0, width: 1_600, height: 1_000)
)
let restored = AppDelegate.resolvedStartupPrimaryWindowFrame(
primarySnapshot: nil,
fallbackFrame: fallbackFrame,
fallbackDisplaySnapshot: fallbackDisplay,
availableDisplays: [display],
fallbackDisplay: display
)
XCTAssertNotNil(restored)
guard let restored else { return }
XCTAssertEqual(restored.minX, 180, accuracy: 0.001)
XCTAssertEqual(restored.minY, 140, accuracy: 0.001)
XCTAssertEqual(restored.width, 900, accuracy: 0.001)
XCTAssertEqual(restored.height, 640, accuracy: 0.001)
}
func testResolvedStartupPrimaryWindowFramePrefersPrimarySnapshotOverFallback() {
let primarySnapshot = SessionWindowSnapshot(
frame: SessionRectSnapshot(x: 220, y: 160, width: 980, height: 700),
display: SessionDisplaySnapshot(
displayID: 1,
frame: SessionRectSnapshot(x: 0, y: 0, width: 1_600, height: 1_000),
visibleFrame: SessionRectSnapshot(x: 0, y: 0, width: 1_600, height: 1_000)
),
tabManager: SessionTabManagerSnapshot(selectedWorkspaceIndex: nil, workspaces: []),
sidebar: SessionSidebarSnapshot(isVisible: true, selection: .tabs, width: 220)
)
let fallbackFrame = SessionRectSnapshot(x: 40, y: 30, width: 700, height: 500)
let fallbackDisplay = SessionDisplaySnapshot(
displayID: 1,
frame: SessionRectSnapshot(x: 0, y: 0, width: 1_600, height: 1_000),
visibleFrame: SessionRectSnapshot(x: 0, y: 0, width: 1_600, height: 1_000)
)
let display = AppDelegate.SessionDisplayGeometry(
displayID: 1,
frame: CGRect(x: 0, y: 0, width: 1_600, height: 1_000),
visibleFrame: CGRect(x: 0, y: 0, width: 1_600, height: 1_000)
)
let restored = AppDelegate.resolvedStartupPrimaryWindowFrame(
primarySnapshot: primarySnapshot,
fallbackFrame: fallbackFrame,
fallbackDisplaySnapshot: fallbackDisplay,
availableDisplays: [display],
fallbackDisplay: display
)
XCTAssertNotNil(restored)
guard let restored else { return }
XCTAssertEqual(restored.minX, 220, accuracy: 0.001)
XCTAssertEqual(restored.minY, 160, accuracy: 0.001)
XCTAssertEqual(restored.width, 980, accuracy: 0.001)
XCTAssertEqual(restored.height, 700, accuracy: 0.001)
}
func testResolvedWindowFrameCentersInFallbackDisplayWhenOffscreen() {
let savedFrame = SessionRectSnapshot(x: 4_000, y: 4_000, width: 900, height: 700)
let display = AppDelegate.SessionDisplayGeometry(
displayID: 1,
frame: CGRect(x: 0, y: 0, width: 1_000, height: 800),
visibleFrame: CGRect(x: 0, y: 0, width: 1_000, height: 800)
)
let restored = AppDelegate.resolvedWindowFrame(
from: savedFrame,
display: nil,
availableDisplays: [display],
fallbackDisplay: display
)
XCTAssertNotNil(restored)
guard let restored else { return }
XCTAssertTrue(display.visibleFrame.contains(restored))
XCTAssertEqual(restored.minX, 50, accuracy: 0.001)
XCTAssertEqual(restored.minY, 50, accuracy: 0.001)
XCTAssertEqual(restored.width, 900, accuracy: 0.001)
XCTAssertEqual(restored.height, 700, accuracy: 0.001)
}
func testResolvedWindowFramePreservesExactGeometryWhenDisplayIsUnchanged() {
let savedFrame = SessionRectSnapshot(x: 1_303, y: -90, width: 1_280, height: 1_410)
let savedDisplay = SessionDisplaySnapshot(
displayID: 2,
frame: SessionRectSnapshot(x: 0, y: 0, width: 2_560, height: 1_440),
visibleFrame: SessionRectSnapshot(x: 0, y: 0, width: 2_560, height: 1_410)
)
let display = AppDelegate.SessionDisplayGeometry(
displayID: 2,
frame: CGRect(x: 0, y: 0, width: 2_560, height: 1_440),
visibleFrame: CGRect(x: 0, y: 0, width: 2_560, height: 1_410)
)
let restored = AppDelegate.resolvedWindowFrame(
from: savedFrame,
display: savedDisplay,
availableDisplays: [display],
fallbackDisplay: display
)
XCTAssertNotNil(restored)
guard let restored else { return }
XCTAssertEqual(restored.minX, 1_303, accuracy: 0.001)
XCTAssertEqual(restored.minY, -90, accuracy: 0.001)
XCTAssertEqual(restored.width, 1_280, accuracy: 0.001)
XCTAssertEqual(restored.height, 1_410, accuracy: 0.001)
}
func testResolvedWindowFrameClampsWhenDisplayGeometryChangesEvenWithSameDisplayID() {
let savedFrame = SessionRectSnapshot(x: 1_303, y: -90, width: 1_280, height: 1_410)
let savedDisplay = SessionDisplaySnapshot(
displayID: 2,
frame: SessionRectSnapshot(x: 0, y: 0, width: 2_560, height: 1_440),
visibleFrame: SessionRectSnapshot(x: 0, y: 0, width: 2_560, height: 1_410)
)
let resizedDisplay = AppDelegate.SessionDisplayGeometry(
displayID: 2,
frame: CGRect(x: 0, y: 0, width: 1_920, height: 1_080),
visibleFrame: CGRect(x: 0, y: 0, width: 1_920, height: 1_050)
)
let restored = AppDelegate.resolvedWindowFrame(
from: savedFrame,
display: savedDisplay,
availableDisplays: [resizedDisplay],
fallbackDisplay: resizedDisplay
)
XCTAssertNotNil(restored)
guard let restored else { return }
XCTAssertTrue(resizedDisplay.visibleFrame.contains(restored))
XCTAssertNotEqual(restored.minX, 1_303, "Changed display geometry should clamp/remap frame")
XCTAssertNotEqual(restored.minY, -90, "Changed display geometry should clamp/remap frame")
}
func testResolvedSnapshotTerminalScrollbackPrefersCaptured() {
let resolved = Workspace.resolvedSnapshotTerminalScrollback(
capturedScrollback: "captured-value",
fallbackScrollback: "fallback-value"
)
XCTAssertEqual(resolved, "captured-value")
}
func testResolvedSnapshotTerminalScrollbackFallsBackWhenCaptureMissing() {
let resolved = Workspace.resolvedSnapshotTerminalScrollback(
capturedScrollback: nil,
fallbackScrollback: "fallback-value"
)
XCTAssertEqual(resolved, "fallback-value")
}
func testResolvedSnapshotTerminalScrollbackTruncatesFallback() {
let oversizedFallback = String(
repeating: "x",
count: SessionPersistencePolicy.maxScrollbackCharactersPerTerminal + 37
)
let resolved = Workspace.resolvedSnapshotTerminalScrollback(
capturedScrollback: nil,
fallbackScrollback: oversizedFallback
)
XCTAssertEqual(
resolved?.count,
SessionPersistencePolicy.maxScrollbackCharactersPerTerminal
)
}
private func makeSnapshot(version: Int) -> AppSessionSnapshot {
let workspace = SessionWorkspaceSnapshot(
processTitle: "Terminal",
customTitle: "Restored",
customColor: nil,
isPinned: true,
currentDirectory: "/tmp",
focusedPanelId: nil,
layout: .pane(SessionPaneLayoutSnapshot(panelIds: [], selectedPanelId: nil)),
panels: [],
statusEntries: [],
logEntries: [],
progress: nil,
gitBranch: nil
)
let tabManager = SessionTabManagerSnapshot(
selectedWorkspaceIndex: 0,
workspaces: [workspace]
)
let window = SessionWindowSnapshot(
frame: SessionRectSnapshot(x: 10, y: 20, width: 900, height: 700),
display: SessionDisplaySnapshot(
displayID: 42,
frame: SessionRectSnapshot(x: 0, y: 0, width: 1920, height: 1200),
visibleFrame: SessionRectSnapshot(x: 0, y: 25, width: 1920, height: 1175)
),
tabManager: tabManager,
sidebar: SessionSidebarSnapshot(isVisible: true, selection: .tabs, width: 240)
)
return AppSessionSnapshot(
version: version,
createdAt: Date().timeIntervalSince1970,
windows: [window]
)
}
}
final class SocketListenerAcceptPolicyTests: XCTestCase {
func testAcceptErrorClassificationBucketsExpectedErrnos() {
XCTAssertEqual(
TerminalController.acceptErrorClassification(errnoCode: EINTR),
"immediate_retry"
)
XCTAssertEqual(
TerminalController.acceptErrorClassification(errnoCode: ECONNABORTED),
"immediate_retry"
)
XCTAssertEqual(
TerminalController.acceptErrorClassification(errnoCode: EMFILE),
"resource_pressure"
)
XCTAssertEqual(
TerminalController.acceptErrorClassification(errnoCode: ENOMEM),
"resource_pressure"
)
XCTAssertEqual(
TerminalController.acceptErrorClassification(errnoCode: EBADF),
"fatal"
)
XCTAssertEqual(
TerminalController.acceptErrorClassification(errnoCode: EINVAL),
"fatal"
)
}
func testAcceptErrorPolicySignalsRearmOnlyForFatalErrors() {
XCTAssertTrue(TerminalController.shouldRearmListenerForAcceptError(errnoCode: EBADF))
XCTAssertTrue(TerminalController.shouldRearmListenerForAcceptError(errnoCode: ENOTSOCK))
XCTAssertFalse(TerminalController.shouldRearmListenerForAcceptError(errnoCode: EMFILE))
XCTAssertFalse(TerminalController.shouldRearmListenerForAcceptError(errnoCode: EINTR))
}
func testAcceptErrorPolicyRearmsAfterPersistentFailures() {
XCTAssertFalse(TerminalController.shouldRearmForConsecutiveAcceptFailures(consecutiveFailures: 0))
XCTAssertFalse(TerminalController.shouldRearmForConsecutiveAcceptFailures(consecutiveFailures: 49))
XCTAssertTrue(TerminalController.shouldRearmForConsecutiveAcceptFailures(consecutiveFailures: 50))
XCTAssertTrue(TerminalController.shouldRearmForConsecutiveAcceptFailures(consecutiveFailures: 120))
}
func testAcceptFailureBackoffIsExponentialAndCapped() {
XCTAssertEqual(
TerminalController.acceptFailureBackoffMilliseconds(consecutiveFailures: 0),
0
)
XCTAssertEqual(
TerminalController.acceptFailureBackoffMilliseconds(consecutiveFailures: 1),
10
)
XCTAssertEqual(
TerminalController.acceptFailureBackoffMilliseconds(consecutiveFailures: 2),
20
)
XCTAssertEqual(
TerminalController.acceptFailureBackoffMilliseconds(consecutiveFailures: 6),
320
)
XCTAssertEqual(
TerminalController.acceptFailureBackoffMilliseconds(consecutiveFailures: 12),
5_000
)
XCTAssertEqual(
TerminalController.acceptFailureBackoffMilliseconds(consecutiveFailures: 50),
5_000
)
}
func testAcceptFailureRearmDelayAppliesMinimumThrottle() {
XCTAssertEqual(
TerminalController.acceptFailureRearmDelayMilliseconds(consecutiveFailures: 0),
100
)
XCTAssertEqual(
TerminalController.acceptFailureRearmDelayMilliseconds(consecutiveFailures: 1),
100
)
XCTAssertEqual(
TerminalController.acceptFailureRearmDelayMilliseconds(consecutiveFailures: 2),
100
)
XCTAssertEqual(
TerminalController.acceptFailureRearmDelayMilliseconds(consecutiveFailures: 6),
320
)
XCTAssertEqual(
TerminalController.acceptFailureRearmDelayMilliseconds(consecutiveFailures: 12),
5_000
)
}
func testAcceptFailureBreadcrumbSamplingPrefersEarlyAndPowerOfTwoMilestones() {
XCTAssertTrue(TerminalController.shouldEmitAcceptFailureBreadcrumb(consecutiveFailures: 1))
XCTAssertTrue(TerminalController.shouldEmitAcceptFailureBreadcrumb(consecutiveFailures: 2))
XCTAssertTrue(TerminalController.shouldEmitAcceptFailureBreadcrumb(consecutiveFailures: 3))
XCTAssertFalse(TerminalController.shouldEmitAcceptFailureBreadcrumb(consecutiveFailures: 5))
XCTAssertTrue(TerminalController.shouldEmitAcceptFailureBreadcrumb(consecutiveFailures: 8))
XCTAssertFalse(TerminalController.shouldEmitAcceptFailureBreadcrumb(consecutiveFailures: 9))
XCTAssertTrue(TerminalController.shouldEmitAcceptFailureBreadcrumb(consecutiveFailures: 16))
}
func testAcceptLoopCleanupUnlinkPolicySkipsDuringListenerStartup() {
XCTAssertFalse(
TerminalController.shouldUnlinkSocketPathAfterAcceptLoopCleanup(
pathMatches: true,
isRunning: false,
activeGeneration: 0,
listenerStartInProgress: true
)
)
XCTAssertFalse(
TerminalController.shouldUnlinkSocketPathAfterAcceptLoopCleanup(
pathMatches: false,
isRunning: false,
activeGeneration: 0,
listenerStartInProgress: false
)
)
XCTAssertFalse(
TerminalController.shouldUnlinkSocketPathAfterAcceptLoopCleanup(
pathMatches: true,
isRunning: true,
activeGeneration: 7,
listenerStartInProgress: false
)
)
XCTAssertTrue(
TerminalController.shouldUnlinkSocketPathAfterAcceptLoopCleanup(
pathMatches: true,
isRunning: false,
activeGeneration: 0,
listenerStartInProgress: false
)
)
}
}