Summary
Both test-runner endpoints accept directory= and validate it against an allowlist regex (core: ^(wheels\.tests|vendor\.<pkg>\.tests)(\.\w+)*$ in vendor/wheels/tests/runner.cfm:122-135; the app runner has an equivalent ^tests(\.\w+)*$ shape). When the value fails the regex, the runner silently substitutes the default root and runs the entire suite. When the value passes but names a spec FILE, TestBox discovers 0 bundles and reports green.
Repro (Lucee 7 + SQLite core endpoint, develop)
GET /wheels/core/tests?db=sqlite&format=json&directory=tests.specs.model → 314 bundles / 4420 passes (full core suite) with no indication anything was rejected — this exact example appears in testing/index.mdx, where the reader asked for "just the model specs" and got a multi-minute full run that looks like a green model run.
- Correct value
directory=wheels.tests.specs.model → 48 bundles / 893 passes.
directory=wheels.tests.specs.model.callbacksSpec (single file) → 0 bundles, 0 specs, green — the inverse trap. testBundles= is the working single-file filter (→ 43 passes, just that bundle).
The CLI already works around the first trap client-side ($normalizeTestFilter, whose comment notes the runners "silently fall back to the default scope, running the entire suite"), but anyone driving the URL directly — which the docs recommend for the inner loop — gets no signal.
Proposed direction
When url.directory is present but rejected by the allowlist, or accepted but discovers 0 bundles:
- include
directoryRequested / directoryResolved / bundlesDiscovered fields in the JSON payload, and
- either respond 400, or at minimum emit a
warnings array entry — so humans and CI scripts can detect the mismatch instead of trusting a green total from the wrong scope.
Defense-in-depth allowlisting stays unchanged; this only adds visibility. Docs-side example fixes (correct directory= shapes; testBundles= for single files) ride the guide-audit docs wave.
Acceptance
- A rejected
directory= is detectable from the response (status or explicit fields/warnings) on both core and app runners.
- A 0-bundle discovery is distinguishable from a passing run.
- Specs cover both shapes.
Not a duplicate of #3025 (application-scope mutation) — this is request-scoped input handling.
Reported by the guide-behavioral-audit P1 batch 2 (work item p1-15-testing, claims IDX-07/RUN-06; live on the Lucee 7 docker harness).
Summary
Both test-runner endpoints accept
directory=and validate it against an allowlist regex (core:^(wheels\.tests|vendor\.<pkg>\.tests)(\.\w+)*$invendor/wheels/tests/runner.cfm:122-135; the app runner has an equivalent^tests(\.\w+)*$shape). When the value fails the regex, the runner silently substitutes the default root and runs the entire suite. When the value passes but names a spec FILE, TestBox discovers 0 bundles and reports green.Repro (Lucee 7 + SQLite core endpoint, develop)
GET /wheels/core/tests?db=sqlite&format=json&directory=tests.specs.model→ 314 bundles / 4420 passes (full core suite) with no indication anything was rejected — this exact example appears intesting/index.mdx, where the reader asked for "just the model specs" and got a multi-minute full run that looks like a green model run.directory=wheels.tests.specs.model→ 48 bundles / 893 passes.directory=wheels.tests.specs.model.callbacksSpec(single file) → 0 bundles, 0 specs, green — the inverse trap.testBundles=is the working single-file filter (→ 43 passes, just that bundle).The CLI already works around the first trap client-side (
$normalizeTestFilter, whose comment notes the runners "silently fall back to the default scope, running the entire suite"), but anyone driving the URL directly — which the docs recommend for the inner loop — gets no signal.Proposed direction
When
url.directoryis present but rejected by the allowlist, or accepted but discovers 0 bundles:directoryRequested/directoryResolved/bundlesDiscoveredfields in the JSON payload, andwarningsarray entry — so humans and CI scripts can detect the mismatch instead of trusting a green total from the wrong scope.Defense-in-depth allowlisting stays unchanged; this only adds visibility. Docs-side example fixes (correct
directory=shapes;testBundles=for single files) ride the guide-audit docs wave.Acceptance
directory=is detectable from the response (status or explicit fields/warnings) on both core and app runners.Not a duplicate of #3025 (application-scope mutation) — this is request-scoped input handling.
Reported by the guide-behavioral-audit P1 batch 2 (work item p1-15-testing, claims IDX-07/RUN-06; live on the Lucee 7 docker harness).