From 2c2dfe58de45a2e6e22ac5e8c247b5215a44b85a Mon Sep 17 00:00:00 2001 From: BenSheridanEdwards Date: Thu, 2 Jul 2026 18:55:08 +0100 Subject: [PATCH] fix(config): explicit platform selection fails loudly; preflight honors runner env resolveProject fell back to projects[0] when an explicit platform matched nothing, so --ios against an android-only config silently ran an android suite with android evidence. It now errors listing the available projects. The CLI preflight also resolved the project from flags alone while the runner honored PLATFORM/NATIVEPROOF_PROJECT, so PLATFORM=ios nativeproof ensured the wrong Appium driver and skipped the macOS guard before running the ios project. Preflight now reads the same env fallback (runSelection). --- src/cli.ts | 20 ++++++++++++++++---- src/config.ts | 8 +++++++- test/cli.test.ts | 10 ++++++++++ test/config.test.ts | 8 ++++++++ 4 files changed, 41 insertions(+), 5 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index ae37e4d..82d9d69 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1129,13 +1129,25 @@ function nativeProofConfigFromModule(loaded: unknown): NativeProofConfig | undef return isNativeProofConfig(current) ? current : undefined; } +/** + * The selection the preflight resolves the project with: CLI flags first, then the same + * env vars the runner itself reads (PLATFORM / NATIVEPROOF_PROJECT). Without the env + * fallback, `PLATFORM=ios nativeproof` preflighted the android project (wrong Appium + * driver ensured, macOS guard skipped) and then ran the ios one. + */ +export function runSelection(args: CliArgs, env: NodeJS.ProcessEnv = process.env): RunnerEnv { + const selection: RunnerEnv = {}; + const platform = args.platform ?? env.PLATFORM; + const project = args.project ?? env.NATIVEPROOF_PROJECT; + if (platform) selection.platform = platform; + if (project) selection.project = project; + return selection; +} + async function runTests(args: CliArgs): Promise { const { wdioConfig, configPath, extraEnv } = resolveRunner(args); const userConfig = await loadNativeProofConfig(configPath); - const selection: RunnerEnv = {}; - if (args.platform) selection.platform = args.platform; - if (args.project) selection.project = args.project; - const project = resolveProject(userConfig, selection); + const project = resolveProject(userConfig, runSelection(args)); const appium = await ensureAppium(userConfig.appium, args.startAppium, project.platform); try { return await new Promise((resolve, reject) => { diff --git a/src/config.ts b/src/config.ts index a08a582..06001b7 100644 --- a/src/config.ts +++ b/src/config.ts @@ -181,7 +181,7 @@ export interface RunnerEnv { spec?: string; } -/** Pick the project by explicit name, else by platform, else the first one. */ +/** Pick the project by explicit name, else by platform (loudly failing on no match), else the first one. */ export function resolveProject(config: RunnerConfig, env: RunnerEnv = {}): DeviceProject { if (env.project) { const named = config.projects.find((project) => project.name === env.project); @@ -191,6 +191,12 @@ export function resolveProject(config: RunnerConfig, env: RunnerEnv = {}): Devic if (env.platform) { const byPlatform = config.projects.find((project) => project.platform === env.platform); if (byPlatform) return byPlatform; + // Falling back to projects[0] here silently ran the WRONG platform: `--ios` with an + // android-only config did an android run with android evidence and zero warning. + const available = config.projects.map((project) => `${project.name} (${project.platform})`).join(", "); + throw new Error( + `nativeproof: no ${env.platform} project in nativeproof.config.ts — available: ${available}`, + ); } const first = config.projects[0]; if (!first) throw new Error("nativeproof: config has no `projects`"); diff --git a/test/cli.test.ts b/test/cli.test.ts index 09dd724..9430c36 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -17,6 +17,7 @@ import { onboardCommand, parseArgs, resolveRunner, + runSelection, type ScaffoldIo, scaffold, scaffoldFiles, @@ -649,3 +650,12 @@ test("updateConfigAppPath survives comments containing apostrophes and braces", assert.match(updated.slice(androidAt), /"appium:app": "\.\/old\.apk"/); // android untouched assert.doesNotMatch(updated.slice(androidAt), /New\.app/); }); + +test("runSelection falls back to the env vars the runner itself reads", () => { + const args = parseArgs([]); + // Flags win; without them, PLATFORM/NATIVEPROOF_PROJECT steer the preflight exactly + // like they steer the runner — previously the CLI preflighted projects[0] instead. + assert.deepEqual(runSelection(args, { PLATFORM: "ios" }), { platform: "ios" }); + assert.deepEqual(runSelection(args, { NATIVEPROOF_PROJECT: "beta" }), { project: "beta" }); + assert.deepEqual(runSelection(parseArgs(["--android"]), { PLATFORM: "ios" }), { platform: "android" }); +}); diff --git a/test/config.test.ts b/test/config.test.ts index 99c90d9..e09a68b 100644 --- a/test/config.test.ts +++ b/test/config.test.ts @@ -214,3 +214,11 @@ test("findConfigFile locates nativeproof.config.* via the injected exists check" null, ); }); + +test("resolveProject errors when an explicit platform has no matching project", () => { + // Falling back to projects[0] silently ran the wrong platform for `--ios`. + assert.throws( + () => resolveProject({ projects }, { platform: "ios2" }), + /no ios2 project in nativeproof\.config\.ts — available: .*android/, + ); +});