CommanderBinderCommandBindingTests.swift•21.5 kB
import Commander
import Testing
@testable import PeekabooCLI
@Suite("Commander Binder Command Binding")
struct CommanderBinderCommandBindingTests {
@Test("Sleep command duration binding")
func bindSleepCommand() throws {
let parsed = ParsedValues(positional: ["2500"], options: [:], flags: [])
let command = try CommanderCLIBinder.instantiateCommand(ofType: SleepCommand.self, parsedValues: parsed)
#expect(command.duration == 2500)
}
@Test("Sleep command binding errors")
func bindSleepCommandErrors() {
let missing = ParsedValues(positional: [], options: [:], flags: [])
#expect(throws: CommanderBindingError.missingArgument(label: "duration")) {
_ = try CommanderCLIBinder.instantiateCommand(ofType: SleepCommand.self, parsedValues: missing)
}
let invalid = ParsedValues(positional: ["abc"], options: [:], flags: [])
#expect(throws: CommanderBindingError.invalidArgument(
label: "duration",
value: "abc",
reason: "Unable to parse Int"
)) {
_ = try CommanderCLIBinder.instantiateCommand(ofType: SleepCommand.self, parsedValues: invalid)
}
}
@Test("Clean command option + flag binding")
func bindCleanCommand() throws {
let parsed = ParsedValues(
positional: [],
options: ["olderThan": ["48"], "session": ["ignored"]],
flags: ["dryRun"]
)
var command = try CommanderCLIBinder.instantiateCommand(ofType: CleanCommand.self, parsedValues: parsed)
#expect(command.dryRun == true)
#expect(command.olderThan == 48)
#expect(command.session == "ignored")
let allSessions = ParsedValues(positional: [], options: [:], flags: ["allSessions"])
command = try CommanderCLIBinder.instantiateCommand(ofType: CleanCommand.self, parsedValues: allSessions)
#expect(command.allSessions == true)
}
@Test("Run command binding")
func bindRunCommand() throws {
let parsed = ParsedValues(
positional: ["/tmp/demo.peekaboo.json"],
options: ["output": ["/tmp/result.json"]],
flags: ["noFailFast"]
)
let command = try CommanderCLIBinder.instantiateCommand(ofType: RunCommand.self, parsedValues: parsed)
#expect(command.scriptPath == "/tmp/demo.peekaboo.json")
#expect(command.output == "/tmp/result.json")
#expect(command.noFailFast == true)
}
@Test("Run command requires script path")
func bindRunCommandErrors() {
let parsed = ParsedValues(positional: [], options: [:], flags: [])
#expect(throws: CommanderBindingError.missingArgument(label: "scriptPath")) {
_ = try CommanderCLIBinder.instantiateCommand(ofType: RunCommand.self, parsedValues: parsed)
}
}
@Test("Image command binding")
func bindImageCommand() throws {
let parsed = ParsedValues(
positional: [],
options: [
"app": ["Safari"],
"pid": ["123"],
"path": ["/tmp/out.png"],
"mode": ["screen"],
"windowTitle": ["Inbox"],
"windowIndex": ["2"],
"screenIndex": ["1"],
"format": ["jpg"],
"captureFocus": ["foreground"],
"analyze": ["describe"]
],
flags: []
)
let command = try CommanderCLIBinder.instantiateCommand(ofType: ImageCommand.self, parsedValues: parsed)
#expect(command.app == "Safari")
#expect(command.pid == 123)
#expect(command.path == "/tmp/out.png")
#expect(command.mode == .screen)
#expect(command.windowTitle == "Inbox")
#expect(command.windowIndex == 2)
#expect(command.screenIndex == 1)
#expect(command.format == .jpg)
#expect(command.captureFocus == .foreground)
#expect(command.analyze == "describe")
}
@Test("Image command invalid mode")
func bindImageCommandErrors() {
let parsed = ParsedValues(positional: [], options: ["mode": ["banana"]], flags: [])
#expect(throws: CommanderBindingError.invalidArgument(
label: "mode",
value: "banana",
reason: "Unknown value for CaptureMode"
)) {
_ = try CommanderCLIBinder.instantiateCommand(ofType: ImageCommand.self, parsedValues: parsed)
}
}
@Test("See command binding")
func bindSeeCommand() throws {
let parsed = ParsedValues(
positional: [],
options: [
"app": ["Safari"],
"pid": ["4321"],
"windowTitle": ["Inbox"],
"mode": ["screen"],
"path": ["/tmp/see.png"],
"screenIndex": ["2"],
"analyze": ["describe"]
],
flags: ["annotate"]
)
let command = try CommanderCLIBinder.instantiateCommand(ofType: SeeCommand.self, parsedValues: parsed)
#expect(command.app == "Safari")
#expect(command.pid == 4321)
#expect(command.windowTitle == "Inbox")
#expect(command.mode == .screen)
#expect(command.path == "/tmp/see.png")
#expect(command.screenIndex == 2)
#expect(command.annotate == true)
#expect(command.analyze == "describe")
}
@Test("Tools command binding")
func bindToolsCommand() throws {
let parsed = ParsedValues(
positional: [],
options: ["mcp": ["github"]],
flags: ["nativeOnly", "includeDisabled", "noSort", "groupByServer"]
)
let command = try CommanderCLIBinder.instantiateCommand(ofType: ToolsCommand.self, parsedValues: parsed)
#expect(command.nativeOnly == true)
#expect(command.mcpOnly == false)
#expect(command.includeDisabled == true)
#expect(command.noSort == true)
#expect(command.groupByServer == true)
#expect(command.mcp == "github")
}
@Test("List menubar binding")
func bindListMenubarCommand() throws {
let parsed = ParsedValues(positional: [], options: [:], flags: [])
_ = try CommanderCLIBinder.instantiateCommand(ofType: ListCommand.MenuBarSubcommand.self, parsedValues: parsed)
}
@Test("List windows binding")
func bindListWindowsCommand() throws {
let parsed = ParsedValues(
positional: [],
options: [
"app": ["Safari"],
"pid": ["123"],
"includeDetails": ["bounds,ids"]
],
flags: []
)
let command = try CommanderCLIBinder.instantiateCommand(
ofType: ListCommand.WindowsSubcommand.self,
parsedValues: parsed
)
#expect(command.app == "Safari")
#expect(command.pid == 123)
#expect(command.includeDetails == "bounds,ids")
}
@Test("List windows requires app")
func bindListWindowsCommandError() {
let parsed = ParsedValues(positional: [], options: [:], flags: [])
#expect(throws: CommanderBindingError.missingArgument(label: "app")) {
_ = try CommanderCLIBinder.instantiateCommand(
ofType: ListCommand.WindowsSubcommand.self,
parsedValues: parsed
)
}
}
@Test("Permissions status binding")
func bindPermissionsStatus() throws {
let parsed = ParsedValues(positional: [], options: [:], flags: [])
_ = try CommanderCLIBinder.instantiateCommand(
ofType: PermissionsCommand.StatusSubcommand.self,
parsedValues: parsed
)
}
@Test("Permissions grant binding")
func bindPermissionsGrant() throws {
let parsed = ParsedValues(positional: [], options: [:], flags: [])
_ = try CommanderCLIBinder.instantiateCommand(
ofType: PermissionsCommand.GrantSubcommand.self,
parsedValues: parsed
)
}
@Test("Window close binding populates identification options")
func bindWindowClose() throws {
let parsed = ParsedValues(
positional: [],
options: [
"app": ["Safari"],
"pid": ["4321"],
"windowTitle": ["Inbox"],
"windowIndex": ["2"]
],
flags: []
)
let command = try CommanderCLIBinder.instantiateCommand(
ofType: WindowCommand.CloseSubcommand.self,
parsedValues: parsed
)
#expect(command.windowOptions.app == "Safari")
#expect(command.windowOptions.pid == 4321)
#expect(command.windowOptions.windowTitle == "Inbox")
#expect(command.windowOptions.windowIndex == 2)
}
@Test("Window move binding handles coordinates")
func bindWindowMove() throws {
let parsed = ParsedValues(
positional: [],
options: [
"app": ["Safari"],
"x": ["120"],
"y": ["340"]
],
flags: []
)
let command = try CommanderCLIBinder.instantiateCommand(
ofType: WindowCommand.MoveSubcommand.self,
parsedValues: parsed
)
#expect(command.windowOptions.app == "Safari")
#expect(command.x == 120)
#expect(command.y == 340)
}
@Test("Window move requires coordinates")
func bindWindowMoveMissingCoordinate() {
let parsed = ParsedValues(
positional: [],
options: ["app": ["Safari"], "x": ["50"]],
flags: []
)
#expect(throws: CommanderBindingError.missingArgument(label: "y")) {
_ = try CommanderCLIBinder.instantiateCommand(
ofType: WindowCommand.MoveSubcommand.self,
parsedValues: parsed
)
}
}
@Test("Window focus binding maps focus options")
func bindWindowFocus() throws {
let parsed = ParsedValues(
positional: [],
options: [
"app": ["Terminal"],
"focusTimeoutSeconds": ["5.5"],
"focusRetryCountValue": ["3"]
],
flags: ["noAutoFocus", "spaceSwitch", "bringToCurrentSpace"]
)
let command = try CommanderCLIBinder.instantiateCommand(
ofType: WindowCommand.FocusSubcommand.self,
parsedValues: parsed
)
#expect(command.windowOptions.app == "Terminal")
#expect(command.focusOptions.noAutoFocus == true)
#expect(command.focusOptions.spaceSwitch == true)
#expect(command.focusOptions.bringToCurrentSpace == true)
#expect(command.focusOptions.focusTimeoutSeconds == 5.5)
#expect(command.focusOptions.focusRetryCountValue == 3)
}
@Test("Window list binding")
func bindWindowList() throws {
let parsed = ParsedValues(
positional: [],
options: [
"app": ["Finder"],
"pid": ["999"]
],
flags: []
)
let command = try CommanderCLIBinder.instantiateCommand(
ofType: WindowCommand.WindowListSubcommand.self,
parsedValues: parsed
)
#expect(command.app == "Finder")
#expect(command.pid == 999)
}
@Test("Click command binding")
func bindClickCommand() throws {
let parsed = ParsedValues(
positional: ["Submit"],
options: [
"session": ["abc"],
"on": ["B1"],
"app": ["Safari"],
"waitFor": ["2500"]
],
flags: ["double", "noAutoFocus"]
)
let command = try CommanderCLIBinder.instantiateCommand(ofType: ClickCommand.self, parsedValues: parsed)
#expect(command.query == "Submit")
#expect(command.session == "abc")
#expect(command.on == "B1")
#expect(command.app == "Safari")
#expect(command.waitFor == 2500)
#expect(command.double == true)
#expect(command.focusOptions.noAutoFocus == true)
}
@Test("Type command binding")
func bindTypeCommand() throws {
let parsed = ParsedValues(
positional: ["Hello"],
options: [
"session": ["xyz"],
"delay": ["10"],
"wpm": ["150"],
"tab": ["2"],
"app": ["Notes"],
"focusTimeoutSeconds": ["3.5"]
],
flags: ["pressReturn", "escape", "delete", "clear", "spaceSwitch"]
)
let command = try CommanderCLIBinder.instantiateCommand(ofType: TypeCommand.self, parsedValues: parsed)
#expect(command.text == "Hello")
#expect(command.session == "xyz")
#expect(command.delay == 10)
#expect(command.profileOption?.lowercased() == "human")
#expect(command.wordsPerMinute == 150)
#expect(command.tab == 2)
#expect(command.pressReturn == true)
#expect(command.escape == true)
#expect(command.delete == true)
#expect(command.clear == true)
#expect(command.app == "Notes")
#expect(command.focusOptions.spaceSwitch == true)
#expect(command.focusOptions.focusTimeoutSeconds == 3.5)
}
@Test("Type command binding with text option")
func bindTypeCommandTextOption() throws {
let parsed = ParsedValues(
positional: [],
options: [
"text": ["OptionText"],
"session": ["abc"]
],
flags: []
)
let command = try CommanderCLIBinder.instantiateCommand(ofType: TypeCommand.self, parsedValues: parsed)
#expect(command.text == nil)
#expect(command.textOption == "OptionText")
#expect(command.session == "abc")
}
@Test("Press command binding")
func bindPressCommand() throws {
let parsed = ParsedValues(
positional: ["cmd", "c"],
options: [
"count": ["3"],
"delay": ["25"],
"hold": ["75"],
"session": ["sess-123"]
],
flags: ["noAutoFocus"]
)
let command = try CommanderCLIBinder.instantiateCommand(ofType: PressCommand.self, parsedValues: parsed)
#expect(command.keys == ["cmd", "c"])
#expect(command.count == 3)
#expect(command.delay == 25)
#expect(command.hold == 75)
#expect(command.session == "sess-123")
#expect(command.focusOptions.noAutoFocus == true)
}
@Test("Capture video command binding")
func bindCaptureVideoCommand() throws {
let parsed = ParsedValues(
positional: ["/tmp/demo.mov"],
options: [
"sampleFps": ["2"],
"startMs": ["1000"],
"endMs": ["2000"],
"maxFrames": ["123"],
"maxMb": ["10"],
"resolutionCap": ["720"],
"diffStrategy": ["quality"],
"diffBudgetMs": ["50"],
"path": ["/tmp/outdir"],
"autocleanMinutes": ["15"],
"videoOut": ["/tmp/out.mp4"]
],
flags: ["noDiff"]
)
let command = try CommanderCLIBinder.instantiateCommand(
ofType: CaptureVideoCommand.self,
parsedValues: parsed
)
#expect(command.input == "/tmp/demo.mov")
#expect(command.sampleFps == 2)
#expect(command.everyMs == nil)
#expect(command.startMs == 1000)
#expect(command.endMs == 2000)
#expect(command.noDiff == true)
#expect(command.maxFrames == 123)
#expect(command.maxMb == 10)
#expect(command.resolutionCap == 720)
#expect(command.diffStrategy == "quality")
#expect(command.diffBudgetMs == 50)
#expect(command.path == "/tmp/outdir")
#expect(command.autocleanMinutes == 15)
#expect(command.videoOut == "/tmp/out.mp4")
}
@Test("Capture video commander signature exposes required input")
func captureVideoCommanderSignatureHasInputArgument() {
let signature = CaptureVideoCommand.commanderSignature()
let input = signature.arguments.first { $0.label == "input" }
#expect(input?.isOptional == false)
#expect(input?.help == "Input video file")
}
@Test("Capture video command requires input")
func bindCaptureVideoCommandRequiresInput() {
let parsed = ParsedValues(positional: [], options: [:], flags: [])
#expect(throws: CommanderBindingError.missingArgument(label: "input")) {
_ = try CommanderCLIBinder.instantiateCommand(
ofType: CaptureVideoCommand.self,
parsedValues: parsed
)
}
}
@Test("Hotkey command binding (positional wins)")
func bindHotkeyCommand() throws {
let parsed = ParsedValues(
positional: ["cmd,space"],
options: ["keys": ["cmd,c"], "holdDuration": ["120"]],
flags: []
)
let command = try CommanderCLIBinder.instantiateCommand(ofType: HotkeyCommand.self, parsedValues: parsed)
#expect(command.resolvedKeys == "cmd,space")
#expect(command.holdDuration == 120)
}
@Test("Hotkey command requires keys")
func bindHotkeyCommandMissingKeys() {
let parsed = ParsedValues(positional: [], options: [:], flags: [])
#expect(throws: ValidationError.self) {
_ = try CommanderCLIBinder.instantiateCommand(ofType: HotkeyCommand.self, parsedValues: parsed)
}
}
@Test("See command respects capture-engine option")
func bindSeeCommandCaptureEngine() throws {
let parsed = ParsedValues(
positional: [],
options: ["captureEngine": ["classic"]],
flags: []
)
let runtimeOptions = try CommanderCLIBinder.makeRuntimeOptions(from: parsed)
#expect(runtimeOptions.captureEnginePreference == "classic")
}
@Test("Move command binding with coordinates")
func bindMoveCommand() throws {
let parsed = ParsedValues(
positional: ["100,200"],
options: [
"duration": ["750"],
"steps": ["30"],
"profile": ["human"],
"session": ["sess-1"]
],
flags: ["smooth"]
)
let command = try CommanderCLIBinder.instantiateCommand(ofType: MoveCommand.self, parsedValues: parsed)
#expect(command.coordinates == "100,200")
#expect(command.duration == 750)
#expect(command.steps == 30)
#expect(command.profile == "human")
#expect(command.session == "sess-1")
#expect(command.smooth == true)
}
@Test("Move command requires a target (validation)")
func bindMoveCommandMissingTarget() throws {
let parsed = ParsedValues(positional: [], options: [:], flags: [])
var command = try CommanderCLIBinder.instantiateCommand(ofType: MoveCommand.self, parsedValues: parsed)
#expect(throws: ValidationError.self) {
try command.validate()
}
}
@Test("Drag command binding")
func bindDragCommand() throws {
let parsed = ParsedValues(
positional: [],
options: [
"from": ["B1"],
"to": ["T2"],
"duration": ["1200"],
"steps": ["15"],
"modifiers": ["cmd,shift"],
"profile": ["human"],
"session": ["sess-drag"]
],
flags: ["spaceSwitch"]
)
let command = try CommanderCLIBinder.instantiateCommand(ofType: DragCommand.self, parsedValues: parsed)
#expect(command.from == "B1")
#expect(command.to == "T2")
#expect(command.duration == 1200)
#expect(command.steps == 15)
#expect(command.modifiers == "cmd,shift")
#expect(command.profile == "human")
#expect(command.session == "sess-drag")
#expect(command.focusOptions.spaceSwitch == true)
}
@Test("Swipe command binding")
func bindSwipeCommand() throws {
let parsed = ParsedValues(
positional: [],
options: [
"fromCoords": ["10,20"],
"toCoords": ["30,40"],
"duration": ["900"],
"steps": ["25"],
"profile": ["linear"],
"session": ["sess-swipe"]
],
flags: []
)
let command = try CommanderCLIBinder.instantiateCommand(ofType: SwipeCommand.self, parsedValues: parsed)
#expect(command.fromCoords == "10,20")
#expect(command.toCoords == "30,40")
#expect(command.duration == 900)
#expect(command.steps == 25)
#expect(command.profile == "linear")
#expect(command.session == "sess-swipe")
}
@Test("Swipe command requires from/to")
func bindSwipeCommandMissingEndpoints() async throws {
let parsed = ParsedValues(positional: [], options: [:], flags: [])
var command = try CommanderCLIBinder.instantiateCommand(ofType: SwipeCommand.self, parsedValues: parsed)
await #expect(throws: ExitCode.self) {
try await command.run(using: CommandRuntime.makeDefault())
}
}
}