native macOS codings agent orchestrator prowl.onev.cat
Something went wrong. Try again.
16 kB · 434 lines
Swift
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435import Foundationimport Testing
@testable import supacode
/// Terminal-layer behavior of `launchAgentProfile` that is testable without a/// live Ghostty surface (docs-ai 053/005).@MainActorstruct WorktreeTerminalStateAgentProfileTests { @Test func provisionFailureAbortsTheLaunchEntirely() { let state = makeState() // A dedicated home outside the profile-home base fails the containment // gate before any surface is created. let plan = makePlan( dedicatedHome: URL(filePath: "/tmp/prowl-test-outside-base/home", directoryHint: .isDirectory) ) let request = AgentProfileLaunchRequest( plan: plan, placement: .tab(background: false) )
let result = state.launchAgentProfile(request)
#expect(result == .failure(.homeProvisioningFailed)) #expect(state.tabManager.tabs.isEmpty) #expect(state.launchProfilesBySurface.isEmpty) }
@Test func explicitAnchorWinsOverCurrentSelectionAndReturnsBothIdentities() throws { let state = makeState() let anchor = try state.launchAgentProfile( AgentProfileLaunchRequest( plan: makePlan(dedicatedHome: nil), placement: .tab(background: false) ) ).get() let selected = try state.launchAgentProfile( AgentProfileLaunchRequest( plan: makePlan(dedicatedHome: nil), placement: .tab(background: false) ) ).get() #expect(state.tabManager.selectedTabId == selected.tabID)
let launched = try state.launchAgentProfile( AgentProfileLaunchRequest( plan: makePlan(dedicatedHome: nil, runtime: .claude), placement: .split(anchor: anchor.surfaceID, direction: .left, background: false) ) ).get()
#expect(launched.tabID == anchor.tabID) #expect(launched.surfaceID != anchor.surfaceID) #expect(state.tabID(containing: launched.surfaceID) == anchor.tabID) #expect(state.focusedSurfaceId(in: anchor.tabID) == launched.surfaceID) #expect(state.launchProfilesBySurface[launched.surfaceID]?.runtime == .claude) }
@Test func requestPlacementOverridesTheProfilePlan() throws { let state = makeState() let anchor = try state.launchAgentProfile( AgentProfileLaunchRequest( plan: makePlan(dedicatedHome: nil), placement: .tab(background: false) ) ).get() let plan = makePlan(dedicatedHome: nil, runtime: .claude, placement: .tab)
let launched = try state.launchAgentProfile( AgentProfileLaunchRequest( plan: plan, placement: .split(anchor: anchor.surfaceID, direction: .down, background: false) ) ).get()
#expect(state.tabManager.tabs.count == 1) #expect(launched.tabID == anchor.tabID) #expect(state.tabID(containing: launched.surfaceID) == anchor.tabID) }
@Test func backgroundTabPreservesTheSelectedTabAndFocusedSurface() throws { let state = makeState() let foreground = try state.launchAgentProfile( AgentProfileLaunchRequest( plan: makePlan(dedicatedHome: nil), placement: .tab(background: false) ) ).get()
let background = try state.launchAgentProfile( AgentProfileLaunchRequest( plan: makePlan(dedicatedHome: nil, runtime: .claude), placement: .tab(background: true) ) ).get()
#expect(background.tabID != foreground.tabID) #expect(background.surfaceID != foreground.surfaceID) #expect(state.tabManager.selectedTabId == foreground.tabID) #expect(state.currentFocusedSurfaceId() == foreground.surfaceID) #expect(state.tabID(containing: background.surfaceID) == background.tabID) }
@Test func backgroundSplitPreservesTheAnchorFocus() throws { let state = makeState() let anchor = try state.launchAgentProfile( AgentProfileLaunchRequest( plan: makePlan(dedicatedHome: nil), placement: .tab(background: false) ) ).get()
let background = try state.launchAgentProfile( AgentProfileLaunchRequest( plan: makePlan(dedicatedHome: nil, runtime: .claude), placement: .split(anchor: anchor.surfaceID, direction: .right, background: true) ) ).get()
#expect(background.tabID == anchor.tabID) #expect(state.focusedSurfaceId(in: anchor.tabID) == anchor.surfaceID) #expect(state.currentFocusedSurfaceId() == anchor.surfaceID) }
@Test func frozenDynamicProfileContextRejectsFocusAndInheritedCWDDrift() throws { let state = makeState() let first = try state.launchAgentProfile( AgentProfileLaunchRequest( plan: makePlan(dedicatedHome: nil), placement: .tab(background: false) ) ).get() _ = try state.launchAgentProfile( AgentProfileLaunchRequest( plan: makePlan(dedicatedHome: nil), placement: .tab(background: false) ) ).get() let request = AgentProfileLaunchRequest( plan: makePlan(dedicatedHome: nil), placement: .tab(background: false) ) let frozen = try state.freezeAgentProfileLaunchContext(request).get() #expect(state.isAgentProfileLaunchContextValid(frozen))
#expect(state.focusSurface(id: first.surfaceID)) #expect(!state.isAgentProfileLaunchContextValid(frozen)) let cwdOnly = FrozenAgentProfileLaunchContext( request: frozen.request, inheritedCWD: frozen.inheritedCWD, anchorSurfaceID: frozen.anchorSurfaceID, tracksFocusedAnchor: false, tracksInheritedCWD: true ) #expect( !state.isAgentProfileLaunchContextValid( cwdOnly, inheritedCWDOverride: URL(filePath: "/tmp/repo/changed", directoryHint: .isDirectory) ) ) }
@Test func explicitSplitFailureDoesNotFallBackToATab() { let state = makeState() let missingAnchor = UUID()
let result = state.launchAgentProfile( AgentProfileLaunchRequest( plan: makePlan(dedicatedHome: nil, placement: .split), placement: .split(anchor: missingAnchor, direction: .right, background: false) ) )
#expect(result == .failure(.splitCreationFailed(.anchorNotFound(missingAnchor)))) #expect(state.tabManager.tabs.isEmpty) }
@Test func compatibilityWrapperKeepsSplitToTabFallback() throws { let state = makeState()
let surfaceID = try #require( state.launchAgentProfile( makePlan(dedicatedHome: nil, placement: .split) ) )
#expect(state.tabManager.tabs.count == 1) #expect(state.tabID(containing: surfaceID) == state.tabManager.tabs.first?.id) }
@Test func surfaceIsInstalledAndRegisteredBeforeProfileInputIsArmed() throws { let state = makeState() var callbackSurfaceID: UUID? var callbackObservedInstalledSurface = false var callbackObservedUnarmedSurface = false state.onAgentProfileSurfacePrepared = { surfaceID, _ in callbackSurfaceID = surfaceID callbackObservedInstalledSurface = state.surfaceView(for: surfaceID) != nil && state.launchProfilesBySurface[surfaceID] != nil callbackObservedUnarmedSurface = state.surfaceView(for: surfaceID)?.surfaceCreationArmed == false return true }
let launched = try state.launchAgentProfile( AgentProfileLaunchRequest( plan: makePlan(dedicatedHome: nil), placement: .tab(background: false) ) ).get()
#expect(callbackSurfaceID == launched.surfaceID) #expect(callbackObservedInstalledSurface) #expect(callbackObservedUnarmedSurface) #expect(state.surfaceView(for: launched.surfaceID)?.surfaceCreationArmed == true) }
@Test func deferredProfileAppliesFontSizeAdjustmentAfterSurfaceCreation() throws { let state = makeState( skipsSurfaceCreationForTesting: false, defaultFontSize: 18 ) defer { state.cleanupAllAgentDetectionState() }
let launched = try state.launchAgentProfile( AgentProfileLaunchRequest( plan: makePlan(dedicatedHome: nil), placement: .tab(background: false) ) ).get()
let view = try #require(state.surfaceView(for: launched.surfaceID)) #expect(view.didApplyFontSizeAdjustmentMarker) }
@Test func deferredGhosttyCreationFailureRollsBackRegistrationAndSurface() { let state = makeState( skipsSurfaceCreationForTesting: false, failsSurfaceCreationForTesting: true ) var registeredSurface: UUID? var closedSurface: UUID? state.onAgentProfileSurfacePrepared = { surfaceID, _ in registeredSurface = surfaceID return true } state.onSurfaceClosed = { closedSurface = $0 }
let result = state.launchAgentProfile( AgentProfileLaunchRequest( plan: makePlan(dedicatedHome: nil), placement: .tab(background: false) ) )
#expect(result == .failure(.surfaceCreationFailed)) #expect(closedSurface == registeredSurface) #expect(state.tabManager.tabs.isEmpty) #expect(state.surfaces.isEmpty) #expect(state.launchProfilesBySurface.isEmpty) }
@Test func registrationFailureRollsBackBeforeLeavingALiveSurface() { let state = makeState() state.onAgentProfileSurfacePrepared = { _, _ in false }
let result = state.launchAgentProfile( AgentProfileLaunchRequest( plan: makePlan(dedicatedHome: nil), placement: .tab(background: false) ) )
#expect(result == .failure(.hookRegistrationFailed)) #expect(state.tabManager.tabs.isEmpty) #expect(state.surfaces.isEmpty) #expect(state.launchProfilesBySurface.isEmpty) }
@Test func launchProfileNameOnlyAppliesToTheLaunchedRuntime() { let state = makeState() let surfaceID = UUID() state.launchProfilesBySurface[surfaceID] = WorktreeTerminalState.SurfaceLaunchProfile( profileID: UUID(), name: "Codex · Work", runtime: .codex, dedicatedHome: nil )
#expect(state.launchProfileName(surfaceID: surfaceID, detected: .codex) == "Codex · Work") // A *different* agent started manually in the same pane must not wear the // old profile's name — same gating rule as `configRoot(forDetected:)`. #expect(state.launchProfileName(surfaceID: surfaceID, detected: .claude) == nil) #expect(state.launchProfileName(surfaceID: UUID(), detected: .codex) == nil) }
@Test func launchIdentityClearsWhenTheLaunchedAgentExits() { let state = makeState() let surfaceID = UUID() state.launchProfilesBySurface[surfaceID] = WorktreeTerminalState.SurfaceLaunchProfile( profileID: UUID(), name: "Codex · Work", runtime: .codex, dedicatedHome: nil ) state.surfaceAgentStates[surfaceID] = PaneAgentState(detectedAgent: .codex)
state.removeAgentEntryIfNeeded(surfaceID: surfaceID)
// The identity lives exactly as long as the launched agent: a manually // started agent afterwards is the user's own (docs-ai 053/006). #expect(state.launchProfilesBySurface[surfaceID] == nil) }
@Test func launchTabWearsTheRuntimeIconInsteadOfTheTerminalGlyph() { // No runtime falls back to the generic glyph: a new case added without a // `CommandIconMap` entry would otherwise launch unbranded and silently. for runtime in AgentProfileRuntime.allCases { #expect(WorktreeTerminalState.launchTabIcon(for: runtime) != nil, "\(runtime) has no icon") } #expect(WorktreeTerminalState.launchTabIcon(for: .claude)?.storageString != "terminal") #expect(WorktreeTerminalState.launchTabIcon(for: .codex)?.storageString != "terminal") #expect( WorktreeTerminalState.launchTabIcon(for: .claude)?.storageString != WorktreeTerminalState.launchTabIcon(for: .codex)?.storageString ) }
@Test func launchCreatesTheTabWithTheRuntimeIcon() { let state = makeState()
_ = state.launchAgentProfile(makePlan(dedicatedHome: nil))
// Guards the call site, not just the helper: a hardcoded glyph here is the // original bug, and it survives a helper-only assertion. #expect(state.tabManager.tabs.count == 1) #expect( state.tabManager.tabs.first?.icon == WorktreeTerminalState.launchTabIcon(for: .codex)?.storageString ) #expect(state.tabManager.tabs.first?.icon != "terminal") // Left claimable, so a later command in the same tab still wins the slot. #expect(state.tabManager.tabs.first?.iconLock == .auto) }
@Test func splitLaunchRebrandsTheContainingTab() throws { let state = makeState() _ = state.launchAgentProfile(makePlan(dedicatedHome: nil)) let tabID = try #require(state.tabManager.tabs.first?.id) state.tabManager.updateIcon(tabID, icon: "@asset:Git")
let surfaceID = state.launchAgentProfile( makePlan(dedicatedHome: nil, runtime: .claude, placement: .split) )
// The split becomes the focused surface, so its runtime owns the tab icon; // its `env …` title never reaches `CommandIconMap`. #expect(state.tabID(containing: try #require(surfaceID)) == tabID) #expect(state.tabManager.tabs.count == 1) #expect( state.tabManager.tabs.first?.icon == WorktreeTerminalState.launchTabIcon(for: .claude)?.storageString ) #expect(state.tabManager.tabs.first?.iconLock == .auto) }
@Test func splitLaunchLeavesAClaimedIconAlone() throws { let state = makeState() _ = state.launchAgentProfile(makePlan(dedicatedHome: nil)) let tabID = try #require(state.tabManager.tabs.first?.id) state.tabManager.overrideIcon(tabID, icon: "@asset:Git")
_ = state.launchAgentProfile( makePlan(dedicatedHome: nil, runtime: .claude, placement: .split) )
#expect(state.tabManager.tabs.first?.icon == "@asset:Git") #expect(state.tabManager.tabs.first?.iconLock == .user) }
@Test func splitLaunchLeavesAScriptIconAlone() throws { let state = makeState() _ = state.launchAgentProfile(makePlan(dedicatedHome: nil)) let tabID = try #require(state.tabManager.tabs.first?.id) state.tabManager.setScriptIcon(tabID, icon: "@asset:Git")
_ = state.launchAgentProfile( makePlan(dedicatedHome: nil, runtime: .claude, placement: .split) )
// A launch is auto-detection's peer, not a script: it yields to `.script` // just as `CommandIconMap` does. #expect(state.tabManager.tabs.first?.icon == "@asset:Git") #expect(state.tabManager.tabs.first?.iconLock == .script) }
private func makeState( skipsSurfaceCreationForTesting: Bool = true, failsSurfaceCreationForTesting: Bool = false, defaultFontSize: Float32? = nil ) -> WorktreeTerminalState { WorktreeTerminalState( runtime: GhosttyRuntime(), worktree: Worktree( id: "/tmp/repo/wt-1", name: "wt-1", detail: "", workingDirectory: URL(fileURLWithPath: "/tmp/repo/wt-1"), repositoryRootURL: URL(fileURLWithPath: "/tmp/repo") ), defaultFontSize: defaultFontSize, skipsSurfaceCreationForTesting: skipsSurfaceCreationForTesting, failsSurfaceCreationForTesting: failsSurfaceCreationForTesting ) }
private func makePlan( dedicatedHome: URL?, runtime: AgentProfileRuntime = .codex, placement: AgentProfilePlacement = .tab ) -> AgentProfileLaunchPlan { AgentProfileLaunchPlan( profileID: UUID(), profileName: "Codex · Bound", runtime: runtime, invocation: AgentInvocation(executable: runtime.agent.iconLookupToken, arguments: []), commandEnvironmentTokens: [], placement: placement, splitDirection: .right, surfaceEnvironment: [:], dedicatedHome: dedicatedHome ) }}