From 934217b9d579f2e411f4fd77aaf3565134a69adf Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 29 Aug 2026 03:24:29 +0000 Subject: [PATCH] Fix KeyNotFoundException [url] crashing every CLI run on BoxLang 1.17+ Reported by the maintainer running ./testbox/run --stream --verbose against BoxLang v1.17.0+58: every CLI run crashed immediately after the recent CLI-runner fix (#201) cleared the way to reach this code, with: KeyNotFoundException: The requested key [url] was not located in any scope or it's undefined at ...ScriptingRequestBoxContext.scopeFind at ...Testbox$cfc.invokeFunction_runRaw(TestBox.cfc:408) Root cause: runRaw() and dryRun() unconditionally reference the `url` scope to support web-request query-string test filters (?testBundles=, ?testSuites=, etc.). The `url` scope only exists in a real HTTP request context - it is never registered at all when TestBox runs via the BoxLang CLI. #200 ("support full null runtimes") added a `structKeyExists( url, "testBundles" )` guard, but that doesn't help here: resolving the bare `url` identifier itself is what throws - structKeyExists() never gets a chance to run, since BoxLang has to look up `url` as a scope before it can pass it as an argument. Fix: skip these URL-based filter blocks entirely when variables.IS_CLI is true (already computed at class init). CLI users already have --filter-bundles/--filter-suites/--filter-specs for the same purpose via BoxLangRunner.bx, so nothing is lost. Verified locally against BoxLang v1.17.0+58: reproduced the exact reported crash against the real merged development branch first (structKeyExists guard included), then confirmed the fix with the maintainer's exact command (./testbox/run --streamingj --verbose, 4/4 clean runs), plus --dry-run and --stream, which exercise both patched call sites (runRaw() and dryRun()). --- system/TestBox.cfc | 52 ++++++++++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/system/TestBox.cfc b/system/TestBox.cfc index 72b821c..478ce3d 100644 --- a/system/TestBox.cfc +++ b/system/TestBox.cfc @@ -405,17 +405,22 @@ component accessors="true" { ); // Verify URL conventions for bundle, suites and specs exclusions. - if ( structKeyExists( url, "testBundles" ) && !isNull( url.testBundles ) ) { - testBundles.append( listToArray( urlDecode( url.testBundles ) ), true ); - } - if ( structKeyExists( url, "testSuites" ) && !isNull( url.testSuites ) ) { - arguments.testSuites.append( listToArray( urlDecode( url.testSuites ) ), true ); - } - if ( structKeyExists( url, "testSpecs" ) && !isNull( url.testSpecs ) ) { - arguments.testSpecs.append( listToArray( urlDecode( url.testSpecs ) ), true ); - } - if ( structKeyExists( url, "testMethod" ) && !isNull( url.testMethod ) ) { - arguments.testSpecs.append( listToArray( urlDecode( url.testMethod ) ), true ); + // The `url` scope only exists in a real HTTP request - it isn't registered at all when + // TestBox runs via the BoxLang CLI, so even a `structKeyExists( url, ... )` guard throws + // (resolving the bare `url` identifier is what fails, not the key lookup within it). + if ( !variables.IS_CLI ) { + if ( structKeyExists( url, "testBundles" ) && !isNull( url.testBundles ) ) { + testBundles.append( listToArray( urlDecode( url.testBundles ) ), true ); + } + if ( structKeyExists( url, "testSuites" ) && !isNull( url.testSuites ) ) { + arguments.testSuites.append( listToArray( urlDecode( url.testSuites ) ), true ); + } + if ( structKeyExists( url, "testSpecs" ) && !isNull( url.testSpecs ) ) { + arguments.testSpecs.append( listToArray( urlDecode( url.testSpecs ) ), true ); + } + if ( structKeyExists( url, "testMethod" ) && !isNull( url.testMethod ) ) { + arguments.testSpecs.append( listToArray( urlDecode( url.testMethod ) ), true ); + } } // Using a directory runner? @@ -524,17 +529,20 @@ component accessors="true" { isSimpleValue( arguments.testSpecs ) ? listToArray( arguments.testSpecs ) : arguments.testSpecs ); - if ( structKeyExists( url, "testBundles" ) && !isNull( url.testBundles ) ) { - arguments.testBundles.append( listToArray( urlDecode( url.testBundles ) ), true ); - } - if ( structKeyExists( url, "testSuites" ) && !isNull( url.testSuites ) ) { - arguments.testSuites.append( listToArray( urlDecode( url.testSuites ) ), true ); - } - if ( structKeyExists( url, "testSpecs" ) && !isNull( url.testSpecs ) ) { - arguments.testSpecs.append( listToArray( urlDecode( url.testSpecs ) ), true ); - } - if ( structKeyExists( url, "testMethod" ) && !isNull( url.testMethod ) ) { - arguments.testSpecs.append( listToArray( urlDecode( url.testMethod ) ), true ); + // The `url` scope only exists in a real HTTP request - see the identical guard in runRaw(). + if ( !variables.IS_CLI ) { + if ( structKeyExists( url, "testBundles" ) && !isNull( url.testBundles ) ) { + arguments.testBundles.append( listToArray( urlDecode( url.testBundles ) ), true ); + } + if ( structKeyExists( url, "testSuites" ) && !isNull( url.testSuites ) ) { + arguments.testSuites.append( listToArray( urlDecode( url.testSuites ) ), true ); + } + if ( structKeyExists( url, "testSpecs" ) && !isNull( url.testSpecs ) ) { + arguments.testSpecs.append( listToArray( urlDecode( url.testSpecs ) ), true ); + } + if ( structKeyExists( url, "testMethod" ) && !isNull( url.testMethod ) ) { + arguments.testSpecs.append( listToArray( urlDecode( url.testMethod ) ), true ); + } } var filterState = new testbox.system.TestResult(