fix(contracts): refuse launch-only open options before the lifecycle touches the device

TH
12h ago
+332-54

Summary

open <app> --relaunch --launch-console against a provider-owned iOS simulator (Limrun/TestMu shape) closed the running app and only then failed. The lifecycle binding now carries a supportsLocalSimulatorLaunch owner capability, and assertOpenAdmitted / assertOpenRequestShape (packages/contracts/src/application-lifecycle-interaction.ts) run first in openDirectApplication, openAndroidApplication and openAppleApplication: an open option the bound owner cannot honor, or a launch-only option on a request that cannot carry one (no app target, URL pair, deep link), is refused with the existing codes and typed reasons before any relaunch close, runner stop, or runtime-hint write. Extracted from #3928 (the --capture-network parts stay there; only options that exist on main are covered). Scope expansion: none.

Review follow-ups

  • Provider scenario seam: the test-only provider module resolved an interactor and opened, skipping the admission every production provider runtime runs first. It now calls assertOpenAdmitted, and a new scenario proves the refusal on a provider-owned iOS simulator — it names the typed reason and the device, and the fake device records no open and no close. A new test removes that admission and fails, so the assertion is not vacuous.
  • launch-console-owner-unadmitted is documented: named in the open help the command owns, in the command reference, and in the debugging guide (the session must own the simulator locally).
  • Deliberately not applied: running the request-shape check before the owner check would change the error code for a request that is both malformed and aimed at an owner that cannot honor the option (INVALID_ARGS where it is now UNSUPPORTED_OPERATION). That is a caller-visible behavior change beyond this fix, which refuses before the device is touched, so it stays out of scope.

Validation

  • Tested SHA: e8b9aa617e (rebased onto main at ed798f9cfe).
  • pnpm check:affected --run --head e8b9aa617e: all runnable checks passed (exactCommit=true; format, lint, typecheck, layering, fallow, build, vitest-related, command-docs, docs-links, docs-prose).
  • unit-core daemon session-lifecycle and the lifecycle/contract suites pass together (237 tests), so this fix and the alert-recovery fix that landed in ed798f9cfe are checked on the same open path.
  • CI lanes on this head are green, including the unit, integration, repo-guard and coverage lanes.
  • New contract tests cover the provider-shaped relaunch refusal asserting close is never called, the local non-simulator refusal, and the URL-pair refusal before any close; Android and Apple entry-level tests prove the refusal precedes close/runner/hint steps.
  • Not device-facing beyond refusal ordering; provider integration lanes run in CI.

šŸ¤– Generated with Claude Code