Remove source-shape regression tests
This commit is contained in:
parent
c24ca32614
commit
8b65627750
29 changed files with 0 additions and 3402 deletions
|
|
@ -939,52 +939,6 @@ final class RecentlyClosedBrowserStackTests: XCTestCase {
|
|||
}
|
||||
}
|
||||
|
||||
final class TabManagerNotificationOrderingSourceTests: XCTestCase {
|
||||
func testGhosttyDidSetTitleObserverDoesNotHopThroughTask() throws {
|
||||
let projectRoot = findProjectRoot()
|
||||
let tabManagerURL = projectRoot.appendingPathComponent("Sources/TabManager.swift")
|
||||
let source = try String(contentsOf: tabManagerURL, encoding: .utf8)
|
||||
|
||||
guard let titleObserverStart = source.range(of: "forName: .ghosttyDidSetTitle"),
|
||||
let focusObserverStart = source.range(
|
||||
of: "forName: .ghosttyDidFocusSurface",
|
||||
range: titleObserverStart.upperBound..<source.endIndex
|
||||
) else {
|
||||
XCTFail("Failed to locate TabManager notification observer block in Sources/TabManager.swift")
|
||||
return
|
||||
}
|
||||
|
||||
let block = String(source[titleObserverStart.lowerBound..<focusObserverStart.lowerBound])
|
||||
XCTAssertFalse(
|
||||
block.contains("Task {"),
|
||||
"""
|
||||
The .ghosttyDidSetTitle observer must update model state in the notification callback.
|
||||
Using Task can reorder updates and leave titlebar/toolbar one event behind.
|
||||
"""
|
||||
)
|
||||
XCTAssertTrue(
|
||||
block.contains("MainActor.assumeIsolated"),
|
||||
"Expected .ghosttyDidSetTitle observer to run synchronously on MainActor."
|
||||
)
|
||||
XCTAssertTrue(
|
||||
block.contains("enqueuePanelTitleUpdate"),
|
||||
"Expected .ghosttyDidSetTitle observer to enqueue panel title updates."
|
||||
)
|
||||
}
|
||||
|
||||
private func findProjectRoot() -> URL {
|
||||
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()
|
||||
}
|
||||
return URL(fileURLWithPath: FileManager.default.currentDirectoryPath)
|
||||
}
|
||||
}
|
||||
|
||||
final class SocketControlSettingsTests: XCTestCase {
|
||||
func testMigrateModeSupportsExpandedSocketModes() {
|
||||
XCTAssertEqual(SocketControlSettings.migrateMode("off"), .off)
|
||||
|
|
|
|||
|
|
@ -8,163 +8,6 @@ import AppKit
|
|||
@testable import cmux
|
||||
#endif
|
||||
|
||||
/// 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",
|
||||
]
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
/// Regression test: ensure WKWebView can load HTTP development URLs (e.g. *.localtest.me).
|
||||
final class AppTransportSecurityTests: XCTestCase {
|
||||
func testInfoPlistAllowsArbitraryLoadsInWebContent() throws {
|
||||
let projectRoot = findProjectRoot()
|
||||
let infoPlistURL = projectRoot.appendingPathComponent("Resources/Info.plist")
|
||||
let data = try Data(contentsOf: infoPlistURL)
|
||||
var format = PropertyListSerialization.PropertyListFormat.xml
|
||||
let plist = try XCTUnwrap(
|
||||
PropertyListSerialization.propertyList(from: data, options: [], format: &format) as? [String: Any]
|
||||
)
|
||||
let ats = try XCTUnwrap(plist["NSAppTransportSecurity"] as? [String: Any])
|
||||
XCTAssertEqual(
|
||||
ats["NSAllowsArbitraryLoadsInWebContent"] as? Bool,
|
||||
true,
|
||||
"Resources/Info.plist must allow HTTP loads in WKWebView for local dev hostnames."
|
||||
)
|
||||
}
|
||||
|
||||
private func findProjectRoot() -> URL {
|
||||
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()
|
||||
}
|
||||
return URL(fileURLWithPath: FileManager.default.currentDirectoryPath)
|
||||
}
|
||||
}
|
||||
|
||||
final class AppBrowserURLSchemeTests: XCTestCase {
|
||||
func testInfoPlistRegistersHTTPAndHTTPSAsHandledSchemes() throws {
|
||||
let projectRoot = findProjectRoot()
|
||||
let infoPlistURL = projectRoot.appendingPathComponent("Resources/Info.plist")
|
||||
let data = try Data(contentsOf: infoPlistURL)
|
||||
var format = PropertyListSerialization.PropertyListFormat.xml
|
||||
let plist = try XCTUnwrap(
|
||||
PropertyListSerialization.propertyList(from: data, options: [], format: &format) as? [String: Any]
|
||||
)
|
||||
let urlTypes = try XCTUnwrap(plist["CFBundleURLTypes"] as? [[String: Any]])
|
||||
|
||||
let schemes = Set(
|
||||
urlTypes
|
||||
.flatMap { $0["CFBundleURLSchemes"] as? [String] ?? [] }
|
||||
.map { $0.lowercased() }
|
||||
)
|
||||
|
||||
XCTAssertTrue(
|
||||
schemes.contains("http"),
|
||||
"Resources/Info.plist must register the http URL scheme so macOS can treat cmux as a browser candidate."
|
||||
)
|
||||
XCTAssertTrue(
|
||||
schemes.contains("https"),
|
||||
"Resources/Info.plist must register the https URL scheme so macOS can treat cmux as a browser candidate."
|
||||
)
|
||||
|
||||
let browserURLTypes = urlTypes.filter { urlType in
|
||||
let urlSchemes = Set((urlType["CFBundleURLSchemes"] as? [String] ?? []).map { $0.lowercased() })
|
||||
return !urlSchemes.isDisjoint(with: ["http", "https"])
|
||||
}
|
||||
XCTAssertFalse(
|
||||
browserURLTypes.isEmpty,
|
||||
"Resources/Info.plist must include a browser URL type entry for http and https."
|
||||
)
|
||||
|
||||
for urlType in browserURLTypes {
|
||||
XCTAssertEqual(
|
||||
urlType["CFBundleTypeRole"] as? String,
|
||||
"Viewer",
|
||||
"Browser URL schemes should be declared with the Viewer role."
|
||||
)
|
||||
XCTAssertEqual(
|
||||
urlType["LSHandlerRank"] as? String,
|
||||
"Default",
|
||||
"Browser URL schemes should advertise a default-handler rank."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func findProjectRoot() -> URL {
|
||||
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()
|
||||
}
|
||||
return URL(fileURLWithPath: FileManager.default.currentDirectoryPath)
|
||||
}
|
||||
}
|
||||
|
||||
final class BrowserInsecureHTTPSettingsTests: XCTestCase {
|
||||
func testDefaultAllowlistPatternsArePresent() {
|
||||
XCTAssertEqual(
|
||||
|
|
@ -334,45 +177,3 @@ final class TitlebarControlsHoverPolicyTests: XCTestCase {
|
|||
XCTAssertFalse(titlebarControlsShouldTrackButtonHover(config: TitlebarControlsStyle.softButtons.config))
|
||||
}
|
||||
}
|
||||
|
||||
/// Regression test: ensure new terminal windows are born in full-size content mode so
|
||||
/// titlebar/content offsets are correct before the first resize.
|
||||
final class MainWindowLayoutStyleTests: XCTestCase {
|
||||
func testCreateMainWindowUsesFullSizeContentViewStyleMask() throws {
|
||||
let projectRoot = findProjectRoot()
|
||||
let appDelegateURL = projectRoot.appendingPathComponent("Sources/AppDelegate.swift")
|
||||
let source = try String(contentsOf: appDelegateURL, encoding: .utf8)
|
||||
|
||||
guard let start = source.range(of: "func createMainWindow("),
|
||||
let end = source.range(of: "@objc func checkForUpdates", range: start.upperBound..<source.endIndex) else {
|
||||
XCTFail("Could not locate createMainWindow block in Sources/AppDelegate.swift")
|
||||
return
|
||||
}
|
||||
|
||||
let block = String(source[start.lowerBound..<end.lowerBound])
|
||||
let regex = try NSRegularExpression(
|
||||
pattern: #"styleMask:\s*\[[^\]]*\.fullSizeContentView"#,
|
||||
options: [.dotMatchesLineSeparators]
|
||||
)
|
||||
let range = NSRange(block.startIndex..<block.endIndex, in: block)
|
||||
XCTAssertNotNil(
|
||||
regex.firstMatch(in: block, options: [], range: range),
|
||||
"""
|
||||
createMainWindow must include `.fullSizeContentView` in the NSWindow style mask.
|
||||
Without it, initial titlebar/content offsets can be wrong until a manual resize.
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
private func findProjectRoot() -> URL {
|
||||
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()
|
||||
}
|
||||
return URL(fileURLWithPath: FileManager.default.currentDirectoryPath)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,126 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Static regression guards for browser chrome contrast in mixed theme setups."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def repo_root() -> Path:
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "--show-toplevel"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return Path(result.stdout.strip())
|
||||
return Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def extract_block(source: str, signature: str) -> str:
|
||||
start = source.find(signature)
|
||||
if start < 0:
|
||||
raise ValueError(f"Missing signature: {signature}")
|
||||
|
||||
brace_start = source.find("{", start)
|
||||
if brace_start < 0:
|
||||
raise ValueError(f"Missing opening brace for: {signature}")
|
||||
|
||||
depth = 0
|
||||
for idx in range(brace_start, len(source)):
|
||||
char = source[idx]
|
||||
if char == "{":
|
||||
depth += 1
|
||||
elif char == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return source[brace_start : idx + 1]
|
||||
|
||||
raise ValueError(f"Unbalanced braces for: {signature}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
root = repo_root()
|
||||
view_path = root / "Sources" / "Panels" / "BrowserPanelView.swift"
|
||||
source = view_path.read_text(encoding="utf-8")
|
||||
failures: list[str] = []
|
||||
|
||||
try:
|
||||
browser_panel_view_block = extract_block(source, "struct BrowserPanelView: View")
|
||||
except ValueError as error:
|
||||
failures.append(str(error))
|
||||
browser_panel_view_block = ""
|
||||
|
||||
try:
|
||||
resolver_block = extract_block(source, "func resolvedBrowserChromeColorScheme(")
|
||||
except ValueError as error:
|
||||
failures.append(str(error))
|
||||
resolver_block = ""
|
||||
|
||||
if resolver_block:
|
||||
if "backgroundColor.isLightColor ? .light : .dark" not in resolver_block:
|
||||
failures.append(
|
||||
"resolvedBrowserChromeColorScheme must map luminance to a light/dark ColorScheme"
|
||||
)
|
||||
|
||||
try:
|
||||
chrome_scheme_block = extract_block(
|
||||
browser_panel_view_block,
|
||||
"private var browserChromeColorScheme: ColorScheme",
|
||||
)
|
||||
except ValueError as error:
|
||||
failures.append(str(error))
|
||||
chrome_scheme_block = ""
|
||||
|
||||
if chrome_scheme_block and "resolvedBrowserChromeColorScheme(" not in chrome_scheme_block:
|
||||
failures.append("browserChromeColorScheme must use resolvedBrowserChromeColorScheme")
|
||||
|
||||
try:
|
||||
omnibar_background_block = extract_block(
|
||||
browser_panel_view_block,
|
||||
"private var omnibarPillBackgroundColor: NSColor",
|
||||
)
|
||||
except ValueError as error:
|
||||
failures.append(str(error))
|
||||
omnibar_background_block = ""
|
||||
|
||||
if omnibar_background_block and "for: browserChromeColorScheme" not in omnibar_background_block:
|
||||
failures.append("omnibar pill background must use browserChromeColorScheme")
|
||||
|
||||
try:
|
||||
address_bar_block = extract_block(
|
||||
browser_panel_view_block,
|
||||
"private var addressBar: some View",
|
||||
)
|
||||
except ValueError as error:
|
||||
failures.append(str(error))
|
||||
address_bar_block = ""
|
||||
|
||||
if address_bar_block and ".environment(\\.colorScheme, browserChromeColorScheme)" not in address_bar_block:
|
||||
failures.append("addressBar must apply browserChromeColorScheme via environment")
|
||||
|
||||
try:
|
||||
body_block = extract_block(browser_panel_view_block, "var body: some View")
|
||||
except ValueError as error:
|
||||
failures.append(str(error))
|
||||
body_block = ""
|
||||
|
||||
if body_block:
|
||||
if "OmnibarSuggestionsView(" not in body_block:
|
||||
failures.append("Expected OmnibarSuggestionsView block in BrowserPanelView body")
|
||||
elif ".environment(\\.colorScheme, browserChromeColorScheme)" not in body_block:
|
||||
failures.append("Omnibar suggestions must apply browserChromeColorScheme via environment")
|
||||
|
||||
if failures:
|
||||
print("FAIL: browser chrome contrast regression guards failed")
|
||||
for failure in failures:
|
||||
print(f" - {failure}")
|
||||
return 1
|
||||
|
||||
print("PASS: browser chrome contrast regression guards are in place")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Static regression guard for browser console/errors CLI output formatting.
|
||||
|
||||
Ensures non-JSON `browser console list` and `browser errors list` do not fall
|
||||
back to unconditional `OK` when logs exist.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def repo_root() -> Path:
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "--show-toplevel"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return Path(result.stdout.strip())
|
||||
return Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def extract_block(source: str, signature: str) -> str:
|
||||
start = source.find(signature)
|
||||
if start < 0:
|
||||
raise ValueError(f"Missing signature: {signature}")
|
||||
brace_start = source.find("{", start)
|
||||
if brace_start < 0:
|
||||
raise ValueError(f"Missing opening brace for: {signature}")
|
||||
depth = 0
|
||||
for idx in range(brace_start, len(source)):
|
||||
char = source[idx]
|
||||
if char == "{":
|
||||
depth += 1
|
||||
elif char == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return source[brace_start : idx + 1]
|
||||
raise ValueError(f"Unbalanced braces for: {signature}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
root = repo_root()
|
||||
failures: list[str] = []
|
||||
|
||||
cli_path = root / "CLI" / "cmux.swift"
|
||||
cli_source = cli_path.read_text(encoding="utf-8")
|
||||
browser_block = extract_block(cli_source, "private func runBrowserCommand(")
|
||||
|
||||
if "func displayBrowserLogItems(_ value: Any?) -> String?" not in browser_block:
|
||||
failures.append("runBrowserCommand() is missing displayBrowserLogItems() helper")
|
||||
else:
|
||||
helper_block = extract_block(browser_block, "func displayBrowserLogItems(_ value: Any?) -> String?")
|
||||
if "return \"[\\(level)] \\(text)\"" not in helper_block:
|
||||
failures.append("displayBrowserLogItems() no longer renders level-prefixed log lines")
|
||||
if "return \"[error] \\(message)\"" not in helper_block:
|
||||
failures.append("displayBrowserLogItems() no longer renders concise JS error messages")
|
||||
if "return displayBrowserValue(dict)" not in helper_block:
|
||||
failures.append("displayBrowserLogItems() no longer falls back to structured formatting")
|
||||
|
||||
console_block = extract_block(browser_block, 'if subcommand == "console"')
|
||||
if 'displayBrowserLogItems(payload["entries"])' not in console_block:
|
||||
failures.append("browser console path no longer formats entries for non-JSON output")
|
||||
if 'output(payload, fallback: "OK")' in console_block:
|
||||
failures.append("browser console path regressed to unconditional OK output")
|
||||
|
||||
errors_block = extract_block(browser_block, 'if subcommand == "errors"')
|
||||
if 'displayBrowserLogItems(payload["errors"])' not in errors_block:
|
||||
failures.append("browser errors path no longer formats errors for non-JSON output")
|
||||
if 'output(payload, fallback: "OK")' in errors_block:
|
||||
failures.append("browser errors path regressed to unconditional OK output")
|
||||
|
||||
if failures:
|
||||
print("FAIL: browser console/errors CLI output regression guard failed")
|
||||
for item in failures:
|
||||
print(f" - {item}")
|
||||
return 1
|
||||
|
||||
print("PASS: browser console/errors CLI output regression guard is in place")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -1,92 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Static regression checks for browser DevTools/portal review fixes.
|
||||
|
||||
Guards two follow-up fixes:
|
||||
1) DevTools toggle path must retry restore when inspector show is transiently ignored.
|
||||
2) Browser portal visibility must propagate even if host is temporarily off-window.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def repo_root() -> Path:
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "--show-toplevel"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return Path(result.stdout.strip())
|
||||
return Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def extract_block(source: str, signature: str) -> str:
|
||||
start = source.find(signature)
|
||||
if start < 0:
|
||||
raise ValueError(f"Missing signature: {signature}")
|
||||
brace_start = source.find("{", start)
|
||||
if brace_start < 0:
|
||||
raise ValueError(f"Missing opening brace for: {signature}")
|
||||
depth = 0
|
||||
for idx in range(brace_start, len(source)):
|
||||
char = source[idx]
|
||||
if char == "{":
|
||||
depth += 1
|
||||
elif char == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return source[brace_start : idx + 1]
|
||||
raise ValueError(f"Unbalanced braces for: {signature}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
root = repo_root()
|
||||
failures: list[str] = []
|
||||
|
||||
panel_path = root / "Sources" / "Panels" / "BrowserPanel.swift"
|
||||
panel_source = panel_path.read_text(encoding="utf-8")
|
||||
toggle_block = extract_block(panel_source, "func toggleDeveloperTools() -> Bool")
|
||||
if "visibleAfterToggle" not in toggle_block:
|
||||
failures.append("toggleDeveloperTools() no longer re-checks inspector visibility")
|
||||
if "scheduleDeveloperToolsRestoreRetry()" not in toggle_block:
|
||||
failures.append("toggleDeveloperTools() no longer schedules a DevTools restore retry")
|
||||
|
||||
view_path = root / "Sources" / "Panels" / "BrowserPanelView.swift"
|
||||
view_source = view_path.read_text(encoding="utf-8")
|
||||
portal_update_block = extract_block(view_source, "private func updateUsingWindowPortal(")
|
||||
if "BrowserWindowPortalRegistry.updateEntryVisibility(" not in portal_update_block:
|
||||
failures.append("BrowserPanelView.updateUsingWindowPortal() is missing deferred portal visibility propagation")
|
||||
if "zPriority: coordinator.desiredPortalZPriority" not in portal_update_block:
|
||||
failures.append("BrowserPanelView deferred portal update no longer propagates zPriority")
|
||||
|
||||
portal_path = root / "Sources" / "BrowserWindowPortal.swift"
|
||||
portal_source = portal_path.read_text(encoding="utf-8")
|
||||
if not re.search(
|
||||
r"func\s+updateEntryVisibility\s*\(\s*forWebViewId\s+webViewId:\s*ObjectIdentifier,\s*visibleInUI:\s*Bool,\s*zPriority:\s*Int\s*\)",
|
||||
portal_source,
|
||||
flags=re.MULTILINE,
|
||||
):
|
||||
failures.append("WindowBrowserPortal is missing updateEntryVisibility(forWebViewId:visibleInUI:zPriority:)")
|
||||
if not re.search(
|
||||
r"static\s+func\s+updateEntryVisibility\s*\(\s*for\s+webView:\s*WKWebView,\s*visibleInUI:\s*Bool,\s*zPriority:\s*Int\s*\)",
|
||||
portal_source,
|
||||
flags=re.MULTILINE,
|
||||
):
|
||||
failures.append("BrowserWindowPortalRegistry is missing updateEntryVisibility(for:visibleInUI:zPriority:)")
|
||||
|
||||
if failures:
|
||||
print("FAIL: browser devtools/portal regression guards failed")
|
||||
for item in failures:
|
||||
print(f" - {item}")
|
||||
return 1
|
||||
|
||||
print("PASS: browser devtools/portal regression guards are in place")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -1,128 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Static regression guard for browser eval async wrapping + telemetry injection."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def repo_root() -> Path:
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "--show-toplevel"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return Path(result.stdout.strip())
|
||||
return Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def extract_block(source: str, signature: str) -> str:
|
||||
start = source.find(signature)
|
||||
if start < 0:
|
||||
raise ValueError(f"Missing signature: {signature}")
|
||||
brace_start = source.find("{", start)
|
||||
if brace_start < 0:
|
||||
raise ValueError(f"Missing opening brace for: {signature}")
|
||||
depth = 0
|
||||
for idx in range(brace_start, len(source)):
|
||||
char = source[idx]
|
||||
if char == "{":
|
||||
depth += 1
|
||||
elif char == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return source[brace_start : idx + 1]
|
||||
raise ValueError(f"Unbalanced braces for: {signature}")
|
||||
|
||||
|
||||
def extract_span(source: str, start_marker: str, end_marker: str) -> str:
|
||||
start = source.find(start_marker)
|
||||
if start < 0:
|
||||
raise ValueError(f"Missing start marker: {start_marker}")
|
||||
end = source.find(end_marker, start)
|
||||
if end < 0:
|
||||
raise ValueError(f"Missing end marker: {end_marker}")
|
||||
return source[start:end]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
root = repo_root()
|
||||
failures: list[str] = []
|
||||
|
||||
terminal_path = root / "Sources" / "TerminalController.swift"
|
||||
panel_path = root / "Sources" / "Panels" / "BrowserPanel.swift"
|
||||
terminal_source = terminal_path.read_text(encoding="utf-8")
|
||||
panel_source = panel_path.read_text(encoding="utf-8")
|
||||
|
||||
if "preferAsync: Bool = false" not in terminal_source:
|
||||
failures.append("v2RunJavaScript() no longer exposes preferAsync toggle")
|
||||
run_js_block = extract_block(terminal_source, "private func v2RunJavaScript(")
|
||||
if "callAsyncJavaScript" not in run_js_block:
|
||||
failures.append("v2RunJavaScript() no longer uses callAsyncJavaScript for async JS")
|
||||
|
||||
run_browser_js_block = extract_block(terminal_source, "private func v2RunBrowserJavaScript(")
|
||||
required_wrapper_tokens = [
|
||||
"let asyncFunctionBody =",
|
||||
"__cmuxMaybeAwait",
|
||||
"__cmux_t",
|
||||
"__cmux_v",
|
||||
"return await __cmuxEvalInFrame();",
|
||||
"preferAsync: true",
|
||||
]
|
||||
for token in required_wrapper_tokens:
|
||||
if token not in run_browser_js_block:
|
||||
failures.append(f"v2RunBrowserJavaScript() missing async eval wrapper token: {token}")
|
||||
|
||||
if "v2BrowserUndefinedSentinel" not in terminal_source:
|
||||
failures.append("TerminalController is missing undefined sentinel handling")
|
||||
if "v2BrowserEvalEnvelopeTypeUndefined" not in terminal_source:
|
||||
failures.append("TerminalController is missing undefined envelope decode constant")
|
||||
|
||||
hook_block = extract_block(terminal_source, "private func v2BrowserEnsureTelemetryHooks(")
|
||||
if "BrowserPanel.telemetryHookBootstrapScriptSource" not in hook_block:
|
||||
failures.append("v2BrowserEnsureTelemetryHooks() no longer uses shared BrowserPanel telemetry source")
|
||||
|
||||
if "static let telemetryHookBootstrapScriptSource" not in panel_source:
|
||||
failures.append("BrowserPanel is missing telemetryHookBootstrapScriptSource")
|
||||
if "static let dialogTelemetryHookBootstrapScriptSource" not in panel_source:
|
||||
failures.append("BrowserPanel is missing dialogTelemetryHookBootstrapScriptSource")
|
||||
|
||||
base_script_span = extract_span(
|
||||
panel_source,
|
||||
"static let telemetryHookBootstrapScriptSource =",
|
||||
"static let dialogTelemetryHookBootstrapScriptSource =",
|
||||
)
|
||||
if "window.alert = function(message)" in base_script_span:
|
||||
failures.append("Document-start telemetry script should not override alert dialogs")
|
||||
if "window.confirm = function(message)" in base_script_span:
|
||||
failures.append("Document-start telemetry script should not override confirm dialogs")
|
||||
if "window.prompt = function(message, defaultValue)" in base_script_span:
|
||||
failures.append("Document-start telemetry script should not override prompt dialogs")
|
||||
|
||||
panel_init_block = extract_block(
|
||||
panel_source,
|
||||
"init(workspaceId: UUID, initialURL: URL? = nil, bypassInsecureHTTPHostOnce: String? = nil)",
|
||||
)
|
||||
required_init_tokens = [
|
||||
"config.userContentController.addUserScript(",
|
||||
"source: Self.telemetryHookBootstrapScriptSource",
|
||||
"injectionTime: .atDocumentStart",
|
||||
]
|
||||
for token in required_init_tokens:
|
||||
if token not in panel_init_block:
|
||||
failures.append(f"BrowserPanel init() missing telemetry user-script token: {token}")
|
||||
|
||||
if failures:
|
||||
print("FAIL: browser eval async wrapper / telemetry injection regression guard failed")
|
||||
for item in failures:
|
||||
print(f" - {item}")
|
||||
return 1
|
||||
|
||||
print("PASS: browser eval async wrapper / telemetry injection regression guard is in place")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -1,89 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Static regression guard for browser eval CLI output formatting.
|
||||
|
||||
Ensures `cmux browser <surface> eval <script>` prints the evaluated value
|
||||
instead of always printing `OK`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def repo_root() -> Path:
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "--show-toplevel"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return Path(result.stdout.strip())
|
||||
return Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def extract_block(source: str, signature: str) -> str:
|
||||
start = source.find(signature)
|
||||
if start < 0:
|
||||
raise ValueError(f"Missing signature: {signature}")
|
||||
brace_start = source.find("{", start)
|
||||
if brace_start < 0:
|
||||
raise ValueError(f"Missing opening brace for: {signature}")
|
||||
depth = 0
|
||||
for idx in range(brace_start, len(source)):
|
||||
char = source[idx]
|
||||
if char == "{":
|
||||
depth += 1
|
||||
elif char == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return source[brace_start : idx + 1]
|
||||
raise ValueError(f"Unbalanced braces for: {signature}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
root = repo_root()
|
||||
failures: list[str] = []
|
||||
|
||||
cli_path = root / "CLI" / "cmux.swift"
|
||||
cli_source = cli_path.read_text(encoding="utf-8")
|
||||
browser_block = extract_block(cli_source, "private func runBrowserCommand(")
|
||||
|
||||
if "func displayBrowserValue(_ value: Any) -> String" not in browser_block:
|
||||
failures.append("runBrowserCommand() is missing displayBrowserValue() helper")
|
||||
else:
|
||||
value_block = extract_block(browser_block, "func displayBrowserValue(_ value: Any) -> String")
|
||||
if 'dict["__cmux_t"] as? String' not in value_block or 'type == "undefined"' not in value_block:
|
||||
failures.append("displayBrowserValue() no longer maps __cmux_t=undefined to literal 'undefined'")
|
||||
required_guards = [
|
||||
"if value is NSNull",
|
||||
"if let string = value as? String",
|
||||
"if let bool = value as? Bool",
|
||||
"if let number = value as? NSNumber",
|
||||
]
|
||||
for guard in required_guards:
|
||||
if guard not in value_block:
|
||||
failures.append(f"displayBrowserValue() no longer handles: {guard}")
|
||||
|
||||
eval_block = extract_block(browser_block, 'if subcommand == "eval"')
|
||||
if 'let payload = try client.sendV2(method: "browser.eval"' not in eval_block:
|
||||
failures.append("browser eval path no longer calls browser.eval v2 method")
|
||||
if 'if let value = payload["value"]' not in eval_block:
|
||||
failures.append("browser eval path no longer reads payload value")
|
||||
if "fallback = displayBrowserValue(value)" not in eval_block:
|
||||
failures.append("browser eval path no longer formats payload value for CLI output")
|
||||
if 'output(payload, fallback: "OK")' in eval_block:
|
||||
failures.append("browser eval path regressed to unconditional OK output")
|
||||
|
||||
if failures:
|
||||
print("FAIL: browser eval CLI output regression guard failed")
|
||||
for item in failures:
|
||||
print(f" - {item}")
|
||||
return 1
|
||||
|
||||
print("PASS: browser eval CLI output regression guard is in place")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Static regression checks for favicon sync during browser navigation.
|
||||
|
||||
Guards the race fix where stale async favicon fetches must not overwrite the
|
||||
icon after the user navigates (including back/forward and same-URL reloads),
|
||||
while still allowing same-document URL changes (pushState/hash updates).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def repo_root() -> Path:
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "--show-toplevel"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return Path(result.stdout.strip())
|
||||
return Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def extract_block(source: str, signature: str) -> str:
|
||||
start = source.find(signature)
|
||||
if start < 0:
|
||||
raise ValueError(f"Missing signature: {signature}")
|
||||
brace_start = source.find("{", start)
|
||||
if brace_start < 0:
|
||||
raise ValueError(f"Missing opening brace for: {signature}")
|
||||
depth = 0
|
||||
for idx in range(brace_start, len(source)):
|
||||
char = source[idx]
|
||||
if char == "{":
|
||||
depth += 1
|
||||
elif char == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return source[brace_start : idx + 1]
|
||||
raise ValueError(f"Unbalanced braces for: {signature}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
root = repo_root()
|
||||
failures: list[str] = []
|
||||
|
||||
panel_path = root / "Sources" / "Panels" / "BrowserPanel.swift"
|
||||
panel_source = panel_path.read_text(encoding="utf-8")
|
||||
|
||||
if "private var faviconRefreshGeneration: Int = 0" not in panel_source:
|
||||
failures.append("BrowserPanel is missing faviconRefreshGeneration state")
|
||||
|
||||
refresh_block = extract_block(panel_source, "private func refreshFavicon(from webView: WKWebView)")
|
||||
if refresh_block.count("isCurrentFaviconRefresh(") < 3:
|
||||
failures.append("refreshFavicon() no longer checks staleness at each async stage")
|
||||
|
||||
current_guard_block = extract_block(panel_source, "private func isCurrentFaviconRefresh(")
|
||||
if "generation == faviconRefreshGeneration" not in current_guard_block:
|
||||
failures.append("isCurrentFaviconRefresh() no longer validates refresh generation")
|
||||
if "webView.url?.absoluteString == pageURLString" in current_guard_block:
|
||||
failures.append("isCurrentFaviconRefresh() still blocks same-document history URL changes")
|
||||
|
||||
loading_block = extract_block(panel_source, "private func handleWebViewLoadingChanged(_ newValue: Bool)")
|
||||
if "faviconRefreshGeneration &+= 1" not in loading_block:
|
||||
failures.append("handleWebViewLoadingChanged() no longer invalidates old favicon refreshes")
|
||||
if "faviconTask?.cancel()" not in loading_block:
|
||||
failures.append("handleWebViewLoadingChanged() no longer cancels stale favicon tasks")
|
||||
if "lastFaviconURLString = nil" not in loading_block:
|
||||
failures.append("handleWebViewLoadingChanged() no longer resets favicon URL cache on new loads")
|
||||
|
||||
if failures:
|
||||
print("FAIL: browser favicon navigation regression guard failed")
|
||||
for item in failures:
|
||||
print(f" - {item}")
|
||||
return 1
|
||||
|
||||
print("PASS: browser favicon navigation guard is in place")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -1,203 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Regression guards for browser Cmd+F overlay layering in portal mode."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from regression_helpers import extract_block, repo_root
|
||||
|
||||
|
||||
def main() -> int:
|
||||
root = repo_root()
|
||||
view_path = root / "Sources" / "Panels" / "BrowserPanelView.swift"
|
||||
panel_path = root / "Sources" / "Panels" / "BrowserPanel.swift"
|
||||
overlay_path = root / "Sources" / "Find" / "BrowserSearchOverlay.swift"
|
||||
source = view_path.read_text(encoding="utf-8")
|
||||
panel_source = panel_path.read_text(encoding="utf-8")
|
||||
overlay_source = overlay_path.read_text(encoding="utf-8")
|
||||
failures: list[str] = []
|
||||
|
||||
try:
|
||||
browser_panel_view_block = extract_block(
|
||||
source, "struct BrowserPanelView: View"
|
||||
)
|
||||
except ValueError as error:
|
||||
failures.append(str(error))
|
||||
browser_panel_view_block = ""
|
||||
|
||||
try:
|
||||
body_block = extract_block(browser_panel_view_block, "var body: some View")
|
||||
except ValueError as error:
|
||||
failures.append(str(error))
|
||||
body_block = ""
|
||||
|
||||
fallback_signature = (
|
||||
"if !panel.shouldRenderWebView, let searchState = panel.searchState {"
|
||||
)
|
||||
fallback_block = ""
|
||||
if body_block:
|
||||
try:
|
||||
fallback_block = extract_block(body_block, fallback_signature)
|
||||
except ValueError:
|
||||
failures.append(
|
||||
"BrowserPanelView must provide BrowserSearchOverlay fallback for new-tab state "
|
||||
"(when WKWebView is not mounted)"
|
||||
)
|
||||
if fallback_block and "BrowserSearchOverlay(" not in fallback_block:
|
||||
failures.append(
|
||||
"BrowserPanelView fallback branch must mount BrowserSearchOverlay for new-tab state"
|
||||
)
|
||||
|
||||
try:
|
||||
webview_repr_block = extract_block(
|
||||
source, "struct WebViewRepresentable: NSViewRepresentable"
|
||||
)
|
||||
except ValueError as error:
|
||||
failures.append(str(error))
|
||||
webview_repr_block = ""
|
||||
|
||||
if webview_repr_block:
|
||||
if "let browserSearchState: BrowserSearchState?" not in webview_repr_block:
|
||||
failures.append(
|
||||
"WebViewRepresentable must include browserSearchState so Cmd+F state changes trigger updates"
|
||||
)
|
||||
if (
|
||||
"var searchOverlayHostingView: NSHostingView<BrowserSearchOverlay>?"
|
||||
not in webview_repr_block
|
||||
):
|
||||
failures.append(
|
||||
"WebViewRepresentable.Coordinator must own a BrowserSearchOverlay hosting view"
|
||||
)
|
||||
if "private static func updateSearchOverlay(" not in webview_repr_block:
|
||||
failures.append(
|
||||
"WebViewRepresentable must define updateSearchOverlay helper"
|
||||
)
|
||||
if "containerView: webView.superview" not in webview_repr_block:
|
||||
failures.append(
|
||||
"Portal updates must sync BrowserSearchOverlay against the web view container"
|
||||
)
|
||||
if "removeSearchOverlay(from: coordinator)" not in webview_repr_block:
|
||||
failures.append(
|
||||
"WebViewRepresentable must remove browser search overlays during teardown/rebind"
|
||||
)
|
||||
|
||||
if "browserSearchState: panel.searchState" not in source:
|
||||
failures.append(
|
||||
"BrowserPanelView must pass panel.searchState into WebViewRepresentable"
|
||||
)
|
||||
|
||||
try:
|
||||
update_ns_view_block = extract_block(
|
||||
webview_repr_block, "func updateNSView(_ nsView: NSView, context: Context)"
|
||||
)
|
||||
except ValueError as error:
|
||||
failures.append(str(error))
|
||||
update_ns_view_block = ""
|
||||
|
||||
if "updateSearchOverlay(" in update_ns_view_block:
|
||||
failures.append(
|
||||
"updateNSView must not re-run updateSearchOverlay outside portal lifecycle paths"
|
||||
)
|
||||
|
||||
try:
|
||||
suppress_focus_block = extract_block(
|
||||
panel_source, "func shouldSuppressWebViewFocus() -> Bool"
|
||||
)
|
||||
except ValueError as error:
|
||||
failures.append(str(error))
|
||||
suppress_focus_block = ""
|
||||
|
||||
if "if searchState != nil {" not in suppress_focus_block:
|
||||
failures.append(
|
||||
"BrowserPanel.shouldSuppressWebViewFocus must suppress focus while find-in-page is active"
|
||||
)
|
||||
|
||||
try:
|
||||
start_find_block = extract_block(panel_source, "func startFind()")
|
||||
except ValueError as error:
|
||||
failures.append(str(error))
|
||||
start_find_block = ""
|
||||
|
||||
if start_find_block:
|
||||
if "postBrowserSearchFocusNotification()" not in start_find_block:
|
||||
failures.append(
|
||||
"BrowserPanel.startFind must publish browserSearchFocus notifications"
|
||||
)
|
||||
if "DispatchQueue.main.async {" not in start_find_block:
|
||||
failures.append(
|
||||
"BrowserPanel.startFind must re-post focus on next runloop to avoid mount races"
|
||||
)
|
||||
if "DispatchQueue.main.asyncAfter" not in start_find_block:
|
||||
failures.append(
|
||||
"BrowserPanel.startFind must re-post focus shortly after to avoid portal mount races"
|
||||
)
|
||||
|
||||
try:
|
||||
init_block = extract_block(panel_source, "init(workspaceId: UUID")
|
||||
except ValueError as error:
|
||||
failures.append(str(error))
|
||||
init_block = ""
|
||||
|
||||
if init_block:
|
||||
if (
|
||||
"self?.searchState = nil" in init_block
|
||||
or "self.searchState = nil" in init_block
|
||||
):
|
||||
failures.append(
|
||||
"BrowserPanel navigation callbacks must not clear searchState entirely to avoid losing find bar focus"
|
||||
)
|
||||
if "restoreFindStateAfterNavigation(replaySearch: true)" not in init_block:
|
||||
failures.append(
|
||||
"BrowserPanel.didFinish must preserve find state and replay search on the new page"
|
||||
)
|
||||
if "restoreFindStateAfterNavigation(replaySearch: false)" not in init_block:
|
||||
failures.append(
|
||||
"BrowserPanel.didFailNavigation must preserve find state without replaying search"
|
||||
)
|
||||
|
||||
try:
|
||||
restore_find_state_block = extract_block(
|
||||
panel_source, "private func restoreFindStateAfterNavigation(replaySearch: Bool)"
|
||||
)
|
||||
except ValueError as error:
|
||||
failures.append(str(error))
|
||||
restore_find_state_block = ""
|
||||
|
||||
if restore_find_state_block:
|
||||
if "state.total = nil" not in restore_find_state_block:
|
||||
failures.append(
|
||||
"BrowserPanel restoreFindStateAfterNavigation must clear stale find total count"
|
||||
)
|
||||
if "state.selected = nil" not in restore_find_state_block:
|
||||
failures.append(
|
||||
"BrowserPanel restoreFindStateAfterNavigation must clear stale selected match"
|
||||
)
|
||||
if "if replaySearch, !state.needle.isEmpty {" not in restore_find_state_block:
|
||||
failures.append(
|
||||
"BrowserPanel restoreFindStateAfterNavigation must only replay search for successful navigations"
|
||||
)
|
||||
if "postBrowserSearchFocusNotification()" not in restore_find_state_block:
|
||||
failures.append(
|
||||
"BrowserPanel restoreFindStateAfterNavigation must reassert find field focus"
|
||||
)
|
||||
|
||||
if "private func requestSearchFieldFocus(" not in overlay_source:
|
||||
failures.append(
|
||||
"BrowserSearchOverlay must define requestSearchFieldFocus retry helper"
|
||||
)
|
||||
if "requestSearchFieldFocus()" not in overlay_source:
|
||||
failures.append(
|
||||
"BrowserSearchOverlay must request text focus from appear/notification paths"
|
||||
)
|
||||
|
||||
if failures:
|
||||
print("FAIL: browser find overlay portal regression guards failed")
|
||||
for failure in failures:
|
||||
print(f" - {failure}")
|
||||
return 1
|
||||
|
||||
print("PASS: browser find overlay remains mounted in portal-hosted AppKit layer")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -1,125 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Static regression guards for compact browser omnibar sizing."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def repo_root() -> Path:
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "--show-toplevel"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return Path(result.stdout.strip())
|
||||
return Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def extract_block(source: str, signature: str) -> str:
|
||||
start = source.find(signature)
|
||||
if start < 0:
|
||||
raise ValueError(f"Missing signature: {signature}")
|
||||
brace_start = source.find("{", start)
|
||||
if brace_start < 0:
|
||||
raise ValueError(f"Missing opening brace for: {signature}")
|
||||
depth = 0
|
||||
for idx in range(brace_start, len(source)):
|
||||
char = source[idx]
|
||||
if char == "{":
|
||||
depth += 1
|
||||
elif char == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return source[brace_start : idx + 1]
|
||||
raise ValueError(f"Unbalanced braces for: {signature}")
|
||||
|
||||
|
||||
def parse_cgfloat_constant(source: str, name: str) -> float | None:
|
||||
match = re.search(
|
||||
rf"private let {re.escape(name)}: CGFloat = ([0-9]+(?:\.[0-9]+)?)",
|
||||
source,
|
||||
)
|
||||
if not match:
|
||||
return None
|
||||
return float(match.group(1))
|
||||
|
||||
|
||||
def main() -> int:
|
||||
root = repo_root()
|
||||
failures: list[str] = []
|
||||
|
||||
view_path = root / "Sources" / "Panels" / "BrowserPanelView.swift"
|
||||
view_source = view_path.read_text(encoding="utf-8")
|
||||
|
||||
hit_size = parse_cgfloat_constant(view_source, "addressBarButtonHitSize")
|
||||
if hit_size is None:
|
||||
failures.append("addressBarButtonHitSize constant is missing")
|
||||
elif hit_size > 26:
|
||||
failures.append(
|
||||
f"addressBarButtonHitSize regressed to {hit_size:g}; expected <= 26 for compact omnibar height"
|
||||
)
|
||||
|
||||
vertical_padding = parse_cgfloat_constant(view_source, "addressBarVerticalPadding")
|
||||
if vertical_padding is None:
|
||||
failures.append("addressBarVerticalPadding constant is missing")
|
||||
elif vertical_padding > 4:
|
||||
failures.append(
|
||||
f"addressBarVerticalPadding regressed to {vertical_padding:g}; expected <= 4 for compact omnibar height"
|
||||
)
|
||||
|
||||
omnibar_corner_radius = parse_cgfloat_constant(view_source, "omnibarPillCornerRadius")
|
||||
if omnibar_corner_radius is None:
|
||||
failures.append("omnibarPillCornerRadius constant is missing")
|
||||
elif omnibar_corner_radius > 10:
|
||||
failures.append(
|
||||
f"omnibarPillCornerRadius regressed to {omnibar_corner_radius:g}; expected <= 10 to keep a squircle profile"
|
||||
)
|
||||
|
||||
address_bar_block = extract_block(view_source, "private var addressBar: some View")
|
||||
if ".padding(.vertical, addressBarVerticalPadding)" not in address_bar_block:
|
||||
failures.append("addressBar no longer applies compact vertical padding via addressBarVerticalPadding")
|
||||
|
||||
omnibar_field_block = extract_block(view_source, "private var omnibarField: some View")
|
||||
if omnibar_field_block.count(
|
||||
"RoundedRectangle(cornerRadius: omnibarPillCornerRadius, style: .continuous)"
|
||||
) < 2:
|
||||
failures.append(
|
||||
"omnibarField no longer uses continuous rounded-rectangle background+stroke tied to omnibarPillCornerRadius"
|
||||
)
|
||||
|
||||
button_bar_block = extract_block(view_source, "private var addressBarButtonBar: some View")
|
||||
hit_frame_uses = button_bar_block.count("addressBarButtonHitSize")
|
||||
if hit_frame_uses < 3:
|
||||
failures.append(
|
||||
"navigation buttons no longer consistently use addressBarButtonHitSize frames (padding may be lost)"
|
||||
)
|
||||
|
||||
extract_block(view_source, "private struct OmnibarAddressButtonStyle: ButtonStyle")
|
||||
style_body_block = extract_block(view_source, "private struct OmnibarAddressButtonStyleBody: View")
|
||||
if "configuration.isPressed" not in style_body_block:
|
||||
failures.append("OmnibarAddressButtonStyleBody is missing pressed-state styling")
|
||||
if "isHovered" not in style_body_block or ".onHover" not in style_body_block:
|
||||
failures.append("OmnibarAddressButtonStyleBody is missing hover-state styling")
|
||||
|
||||
style_uses = view_source.count(".buttonStyle(OmnibarAddressButtonStyle())")
|
||||
if style_uses < 4:
|
||||
failures.append(
|
||||
"address bar buttons no longer consistently use OmnibarAddressButtonStyle"
|
||||
)
|
||||
|
||||
if failures:
|
||||
print("FAIL: browser omnibar compact layout regression guards failed")
|
||||
for failure in failures:
|
||||
print(f" - {failure}")
|
||||
return 1
|
||||
|
||||
print("PASS: browser omnibar compact layout regression guards are in place")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Static regression checks for deterministic browser lifecycle architecture.
|
||||
|
||||
Guards the long-term browser mounting design:
|
||||
1) BrowserPanelView updateNSView must use a single portal-based mount path.
|
||||
2) Legacy attach-retry and direct attach/detach churn helpers stay removed.
|
||||
3) BrowserPanel handles WebContent termination via deterministic webview replacement,
|
||||
not blind `webView.reload()`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def repo_root() -> Path:
|
||||
git_path = shutil.which("git")
|
||||
git_command = git_path if git_path else "git"
|
||||
result = subprocess.run(
|
||||
[git_command, "rev-parse", "--show-toplevel"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return Path(result.stdout.strip())
|
||||
return Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def extract_block(source: str, signature: str) -> str:
|
||||
start = source.find(signature)
|
||||
if start < 0:
|
||||
raise ValueError(f"Missing signature: {signature}")
|
||||
brace_start = source.find("{", start)
|
||||
if brace_start < 0:
|
||||
raise ValueError(f"Missing opening brace for: {signature}")
|
||||
depth = 0
|
||||
for idx in range(brace_start, len(source)):
|
||||
char = source[idx]
|
||||
if char == "{":
|
||||
depth += 1
|
||||
elif char == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return source[brace_start : idx + 1]
|
||||
raise ValueError(f"Unbalanced braces for: {signature}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
root = repo_root()
|
||||
failures: list[str] = []
|
||||
|
||||
view_path = root / "Sources" / "Panels" / "BrowserPanelView.swift"
|
||||
view_source = view_path.read_text(encoding="utf-8")
|
||||
|
||||
if "updateUsingWindowPortal(nsView, context: context, webView: webView)" not in view_source:
|
||||
failures.append("updateNSView no longer routes through updateUsingWindowPortal")
|
||||
if "scheduleAttachRetry(" in view_source:
|
||||
failures.append("Legacy attach retry helper still present in BrowserPanelView")
|
||||
if "attachRetryWorkItem" in view_source:
|
||||
failures.append("Legacy attachRetryWorkItem state still present in BrowserPanelView")
|
||||
if "usesWindowPortal" in view_source:
|
||||
failures.append("Dual portal/non-portal lifecycle state still present in BrowserPanelView")
|
||||
|
||||
panel_path = root / "Sources" / "Panels" / "BrowserPanel.swift"
|
||||
panel_source = panel_path.read_text(encoding="utf-8")
|
||||
|
||||
if "@Published private(set) var webViewInstanceID" not in panel_source:
|
||||
failures.append("BrowserPanel is missing webViewInstanceID for deterministic instance remounting")
|
||||
if "replaceWebViewAfterContentProcessTermination" not in panel_source:
|
||||
failures.append("BrowserPanel is missing deterministic WebContent termination replacement path")
|
||||
|
||||
terminate_delegate = extract_block(
|
||||
panel_source,
|
||||
"func webViewWebContentProcessDidTerminate(_ webView: WKWebView)",
|
||||
)
|
||||
if "didTerminateWebContentProcess?(webView)" not in terminate_delegate:
|
||||
failures.append("webContentProcessDidTerminate no longer delegates to deterministic replacement handler")
|
||||
if "webView.reload()" in terminate_delegate:
|
||||
failures.append("webContentProcessDidTerminate still does blind webView.reload()")
|
||||
|
||||
if failures:
|
||||
print("FAIL: browser lifecycle architecture regression guards failed")
|
||||
for item in failures:
|
||||
print(f" - {item}")
|
||||
return 1
|
||||
|
||||
print("PASS: browser lifecycle architecture regression guards are in place")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -1,115 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Regression test: CLI socket Sentry telemetry must apply to all commands."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_repo_root() -> Path:
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "--show-toplevel"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return Path(result.stdout.strip())
|
||||
return Path.cwd()
|
||||
|
||||
|
||||
def require(content: str, needle: str, message: str, failures: list[str]) -> None:
|
||||
if needle not in content:
|
||||
failures.append(message)
|
||||
|
||||
|
||||
def reject(content: str, needle: str, message: str, failures: list[str]) -> None:
|
||||
if needle in content:
|
||||
failures.append(message)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
repo_root = get_repo_root()
|
||||
cli_path = repo_root / "CLI" / "cmux.swift"
|
||||
if not cli_path.exists():
|
||||
print(f"FAIL: missing expected file: {cli_path}")
|
||||
return 1
|
||||
|
||||
content = cli_path.read_text(encoding="utf-8")
|
||||
failures: list[str] = []
|
||||
|
||||
require(
|
||||
content,
|
||||
"private final class CLISocketSentryTelemetry {",
|
||||
"Missing CLISocketSentryTelemetry definition",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
content,
|
||||
'processEnv["CMUX_CLI_SENTRY_DISABLED"] == "1" ||',
|
||||
"Missing CMUX_CLI_SENTRY_DISABLED kill switch",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
content,
|
||||
'processEnv["CMUX_CLAUDE_HOOK_SENTRY_DISABLED"] == "1"',
|
||||
"Missing backwards-compatible CMUX_CLAUDE_HOOK_SENTRY_DISABLED kill switch",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
content,
|
||||
"private var shouldEmit: Bool {\n !disabledByEnv\n }",
|
||||
"Telemetry scope should be command-agnostic (only disabled by env kill switch)",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
content,
|
||||
'let crumb = Breadcrumb(level: .info, category: "cmux.cli")',
|
||||
"Telemetry breadcrumb category should be cmux.cli",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
content,
|
||||
'"command": command,',
|
||||
"Base telemetry context must include command name",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
content,
|
||||
"let cliTelemetry = CLISocketSentryTelemetry(",
|
||||
"CLI should initialize generic socket telemetry",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
content,
|
||||
'cliTelemetry.breadcrumb(\n "socket.connect.attempt",',
|
||||
"CLI should emit socket.connect.attempt breadcrumb for commands",
|
||||
failures,
|
||||
)
|
||||
|
||||
reject(
|
||||
content,
|
||||
"self.enabled = command == \"claude-hook\"",
|
||||
"Telemetry regressed to claude-hook-only scope",
|
||||
failures,
|
||||
)
|
||||
reject(
|
||||
content,
|
||||
"enabled && !disabledByEnv",
|
||||
"Telemetry still depends on legacy enabled flag",
|
||||
failures,
|
||||
)
|
||||
|
||||
if failures:
|
||||
print("FAIL: CLI socket telemetry scope regression(s) detected")
|
||||
for failure in failures:
|
||||
print(f"- {failure}")
|
||||
return 1
|
||||
|
||||
print("PASS: CLI socket telemetry scope is command-agnostic")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -1,147 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Regression tests for CLI subcommand help coverage and accuracy."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_repo_root() -> Path:
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "--show-toplevel"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return Path(result.stdout.strip())
|
||||
return Path.cwd()
|
||||
|
||||
|
||||
def require(content: str, needle: str, message: str, failures: list[str]) -> None:
|
||||
if needle not in content:
|
||||
failures.append(message)
|
||||
|
||||
|
||||
def extract_switch_commands(content: str, start_index: int = 0) -> tuple[set[str], int]:
|
||||
marker = "switch command {"
|
||||
marker_index = content.find(marker, start_index)
|
||||
if marker_index == -1:
|
||||
return set(), -1
|
||||
|
||||
open_brace = content.find("{", marker_index)
|
||||
if open_brace == -1:
|
||||
return set(), -1
|
||||
|
||||
depth = 1
|
||||
cursor = open_brace + 1
|
||||
while cursor < len(content) and depth > 0:
|
||||
char = content[cursor]
|
||||
if char == "{":
|
||||
depth += 1
|
||||
elif char == "}":
|
||||
depth -= 1
|
||||
cursor += 1
|
||||
|
||||
block = content[open_brace + 1:cursor - 1]
|
||||
commands: set[str] = set()
|
||||
collecting_case = False
|
||||
case_lines: list[str] = []
|
||||
|
||||
for line in block.splitlines():
|
||||
stripped = line.strip()
|
||||
if stripped.startswith("case "):
|
||||
collecting_case = True
|
||||
case_lines = [line]
|
||||
elif collecting_case:
|
||||
case_lines.append(line)
|
||||
|
||||
if collecting_case and ":" in line:
|
||||
case_text = "\n".join(case_lines)
|
||||
commands.update(re.findall(r'"([^"]+)"', case_text))
|
||||
collecting_case = False
|
||||
case_lines = []
|
||||
|
||||
return commands, cursor
|
||||
|
||||
|
||||
def main() -> int:
|
||||
repo_root = get_repo_root()
|
||||
cli_path = repo_root / "CLI" / "cmux.swift"
|
||||
if not cli_path.exists():
|
||||
print(f"FAIL: missing expected file: {cli_path}")
|
||||
return 1
|
||||
|
||||
content = cli_path.read_text(encoding="utf-8")
|
||||
failures: list[str] = []
|
||||
|
||||
require(
|
||||
content,
|
||||
'if commandArgs.contains("--help") || commandArgs.contains("-h") {',
|
||||
"Subcommand help pre-dispatch gate is missing",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
content,
|
||||
'if dispatchSubcommandHelp(command: command, commandArgs: commandArgs) {',
|
||||
"Subcommand help dispatch call is missing",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
content,
|
||||
"print(\"Unknown command '\\(command)'. Run 'cmux help' to see available commands.\")",
|
||||
"Subcommand help fallback unknown-command line is missing",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
content,
|
||||
"print(\"Unknown command '\\(command)'. Run 'cmux help' to see available commands.\")\n return",
|
||||
"Subcommand help fallback must return before command execution",
|
||||
failures,
|
||||
)
|
||||
|
||||
dispatch_commands, next_index = extract_switch_commands(content, 0)
|
||||
subcommand_usage_commands, _ = extract_switch_commands(content, next_index if next_index != -1 else 0)
|
||||
if not dispatch_commands:
|
||||
failures.append("Failed to parse main dispatch switch command list")
|
||||
if not subcommand_usage_commands:
|
||||
failures.append("Failed to parse subcommandUsage switch command list")
|
||||
|
||||
missing_help_entries = sorted(dispatch_commands - subcommand_usage_commands)
|
||||
if missing_help_entries:
|
||||
failures.append(
|
||||
"Missing subcommandUsage entries for dispatch command(s): "
|
||||
+ ", ".join(missing_help_entries)
|
||||
)
|
||||
|
||||
# Regression checks for concrete help text that previously drifted from dispatch logic.
|
||||
for needle, message in [
|
||||
('case "help":', "Missing subcommandUsage entry for help"),
|
||||
("Usage: cmux help", "help subcommand usage text is missing"),
|
||||
("Usage: cmux move-workspace-to-window --workspace <id|ref|index> --window <id|ref|index>", "move-workspace-to-window help must document index handles"),
|
||||
("--tab <id|ref|index> Target tab (accepts tab:<n> or surface:<n>; default: $CMUX_TAB_ID, then $CMUX_SURFACE_ID, then focused tab)", "tab-action help must document CMUX_TAB_ID/CMUX_SURFACE_ID fallback"),
|
||||
("--workspace <id|ref|index> Workspace to rename (default: current/$CMUX_WORKSPACE_ID)", "rename-workspace help must document CMUX_WORKSPACE_ID fallback"),
|
||||
("text|html|value|count|box|styles|attr: [--selector <css> | <css>]", "browser get help must document --selector"),
|
||||
("attr: [--attr <name> | <name>]", "browser get attr help must document --attr"),
|
||||
("styles: [--property <name>]", "browser get styles help must document --property"),
|
||||
("role: [--name <text>] [--exact] <role>", "browser find role help must document --name/--exact"),
|
||||
("text|label|placeholder|alt|title|testid: [--exact] <text>", "browser find text-like help must document --exact"),
|
||||
("nth: [--index <n> | <n>] [--selector <css> | <css>]", "browser find nth help must document --index/--selector"),
|
||||
("route <pattern> [--abort] [--body <text>]", "browser network route help must document --abort/--body"),
|
||||
]:
|
||||
require(content, needle, message, failures)
|
||||
|
||||
if failures:
|
||||
print("FAIL: CLI subcommand help regression(s) detected")
|
||||
for failure in failures:
|
||||
print(f"- {failure}")
|
||||
return 1
|
||||
|
||||
print("PASS: CLI subcommand help coverage and flag/env documentation are present")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -1,149 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Regression test: `cmux tree` command wiring and output contract."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_repo_root() -> Path:
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "--show-toplevel"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return Path(result.stdout.strip())
|
||||
return Path.cwd()
|
||||
|
||||
|
||||
def require(content: str, needle: str, message: str, failures: list[str]) -> None:
|
||||
if needle not in content:
|
||||
failures.append(message)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
repo_root = get_repo_root()
|
||||
cli_path = repo_root / "CLI" / "cmux.swift"
|
||||
controller_path = repo_root / "Sources" / "TerminalController.swift"
|
||||
if not cli_path.exists():
|
||||
print(f"FAIL: missing expected file: {cli_path}")
|
||||
return 1
|
||||
if not controller_path.exists():
|
||||
print(f"FAIL: missing expected file: {controller_path}")
|
||||
return 1
|
||||
|
||||
content = cli_path.read_text(encoding="utf-8")
|
||||
controller_content = controller_path.read_text(encoding="utf-8")
|
||||
failures: list[str] = []
|
||||
|
||||
require(
|
||||
content,
|
||||
'case "tree":\n try runTreeCommand(commandArgs: commandArgs, client: client, jsonOutput: jsonOutput, idFormat: idFormat)',
|
||||
"Missing `tree` command dispatch",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
content,
|
||||
"tree [--all] [--workspace <id|ref|index>]",
|
||||
"Top-level usage text missing tree command",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
content,
|
||||
"Usage: cmux tree [flags]",
|
||||
"Subcommand help for `cmux tree --help` is missing",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
content,
|
||||
"Known flags: --all --workspace <id|ref|index> --json",
|
||||
"Tree flag validation for --all/--workspace is missing",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
content,
|
||||
"--json Structured JSON output",
|
||||
"Tree help text should document --json",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
content,
|
||||
'print(jsonString(formatIDs(payload, mode: idFormat)))',
|
||||
"Tree command JSON output should honor --id-format conversion",
|
||||
failures,
|
||||
)
|
||||
|
||||
# Data sources needed for full hierarchy + browser URLs.
|
||||
for method in [
|
||||
'method: "system.tree"',
|
||||
'method: "system.identify"',
|
||||
'method: "window.list"',
|
||||
'method: "workspace.list"',
|
||||
'method: "pane.list"',
|
||||
'method: "surface.list"',
|
||||
'method: "browser.tab.list"',
|
||||
'method: "browser.url.get"',
|
||||
]:
|
||||
require(
|
||||
content,
|
||||
method,
|
||||
f"Tree command is missing expected API call: {method}",
|
||||
failures,
|
||||
)
|
||||
|
||||
# Text tree rendering contract.
|
||||
for glyph in ['"├── "', '"└── "', '"│ "']:
|
||||
require(
|
||||
content,
|
||||
glyph,
|
||||
f"Tree output missing box-drawing glyph: {glyph}",
|
||||
failures,
|
||||
)
|
||||
|
||||
for marker in ["[current]", "[selected]", "[focused]", "◀ active", "◀ here"]:
|
||||
require(
|
||||
content,
|
||||
marker,
|
||||
f"Tree output missing required marker: {marker}",
|
||||
failures,
|
||||
)
|
||||
|
||||
require(
|
||||
content,
|
||||
'surfaceType.lowercased() == "browser"',
|
||||
"Tree surface rendering should special-case browser surfaces",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
content,
|
||||
'let url = surface["url"] as? String',
|
||||
"Tree surface rendering should include browser URL when available",
|
||||
failures,
|
||||
)
|
||||
|
||||
# Server-side one-shot hierarchy path for performance.
|
||||
for needle, message in [
|
||||
('case "system.tree":', "Socket router is missing system.tree dispatch"),
|
||||
('"system.tree"', "Capabilities list should advertise system.tree"),
|
||||
("private func v2SystemTree(params: [String: Any]) -> V2CallResult {", "Missing v2SystemTree implementation"),
|
||||
('"active":', "system.tree payload should include focused path"),
|
||||
('"caller":', "system.tree payload should include caller path"),
|
||||
('"windows":', "system.tree payload should include hierarchy windows"),
|
||||
]:
|
||||
require(controller_content, needle, message, failures)
|
||||
|
||||
if failures:
|
||||
print("FAIL: cmux tree command regression(s) detected")
|
||||
for failure in failures:
|
||||
print(f"- {failure}")
|
||||
return 1
|
||||
|
||||
print("PASS: cmux tree command wiring and output contract are present")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Regression test: CLI version output wiring keeps commit metadata support."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_repo_root() -> Path:
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "--show-toplevel"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return Path(result.stdout.strip())
|
||||
return Path.cwd()
|
||||
|
||||
|
||||
def require(content: str, needle: str, message: str, failures: list[str]) -> None:
|
||||
if needle not in content:
|
||||
failures.append(message)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
repo_root = get_repo_root()
|
||||
cli_path = repo_root / "CLI" / "cmux.swift"
|
||||
if not cli_path.exists():
|
||||
print(f"FAIL: missing expected file: {cli_path}")
|
||||
return 1
|
||||
|
||||
content = cli_path.read_text(encoding="utf-8")
|
||||
failures: list[str] = []
|
||||
|
||||
require(
|
||||
content,
|
||||
'let commit = info["CMUXCommit"].flatMap { normalizedCommitHash($0) }',
|
||||
"versionSummary no longer reads CMUXCommit metadata",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
content,
|
||||
'return "\\(baseSummary) [\\(commit)]"',
|
||||
"versionSummary no longer appends commit metadata",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
content,
|
||||
'if let commit = dictionary["CMUXCommit"] as? String,',
|
||||
"Info.plist parsing no longer reads CMUXCommit",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
content,
|
||||
"if let commit = gitCommitHash(at: current) {",
|
||||
"Project fallback no longer probes git commit hash",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
content,
|
||||
'["git", "-C", directory.path, "rev-parse", "--short=9", "HEAD"]',
|
||||
"Git commit probe command changed unexpectedly",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
content,
|
||||
'normalizedCommitHash(ProcessInfo.processInfo.environment["CMUX_COMMIT"])',
|
||||
"Environment commit fallback (CMUX_COMMIT) is missing",
|
||||
failures,
|
||||
)
|
||||
|
||||
if failures:
|
||||
print("FAIL: CLI version commit metadata regression(s) detected")
|
||||
for failure in failures:
|
||||
print(f"- {failure}")
|
||||
return 1
|
||||
|
||||
print("PASS: CLI version commit metadata wiring is intact")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -1,118 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Regression test for command-palette socket-listener restart command wiring."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_repo_root() -> Path:
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "--show-toplevel"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return Path(result.stdout.strip())
|
||||
return Path.cwd()
|
||||
|
||||
|
||||
def read_text(path: Path) -> str:
|
||||
return path.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def require(content: str, needle: str, message: str, failures: list[str]) -> None:
|
||||
if needle not in content:
|
||||
failures.append(message)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
repo_root = get_repo_root()
|
||||
content_view_path = repo_root / "Sources" / "ContentView.swift"
|
||||
app_delegate_path = repo_root / "Sources" / "AppDelegate.swift"
|
||||
|
||||
missing_paths = [
|
||||
str(path)
|
||||
for path in [content_view_path, app_delegate_path]
|
||||
if not path.exists()
|
||||
]
|
||||
if missing_paths:
|
||||
print("Missing expected files:")
|
||||
for path in missing_paths:
|
||||
print(f" - {path}")
|
||||
return 1
|
||||
|
||||
content_view = read_text(content_view_path)
|
||||
app_delegate = read_text(app_delegate_path)
|
||||
|
||||
failures: list[str] = []
|
||||
|
||||
require(
|
||||
content_view,
|
||||
'commandId: "palette.restartSocketListener"',
|
||||
"Missing `palette.restartSocketListener` command contribution",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
content_view,
|
||||
'title: constant("Restart CLI Listener")',
|
||||
"Missing `Restart CLI Listener` command title",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
content_view,
|
||||
'registry.register(commandId: "palette.restartSocketListener") {',
|
||||
"Missing command handler registration for `palette.restartSocketListener`",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
content_view,
|
||||
"AppDelegate.shared?.restartSocketListener(nil)",
|
||||
"Socket restart command handler does not call `AppDelegate.restartSocketListener`",
|
||||
failures,
|
||||
)
|
||||
|
||||
require(
|
||||
app_delegate,
|
||||
"@objc func restartSocketListener(_ sender: Any?) {",
|
||||
"Missing `AppDelegate.restartSocketListener` action",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
app_delegate,
|
||||
"private func socketListenerConfigurationIfEnabled() -> (mode: SocketControlMode, path: String)? {",
|
||||
"Missing shared socket listener configuration helper",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
app_delegate,
|
||||
'restartSocketListenerIfEnabled(source: "menu.command")',
|
||||
"`restartSocketListener` no longer delegates to restart helper",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
app_delegate,
|
||||
"TerminalController.shared.stop()",
|
||||
"`restartSocketListenerIfEnabled` no longer stops current listener before restart",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
app_delegate,
|
||||
"TerminalController.shared.start(tabManager: tabManager, socketPath: config.path, accessMode: config.mode)",
|
||||
"`restartSocketListenerIfEnabled` no longer starts listener with current settings",
|
||||
failures,
|
||||
)
|
||||
|
||||
if failures:
|
||||
print("FAIL: command-palette socket restart command regression(s) detected")
|
||||
for failure in failures:
|
||||
print(f"- {failure}")
|
||||
return 1
|
||||
|
||||
print("PASS: command-palette socket restart command wiring is intact")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -1,126 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Regression test for command-palette update command wiring."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_repo_root() -> Path:
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "--show-toplevel"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return Path(result.stdout.strip())
|
||||
return Path.cwd()
|
||||
|
||||
|
||||
def read_text(path: Path) -> str:
|
||||
return path.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def expect_regex(content: str, pattern: str, message: str, failures: list[str]) -> None:
|
||||
if re.search(pattern, content, flags=re.DOTALL) is None:
|
||||
failures.append(message)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
repo_root = get_repo_root()
|
||||
content_view_path = repo_root / "Sources" / "ContentView.swift"
|
||||
app_delegate_path = repo_root / "Sources" / "AppDelegate.swift"
|
||||
controller_path = repo_root / "Sources" / "Update" / "UpdateController.swift"
|
||||
|
||||
missing_paths = [
|
||||
str(path)
|
||||
for path in [content_view_path, app_delegate_path, controller_path]
|
||||
if not path.exists()
|
||||
]
|
||||
if missing_paths:
|
||||
print("Missing expected files:")
|
||||
for path in missing_paths:
|
||||
print(f" - {path}")
|
||||
return 1
|
||||
|
||||
content_view = read_text(content_view_path)
|
||||
app_delegate = read_text(app_delegate_path)
|
||||
controller = read_text(controller_path)
|
||||
|
||||
failures: list[str] = []
|
||||
|
||||
expect_regex(
|
||||
content_view,
|
||||
r'static\s+let\s+updateHasAvailable\s*=\s*"update\.hasAvailable"',
|
||||
"Missing `CommandPaletteContextKeys.updateHasAvailable`",
|
||||
failures,
|
||||
)
|
||||
expect_regex(
|
||||
content_view,
|
||||
r'if\s+case\s+\.updateAvailable\s*=\s*updateViewModel\.effectiveState\s*\{\s*snapshot\.setBool\(CommandPaletteContextKeys\.updateHasAvailable,\s*true\)\s*\}',
|
||||
"Command palette context no longer tracks update-available state",
|
||||
failures,
|
||||
)
|
||||
expect_regex(
|
||||
content_view,
|
||||
r'commandId:\s*"palette\.applyUpdateIfAvailable".*?title:\s*constant\("Apply Update \(If Available\)"\).*?keywords:\s*\[[^\]]*"apply"[^\]]*"install"[^\]]*"update"[^\]]*"available"[^\]]*\].*?when:\s*\{\s*\$0\.bool\(CommandPaletteContextKeys\.updateHasAvailable\)\s*\}',
|
||||
"Missing or incomplete `palette.applyUpdateIfAvailable` contribution visibility gating",
|
||||
failures,
|
||||
)
|
||||
expect_regex(
|
||||
content_view,
|
||||
r'commandId:\s*"palette\.attemptUpdate".*?title:\s*constant\("Attempt Update"\).*?keywords:\s*\[[^\]]*"attempt"[^\]]*"check"[^\]]*"update"[^\]]*\]',
|
||||
"Missing or incomplete `palette.attemptUpdate` contribution",
|
||||
failures,
|
||||
)
|
||||
expect_regex(
|
||||
content_view,
|
||||
r'registry\.register\(commandId:\s*"palette\.applyUpdateIfAvailable"\)\s*\{\s*AppDelegate\.shared\?\.applyUpdateIfAvailable\(nil\)\s*\}',
|
||||
"Missing handler registration for `palette.applyUpdateIfAvailable`",
|
||||
failures,
|
||||
)
|
||||
expect_regex(
|
||||
content_view,
|
||||
r'registry\.register\(commandId:\s*"palette\.attemptUpdate"\)\s*\{\s*AppDelegate\.shared\?\.attemptUpdate\(nil\)\s*\}',
|
||||
"Missing handler registration for `palette.attemptUpdate`",
|
||||
failures,
|
||||
)
|
||||
|
||||
expect_regex(
|
||||
app_delegate,
|
||||
r'@objc\s+func\s+applyUpdateIfAvailable\(_\s+sender:\s+Any\?\)\s*\{\s*updateViewModel\.overrideState\s*=\s*nil\s*updateController\.installUpdate\(\)\s*\}',
|
||||
"`AppDelegate.applyUpdateIfAvailable` is missing or does not call `updateController.installUpdate()`",
|
||||
failures,
|
||||
)
|
||||
expect_regex(
|
||||
app_delegate,
|
||||
r'@objc\s+func\s+attemptUpdate\(_\s+sender:\s+Any\?\)\s*\{\s*updateViewModel\.overrideState\s*=\s*nil\s*updateController\.attemptUpdate\(\)\s*\}',
|
||||
"`AppDelegate.attemptUpdate` is missing or does not call `updateController.attemptUpdate()`",
|
||||
failures,
|
||||
)
|
||||
|
||||
expect_regex(
|
||||
controller,
|
||||
r'func\s+attemptUpdate\(\)\s*\{',
|
||||
"`UpdateController.attemptUpdate()` is missing",
|
||||
failures,
|
||||
)
|
||||
if "state.confirm()" not in controller:
|
||||
failures.append("`UpdateController.attemptUpdate()` no longer auto-confirms update installation")
|
||||
if "checkForUpdates()" not in controller:
|
||||
failures.append("`UpdateController.attemptUpdate()` no longer triggers a check before install")
|
||||
|
||||
if failures:
|
||||
print("FAIL: command-palette update command regression(s) detected")
|
||||
for failure in failures:
|
||||
print(f"- {failure}")
|
||||
return 1
|
||||
|
||||
print("PASS: command-palette update commands expose apply + attempt wiring")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -1,159 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Automated test for ctrl+enter keybind using real keystrokes.
|
||||
|
||||
Requires:
|
||||
- cmux running
|
||||
- Accessibility permissions for System Events (osascript)
|
||||
- keybind = ctrl+enter=text:\\r (or \\n/\\x0d) configured in Ghostty config
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
# Add the directory containing cmux.py to the path
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from cmux import cmux, cmuxError
|
||||
|
||||
|
||||
def run_osascript(script: str) -> subprocess.CompletedProcess[str]:
|
||||
# Use capture_output so we can detect common permission failures and skip.
|
||||
result = subprocess.run(
|
||||
["osascript", "-e", script],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise subprocess.CalledProcessError(
|
||||
result.returncode,
|
||||
result.args,
|
||||
output=result.stdout,
|
||||
stderr=result.stderr,
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def is_keystroke_permission_error(err: subprocess.CalledProcessError) -> bool:
|
||||
text = f"{getattr(err, 'stderr', '') or ''}\n{getattr(err, 'output', '') or ''}"
|
||||
return "not allowed to send keystrokes" in text or "(1002)" in text
|
||||
|
||||
|
||||
def has_ctrl_enter_keybind(config_text: str) -> bool:
|
||||
for line in config_text.splitlines():
|
||||
stripped = line.strip()
|
||||
if not stripped or stripped.startswith("#"):
|
||||
continue
|
||||
if "ctrl+enter" in stripped and "text:" in stripped:
|
||||
if "\\r" in stripped or "\\n" in stripped or "\\x0d" in stripped:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def find_config_with_keybind() -> Optional[Path]:
|
||||
home = Path.home()
|
||||
candidates = [
|
||||
home / "Library/Application Support/com.mitchellh.ghostty/config.ghostty",
|
||||
home / "Library/Application Support/com.mitchellh.ghostty/config",
|
||||
home / ".config/ghostty/config.ghostty",
|
||||
home / ".config/ghostty/config",
|
||||
]
|
||||
for path in candidates:
|
||||
if not path.exists():
|
||||
continue
|
||||
try:
|
||||
if has_ctrl_enter_keybind(path.read_text(encoding="utf-8")):
|
||||
return path
|
||||
except OSError:
|
||||
continue
|
||||
return None
|
||||
|
||||
|
||||
def test_ctrl_enter_keybind(client: cmux) -> tuple[bool, str]:
|
||||
marker = Path("/tmp") / f"ghostty_ctrl_enter_{os.getpid()}"
|
||||
marker.unlink(missing_ok=True)
|
||||
|
||||
# Create a fresh tab to avoid interfering with existing sessions
|
||||
new_tab_id = client.new_tab()
|
||||
client.select_tab(new_tab_id)
|
||||
time.sleep(0.3)
|
||||
try:
|
||||
# Make sure the app is focused for keystrokes
|
||||
bundle_id = cmux.default_bundle_id()
|
||||
run_osascript(f'tell application id "{bundle_id}" to activate')
|
||||
time.sleep(0.2)
|
||||
|
||||
# Clear any running command
|
||||
try:
|
||||
client.send_key("ctrl-c")
|
||||
time.sleep(0.2)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Type the command (without pressing Enter)
|
||||
run_osascript(f'tell application "System Events" to keystroke "touch {marker}"')
|
||||
time.sleep(0.1)
|
||||
|
||||
# Send Ctrl+Enter (key code 36 = Return)
|
||||
run_osascript('tell application "System Events" to key code 36 using control down')
|
||||
time.sleep(0.5)
|
||||
|
||||
ok = marker.exists()
|
||||
return ok, ("Ctrl+Enter keybind executed command" if ok else "Marker not created by Ctrl+Enter")
|
||||
finally:
|
||||
if marker.exists():
|
||||
marker.unlink(missing_ok=True)
|
||||
try:
|
||||
client.close_tab(new_tab_id)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def run_tests() -> int:
|
||||
print("=" * 60)
|
||||
print("cmux Ctrl+Enter Keybind Test")
|
||||
print("=" * 60)
|
||||
print()
|
||||
|
||||
socket_path = cmux.default_socket_path()
|
||||
if not os.path.exists(socket_path):
|
||||
print(f"SKIP: Socket not found at {socket_path}")
|
||||
print("Tip: start cmux first (or set CMUX_TAG / CMUX_SOCKET_PATH).")
|
||||
return 0
|
||||
|
||||
config_path = find_config_with_keybind()
|
||||
if not config_path:
|
||||
print("SKIP: Required keybind not found in Ghostty config.")
|
||||
print("Expected a line like: keybind = ctrl+enter=text:\\r")
|
||||
return 0
|
||||
|
||||
print(f"Using keybind from: {config_path}")
|
||||
print()
|
||||
|
||||
try:
|
||||
with cmux() as client:
|
||||
ok, message = test_ctrl_enter_keybind(client)
|
||||
status = "✅" if ok else "❌"
|
||||
print(f"{status} {message}")
|
||||
return 0 if ok else 1
|
||||
except cmuxError as e:
|
||||
print(f"SKIP: {e}")
|
||||
return 0
|
||||
except subprocess.CalledProcessError as e:
|
||||
if is_keystroke_permission_error(e):
|
||||
print("SKIP: osascript/System Events not allowed to send keystrokes (Accessibility permission missing)")
|
||||
return 0
|
||||
print(f"Error: osascript failed: {e}")
|
||||
if getattr(e, "stderr", None):
|
||||
print(e.stderr.strip())
|
||||
if getattr(e, "output", None):
|
||||
print(e.output.strip())
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(run_tests())
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Static regression checks for re-entrant terminal focus guard.
|
||||
|
||||
Guards the fix for split-drag focus churn where:
|
||||
becomeFirstResponder -> onFocus -> Workspace.focusPanel -> refocus side-effects
|
||||
could repeatedly re-enter and spike CPU.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def repo_root() -> Path:
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "--show-toplevel"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return Path(result.stdout.strip())
|
||||
return Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
root = repo_root()
|
||||
failures: list[str] = []
|
||||
|
||||
workspace_path = root / "Sources" / "Workspace.swift"
|
||||
workspace_source = workspace_path.read_text(encoding="utf-8")
|
||||
|
||||
required_workspace_snippets = [
|
||||
"enum FocusPanelTrigger {",
|
||||
"case terminalFirstResponder",
|
||||
"trigger: FocusPanelTrigger = .standard",
|
||||
"let shouldSuppressReentrantRefocus = trigger == .terminalFirstResponder && selectionAlreadyConverged",
|
||||
"if let targetPaneId, !shouldSuppressReentrantRefocus {",
|
||||
"reason=firstResponderAlreadyConverged",
|
||||
]
|
||||
for snippet in required_workspace_snippets:
|
||||
if snippet not in workspace_source:
|
||||
failures.append(f"Workspace focus guard missing snippet: {snippet}")
|
||||
|
||||
workspace_content_view_path = root / "Sources" / "WorkspaceContentView.swift"
|
||||
workspace_content_view_source = workspace_content_view_path.read_text(encoding="utf-8")
|
||||
focus_callback_snippet = "workspace.focusPanel(panel.id, trigger: .terminalFirstResponder)"
|
||||
if focus_callback_snippet not in workspace_content_view_source:
|
||||
failures.append(
|
||||
"WorkspaceContentView terminal onFocus callback no longer passes .terminalFirstResponder trigger"
|
||||
)
|
||||
|
||||
if failures:
|
||||
print("FAIL: focus-panel re-entrant guard regression checks failed")
|
||||
for item in failures:
|
||||
print(f" - {item}")
|
||||
return 1
|
||||
|
||||
print("PASS: focus-panel re-entrant guard is in place")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -1,166 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Regression guard for issue #494 (post-wake sidebar git updates freezing)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_repo_root() -> Path:
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "--show-toplevel"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return Path(result.stdout.strip())
|
||||
return Path.cwd()
|
||||
|
||||
|
||||
def read_text(path: Path) -> str:
|
||||
return path.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def require(content: str, needle: str, message: str, failures: list[str]) -> None:
|
||||
if needle not in content:
|
||||
failures.append(message)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
repo_root = get_repo_root()
|
||||
zsh_path = repo_root / "Resources" / "shell-integration" / "cmux-zsh-integration.zsh"
|
||||
bash_path = repo_root / "Resources" / "shell-integration" / "cmux-bash-integration.bash"
|
||||
app_delegate_path = repo_root / "Sources" / "AppDelegate.swift"
|
||||
|
||||
required_paths = [zsh_path, bash_path, app_delegate_path]
|
||||
missing_paths = [str(path) for path in required_paths if not path.exists()]
|
||||
if missing_paths:
|
||||
print("Missing expected files:")
|
||||
for path in missing_paths:
|
||||
print(f" - {path}")
|
||||
return 1
|
||||
|
||||
zsh_content = read_text(zsh_path)
|
||||
bash_content = read_text(bash_path)
|
||||
app_delegate = read_text(app_delegate_path)
|
||||
|
||||
failures: list[str] = []
|
||||
|
||||
require(
|
||||
zsh_content,
|
||||
"_CMUX_GIT_JOB_STARTED_AT",
|
||||
"zsh integration is missing git probe start tracking",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
zsh_content,
|
||||
"_CMUX_PR_JOB_STARTED_AT",
|
||||
"zsh integration is missing PR probe start tracking",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
zsh_content,
|
||||
"_CMUX_ASYNC_JOB_TIMEOUT",
|
||||
"zsh integration is missing async probe timeout guard",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
zsh_content,
|
||||
"now - _CMUX_GIT_JOB_STARTED_AT >= _CMUX_ASYNC_JOB_TIMEOUT",
|
||||
"zsh integration no longer clears stale git probe PID after timeout",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
zsh_content,
|
||||
"now - _CMUX_PR_JOB_STARTED_AT >= _CMUX_ASYNC_JOB_TIMEOUT",
|
||||
"zsh integration no longer clears stale PR probe PID after timeout",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
zsh_content,
|
||||
"ncat -w 1 -U \"$CMUX_SOCKET_PATH\" --send-only",
|
||||
"zsh integration missing ncat socket timeout",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
zsh_content,
|
||||
"socat -T 1 - \"UNIX-CONNECT:$CMUX_SOCKET_PATH\"",
|
||||
"zsh integration missing socat socket timeout",
|
||||
failures,
|
||||
)
|
||||
|
||||
require(
|
||||
bash_content,
|
||||
"_CMUX_GIT_JOB_STARTED_AT",
|
||||
"bash integration is missing git probe start tracking",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
bash_content,
|
||||
"_CMUX_PR_JOB_STARTED_AT",
|
||||
"bash integration is missing PR probe start tracking",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
bash_content,
|
||||
"_CMUX_ASYNC_JOB_TIMEOUT",
|
||||
"bash integration is missing async probe timeout guard",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
bash_content,
|
||||
"now - _CMUX_GIT_JOB_STARTED_AT >= _CMUX_ASYNC_JOB_TIMEOUT",
|
||||
"bash integration no longer clears stale git probe PID after timeout",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
bash_content,
|
||||
"now - _CMUX_PR_JOB_STARTED_AT >= _CMUX_ASYNC_JOB_TIMEOUT",
|
||||
"bash integration no longer clears stale PR probe PID after timeout",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
bash_content,
|
||||
"ncat -w 1 -U \"$CMUX_SOCKET_PATH\" --send-only",
|
||||
"bash integration missing ncat socket timeout",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
bash_content,
|
||||
"socat -T 1 - \"UNIX-CONNECT:$CMUX_SOCKET_PATH\"",
|
||||
"bash integration missing socat socket timeout",
|
||||
failures,
|
||||
)
|
||||
|
||||
require(
|
||||
app_delegate,
|
||||
"NSWorkspace.didWakeNotification",
|
||||
"AppDelegate is missing wake observer for socket listener recovery",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
app_delegate,
|
||||
"restartSocketListenerIfEnabled(source: \"workspace.didWake\")",
|
||||
"Wake observer no longer re-arms the socket listener",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
app_delegate,
|
||||
"private func restartSocketListenerIfEnabled(source: String)",
|
||||
"Missing shared socket-listener restart helper",
|
||||
failures,
|
||||
)
|
||||
|
||||
if failures:
|
||||
print("FAIL: issue #494 regression(s) detected")
|
||||
for failure in failures:
|
||||
print(f"- {failure}")
|
||||
return 1
|
||||
|
||||
print("PASS: issue #494 sleep/wake recovery guards are present")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -1,126 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Regression guard for issue #582 (sidebar git branch updates stalling)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_repo_root() -> Path:
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "--show-toplevel"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return Path(result.stdout.strip())
|
||||
return Path.cwd()
|
||||
|
||||
|
||||
def extract_function(content: str, signature: str) -> str:
|
||||
start = content.find(signature)
|
||||
if start < 0:
|
||||
return ""
|
||||
brace = content.find("{", start)
|
||||
if brace < 0:
|
||||
return ""
|
||||
depth = 0
|
||||
for idx in range(brace, len(content)):
|
||||
ch = content[idx]
|
||||
if ch == "{":
|
||||
depth += 1
|
||||
elif ch == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return content[start : idx + 1]
|
||||
return ""
|
||||
|
||||
|
||||
def require(content: str, needle: str, message: str, failures: list[str]) -> None:
|
||||
if needle not in content:
|
||||
failures.append(message)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
repo_root = get_repo_root()
|
||||
terminal_controller_path = repo_root / "Sources" / "TerminalController.swift"
|
||||
if not terminal_controller_path.exists():
|
||||
print(f"Missing expected file: {terminal_controller_path}")
|
||||
return 1
|
||||
|
||||
terminal_controller = terminal_controller_path.read_text(encoding="utf-8")
|
||||
report_body = extract_function(terminal_controller, "private func reportGitBranch(_ args: String) -> String")
|
||||
clear_body = extract_function(terminal_controller, "private func clearGitBranch(_ args: String) -> String")
|
||||
|
||||
failures: list[str] = []
|
||||
|
||||
if not report_body:
|
||||
failures.append("Unable to locate reportGitBranch implementation")
|
||||
if not clear_body:
|
||||
failures.append("Unable to locate clearGitBranch implementation")
|
||||
|
||||
if report_body:
|
||||
require(
|
||||
report_body,
|
||||
"if let scope = Self.explicitSocketScope(options: parsed.options)",
|
||||
"reportGitBranch is missing explicit-scope fast path",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
report_body,
|
||||
"DispatchQueue.main.async",
|
||||
"reportGitBranch no longer schedules explicit-scope updates with main.async",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
report_body,
|
||||
"tab.updatePanelGitBranch(panelId: scope.panelId",
|
||||
"reportGitBranch fast path no longer writes branch state to the scoped panel",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
report_body,
|
||||
"DispatchQueue.main.sync",
|
||||
"reportGitBranch lost sync fallback path for non-explicit/manual calls",
|
||||
failures,
|
||||
)
|
||||
|
||||
if clear_body:
|
||||
require(
|
||||
clear_body,
|
||||
"if let scope = Self.explicitSocketScope(options: parsed.options)",
|
||||
"clearGitBranch is missing explicit-scope fast path",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
clear_body,
|
||||
"DispatchQueue.main.async",
|
||||
"clearGitBranch no longer schedules explicit-scope clears with main.async",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
clear_body,
|
||||
"tab.clearPanelGitBranch(panelId: scope.panelId)",
|
||||
"clearGitBranch fast path no longer clears branch state for the scoped panel",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
clear_body,
|
||||
"DispatchQueue.main.sync",
|
||||
"clearGitBranch lost sync fallback path for non-explicit/manual calls",
|
||||
failures,
|
||||
)
|
||||
|
||||
if failures:
|
||||
print("FAIL: issue #582 regression(s) detected")
|
||||
for failure in failures:
|
||||
print(f"- {failure}")
|
||||
return 1
|
||||
|
||||
print("PASS: issue #582 git branch socket fast path guards are present")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Regression guard for issue #666 (sidebar branch stuck after checkout)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_repo_root() -> Path:
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "--show-toplevel"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return Path(result.stdout.strip())
|
||||
return Path.cwd()
|
||||
|
||||
|
||||
def require(content: str, needle: str, message: str, failures: list[str]) -> None:
|
||||
if needle not in content:
|
||||
failures.append(message)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
repo_root = get_repo_root()
|
||||
zsh_path = repo_root / "Resources" / "shell-integration" / "cmux-zsh-integration.zsh"
|
||||
bash_path = repo_root / "Resources" / "shell-integration" / "cmux-bash-integration.bash"
|
||||
|
||||
required_paths = [zsh_path, bash_path]
|
||||
missing_paths = [str(path) for path in required_paths if not path.exists()]
|
||||
if missing_paths:
|
||||
print("Missing expected files:")
|
||||
for path in missing_paths:
|
||||
print(f" - {path}")
|
||||
return 1
|
||||
|
||||
zsh_content = zsh_path.read_text(encoding="utf-8")
|
||||
bash_content = bash_path.read_text(encoding="utf-8")
|
||||
|
||||
failures: list[str] = []
|
||||
|
||||
require(
|
||||
zsh_content,
|
||||
"_CMUX_GIT_HEAD_SIGNATURE",
|
||||
"zsh integration is missing git HEAD signature tracking",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
zsh_content,
|
||||
"_cmux_git_head_signature",
|
||||
"zsh integration is missing git HEAD signature helper",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
zsh_content,
|
||||
'"$head_signature" != "$_CMUX_GIT_HEAD_SIGNATURE"',
|
||||
"zsh integration no longer compares git HEAD signatures",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
zsh_content,
|
||||
"_CMUX_GIT_FORCE=1",
|
||||
"zsh integration no longer forces git probe refresh on HEAD changes",
|
||||
failures,
|
||||
)
|
||||
|
||||
require(
|
||||
bash_content,
|
||||
"_CMUX_GIT_HEAD_SIGNATURE",
|
||||
"bash integration is missing git HEAD signature tracking",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
bash_content,
|
||||
"_cmux_git_head_signature",
|
||||
"bash integration is missing git HEAD signature helper",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
bash_content,
|
||||
"git_head_changed=1",
|
||||
"bash integration no longer flags HEAD changes for immediate refresh",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
bash_content,
|
||||
'|| "$git_head_changed" == "1"',
|
||||
"bash integration no longer restarts running git probes on HEAD change",
|
||||
failures,
|
||||
)
|
||||
|
||||
if failures:
|
||||
print("FAIL: issue #666 regression(s) detected")
|
||||
for failure in failures:
|
||||
print(f"- {failure}")
|
||||
return 1
|
||||
|
||||
print("PASS: issue #666 checkout refresh guards are present")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -1,162 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Regression guard for issue #952 (flaky CLI socket connections)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_repo_root() -> Path:
|
||||
"""Return the repository root for source inspections."""
|
||||
fallback_root = Path(__file__).resolve().parents[1]
|
||||
git_path = shutil.which("git")
|
||||
if git_path is None:
|
||||
return fallback_root
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[git_path, "rev-parse", "--show-toplevel"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
except OSError:
|
||||
return fallback_root
|
||||
if result.returncode == 0:
|
||||
return Path(result.stdout.strip())
|
||||
return fallback_root
|
||||
|
||||
|
||||
def require(content: str, needle: str, message: str, failures: list[str], *, regex: bool = False) -> None:
|
||||
"""Record a failure when a required source pattern is missing."""
|
||||
matched = re.search(needle, content, re.MULTILINE) is not None if regex else needle in content
|
||||
if not matched:
|
||||
failures.append(message)
|
||||
|
||||
|
||||
def collect_failures() -> list[str]:
|
||||
"""Collect missing source-level guards for the socket listener recovery fix."""
|
||||
repo_root = get_repo_root()
|
||||
terminal_controller_path = repo_root / "Sources" / "TerminalController.swift"
|
||||
app_delegate_path = repo_root / "Sources" / "AppDelegate.swift"
|
||||
failures: list[str] = []
|
||||
|
||||
missing_paths = [
|
||||
str(path) for path in [terminal_controller_path, app_delegate_path] if not path.exists()
|
||||
]
|
||||
if missing_paths:
|
||||
for path in missing_paths:
|
||||
failures.append(f"Missing expected file: {path}")
|
||||
return failures
|
||||
|
||||
terminal_controller = terminal_controller_path.read_text(encoding="utf-8")
|
||||
app_delegate = app_delegate_path.read_text(encoding="utf-8")
|
||||
|
||||
require(
|
||||
terminal_controller,
|
||||
"let socketProbePerformed: Bool",
|
||||
"Socket health snapshot no longer tracks whether connectability was probed",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
terminal_controller,
|
||||
"let socketConnectable: Bool?",
|
||||
"Socket health snapshot no longer distinguishes unprobed vs connectable sockets",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
terminal_controller,
|
||||
"let socketConnectErrno: Int32?",
|
||||
"Socket health snapshot no longer preserves probe errno",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
terminal_controller,
|
||||
"signals.append(\"socket_unreachable\")",
|
||||
"Socket health failures no longer flag unreachable listeners",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
terminal_controller,
|
||||
r"private\s+nonisolated\s+static\s+func\s+probeSocketConnectability\s*\(\s*path:\s*String\s*\)",
|
||||
"Missing active socket connectability probe helper",
|
||||
failures,
|
||||
regex=True,
|
||||
)
|
||||
require(
|
||||
terminal_controller,
|
||||
r"connect\s*\(\s*probeSocket\s*,\s*sockaddrPtr\s*,\s*socklen_t\s*\(\s*MemoryLayout<sockaddr_un>\.size\s*\)\s*\)",
|
||||
"Socket health probe no longer performs a real connect() check",
|
||||
failures,
|
||||
regex=True,
|
||||
)
|
||||
require(
|
||||
terminal_controller,
|
||||
"stage: \"bind_path_too_long\"",
|
||||
"Socket listener start no longer reports overlong Unix socket paths",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
terminal_controller,
|
||||
"Self.unixSocketPathMaxLength",
|
||||
"Socket listener path-length telemetry was removed",
|
||||
failures,
|
||||
)
|
||||
|
||||
require(
|
||||
app_delegate,
|
||||
"private static let socketListenerHealthCheckInterval: DispatchTimeInterval = .seconds(2)",
|
||||
"Socket health timer interval drifted from the fast recovery setting",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
app_delegate,
|
||||
"\"socketProbePerformed\": health.socketProbePerformed ? 1 : 0",
|
||||
"Health telemetry no longer records whether a connectability probe ran",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
app_delegate,
|
||||
"if let socketConnectable = health.socketConnectable {",
|
||||
"Health telemetry no longer gates connectability on an actual probe result",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
app_delegate,
|
||||
"data[\"socketConnectable\"] = socketConnectable ? 1 : 0",
|
||||
"Health telemetry no longer includes connectability when a probe ran",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
app_delegate,
|
||||
"if let socketConnectErrno = health.socketConnectErrno {",
|
||||
"Health telemetry no longer records connect probe errno when available",
|
||||
failures,
|
||||
)
|
||||
return failures
|
||||
|
||||
|
||||
def test_issue_952_socket_listener_recovery() -> None:
|
||||
"""Keep the source-level recovery guards for issue #952 in CI."""
|
||||
failures = collect_failures()
|
||||
assert not failures, "issue #952 regression(s) detected:\n- " + "\n- ".join(failures)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""Run the regression guard without requiring pytest to be installed."""
|
||||
failures = collect_failures()
|
||||
if failures:
|
||||
print("FAIL: issue #952 regression(s) detected")
|
||||
for failure in failures:
|
||||
print(f"- {failure}")
|
||||
return 1
|
||||
|
||||
print("PASS: issue #952 socket listener recovery guards are present")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -1,190 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Lint test to catch SwiftUI patterns that cause performance issues.
|
||||
|
||||
This test checks for:
|
||||
1. Text(_:style:) with auto-updating date styles (.time, .timer, .relative)
|
||||
These cause continuous view updates and can lead to high CPU usage.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import List, Tuple
|
||||
|
||||
|
||||
def get_repo_root():
|
||||
"""Get the repository root directory."""
|
||||
# Try git first
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "--show-toplevel"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return Path(result.stdout.strip())
|
||||
|
||||
# Fall back to finding GhosttyTabs directory
|
||||
cwd = Path.cwd()
|
||||
if cwd.name == "GhosttyTabs" or (cwd / "Sources").exists():
|
||||
return cwd
|
||||
if (cwd.parent / "GhosttyTabs").exists():
|
||||
return cwd.parent / "GhosttyTabs"
|
||||
|
||||
# Last resort: use current directory
|
||||
return cwd
|
||||
|
||||
|
||||
def find_swift_files(repo_root: Path) -> List[Path]:
|
||||
"""Find all Swift files in Sources directory (excluding vendored code)."""
|
||||
sources_dir = repo_root / "Sources"
|
||||
if not sources_dir.exists():
|
||||
return []
|
||||
return list(sources_dir.rglob("*.swift"))
|
||||
|
||||
|
||||
def check_autoupdating_text_styles(files: List[Path]) -> List[Tuple[Path, int, str]]:
|
||||
"""
|
||||
Check for Text(_:style:) with auto-updating date styles.
|
||||
|
||||
These patterns cause continuous SwiftUI view updates:
|
||||
- Text(date, style: .time) - updates every second/minute
|
||||
- Text(date, style: .timer) - updates continuously
|
||||
- Text(date, style: .relative) - updates periodically
|
||||
- Text(date, style: .offset) - updates periodically
|
||||
|
||||
Instead, use static formatting:
|
||||
- Text(date.formatted(date: .omitted, time: .shortened))
|
||||
"""
|
||||
violations = []
|
||||
|
||||
# Patterns that indicate auto-updating Text with Date
|
||||
# The key patterns are: Text(something, style: .time/timer/relative/offset)
|
||||
problematic_patterns = [
|
||||
"style: .time",
|
||||
"style: .timer",
|
||||
"style: .relative",
|
||||
"style: .offset",
|
||||
"style:.time",
|
||||
"style:.timer",
|
||||
"style:.relative",
|
||||
"style:.offset",
|
||||
]
|
||||
|
||||
for file_path in files:
|
||||
try:
|
||||
content = file_path.read_text()
|
||||
lines = content.split('\n')
|
||||
|
||||
for line_num, line in enumerate(lines, start=1):
|
||||
# Skip comments
|
||||
stripped = line.strip()
|
||||
if stripped.startswith("//"):
|
||||
continue
|
||||
|
||||
for pattern in problematic_patterns:
|
||||
if pattern in line:
|
||||
violations.append((file_path, line_num, line.strip()))
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not read {file_path}: {e}", file=sys.stderr)
|
||||
|
||||
return violations
|
||||
|
||||
|
||||
def check_command_palette_caret_tint(repo_root: Path) -> List[str]:
|
||||
"""Ensure command palette text inputs keep a white caret tint."""
|
||||
content_view = repo_root / "Sources" / "ContentView.swift"
|
||||
if not content_view.exists():
|
||||
return [f"Missing expected file: {content_view}"]
|
||||
|
||||
try:
|
||||
content = content_view.read_text()
|
||||
except Exception as e:
|
||||
return [f"Could not read {content_view}: {e}"]
|
||||
|
||||
checks = [
|
||||
(
|
||||
"search input",
|
||||
r"TextField\(commandPaletteSearchPlaceholder, text: \$commandPaletteQuery\)(?P<body>.*?)"
|
||||
r"\.focused\(\$isCommandPaletteSearchFocused\)",
|
||||
),
|
||||
(
|
||||
"rename input",
|
||||
r"TextField\(target\.placeholder, text: \$commandPaletteRenameDraft\)(?P<body>.*?)"
|
||||
r"\.focused\(\$isCommandPaletteRenameFocused\)",
|
||||
),
|
||||
]
|
||||
|
||||
violations: List[str] = []
|
||||
for label, pattern in checks:
|
||||
match = re.search(pattern, content, flags=re.DOTALL)
|
||||
if not match:
|
||||
violations.append(
|
||||
f"Could not locate command palette {label} TextField block in Sources/ContentView.swift"
|
||||
)
|
||||
continue
|
||||
|
||||
body = match.group("body")
|
||||
if ".tint(.white)" not in body:
|
||||
violations.append(
|
||||
f"Command palette {label} TextField must use `.tint(.white)` in Sources/ContentView.swift"
|
||||
)
|
||||
|
||||
return violations
|
||||
|
||||
|
||||
def main():
|
||||
"""Run the lint checks."""
|
||||
repo_root = get_repo_root()
|
||||
swift_files = find_swift_files(repo_root)
|
||||
|
||||
print(f"Checking {len(swift_files)} Swift files for performance issues...")
|
||||
|
||||
# Check for auto-updating Text styles
|
||||
style_violations = check_autoupdating_text_styles(swift_files)
|
||||
tint_violations = check_command_palette_caret_tint(repo_root)
|
||||
has_failures = False
|
||||
|
||||
if style_violations:
|
||||
has_failures = True
|
||||
print("\n❌ LINT FAILURES: Auto-updating Text styles found")
|
||||
print("=" * 60)
|
||||
print("These patterns cause continuous SwiftUI view updates and high CPU usage:")
|
||||
print()
|
||||
|
||||
for file_path, line_num, line in style_violations:
|
||||
rel_path = file_path.relative_to(repo_root)
|
||||
print(f" {rel_path}:{line_num}")
|
||||
print(f" {line}")
|
||||
print()
|
||||
|
||||
print("FIX: Replace with static formatting:")
|
||||
print(" Instead of: Text(date, style: .time)")
|
||||
print(" Use: Text(date.formatted(date: .omitted, time: .shortened))")
|
||||
print()
|
||||
|
||||
if tint_violations:
|
||||
has_failures = True
|
||||
print("\n❌ LINT FAILURES: Command palette caret tint drifted")
|
||||
print("=" * 60)
|
||||
print("The command palette search and rename text fields must keep a white caret:")
|
||||
print()
|
||||
for message in tint_violations:
|
||||
print(f" {message}")
|
||||
print()
|
||||
print("FIX: Set command palette TextField tint modifiers to `.white`.")
|
||||
print()
|
||||
|
||||
if has_failures:
|
||||
return 1
|
||||
|
||||
print("✅ No linted SwiftUI pattern regressions found")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
|
@ -1,126 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Regression tests for markdown-open CLI parsing/help behavior."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_repo_root() -> Path:
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "--show-toplevel"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return Path(result.stdout.strip())
|
||||
return Path.cwd()
|
||||
|
||||
|
||||
def require(content: str, needle: str, message: str, failures: list[str]) -> None:
|
||||
if needle not in content:
|
||||
failures.append(message)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
repo_root = get_repo_root()
|
||||
cli_path = repo_root / "CLI" / "cmux.swift"
|
||||
panel_path = repo_root / "Sources" / "Panels" / "MarkdownPanel.swift"
|
||||
|
||||
if not cli_path.exists():
|
||||
print(f"FAIL: missing expected file: {cli_path}")
|
||||
return 1
|
||||
if not panel_path.exists():
|
||||
print(f"FAIL: missing expected file: {panel_path}")
|
||||
return 1
|
||||
|
||||
cli_content = cli_path.read_text(encoding="utf-8")
|
||||
panel_content = panel_path.read_text(encoding="utf-8")
|
||||
failures: list[str] = []
|
||||
|
||||
# CLI parser behavior.
|
||||
require(
|
||||
cli_content,
|
||||
'if let first = args.first, first.lowercased() == "open" {',
|
||||
"markdown parser should explicitly support the 'open' subcommand",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
cli_content,
|
||||
"args.count == 1",
|
||||
"markdown parser should accept single-arg shorthand path",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
cli_content,
|
||||
"args.count == 1, let first = args.first, !first.hasPrefix(\"-\")",
|
||||
"markdown parser should reject option-like single args from shorthand path mode",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
cli_content,
|
||||
"let trailingArgs = Array(subArgs.dropFirst())",
|
||||
"markdown parser should validate trailing arguments",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
cli_content,
|
||||
'trailingArgs.first(where: { $0.hasPrefix("-") })',
|
||||
"markdown parser should detect unknown trailing flags",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
cli_content,
|
||||
"markdown open: unexpected argument",
|
||||
"markdown parser should error on unexpected trailing args",
|
||||
failures,
|
||||
)
|
||||
|
||||
# Help text should document shorthand and full index handle support.
|
||||
require(
|
||||
cli_content,
|
||||
"Usage: cmux markdown open <path> [options]\n cmux markdown <path> (shorthand for 'open')",
|
||||
"markdown subcommand help should include shorthand usage",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
cli_content,
|
||||
"--window <id|ref|index> Target window",
|
||||
"markdown subcommand help should document window index handles",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
cli_content,
|
||||
"markdown [open] <path> (open markdown file in formatted viewer panel with live reload)",
|
||||
"top-level help should include markdown shorthand syntax",
|
||||
failures,
|
||||
)
|
||||
|
||||
# Session restore edge case: file missing at startup should still attempt reconnect.
|
||||
require(
|
||||
panel_content,
|
||||
"if isFileUnavailable && fileWatchSource == nil {",
|
||||
"MarkdownPanel should schedule reattach when watcher cannot start at init",
|
||||
failures,
|
||||
)
|
||||
require(
|
||||
panel_content,
|
||||
"scheduleReattach(attempt: 1)",
|
||||
"MarkdownPanel should trigger reattach retries for missing files",
|
||||
failures,
|
||||
)
|
||||
|
||||
if failures:
|
||||
print("FAIL: markdown-open regression(s) detected")
|
||||
for failure in failures:
|
||||
print(f"- {failure}")
|
||||
return 1
|
||||
|
||||
print("PASS: markdown-open CLI/help/reattach regression checks are present")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Regression test: cmux advertises media-capture access metadata."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import plistlib
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_repo_root() -> Path:
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "--show-toplevel"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return Path(result.stdout.strip())
|
||||
return Path.cwd()
|
||||
|
||||
|
||||
def load_plist(path: Path, failures: list[str]) -> dict:
|
||||
if not path.exists():
|
||||
failures.append(f"Missing expected file: {path}")
|
||||
return {}
|
||||
with path.open("rb") as f:
|
||||
return plistlib.load(f)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
repo_root = get_repo_root()
|
||||
failures: list[str] = []
|
||||
|
||||
info = load_plist(repo_root / "Resources" / "Info.plist", failures)
|
||||
entitlements = load_plist(repo_root / "cmux.entitlements", failures)
|
||||
|
||||
mic_usage = info.get("NSMicrophoneUsageDescription")
|
||||
camera_usage = info.get("NSCameraUsageDescription")
|
||||
if not isinstance(mic_usage, str) or not mic_usage.strip():
|
||||
failures.append(
|
||||
"Resources/Info.plist must define a non-empty NSMicrophoneUsageDescription"
|
||||
)
|
||||
elif mic_usage.strip() != "A program running within cmux would like to use your microphone.":
|
||||
failures.append(
|
||||
"Resources/Info.plist NSMicrophoneUsageDescription should match the Ghostty-style wording"
|
||||
)
|
||||
|
||||
if entitlements.get("com.apple.security.device.audio-input") is not True:
|
||||
failures.append(
|
||||
"cmux.entitlements must set com.apple.security.device.audio-input to true"
|
||||
)
|
||||
|
||||
if not isinstance(camera_usage, str) or not camera_usage.strip():
|
||||
failures.append(
|
||||
"Resources/Info.plist must define a non-empty NSCameraUsageDescription"
|
||||
)
|
||||
elif camera_usage.strip() != "A program running within cmux would like to use your camera.":
|
||||
failures.append(
|
||||
"Resources/Info.plist NSCameraUsageDescription should match the Ghostty-style wording"
|
||||
)
|
||||
|
||||
if entitlements.get("com.apple.security.device.camera") is not True:
|
||||
failures.append(
|
||||
"cmux.entitlements must set com.apple.security.device.camera to true"
|
||||
)
|
||||
|
||||
if failures:
|
||||
print("FAIL: media-capture metadata regression(s) detected")
|
||||
for failure in failures:
|
||||
print(f"- {failure}")
|
||||
return 1
|
||||
|
||||
print("PASS: microphone/camera usage descriptions and entitlements are present")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Regression test for the default sidebar active workspace indicator style.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_repo_root() -> Path:
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "--show-toplevel"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return Path(result.stdout.strip())
|
||||
return Path.cwd()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
repo_root = get_repo_root()
|
||||
tab_manager = repo_root / "Sources" / "TabManager.swift"
|
||||
|
||||
if not tab_manager.exists():
|
||||
print(f"FAIL: Missing file {tab_manager}")
|
||||
return 1
|
||||
|
||||
content = tab_manager.read_text(encoding="utf-8")
|
||||
pattern = r"static let defaultStyle:\s*SidebarActiveTabIndicatorStyle\s*=\s*\.leftRail\b"
|
||||
|
||||
if re.search(pattern, content) is None:
|
||||
rel = tab_manager.relative_to(repo_root)
|
||||
print(f"FAIL: Expected default style `.leftRail` in {rel}")
|
||||
return 1
|
||||
|
||||
print("PASS: sidebar indicator default style is left rail")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
|
@ -1,113 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Static regression checks for terminal tiny-pane resize/overflow fixes.
|
||||
|
||||
Guards the key invariants for issue #348:
|
||||
1) Terminal portal sync must stabilize layout and clamp hosted frames to host bounds.
|
||||
2) Surface sizing must prefer live bounds over stale pending values when available.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def repo_root() -> Path:
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "--show-toplevel"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return Path(result.stdout.strip())
|
||||
return Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def extract_block(source: str, signature: str) -> str:
|
||||
start = source.find(signature)
|
||||
if start < 0:
|
||||
raise ValueError(f"Missing signature: {signature}")
|
||||
brace_start = source.find("{", start)
|
||||
if brace_start < 0:
|
||||
raise ValueError(f"Missing opening brace for: {signature}")
|
||||
|
||||
depth = 0
|
||||
for idx in range(brace_start, len(source)):
|
||||
char = source[idx]
|
||||
if char == "{":
|
||||
depth += 1
|
||||
elif char == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return source[brace_start : idx + 1]
|
||||
raise ValueError(f"Unbalanced braces for: {signature}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
root = repo_root()
|
||||
failures: list[str] = []
|
||||
|
||||
portal_path = root / "Sources" / "TerminalWindowPortal.swift"
|
||||
portal_source = portal_path.read_text(encoding="utf-8")
|
||||
|
||||
if "hostView.layer?.masksToBounds = true" not in portal_source:
|
||||
failures.append("WindowTerminalPortal init no longer enables hostView layer clipping")
|
||||
if "hostView.postsFrameChangedNotifications = true" not in portal_source:
|
||||
failures.append("WindowTerminalPortal init no longer enables hostView frame-change notifications")
|
||||
if "hostView.postsBoundsChangedNotifications = true" not in portal_source:
|
||||
failures.append("WindowTerminalPortal init no longer enables hostView bounds-change notifications")
|
||||
|
||||
if "private func synchronizeLayoutHierarchy()" not in portal_source:
|
||||
failures.append("WindowTerminalPortal missing synchronizeLayoutHierarchy()")
|
||||
if "private func synchronizeHostFrameToReference() -> Bool" not in portal_source:
|
||||
failures.append("WindowTerminalPortal missing synchronizeHostFrameToReference()")
|
||||
if "hostedView.reconcileGeometryNow()" not in extract_block(
|
||||
portal_source,
|
||||
"func bind(hostedView: GhosttySurfaceScrollView, to anchorView: NSView, visibleInUI: Bool, zPriority: Int = 0)",
|
||||
):
|
||||
failures.append("bind() no longer pre-reconciles hosted geometry before attach")
|
||||
|
||||
sync_block = extract_block(portal_source, "private func synchronizeHostedView(withId hostedId: ObjectIdentifier)")
|
||||
for required in [
|
||||
"let hostBounds = hostView.bounds",
|
||||
"let clampedFrame = frameInHost.intersection(hostBounds)",
|
||||
"let targetFrame = (hasFiniteFrame && hasVisibleIntersection) ? clampedFrame : frameInHost",
|
||||
"hostedView.reconcileGeometryNow()",
|
||||
"hostedView.refreshSurfaceNow()",
|
||||
]:
|
||||
if required not in sync_block:
|
||||
failures.append(f"terminal portal sync missing: {required}")
|
||||
|
||||
if (
|
||||
"scheduleDeferredFullSynchronizeAll()" not in sync_block
|
||||
and "scheduleTransientRecoveryRetryIfNeeded(" not in sync_block
|
||||
):
|
||||
failures.append(
|
||||
"terminal portal sync no longer schedules deferred recovery for transient geometry states"
|
||||
)
|
||||
|
||||
terminal_view_path = root / "Sources" / "GhosttyTerminalView.swift"
|
||||
terminal_view_source = terminal_view_path.read_text(encoding="utf-8")
|
||||
|
||||
resolved_block = extract_block(terminal_view_source, "private func resolvedSurfaceSize(preferred size: CGSize?) -> CGSize")
|
||||
bounds_index = resolved_block.find("let currentBounds = bounds.size")
|
||||
pending_index = resolved_block.find("if let pending = pendingSurfaceSize")
|
||||
if bounds_index < 0 or pending_index < 0 or bounds_index > pending_index:
|
||||
failures.append("resolvedSurfaceSize() no longer prefers bounds before pendingSurfaceSize")
|
||||
|
||||
update_block = extract_block(terminal_view_source, "private func updateSurfaceSize(size: CGSize? = nil)")
|
||||
if "let size = resolvedSurfaceSize(preferred: size)" not in update_block:
|
||||
failures.append("updateSurfaceSize() no longer resolves size via resolvedSurfaceSize()")
|
||||
|
||||
if failures:
|
||||
print("FAIL: terminal resize/portal regression guards failed")
|
||||
for item in failures:
|
||||
print(f" - {item}")
|
||||
return 1
|
||||
|
||||
print("PASS: terminal resize/portal regression guards are in place")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Verify update UI timing constants so update indicators are visible long enough.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
TIMING_FILE = ROOT / "Sources" / "Update" / "UpdateTiming.swift"
|
||||
|
||||
|
||||
def read_constants(text: str) -> dict[str, float]:
|
||||
constants = {}
|
||||
pattern = re.compile(r"static let (\w+): TimeInterval = ([0-9.]+)")
|
||||
for match in pattern.finditer(text):
|
||||
constants[match.group(1)] = float(match.group(2))
|
||||
return constants
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if not TIMING_FILE.exists():
|
||||
print(f"Missing {TIMING_FILE}")
|
||||
return 1
|
||||
|
||||
constants = read_constants(TIMING_FILE.read_text())
|
||||
required = {
|
||||
"minimumCheckDisplayDuration": 2.0,
|
||||
"noUpdateDisplayDuration": 5.0,
|
||||
}
|
||||
|
||||
failures = []
|
||||
for name, expected in required.items():
|
||||
actual = constants.get(name)
|
||||
if actual is None:
|
||||
failures.append(f"{name} missing")
|
||||
continue
|
||||
if actual != expected:
|
||||
failures.append(f"{name} = {actual} (expected {expected})")
|
||||
|
||||
if failures:
|
||||
print("Update timing test failed:")
|
||||
for failure in failures:
|
||||
print(f" - {failure}")
|
||||
return 1
|
||||
|
||||
print("Update timing test passed.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Loading…
Add table
Add a link
Reference in a new issue