2424use FastForward \DevTools \Console \Input \HasCacheOption ;
2525use FastForward \DevTools \Console \Input \HasJsonOption ;
2626use FastForward \DevTools \Composer \Json \ComposerJsonInterface ;
27+ use FastForward \DevTools \Environment \EnvironmentInterface ;
2728use FastForward \DevTools \Filesystem \FilesystemInterface ;
2829use FastForward \DevTools \Path \DevToolsPathResolver ;
2930use FastForward \DevTools \PhpUnit \Bootstrap \BootstrapShimGenerator ;
@@ -60,9 +61,9 @@ final class TestsCommand extends Command
6061 use HasJsonOption;
6162 use LogsCommandResults;
6263
63- private const string AGENT_ENVIRONMENT_VARIABLE = 'AI_AGENT ' ;
64+ public const string AGENT_ENVIRONMENT_VARIABLE = 'AI_AGENT ' ;
6465
65- private const string AGENT_ENVIRONMENT_VALUE = 'fast-forward/dev-tools ' ;
66+ public const string AGENT_ENVIRONMENT_VALUE = 'fast-forward/dev-tools ' ;
6667
6768 private const string PROCESS_LABEL = 'Running PHPUnit Tests ' ;
6869
@@ -71,6 +72,8 @@ final class TestsCommand extends Command
7172 */
7273 public const string CONFIG = 'phpunit.xml ' ;
7374
75+ public const string ENV_MINIMUM_COVERAGE = 'FAST_FORWARD_MIN_COVERAGE ' ;
76+
7477 /**
7578 * @param CoverageSummaryLoaderInterface $coverageSummaryLoader the loader used for `coverage-php` summaries
7679 * @param ComposerJsonInterface $composer the composer.json reader for autoload information
@@ -80,6 +83,7 @@ final class TestsCommand extends Command
8083 * @param ProcessBuilderInterface $processBuilder the builder used to assemble the PHPUnit process
8184 * @param ProcessQueueInterface $processQueue the queue used to execute PHPUnit
8285 * @param ProjectCapabilitiesResolverInterface $projectCapabilitiesResolver the project capability resolver
86+ * @param EnvironmentInterface $environment the environment resolver for CLI-scoped flags
8387 */
8488 public function __construct (
8589 private readonly CoverageSummaryLoaderInterface $ coverageSummaryLoader ,
@@ -90,6 +94,7 @@ public function __construct(
9094 private readonly ProcessBuilderInterface $ processBuilder ,
9195 private readonly ProcessQueueInterface $ processQueue ,
9296 private readonly ProjectCapabilitiesResolverInterface $ projectCapabilitiesResolver ,
97+ private readonly EnvironmentInterface $ environment ,
9398 ) {
9499 parent ::__construct ();
95100 }
@@ -193,7 +198,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
193198
194199 if (! $ projectCapabilities ->canRunTests ()) {
195200 return $ this ->success (
196- 'Skipping PHPUnit tests because no tests directory or PHP source files were detected. ' ,
201+ 'Skipping PHPUnit tests because no Composer-autoloaded PHP source files were detected. ' ,
197202 $ input ,
198203 [
199204 'output ' => $ processOutput ,
@@ -249,14 +254,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int
249254 $ result = $ this ->processQueue ->run ($ processOutput );
250255 $ processResultContext = $ this ->resolveProcessResultContext ($ processOutput , $ result , $ structuredOutput );
251256
252- if (self ::SUCCESS !== $ result || null === $ minimumCoverage || null === $ coverageReportPath ) {
253- if (self ::SUCCESS === $ result ) {
254- return $ this ->success ('PHPUnit tests completed successfully. ' , $ input , $ processResultContext );
255- }
256-
257+ if (self ::SUCCESS !== $ result ) {
257258 return $ this ->failure ('PHPUnit tests failed. ' , $ input , $ processResultContext );
258259 }
259260
261+ if (null === $ minimumCoverage || null === $ coverageReportPath ) {
262+ return $ this ->success ('PHPUnit tests completed successfully. ' , $ input , $ processResultContext );
263+ }
264+
260265 [$ validationResult , $ message , $ coverageContext ] = $ this ->validateMinimumCoverage (
261266 $ coverageReportPath ,
262267 $ minimumCoverage ,
@@ -314,10 +319,9 @@ private function resolveProcessResultContext(
314319 private function forceAgentReporter (Process $ process ): void
315320 {
316321 $ env = $ process ->getEnv ();
322+ $ parentAgentEnvironment = $ this ->environment ->get (self ::AGENT_ENVIRONMENT_VARIABLE );
317323
318- if (\array_key_exists (self ::AGENT_ENVIRONMENT_VARIABLE , $ env ) || false !== getenv (
319- self ::AGENT_ENVIRONMENT_VARIABLE
320- )) {
324+ if (\array_key_exists (self ::AGENT_ENVIRONMENT_VARIABLE , $ env ) || null !== $ parentAgentEnvironment ) {
321325 return ;
322326 }
323327
@@ -515,9 +519,15 @@ private function resolveMinimumCoverage(InputInterface $input): ?float
515519 $ minimumCoverage = $ input ->getOption ('min-coverage ' );
516520
517521 if (null === $ minimumCoverage ) {
522+ $ minimumCoverage = $ this ->resolveMinimumCoverageFromEnvironment ();
523+ }
524+
525+ if (false === $ minimumCoverage || '' === trim ((string ) $ minimumCoverage )) {
518526 return null ;
519527 }
520528
529+ $ minimumCoverage = trim ((string ) $ minimumCoverage );
530+
521531 if (! is_numeric ($ minimumCoverage )) {
522532 throw new InvalidArgumentException ('The --min-coverage option MUST be a numeric percentage. ' );
523533 }
@@ -531,6 +541,16 @@ private function resolveMinimumCoverage(InputInterface $input): ?float
531541 return $ minimumCoverage ;
532542 }
533543
544+ /**
545+ * Resolves minimum-coverage value from injected environment abstraction.
546+ *
547+ * @return string|false|null the configured coverage threshold or a falsey fallback
548+ */
549+ private function resolveMinimumCoverageFromEnvironment (): ?string
550+ {
551+ return $ this ->environment ->get (self ::ENV_MINIMUM_COVERAGE );
552+ }
553+
534554 /**
535555 * @param InputInterface $input the raw parameter definitions
536556 * @param ProcessBuilderInterface $processBuilder the process builder to extend with coverage arguments
0 commit comments