Skip to content

Commit 8602697

Browse files
committed
Merge remote-tracking branch 'origin/develop' into p0g-smoke-green
2 parents cadfb13 + a6df6be commit 8602697

19 files changed

Lines changed: 1135 additions & 33 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ The framework must run on Lucee 5/6/7, Adobe CF 2018/2021/2023/2025, and BoxLang
4949
10. **Adobe CF 2023 and 2025 reject the `arguments` scope as `attributeCollection` on *any* built-in CFML tag.** Affects every `cfheader` / `cfcache` / `cfcontent` / `cfmail` / `cfdirectory` / `cffile` / `cflocation` / `cfhtmlhead` / `cfimage` / `cfdbinfo` / `cfinvoke` / `cfwddx` / `cfzip` wrapper. Covers both the string-interpolated (`attributeCollection = "#arguments#"`) and direct-struct (`attributeCollection = arguments`) forms. Adobe 2023/2025 throw — `cfheader`'s message is `"Failed to add HTML header"`; other tags surface their own — and `$header()` is catastrophic because it runs on every request. Copy to a plain struct first: `local.args = {}; for (local.key in arguments) { local.args[local.key] = arguments[local.key]; }`. Lucee 6/7, BoxLang, and Adobe 2018/2021 accept both forms; Adobe 2023/2025 require the plain struct. The 13 sites in `vendor/wheels/Global.cfc` were patched uniformly in [#2750](https://github.com/wheels-dev/wheels/pull/2750).
5050
11. **`local.X = ...` inside `catch` doesn't persist on BoxLang.** Catch body runs under a nested `local` that gets discarded on exit, so `expect(local.X)` after the catch reads the un-touched outer value. Use a struct field: `var state = {flag = false}; ... state.flag = true;`. Bare `var bareName` + unscoped `bareName = true` also works but the struct form mirrors `TenantResolverSpec` and is the prior-art pattern.
5151
12. **`for (local.i = ...)` inside `finally` miscompiles on Lucee 7.** Lucee 7.0.1+100 throws `variable [local] doesn't exist` at runtime when a `for` loop declares or iterates `local`-/`var`-scoped variables inside a `finally` block (one probe shape even produced a JVM `Expecting a stackmap frame` verifier error). Bare assignments and function calls in `finally` are fine; loops are not. Hoist the loop into a `public` `$`-prefixed helper and call it from `finally` — reference: `$restoreEmailViewVariables()` in `vendor/wheels/controller/miscellaneous.cfc` ([#2922](https://github.com/wheels-dev/wheels/pull/2922)).
52+
13. **Bare tag-in-script statements without parentheses (e.g. `cfabort;`) are Lucee-only.** Adobe CF compiles the bare token as a reference to an undefined VARIABLE and throws `Variable CFABORT is undefined` at runtime (every Adobe engine, not just one release). Use the script keyword (`abort;`) or the parenthesized call form (`cfheader(...)`-style) instead. The `enablePublicComponent=false` 404 branch in `vendor/wheels/Dispatch.cfc` shipped a bare `cfabort;`, which turned `GET /` on every stock Adobe install in `testing`/`production` into an HTTP 500 ([#3029](https://github.com/wheels-dev/wheels/issues/3029)). Structural guard: `vendor/wheels/tests/specs/security/BareCfabortGuardSpec.cfc` fails the suite if any bare script-context `cfabort` statement reappears under `vendor/wheels/**/*.cfc` (tag-context `<cfabort>` in `.cfm`/tag-based CFCs stays legal).
5253

5354
Verify Adobe CF fixes locally before pushing — don't iterate via CI:
5455
```bash
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- `wheels test` (and `wheels browser test`) now honour the `subpath` setting for subfolder-mounted apps via a new `--base-path` flag, auto-deriving the prefix from `WHEELS_SUBPATH` or `set(subpath=...)` when omitted, so the test runner is reachable under a URL prefix instead of always assuming a root mount (#3026)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Dispatch: the `enablePublicComponent=false` anti-fingerprinting 404 branch ended in a bare `cfabort;`, which is Lucee-only tag-in-script syntax — every Adobe engine threw `Variable CFABORT is undefined` at runtime, turning `GET /` (and every `/wheels/*` request) on a stock app in `testing`/`production` into an HTTP 500 instead of a clean `404 Not Found`. Replaced with the script keyword `abort;`, and a new structural guard spec (`BareCfabortGuardSpec`) fails the suite if a bare script-context `cfabort` statement ever reappears under `vendor/wheels` (#3029)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- URL environment switching (`?reload=<environment>&password=...`) now works through the app template's `applicationStop()` reload flow: the restart redirect preserves `reload` + `password` for environment switches (plain `?reload=true` still strips everything), the configured `reloadPassword` is handed across the restart via a single-use server-scope entry so the framework's switch code can verify it on the cold start, and the reload gate skips the restart once the requested environment is active so the redirect chain always terminates. `allowEnvironmentSwitchViaUrl` is enforced pre-restart: when switching is disallowed — `set(allowEnvironmentSwitchViaUrl=false)` or the framework's production/testing/maintenance auto-disable — the parameters are stripped and the request degrades to a plain restart, preserving the existing hardening (the framework cannot enforce the flag itself after `applicationStop()` destroys its carryover state). Applied to all four `public/Application.cfc` copies (CLI app template, repo demo app, starter-app and tweet examples). Trade-off: `?reload=<current-environment>` is now a no-op — use `?reload=true` for a same-environment restart (#3030)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Explicit `set(allowEnvironmentSwitchViaUrl=true)` in `config/settings.cfm` is now honored in production-like environments (production, testing, maintenance). It used to be indistinguishable from the framework default and was silently discarded, making the documented override impossible (#3031)

cli/lucli/Module.cfc

Lines changed: 95 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,7 @@ component extends="modules.BaseModule" {
274274
.option(name = "directory", default = "", description = "Documented alias for --filter")
275275
.option(name = "reporter", default = "simple", description = "Output format: simple, json, or tap")
276276
.option(name = "db", default = "sqlite", description = "Database the suite runs against")
277+
.option(name = "base-path", default = "", description = "URL prefix the app is mounted under (e.g. /myapp). Auto-derived from WHEELS_SUBPATH or set(subpath=...) when omitted.")
277278
.flag(name = "verbose", default = false, description = "Print per-spec detail instead of the summary rollup")
278279
.flag(name = "ci", default = false, description = "CI mode output")
279280
.flag(name = "core", default = false, description = "Run the framework core suite (vendor/wheels/tests) instead of the app suite")
@@ -713,7 +714,8 @@ component extends="modules.BaseModule" {
713714
core = parsed.core,
714715
db = parsed.db,
715716
dbExplicit = structKeyExists(arguments.coll, "db"),
716-
useTestDB = parsed["test-db"]
717+
useTestDB = parsed["test-db"],
718+
basePath = parsed["base-path"]
717719
};
718720
}
719721

@@ -731,6 +733,7 @@ component extends="modules.BaseModule" {
731733
var db = opts.db;
732734
var dbExplicit = opts.dbExplicit;
733735
var useTestDB = opts.useTestDB;
736+
var basePath = opts.basePath;
734737

735738
// Default to APP mode unless --core is set explicitly. The previous
736739
// auto-detection ("if vendor/wheels/tests/ exists, default to core")
@@ -749,7 +752,7 @@ component extends="modules.BaseModule" {
749752
// expects. Onboarding finding #2.
750753
filter = $normalizeTestFilter(filter, coreTests);
751754

752-
return runTests(filter, reporter, format, verboseOutput, coreTests, db, ciMode, useTestDB, dbExplicit);
755+
return runTests(filter, reporter, format, verboseOutput, coreTests, db, ciMode, useTestDB, dbExplicit, basePath);
753756
}
754757

755758
/**
@@ -4731,14 +4734,21 @@ component extends="modules.BaseModule" {
47314734
string db = "sqlite",
47324735
boolean ciMode = false,
47334736
boolean useTestDB = true,
4734-
boolean dbExplicit = false
4737+
boolean dbExplicit = false,
4738+
string basePath = ""
47354739
) {
47364740
var serverPort = $requireRunningServer([
47374741
"Start one with: wheels start",
47384742
"Or use: bash tools/test-local.sh (auto-manages server)"
47394743
]);
47404744

4741-
var testPath = coreTests ? "/wheels/core/tests" : "/wheels/app/tests";
4745+
// Subfolder-mounted apps (`set(subpath="/myapp")`, #2985/#3026) serve the
4746+
// test runner under a URL prefix the rewrite layer expects — without it
4747+
// the request never routes to the app. Resolve the prefix from the
4748+
// explicit --base-path flag, else WHEELS_SUBPATH, else the subpath
4749+
// setting in config/settings.cfm; root-mounted apps resolve to "".
4750+
var resolvedBasePath = $resolveTestBasePath(basePath);
4751+
var testPath = $buildTestRunnerPath(coreTests, resolvedBasePath);
47424752

47434753
// Print the suite type with a truthful datasource label. Issue #2489:
47444754
// the previous output echoed `--db` even for app tests where the
@@ -6331,6 +6341,78 @@ component extends="modules.BaseModule" {
63316341
return reFindNoCase("_test$", base) ? base : base & "_test";
63326342
}
63336343

6344+
/**
6345+
* Resolve the URL base path the app is mounted under, for the test-runner
6346+
* request. Precedence (issue #3026): an explicit value (the --base-path
6347+
* flag) wins; otherwise the WHEELS_SUBPATH environment variable; otherwise
6348+
* a `set(subpath="...")` call scanned out of config/settings.cfm (CFML
6349+
* comments stripped first — Anti-Pattern 14). Root-mounted apps resolve to
6350+
* "". The returned value is normalized (leading slash, no trailing slash)
6351+
* so callers can prefix it directly onto the runner path.
6352+
*/
6353+
public string function $resolveTestBasePath(string explicit = "") {
6354+
// 1. Explicit flag wins over any derivation.
6355+
if (len(trim(arguments.explicit))) {
6356+
return $normalizeBasePath(arguments.explicit);
6357+
}
6358+
6359+
// 2. WHEELS_SUBPATH environment variable — mirrors how the framework
6360+
// reads it in $resolveFrameworkPaths()/$get("subpath").
6361+
try {
6362+
var envValue = createObject("java", "java.lang.System").getenv("WHEELS_SUBPATH");
6363+
if (!isNull(envValue) && len(trim(envValue))) {
6364+
return $normalizeBasePath(envValue);
6365+
}
6366+
} catch (any e) {}
6367+
6368+
// 3. set(subpath="...") in config/settings.cfm. Strip comments first so a
6369+
// commented-out call can't false-match (Anti-Pattern 14). The word
6370+
// boundary keeps `coreTestSubpath`-style siblings from matching.
6371+
var settingsFile = variables.projectRoot & "/config/settings.cfm";
6372+
if (fileExists(settingsFile)) {
6373+
var settingsContent = stripCfmlComments(fileRead(settingsFile));
6374+
var settingsMatch = reFindNoCase('\bsubpath\b\s*=\s*"([^"]*)"', settingsContent, 1, true);
6375+
if (arrayLen(settingsMatch.match) > 1 && len(trim(settingsMatch.match[2]))) {
6376+
return $normalizeBasePath(settingsMatch.match[2]);
6377+
}
6378+
}
6379+
6380+
return "";
6381+
}
6382+
6383+
/**
6384+
* Normalize a URL base path to a leading slash with no trailing slash,
6385+
* mirroring the framework's $resolveFrameworkPaths() in
6386+
* vendor/wheels/Global.cfc. Empty/whitespace input and a bare root slash
6387+
* both resolve to "" (root mount — no prefix to add).
6388+
*/
6389+
public string function $normalizeBasePath(required string raw) {
6390+
var normalized = trim(arguments.raw);
6391+
if (!len(normalized)) {
6392+
return "";
6393+
}
6394+
if (left(normalized, 1) != "/") {
6395+
normalized = "/" & normalized;
6396+
}
6397+
// Strip trailing slash(es). Guard the Len > 1 floor so we never call
6398+
// Left(str, 0), which crashes Lucee 7 (CLAUDE.md cross-engine invariant 8).
6399+
while (len(normalized) > 1 && right(normalized, 1) == "/") {
6400+
normalized = left(normalized, len(normalized) - 1);
6401+
}
6402+
// A bare "/" means the app is at the server root — no prefix.
6403+
return normalized == "/" ? "" : normalized;
6404+
}
6405+
6406+
/**
6407+
* Build the test-runner request path, prefixed by the (normalized) base
6408+
* path. Core tests hit /wheels/core/tests; app tests hit /wheels/app/tests.
6409+
* issue #3026.
6410+
*/
6411+
public string function $buildTestRunnerPath(boolean coreTests = false, string basePath = "") {
6412+
var prefix = $normalizeBasePath(arguments.basePath);
6413+
return prefix & (arguments.coreTests ? "/wheels/core/tests" : "/wheels/app/tests");
6414+
}
6415+
63346416
/**
63356417
* Check if a port is responding to HTTP requests
63366418
*/
@@ -6750,6 +6832,7 @@ component extends="modules.BaseModule" {
67506832
private string function browserTest(array args = []) {
67516833
var format = "text";
67526834
var verboseOutput = false;
6835+
var basePath = "";
67536836
// Default to the APP's browser specs (tests/specs/browser/) — not the
67546837
// framework's internal browser specs. Onboarding finding F11 reported
67556838
// `wheels browser test` running 0 tests because it pointed at
@@ -6766,6 +6849,8 @@ component extends="modules.BaseModule" {
67666849
format = valueAfterEquals(arg);
67676850
} else if (reFindNoCase("^--directory=", arg)) {
67686851
directory = valueAfterEquals(arg);
6852+
} else if (reFindNoCase("^--base-path=", arg)) {
6853+
basePath = valueAfterEquals(arg);
67696854
} else if (!arg.startsWith("--")) {
67706855
directory = arg;
67716856
}
@@ -6816,7 +6901,12 @@ component extends="modules.BaseModule" {
68166901
// core test runner (`/wheels/core/tests`). The latter only knows
68176902
// about specs under `vendor/wheels/tests/specs/`. Apps live under
68186903
// `tests/specs/`, mounted by the app runner. F11.
6819-
var testUrl = "http://localhost:#serverPort#/wheels/app/tests?db=sqlite&format=json&directory=#directory#";
6904+
//
6905+
// Prefix the subfolder base path (#3026) so browser tests reach the
6906+
// runner on a subpath-mounted app the same way `wheels test` does.
6907+
var resolvedBasePath = $resolveTestBasePath(basePath);
6908+
var runnerPath = $buildTestRunnerPath(false, resolvedBasePath);
6909+
var testUrl = "http://localhost:#serverPort##runnerPath#?db=sqlite&format=json&directory=#directory#";
68206910

68216911
try {
68226912
var httpResult = makeHttpRequest(testUrl);

0 commit comments

Comments
 (0)