feat(doctor): device prereqs replace java/sidecar for ios-device

iosDeviceChecks now verifies devicectl, iproxy on PATH, a connected+paired
device (via ios.ConnectedDevices), and App Store Connect signing creds (via
ioscompanion.VerifyDeviceSigning). The retired JVM sidecar checks stay only
under android.
This commit is contained in:
pj committed 2026-06-08 23:20:35 +05:30
1 parent f5cfc4407e
commit 743b62b2bd
4 files changed
+120 -13

No files matched your search

+47 -6
View File
@@ -14,6 +14,8 @@ import (
"github.com/chromedp/chromedp"
"github.com/priyanshujain/sanderling/internal/driver/ioscompanion"
"github.com/priyanshujain/sanderling/internal/ios"
"github.com/priyanshujain/sanderling/internal/sidecarassets"
)
@@ -59,8 +61,8 @@ func androidChecks() []doctorCheck {
// iosChecks covers the simulator path, which the native companion drives with
// no JVM. A simulator host with no Java still passes. Physical-device runs
// additionally need java and the sidecar JAR, covered by iosDeviceChecks and
// surfaced through the "all" union.
// additionally need devicectl, iproxy, a connected device, and signing
// credentials, covered by iosDeviceChecks and surfaced through the "all" union.
func iosChecks() []doctorCheck {
return []doctorCheck{
{Name: "xcrun on PATH (ios simulator)", Run: checkExecutableOnPath("xcrun")},
@@ -68,12 +70,16 @@ func iosChecks() []doctorCheck {
}
}
// iosDeviceChecks covers the extra prerequisites a physical iOS device needs:
// the JVM and a real sidecar JAR for the sidecar driver path.
// iosDeviceChecks covers the prerequisites a physical iOS device needs: the
// runner is built and driven over a usbmux tunnel, so devicectl installs the
// app, iproxy forwards the tunnel, a device must be connected and paired, and
// App Store Connect signing credentials must be present for the no-UI build.
func iosDeviceChecks() []doctorCheck {
return []doctorCheck{
{Name: "java 17+ on PATH (ios physical device)", Run: checkJavaVersion},
{Name: "sidecar JAR is real (ios physical device)", Run: checkSidecarJAR},
{Name: "devicectl available (ios physical device)", Run: checkDevicectl},
{Name: "iproxy on PATH (ios physical device)", Run: checkExecutableOnPath("iproxy")},
{Name: "an iOS device is connected and paired", Run: checkDeviceConnected},
{Name: "App Store Connect signing credentials present", Run: checkDeviceSigning},
}
}
@@ -120,6 +126,41 @@ func checkSimctl(ctx context.Context) error {
return nil
}
// Device-check seams: package-level so the doctor's device checks run against
// canned results instead of a real device.
var (
doctorConnectedDevices = ios.ConnectedDevices
doctorVerifySigning = ioscompanion.VerifyDeviceSigning
)
// checkDevicectl exercises `xcrun devicectl --version`: devicectl is an xcrun
// subcommand, so a PATH lookup cannot find it.
func checkDevicectl(ctx context.Context) error {
if err := exec.CommandContext(ctx, "xcrun", "devicectl", "--version").Run(); err != nil {
return fmt.Errorf("xcrun devicectl --version: %w", err)
}
return nil
}
// checkDeviceConnected confirms at least one physical iOS device is connected
// and paired, the prerequisite for the tunnel and the install.
func checkDeviceConnected(ctx context.Context) error {
devices, err := doctorConnectedDevices(ctx)
if err != nil {
return err
}
if len(devices) == 0 {
return fmt.Errorf("no connected iOS device; connect and pair an iPhone")
}
return nil
}
// checkDeviceSigning confirms the App Store Connect signing environment is
// complete and the key file exists, so the no-UI device build can sign.
func checkDeviceSigning(_ context.Context) error {
return doctorVerifySigning()
}
func checkSidecarJAR(_ context.Context) error {
if sidecarassets.IsPlaceholder() {
return fmt.Errorf("placeholder JAR embedded; run `make sidecar && make sanderling` to embed the real fat JAR")
+59 -7
View File
@@ -6,8 +6,12 @@ import (
"errors"
"flag"
"io"
"os"
"path/filepath"
"strings"
"testing"
"github.com/priyanshujain/sanderling/internal/ios"
)
func TestRunDoctorChecks_AllPass(t *testing.T) {
@@ -142,15 +146,63 @@ func TestDoctorChecksFor_iOSSimulator_OmitsJava(t *testing.T) {
}
}
func TestDoctorChecksFor_iOSDevice_IncludesJava(t *testing.T) {
found := false
for _, c := range doctorChecksFor("ios-device") {
if strings.Contains(c.Name, "java") {
found = true
func TestDoctorChecksFor_iOSDevice_CoversDevicePrereqs(t *testing.T) {
checks := doctorChecksFor("ios-device")
for _, c := range checks {
if strings.Contains(c.Name, "java") || strings.Contains(c.Name, "sidecar") {
t.Errorf("device checks must not include the retired %q", c.Name)
}
}
if !found {
t.Error("ios-device checks must include java for the sidecar path")
for _, want := range []string{"devicectl", "iproxy", "connected and paired", "signing credentials"} {
found := false
for _, c := range checks {
if strings.Contains(c.Name, want) {
found = true
}
}
if !found {
t.Errorf("ios-device checks missing %q: %+v", want, checks)
}
}
}
func TestCheckDeviceConnected(t *testing.T) {
original := doctorConnectedDevices
t.Cleanup(func() { doctorConnectedDevices = original })
doctorConnectedDevices = func(context.Context) ([]ios.Device, error) {
return []ios.Device{{Name: "iPhone"}}, nil
}
if err := checkDeviceConnected(context.Background()); err != nil {
t.Fatalf("a connected device must pass: %v", err)
}
doctorConnectedDevices = func(context.Context) ([]ios.Device, error) { return nil, nil }
if err := checkDeviceConnected(context.Background()); err == nil {
t.Fatal("no device must fail")
}
}
func TestCheckDeviceSigning_EnvAndKeyFile(t *testing.T) {
// The signing check defers to the driver's credential verification, which
// reads the same environment a real build would.
for _, key := range []string{"SANDERLING_IOS_TEAM", "DEVELOPMENT_TEAM", "ASC_API_KEY_PATH", "ASC_API_KEY_ID", "ASC_API_ISSUER_ID"} {
t.Setenv(key, "")
}
if err := checkDeviceSigning(context.Background()); err == nil {
t.Fatal("missing signing env must fail")
}
keyPath := filepath.Join(t.TempDir(), "AuthKey.p8")
if err := os.WriteFile(keyPath, []byte("key"), 0o600); err != nil {
t.Fatal(err)
}
t.Setenv("SANDERLING_IOS_TEAM", "TEAM1")
t.Setenv("ASC_API_KEY_ID", "KID")
t.Setenv("ASC_API_ISSUER_ID", "ISS")
t.Setenv("ASC_API_KEY_PATH", keyPath)
if err := checkDeviceSigning(context.Background()); err != nil {
t.Fatalf("complete signing env with a present key must pass: %v", err)
}
}