native macOS codings agent orchestrator prowl.onev.cat
Something went wrong. Try again.
30 kB · 903 lines
Swift
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904import Clocksimport Foundationimport ProwlCLISharedimport Testing
@testable import supacode
@MainActorstruct CLILifecycleCommandHandlerTests { @Test func backgroundShellUsesBackgroundCreationProvider() async throws { let base = makeTarget(tabID: "base-tab", paneID: "base-pane") let created = makeTarget(tabID: "created-tab", paneID: "created-pane") var foregroundCalls = 0 var backgroundCalls = 0 let handler = LifecycleCommandHandler( resolveCreateTarget: { _ in .success(base) }, resolveCloseTarget: { _ in .success(.init(resource: .pane, target: base)) }, createTab: { _, _ in foregroundCalls += 1 return nil }, createPane: { _, _ in nil }, createBackgroundTab: { _, path in #expect(path == "/Projects/App") backgroundCalls += 1 return created }, closeTab: { _, _ in false }, closePane: { _, _ in false }) let result = await handler.handle( envelope: .init( output: .json, command: .create( .init( resource: .tab, selector: .worktree("App"), path: "/Projects/App", background: true)))) #expect(result.ok) #expect(backgroundCalls == 1) #expect(foregroundCalls == 0) #expect(try result.data?.decode(as: LifecycleCommandPayload.self).target.tab.id == "created-tab") }
@Test func createPaneDirectionsMapToTerminalDirections() { #expect(CreatePaneDirection.right.terminalSplitDirection == .right) #expect(CreatePaneDirection.left.terminalSplitDirection == .left) #expect(CreatePaneDirection.upward.terminalSplitDirection == .top) #expect(CreatePaneDirection.down.terminalSplitDirection == .down) }
@Test func createTabResolvesWorktreeCreatesTabAndReturnsCreatePayload() async throws { let base = makeTarget(tabID: "base-tab", paneID: "base-pane") let created = makeTarget(tabID: "created-tab", paneID: "created-pane") var resolvedSelector: TargetSelector? var createPath: String? let handler = LifecycleCommandHandler( resolveCreateTarget: { selector in resolvedSelector = selector return .success(base) }, resolveCloseTarget: { _ in .success(LifecycleResolvedTarget(resource: .pane, target: base)) }, createTab: { _, path in createPath = path return created }, createPane: { _, _ in nil }, closeTab: { _, _ in true }, closePane: { _, _ in true } )
let response = await handler.handle( envelope: CommandEnvelope( output: .json, command: .create(CreateInput(resource: .tab, selector: .worktree("App"), path: "/Projects/App")) ) )
#expect(response.ok) #expect(response.command == "create") #expect(response.schemaVersion == "prowl.cli.create.v1") #expect(resolvedSelector == .worktree("App")) #expect(createPath == "/Projects/App") let data = try #require(response.data) let payload = try data.decode(as: LifecycleCommandPayload.self) #expect(payload.resource == .tab) #expect(payload.target.tab.id == "created-tab") }
@Test func createPaneUsesResolvedAnchorAndReturnsCreatePayload() async throws { let anchor = makeTarget(tabID: "anchor-tab", paneID: "anchor-pane") let created = makeTarget(tabID: "anchor-tab", paneID: "created-pane") var resolvedSelector: TargetSelector? var createdFrom: TabResolvedTarget? var createdDirection: CreatePaneDirection? let handler = LifecycleCommandHandler( resolveCreateTarget: { selector in resolvedSelector = selector return .success(anchor) }, resolveCloseTarget: { _ in .success(LifecycleResolvedTarget(resource: .pane, target: anchor)) }, createTab: { _, _ in nil }, createPane: { target, direction in createdFrom = target createdDirection = direction return created }, closeTab: { _, _ in true }, closePane: { _, _ in true } )
let response = await handler.handle( envelope: CommandEnvelope( output: .json, command: .create( CreateInput(resource: .pane, selector: .pane("p12"), direction: .upward) ) ) )
#expect(response.ok) #expect(resolvedSelector == .pane("p12")) #expect(createdFrom == anchor) #expect(createdDirection == .upward) let data = try #require(response.data) let payload = try data.decode(as: LifecycleCommandPayload.self) #expect(payload.resource == .pane) #expect(payload.anchor?.pane.id == "anchor-pane") #expect(payload.direction == .upward) #expect(payload.target.pane.id == "created-pane") }
@Test func createPaneRejectsNonPaneSocketInputBeforeResolution() async { let target = makeTarget() var didResolve = false var didCreate = false let handler = LifecycleCommandHandler( resolveCreateTarget: { _ in didResolve = true return .success(target) }, resolveCloseTarget: { _ in .success(LifecycleResolvedTarget(resource: .pane, target: target)) }, createTab: { _, _ in nil }, createPane: { _, _ in didCreate = true return target }, closeTab: { _, _ in true }, closePane: { _, _ in true } )
let response = await handler.handle( envelope: CommandEnvelope( output: .json, command: .create( CreateInput(resource: .pane, selector: .worktree("App"), direction: .right) ) ) )
#expect(!response.ok) #expect(response.error?.code == CLIErrorCode.invalidArgument) #expect(!didResolve) #expect(!didCreate) }
@Test func createPaneReportsCreateFailedWhenTheSplitCannotBeMade() async throws { let anchor = makeTarget(tabID: "anchor-tab", paneID: "anchor-pane") var createAttempts = 0 let handler = LifecycleCommandHandler( resolveCreateTarget: { _ in .success(anchor) }, resolveCloseTarget: { _ in .success(LifecycleResolvedTarget(resource: .pane, target: anchor)) }, createTab: { _, _ in nil }, createPane: { _, _ in createAttempts += 1 return nil }, closeTab: { _, _ in true }, closePane: { _, _ in true } )
let response = await handler.handle( envelope: CommandEnvelope( output: .json, command: .create( CreateInput(resource: .pane, selector: .pane("p12"), direction: .down) ) ) )
#expect(!response.ok) #expect(response.error?.code == CLIErrorCode.createFailed) #expect(createAttempts == 1) #expect(response.data == nil) }
@Test func createTabLaunchesAnEnabledProfileByExactNameAndReturnsMetadata() async throws { let base = makeTarget(tabID: "base-tab", paneID: "base-pane") let created = makeTarget(tabID: "created-tab", paneID: "created-pane") let profile = AgentProfile(name: "Reviewer", runtime: .claude) var launchRequest: CLIProfileLaunchRequest? var lifecycle: [String] = [] let handler = LifecycleCommandHandler( resolveCreateTarget: { _ in .success(base) }, resolveCloseTarget: { _ in .success(LifecycleResolvedTarget(resource: .pane, target: base)) }, createTab: { _, _ in nil }, createPane: { _, _ in nil }, profiles: { [profile] }, prepareAgentProfile: { request in lifecycle.append("preflight") #expect(request.dispatchID == nil) return .success(request) }, launchAgentProfile: { request in lifecycle.append("launch") launchRequest = request return .success(created) }, issueDispatch: { lifecycle.append("issue") return .success( DispatchPendingRecord(id: "d1", createdAt: "2026-08-23T02:00:00.000Z") ) }, bindDispatch: { dispatchID, target in lifecycle.append("bind:\(dispatchID):\(target.paneID)") return .success(()) }, closeTab: { _, _ in true }, closePane: { _, _ in true } )
let response = await handler.handle( envelope: CommandEnvelope( output: .json, command: .create( CreateInput( resource: .tab, selector: .worktree("App"), launch: CreateLaunchInput(profile: "Reviewer", prompt: "Review the diff."), background: true ) ) ) )
#expect(response.ok) #expect(launchRequest?.profile == profile) #expect(launchRequest?.target == base) #expect(launchRequest?.prompt == "Review the diff.") #expect(launchRequest?.dispatchID == "d1") #expect(launchRequest?.background == true) #expect(lifecycle == ["preflight", "issue", "launch", "bind:d1:created-pane"]) let data = try #require(response.data) let payload = try data.decode(as: LifecycleCommandPayload.self) #expect( payload.launch == LifecycleCommandLaunch( profileID: profile.id.uuidString, profileName: "Reviewer", agent: "claude" ) ) #expect(payload.dispatch?.id == "d1") }
@Test func cancellationDuringPreflightNeverIssuesDispatchOrLaunchesSurface() async { let base = makeTarget() let profile = AgentProfile(name: "Reviewer", runtime: .codex) let clock = TestClock() var issued = false var launched = false var cancelledPreparation = false let handler = LifecycleCommandHandler( resolveCreateTarget: { _ in .success(base) }, resolveCloseTarget: { _ in .success(.init(resource: .pane, target: base)) }, createTab: { _, _ in nil }, createPane: { _, _ in nil }, profiles: { [profile] }, prepareAgentProfile: { request in do { try await clock.sleep(for: .seconds(10)) return .success(request) } catch { // Model a production resolver that catches cancellation and returns // an ordinary degraded/successful preparation. return .success(request) } }, launchAgentProfile: { _ in launched = true return .success(base) }, cancelProfilePreparation: { _ in cancelledPreparation = true }, issueDispatch: { issued = true return .failure(.capacityExceeded) }, closeTab: { _, _ in true }, closePane: { _, _ in true } ) let task = Task { await handler.handle( envelope: CommandEnvelope( output: .json, command: .create( .init( resource: .tab, selector: .worktree("App"), launch: .init(profile: "Reviewer", prompt: "Review") ) ) ) ) } await Task.yield()
task.cancel() _ = await task.value
#expect(!issued) #expect(!launched) #expect(cancelledPreparation) }
@Test func promptedLaunchFailureCancelsIssuedDispatch() async { let base = makeTarget() let profile = AgentProfile(name: "Reviewer", runtime: .claude) var cancelled: [String] = [] let handler = LifecycleCommandHandler( resolveCreateTarget: { _ in .success(base) }, resolveCloseTarget: { _ in .success(LifecycleResolvedTarget(resource: .pane, target: base)) }, createTab: { _, _ in nil }, createPane: { _, _ in nil }, profiles: { [profile] }, launchAgentProfile: { request in #expect(request.dispatchID == "d1") return .failure(.createFailed("Launch failed.")) }, issueDispatch: { .success(DispatchPendingRecord(id: "d1", createdAt: "2026-08-23T02:00:00.000Z")) }, cancelDispatch: { cancelled.append($0) }, closeTab: { _, _ in true }, closePane: { _, _ in true } )
let response = await handler.handle( envelope: CommandEnvelope( output: .json, command: .create( CreateInput( resource: .tab, selector: .worktree("App"), launch: CreateLaunchInput(profile: "Reviewer", prompt: "Review.") ) ) ) )
#expect(!response.ok) #expect(cancelled == ["d1"]) }
@Test func dispatchCapacityFailurePreventsLaunch() async { let base = makeTarget() let profile = AgentProfile(name: "Reviewer", runtime: .claude) var didLaunch = false var cancelledPreparation = false let handler = LifecycleCommandHandler( resolveCreateTarget: { _ in .success(base) }, resolveCloseTarget: { _ in .success(LifecycleResolvedTarget(resource: .pane, target: base)) }, createTab: { _, _ in nil }, createPane: { _, _ in nil }, profiles: { [profile] }, launchAgentProfile: { _ in didLaunch = true return .success(base) }, cancelProfilePreparation: { _ in cancelledPreparation = true }, issueDispatch: { .failure(.capacityExceeded) }, closeTab: { _, _ in true }, closePane: { _, _ in true } )
let response = await handler.handle( envelope: CommandEnvelope( output: .json, command: .create( CreateInput( resource: .tab, selector: .worktree("App"), launch: CreateLaunchInput(profile: "Reviewer", prompt: "Review.") ) ) ) )
#expect(!response.ok) #expect(response.error?.code == CLIErrorCode.dispatchCapacityExceeded) #expect(!didLaunch) #expect(cancelledPreparation) }
@Test func unpromptedProfileLaunchDoesNotIssueOrBindDispatch() async throws { let target = makeTarget() let profile = AgentProfile(name: "Reviewer", runtime: .codex) var issued = false var bound = false let handler = LifecycleCommandHandler( resolveCreateTarget: { _ in .success(target) }, resolveCloseTarget: { _ in .success(.init(resource: .pane, target: target)) }, createTab: { _, _ in nil }, createPane: { _, _ in nil }, profiles: { [profile] }, launchAgentProfile: { request in #expect(request.dispatchID == nil) return .success(target) }, issueDispatch: { issued = true return .failure(.capacityExceeded) }, bindDispatch: { _, _ in bound = true return .success(()) }, closeTab: { _, _ in true }, closePane: { _, _ in true } )
let response = await handler.handle( envelope: CommandEnvelope( output: .json, command: .create( .init( resource: .tab, selector: .worktree("App"), launch: .init(profile: "Reviewer") )) )) #expect(response.ok) #expect(!issued) #expect(!bound) #expect(try #require(response.data).decode(as: LifecycleCommandPayload.self).dispatch == nil) }
@Test func bindFailureRollsBackCreatedSurfaceAndCancelsDispatch() async { let anchor = makeTarget(paneID: "anchor") let created = makeTarget(paneID: "created") let profile = AgentProfile(name: "Reviewer", runtime: .codex) var rolledBack: (LifecycleResource, TabResolvedTarget)? var cancelled: String? let handler = LifecycleCommandHandler( resolveCreateTarget: { _ in .success(anchor) }, resolveCloseTarget: { _ in .success(.init(resource: .pane, target: anchor)) }, createTab: { _, _ in nil }, createPane: { _, _ in nil }, profiles: { [profile] }, launchAgentProfile: { _ in .success(created) }, issueDispatch: { .success(.init(id: "d1", createdAt: "2026-08-23T02:00:00.000Z")) }, bindDispatch: { _, _ in .failure(.notFound) }, cancelDispatch: { cancelled = $0 }, rollbackProfileLaunch: { rolledBack = ($0, $1) }, closeTab: { _, _ in true }, closePane: { _, _ in true } )
let response = await handler.handle( envelope: CommandEnvelope( output: .json, command: .create( .init( resource: .pane, selector: .pane("anchor"), direction: .right, launch: .init(profile: "Reviewer", prompt: "Review") )) )) #expect(response.error?.code == CLIErrorCode.createFailed) #expect(rolledBack?.0 == .pane) #expect(rolledBack?.1 == created) #expect(cancelled == "d1") }
@Test func disabledNamesDoNotMakeAnEnabledProfileNonUnique() async { let target = makeTarget() let enabled = AgentProfile(name: "Reviewer", runtime: .claude) let disabled = AgentProfile(name: "Reviewer", isEnabled: false, runtime: .codex) var launchedProfile: AgentProfile? let handler = LifecycleCommandHandler( resolveCreateTarget: { _ in .success(target) }, resolveCloseTarget: { _ in .success(LifecycleResolvedTarget(resource: .pane, target: target)) }, createTab: { _, _ in nil }, createPane: { _, _ in nil }, profiles: { [disabled, enabled] }, launchAgentProfile: { request in launchedProfile = request.profile return .success(target) }, closeTab: { _, _ in true }, closePane: { _, _ in true } )
let response = await handler.handle( envelope: CommandEnvelope( output: .json, command: .create( CreateInput( resource: .pane, selector: .pane("p12"), direction: .right, launch: CreateLaunchInput(profile: "Reviewer") ) ) ) )
#expect(response.ok) #expect(launchedProfile?.id == enabled.id) }
@Test func profileUUIDWinsOverAnExactNameMatch() async { let target = makeTarget() let byID = AgentProfile(name: "By ID", runtime: .claude) let byName = AgentProfile(name: byID.id.uuidString, runtime: .codex) var launchedProfile: AgentProfile? let handler = LifecycleCommandHandler( resolveCreateTarget: { _ in .success(target) }, resolveCloseTarget: { _ in .success(LifecycleResolvedTarget(resource: .pane, target: target)) }, createTab: { _, _ in nil }, createPane: { _, _ in nil }, profiles: { [byName, byID] }, launchAgentProfile: { request in launchedProfile = request.profile return .success(target) }, closeTab: { _, _ in true }, closePane: { _, _ in true } )
let response = await handler.handle( envelope: CommandEnvelope( output: .json, command: .create( CreateInput( resource: .tab, selector: .worktree("App"), launch: CreateLaunchInput(profile: byID.id.uuidString) ) ) ) )
#expect(response.ok) #expect(launchedProfile?.id == byID.id) }
@Test func disabledProfileUUIDIsNotLaunchable() async { let target = makeTarget() let disabled = AgentProfile(name: "Reviewer", isEnabled: false, runtime: .claude) let handler = makeProfileLookupHandler(target: target, profiles: [disabled])
let response = await handler.handle( envelope: CommandEnvelope( output: .json, command: .create( CreateInput( resource: .tab, selector: .worktree("App"), launch: CreateLaunchInput(profile: disabled.id.uuidString) ) ) ) )
#expect(!response.ok) #expect(response.error?.code == CLIErrorCode.profileNotFound) }
@Test func duplicateEnabledProfileNameIsRejected() async { let target = makeTarget() let profiles = [ AgentProfile(name: "Reviewer", runtime: .claude), AgentProfile(name: "Reviewer", runtime: .codex), ] let handler = makeProfileLookupHandler(target: target, profiles: profiles)
let response = await handler.handle( envelope: CommandEnvelope( output: .json, command: .create( CreateInput( resource: .tab, selector: .worktree("App"), launch: CreateLaunchInput(profile: "Reviewer") ) ) ) )
#expect(!response.ok) #expect(response.error?.code == CLIErrorCode.profileNotUnique) }
@Test func profileLaunchFailureClassifiesPlanningAndCreationErrors() { let profile = AgentProfile(name: "Reviewer", runtime: .amp)
#expect( CLIProfileLaunchFailure.planning( AgentRuntimeError.unsupportedStartIntent(.amp, .prompt("Review.")), profile: profile ) == .invalidArgument("Agent Profile “Reviewer” does not support kickoff prompts.") ) #expect( CLIProfileLaunchFailure.creation(.splitAnchorUnavailable, profile: profile) == .createFailed("The split anchor for Agent Profile “Reviewer” is no longer available.") ) }
@Test func profileLaunchFailurePreservesTheCreationReason() async { let target = makeTarget() let profile = AgentProfile(name: "Reviewer", runtime: .claude) let handler = LifecycleCommandHandler( resolveCreateTarget: { _ in .success(target) }, resolveCloseTarget: { _ in .success(LifecycleResolvedTarget(resource: .pane, target: target)) }, createTab: { _, _ in nil }, createPane: { _, _ in nil }, profiles: { [profile] }, launchAgentProfile: { _ in .failure(.createFailed("The split anchor is no longer available.")) }, closeTab: { _, _ in true }, closePane: { _, _ in true } )
let response = await handler.handle( envelope: CommandEnvelope( output: .json, command: .create( CreateInput( resource: .tab, selector: .worktree("App"), launch: CreateLaunchInput(profile: "Reviewer") ) ) ) )
#expect(!response.ok) #expect(response.error?.code == CLIErrorCode.createFailed) #expect(response.error?.message == "The split anchor is no longer available.") }
@Test func unsupportedPromptIntentMapsToInvalidArgument() async { let target = makeTarget() let profile = AgentProfile(name: "Reviewer", runtime: .amp) let handler = LifecycleCommandHandler( resolveCreateTarget: { _ in .success(target) }, resolveCloseTarget: { _ in .success(LifecycleResolvedTarget(resource: .pane, target: target)) }, createTab: { _, _ in nil }, createPane: { _, _ in nil }, profiles: { [profile] }, launchAgentProfile: { _ in .failure(.invalidArgument("Agent Profile “Reviewer” does not support kickoff prompts.")) }, issueDispatch: { .success(DispatchPendingRecord(id: "d1", createdAt: "2026-08-23T02:00:00.000Z")) }, closeTab: { _, _ in true }, closePane: { _, _ in true } )
let response = await handler.handle( envelope: CommandEnvelope( output: .json, command: .create( CreateInput( resource: .tab, selector: .worktree("App"), launch: CreateLaunchInput(profile: "Reviewer", prompt: "Review.") ) ) ) )
#expect(!response.ok) #expect(response.error?.code == CLIErrorCode.invalidArgument) #expect(response.error?.message == "Agent Profile “Reviewer” does not support kickoff prompts.") }
@Test func emptyProfilePromptIsRejectedBeforeResolution() async { let target = makeTarget() var didResolve = false let handler = LifecycleCommandHandler( resolveCreateTarget: { _ in didResolve = true return .success(target) }, resolveCloseTarget: { _ in .success(LifecycleResolvedTarget(resource: .pane, target: target)) }, createTab: { _, _ in nil }, createPane: { _, _ in nil }, closeTab: { _, _ in true }, closePane: { _, _ in true } )
let response = await handler.handle( envelope: CommandEnvelope( output: .json, command: .create( CreateInput( resource: .tab, selector: .worktree("App"), launch: CreateLaunchInput(profile: "Reviewer", prompt: " \n") ) ) ) )
#expect(!response.ok) #expect(response.error?.code == CLIErrorCode.emptyInput) #expect(!didResolve) }
@Test func profilePromptContainingNULIsRejectedBeforeResolution() async { let target = makeTarget() var didResolve = false let handler = LifecycleCommandHandler( resolveCreateTarget: { _ in didResolve = true return .success(target) }, resolveCloseTarget: { _ in .success(LifecycleResolvedTarget(resource: .pane, target: target)) }, createTab: { _, _ in nil }, createPane: { _, _ in nil }, closeTab: { _, _ in true }, closePane: { _, _ in true } )
let response = await handler.handle( envelope: CommandEnvelope( output: .json, command: .create( CreateInput( resource: .tab, selector: .worktree("App"), launch: CreateLaunchInput(profile: "Reviewer", prompt: "Review\0this") ) ) ) )
#expect(!response.ok) #expect(response.error?.code == CLIErrorCode.invalidArgument) #expect(!didResolve) }
@Test func oversizedProfilePromptIsRejectedBeforeResolution() async { let target = makeTarget() var didResolve = false let handler = LifecycleCommandHandler( resolveCreateTarget: { _ in didResolve = true return .success(target) }, resolveCloseTarget: { _ in .success(LifecycleResolvedTarget(resource: .pane, target: target)) }, createTab: { _, _ in nil }, createPane: { _, _ in nil }, closeTab: { _, _ in true }, closePane: { _, _ in true } )
let response = await handler.handle( envelope: CommandEnvelope( output: .json, command: .create( CreateInput( resource: .tab, selector: .worktree("App"), launch: CreateLaunchInput( profile: "Reviewer", prompt: String( repeating: "x", count: CreateLaunchInput.maximumPromptUTF8ByteCount + 1 ) ) ) ) ) )
#expect(!response.ok) #expect(response.error?.code == CLIErrorCode.invalidArgument) #expect(!didResolve) }
@Test func backgroundWithoutProfileIsRejectedBeforeResolution() async { let target = makeTarget() var didResolve = false let handler = LifecycleCommandHandler( resolveCreateTarget: { _ in didResolve = true return .success(target) }, resolveCloseTarget: { _ in .success(LifecycleResolvedTarget(resource: .pane, target: target)) }, createTab: { _, _ in nil }, createPane: { _, _ in nil }, closeTab: { _, _ in true }, closePane: { _, _ in true } )
let response = await handler.handle( envelope: CommandEnvelope( output: .json, command: .create( CreateInput(resource: .tab, selector: .worktree("App"), background: true) ) ) )
#expect(!response.ok) #expect(response.error?.code == CLIErrorCode.invalidArgument) #expect(!didResolve) }
@Test func closeUsesResolvedResourceAndReturnsClosePayload() async throws { let target = makeTarget(tabID: "tab-to-close", paneID: "pane-to-close") var closedPane: TabResolvedTarget? let handler = LifecycleCommandHandler( resolveCreateTarget: { _ in .success(target) }, resolveCloseTarget: { selector in #expect(selector == .auto("p12")) return .success(LifecycleResolvedTarget(resource: .pane, target: target)) }, createTab: { _, _ in nil }, createPane: { _, _ in nil }, closeTab: { _, _ in false }, closePane: { target, force in #expect(force) closedPane = target return true } )
let response = await handler.handle( envelope: CommandEnvelope(output: .json, command: .close(CloseInput(selector: .auto("p12"), force: true))) )
#expect(response.ok) #expect(response.command == "close") #expect(response.schemaVersion == "prowl.cli.close.v1") #expect(closedPane == target) let data = try #require(response.data) let payload = try data.decode(as: LifecycleCommandPayload.self) #expect(payload.resource == .pane) #expect(payload.target.pane.id == "pane-to-close") }
private func makeProfileLookupHandler( target: TabResolvedTarget, profiles: [AgentProfile] ) -> LifecycleCommandHandler { LifecycleCommandHandler( resolveCreateTarget: { _ in .success(target) }, resolveCloseTarget: { _ in .success(LifecycleResolvedTarget(resource: .pane, target: target)) }, createTab: { _, _ in nil }, createPane: { _, _ in nil }, profiles: { profiles }, launchAgentProfile: { _ in .success(target) }, closeTab: { _, _ in true }, closePane: { _, _ in true } ) }
private func makeTarget( worktreeID: String = "App:/Projects/App", worktreeName: String = "App", worktreePath: String = "/Projects/App", worktreeRootPath: String = "/Projects/App", worktreeKind: String = "git", tabID: String = "tab-1", tabTitle: String = "App 1", tabSelected: Bool = true, paneID: String = "pane-1", paneTitle: String = "zsh", paneCWD: String? = "/Projects/App", paneFocused: Bool = true ) -> TabResolvedTarget { TabResolvedTarget( worktreeID: worktreeID, worktreeName: worktreeName, worktreePath: worktreePath, worktreeRootPath: worktreeRootPath, worktreeKind: worktreeKind, tabID: tabID, tabTitle: tabTitle, tabSelected: tabSelected, paneID: paneID, paneTitle: paneTitle, paneCWD: paneCWD, paneFocused: paneFocused ) }}