Skip to content
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- **`--persist` flag** (`MAESTRO_PERSIST` env var) — keeps WebDriverAgent running between invocations for faster repeated test runs. On startup, reuses an existing healthy WDA session (leaving the app alive); on exit, leaves WDA running instead of tearing it down. The xcodebuild process is detached into its own process group so it survives `maestro-runner` exiting.
```bash
# First run: builds and launches WDA normally
maestro-runner --persist --platform ios test flow.yaml

# Subsequent runs: reuses existing WDA session (~30-60s faster)
maestro-runner --persist --platform ios test flow.yaml
```

## [1.1.0] - 2026-03-25

### Added
Expand Down
5 changes: 5 additions & 0 deletions pkg/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@ var GlobalFlags = []cli.Flag{
Value: 180,
Usage: "Device boot timeout in seconds",
},
&cli.BoolFlag{
Name: "persist",
Usage: "Keep WDA running between invocations. Reuses an existing WDA session on startup and leaves it running on exit.",
EnvVars: []string{"MAESTRO_PERSIST"},
},
}

// Execute runs the CLI.
Expand Down
44 changes: 44 additions & 0 deletions pkg/cli/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2234,3 +2234,47 @@ func TestIsSocketInUse_SocketAndPidWithDeadProcess(t *testing.T) {
t.Error("expected socket with dead owner PID to not be in use")
}
}

// ============================================================
// Tests for killProcessOnPort
// ============================================================

// ============================================================
// Tests for --persist flag and RunConfig.Persist field
// ============================================================

func TestGlobalFlags_PersistFlag(t *testing.T) {
flagNames := make(map[string]bool)
for _, f := range GlobalFlags {
for _, name := range f.Names() {
flagNames[name] = true
}
}
if !flagNames["persist"] {
t.Error("expected --persist flag to be defined in GlobalFlags")
}
}

func TestRunConfig_PersistField(t *testing.T) {
cfg := &RunConfig{}
if cfg.Persist {
t.Error("expected Persist to default to false")
}
cfg.Persist = true
if !cfg.Persist {
t.Error("expected Persist to be true after setting")
}
}

func TestKillProcessOnPort_FreePort(t *testing.T) {
port := uint16(49200 + (time.Now().UnixNano() % 100))
if isPortInUse(port) {
t.Skipf("port %d unexpectedly in use, skipping", port)
}
// Should not panic or error when nothing holds the port
killProcessOnPort(port)
// Port should still be free after the call
if isPortInUse(port) {
t.Errorf("port %d in use after killProcessOnPort on free port", port)
}
}
Loading