diff --git a/GhosttyTabs.xcodeproj/project.pbxproj b/GhosttyTabs.xcodeproj/project.pbxproj index 899b5fba..0ecd97c9 100644 --- a/GhosttyTabs.xcodeproj/project.pbxproj +++ b/GhosttyTabs.xcodeproj/project.pbxproj @@ -69,6 +69,7 @@ D0E0F0B2A1B2C3D4E5F60718 /* BrowserOmnibarSuggestionsUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E0F0B3A1B2C3D4E5F60718 /* BrowserOmnibarSuggestionsUITests.swift */; }; E1000000A1B2C3D4E5F60718 /* MenuKeyEquivalentRoutingUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1000001A1B2C3D4E5F60718 /* MenuKeyEquivalentRoutingUITests.swift */; }; F1000000A1B2C3D4E5F60718 /* CmuxWebViewKeyEquivalentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1000001A1B2C3D4E5F60718 /* CmuxWebViewKeyEquivalentTests.swift */; }; + F2000000A1B2C3D4E5F60718 /* UpdatePillReleaseVisibilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2000001A1B2C3D4E5F60718 /* UpdatePillReleaseVisibilityTests.swift */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -185,6 +186,7 @@ D0E0F0B3A1B2C3D4E5F60718 /* BrowserOmnibarSuggestionsUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserOmnibarSuggestionsUITests.swift; sourceTree = ""; }; E1000001A1B2C3D4E5F60718 /* MenuKeyEquivalentRoutingUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuKeyEquivalentRoutingUITests.swift; sourceTree = ""; }; F1000001A1B2C3D4E5F60718 /* CmuxWebViewKeyEquivalentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxWebViewKeyEquivalentTests.swift; sourceTree = ""; }; + F2000001A1B2C3D4E5F60718 /* UpdatePillReleaseVisibilityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatePillReleaseVisibilityTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -384,6 +386,7 @@ isa = PBXGroup; children = ( F1000001A1B2C3D4E5F60718 /* CmuxWebViewKeyEquivalentTests.swift */, + F2000001A1B2C3D4E5F60718 /* UpdatePillReleaseVisibilityTests.swift */, ); path = GhosttyTabsTests; sourceTree = ""; @@ -578,6 +581,7 @@ buildActionMask = 2147483647; files = ( F1000000A1B2C3D4E5F60718 /* CmuxWebViewKeyEquivalentTests.swift in Sources */, + F2000000A1B2C3D4E5F60718 /* UpdatePillReleaseVisibilityTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/GhosttyTabsTests/UpdatePillReleaseVisibilityTests.swift b/GhosttyTabsTests/UpdatePillReleaseVisibilityTests.swift new file mode 100644 index 00000000..b862e931 --- /dev/null +++ b/GhosttyTabsTests/UpdatePillReleaseVisibilityTests.swift @@ -0,0 +1,67 @@ +import XCTest +import Foundation + +/// Regression test: ensures UpdatePill is never gated behind #if DEBUG in production code paths. +/// This prevents accidentally hiding the update UI in Release builds. +final class UpdatePillReleaseVisibilityTests: XCTestCase { + + /// Source files that must show UpdatePill without #if DEBUG guards. + private let filesToCheck = [ + "Sources/Update/UpdateTitlebarAccessory.swift", + "Sources/ContentView.swift", + "Sources/WindowToolbarController.swift", + ] + + func testUpdatePillNotGatedBehindDebug() throws { + let projectRoot = findProjectRoot() + + for relativePath in filesToCheck { + let url = projectRoot.appendingPathComponent(relativePath) + let source = try String(contentsOf: url, encoding: .utf8) + let lines = source.components(separatedBy: .newlines) + + // Track #if DEBUG nesting depth. + var debugDepth = 0 + + for (index, line) in lines.enumerated() { + let trimmed = line.trimmingCharacters(in: .whitespaces) + + if trimmed == "#if DEBUG" || trimmed.hasPrefix("#if DEBUG ") { + debugDepth += 1 + } else if trimmed == "#endif" && debugDepth > 0 { + debugDepth -= 1 + } else if trimmed == "#else" && debugDepth > 0 { + // #else inside #if DEBUG means we're in the non-debug branch — that's fine. + // But UpdatePill in the #if DEBUG branch (before #else) is the problem. + // We handle this by only flagging UpdatePill when debugDepth > 0 and we haven't + // hit #else yet. For simplicity, treat #else as flipping out of the guarded section. + debugDepth -= 1 + } + + if debugDepth > 0 && trimmed.contains("UpdatePill") { + XCTFail( + """ + \(relativePath):\(index + 1) — UpdatePill is inside #if DEBUG. \ + This hides the update UI in Release builds. Remove the #if DEBUG guard \ + or move UpdatePill to the #else branch. + """ + ) + } + } + } + } + + private func findProjectRoot() -> URL { + // Walk up from the test bundle to find the project root (contains GhosttyTabs.xcodeproj). + var dir = URL(fileURLWithPath: #file).deletingLastPathComponent().deletingLastPathComponent() + for _ in 0..<10 { + let marker = dir.appendingPathComponent("GhosttyTabs.xcodeproj") + if FileManager.default.fileExists(atPath: marker.path) { + return dir + } + dir = dir.deletingLastPathComponent() + } + // Fallback: assume CWD is project root. + return URL(fileURLWithPath: FileManager.default.currentDirectoryPath) + } +}