From f4b614d3bae85f8862483f21ea310a7fd45fc18c Mon Sep 17 00:00:00 2001 From: PJ Date: Mon, 31 Aug 2026 10:21:51 +0530 Subject: [PATCH] feat(cli): let --android-app-path supply the bundle id --bundle-id stays required everywhere else, and an explicit one still wins, so the apk can never quietly override what was asked for. Claude-Session: https://claude.ai/code/session_012PVErdr3ZzyUASeVQDWsUc --- cmd/sanderling/main.go | 12 ++++++++-- cmd/sanderling/main_test.go | 46 +++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/cmd/sanderling/main.go b/cmd/sanderling/main.go index 6032b66..3bd68fa 100644 --- a/cmd/sanderling/main.go +++ b/cmd/sanderling/main.go @@ -12,6 +12,7 @@ import ( "syscall" "time" + "github.com/priyanshujain/sanderling/internal/android" "github.com/priyanshujain/sanderling/internal/testrun" ) @@ -60,7 +61,7 @@ func parseTestArgs(args []string, stderr io.Writer) (testOptions, error) { flagSet.SetOutput(stderr) var options testOptions flagSet.StringVar(&options.spec, "spec", "", "path to the TypeScript spec (required)") - flagSet.StringVar(&options.bundleID, "bundle-id", "", "target app bundle ID (required)") + flagSet.StringVar(&options.bundleID, "bundle-id", "", "target app bundle ID (required, except on android with --android-app-path: the APK names the package itself)") flagSet.StringVar(&options.platform, "platform", "android", "target platform: android, ios, web") flagSet.StringVar(&options.avd, "avd", "", "Android AVD name to boot if no device is connected") flagSet.StringVar(&options.device, "device", "", "Android device serial (from `adb devices`) to target when several are connected") @@ -85,7 +86,14 @@ func parseTestArgs(args []string, stderr io.Writer) (testOptions, error) { return testOptions{}, errors.New("--spec is required") } if options.bundleID == "" { - return testOptions{}, errors.New("--bundle-id is required") + if options.platform != "android" || options.androidAppPath == "" { + return testOptions{}, errors.New("--bundle-id is required") + } + packageName, err := android.PackageName(options.androidAppPath) + if err != nil { + return testOptions{}, err + } + options.bundleID = packageName } switch options.platform { case "android", "ios", "web": diff --git a/cmd/sanderling/main_test.go b/cmd/sanderling/main_test.go index 25e70b3..129ba90 100644 --- a/cmd/sanderling/main_test.go +++ b/cmd/sanderling/main_test.go @@ -88,6 +88,52 @@ func TestParseTestArgs_RequiresBundleID(t *testing.T) { } } +// folioAPK is the android package's fixture, borrowed rather than copied so +// there is one compiled manifest in the tree to keep current. +const folioAPK = "../../internal/android/testdata/folio.apk" + +func TestParseTestArgs_ReadsBundleIDFromTheAPK(t *testing.T) { + options, err := parseTestArgs([]string{"--spec", "s.ts", "--android-app-path", folioAPK}, io.Discard) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if options.bundleID != "app.folio" { + t.Fatalf("bundle id: got %q, want app.folio", options.bundleID) + } +} + +func TestParseTestArgs_ExplicitBundleIDOutranksTheAPK(t *testing.T) { + options, err := parseTestArgs([]string{ + "--spec", "s.ts", + "--bundle-id", "app.folio.debug", + "--android-app-path", folioAPK, + }, io.Discard) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if options.bundleID != "app.folio.debug" { + t.Fatalf("bundle id: got %q, want app.folio.debug", options.bundleID) + } +} + +func TestParseTestArgs_APKDoesNotNameTheBundleOffAndroid(t *testing.T) { + _, err := parseTestArgs([]string{ + "--spec", "s.ts", + "--platform", "ios", + "--android-app-path", folioAPK, + }, io.Discard) + if err == nil || !strings.Contains(err.Error(), "--bundle-id") { + t.Fatalf("expected missing --bundle-id error, got %v", err) + } +} + +func TestParseTestArgs_UnreadableAPKIsReported(t *testing.T) { + _, err := parseTestArgs([]string{"--spec", "s.ts", "--android-app-path", "testdata/absent.apk"}, io.Discard) + if err == nil || !strings.Contains(err.Error(), "testdata/absent.apk") { + t.Fatalf("expected an error naming the apk, got %v", err) + } +} + func TestParseTestArgs_AVDIsOptional(t *testing.T) { options, err := parseTestArgs([]string{"--spec", "s.ts", "--bundle-id", "com.example"}, io.Discard) if err != nil {