Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,12 @@ final class GhosttyRuntime: @unchecked Sendable {
return Unmanaged<GhosttyTerminalNSView>.fromOpaque(userdata).takeUnretainedValue()
}

/// Who actually opens an intercepted link. Tests swap in a recorder so a URL no
/// application claims (the test-only `x-graphcode-test://` scheme) never reaches
/// LaunchServices — with the real opener, every `xcodebuild test` run popped the
/// "There is no application set to open the URL" dialog.
static var openURL: (URL) -> Void = { NSWorkspace.shared.open($0) }

/// The one action graphcode handles: a ⌘-clicked link. Everything else returns
/// false and libghostty's defaults apply — the same posture as before, just no
/// longer for URLs, because who opens a link is where remote sign-ins are won.
Expand Down Expand Up @@ -184,7 +190,7 @@ final class GhosttyRuntime: @unchecked Sendable {
Task { await RemoteAuthPortForwarder.shared.ensureForwarding(port: port, to: location) }
}
}
DispatchQueue.main.async { NSWorkspace.shared.open(url) }
DispatchQueue.main.async { Self.openURL(url) }
return true
}

Expand Down
17 changes: 14 additions & 3 deletions graphcode/Tests/AuthPortForwardingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Foundation
import GhosttyKit
import GraphcodeKit
import Testing
import os

@testable import graphcode

Expand Down Expand Up @@ -91,17 +92,27 @@ struct AuthPortForwardingTests {
}

@Test
func onlySchemeCarryingUnknownURLsAreIntercepted() {
func onlySchemeCarryingUnknownURLsAreIntercepted() async {
// ⌘⇧-click on a local folder's surface must not regress: a clicked *file path*
// (Ghostty hands over a bare absolute path for resolved links) and the
// editor-opening scrollback kinds return false, so libghostty's own opener runs
// exactly as it did before interception existed.
#expect(!linkOpenHandled("/Users/dev/notes.txt"))
#expect(!linkOpenHandled("https://example.com/dump", kind: GHOSTTY_ACTION_OPEN_URL_KIND_TEXT))
#expect(!linkOpenHandled("https://example.com/dump", kind: GHOSTTY_ACTION_OPEN_URL_KIND_HTML))
// A real link is ours — the interception the forwarder rides on. A test-only
// scheme, so the open attempt resolves to no application.
// A real link is ours — the interception the forwarder rides on. The opener is a
// test recorder, so the URL lands in a list instead of LaunchServices: nothing
// claims this scheme, and the real opener popped the "no application set to open
// the URL" dialog on every test run.
let opened = OSAllocatedUnfairLock(initialState: [URL]())
let systemOpener = GhosttyRuntime.openURL
GhosttyRuntime.openURL = { url in opened.withLock { $0.append(url) } }
defer { GhosttyRuntime.openURL = systemOpener }
#expect(linkOpenHandled("x-graphcode-test://sign-in"))
// The open is dispatched to the main queue; a sentinel enqueued after it runs
// once the recorded open has happened.
await MainActor.run {}
#expect(opened.withLock { $0 } == [URL(string: "x-graphcode-test://sign-in")!])
}

@Test
Expand Down
Loading