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
This commit is contained in:
pj committed 2026-08-31 10:21:51 +05:30
1 parent 51d3504e9a
commit f4b614d3ba
2 files changed
+56 -2

No files matched your search

+10 -2
View File
@@ -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":
+46
View File
@@ -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 {