From 54c2f95d04dac833ec0006a59b2c006b87a65ffe Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 23:15:46 +0000 Subject: [PATCH 1/3] fix(test): auto-bind include-injected globals into WheelsTest spec scope MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `WheelsTest`'s pseudo-constructor used `getMetaData(application.wo).functions` to discover which Wheels globals to copy into a spec's `variables`/`this` scope. That metadata enumerates only methods declared on the CFC body and silently skips symbols merged in via `cfinclude` — which is how `vendor/wheels/Global.cfc` pulls user helpers from `app/global/functions.cfm`. Apps with custom helpers (`can()`, `hasRole()`, etc.) had to manually rebind each one in `beforeAll()`. The loop now iterates `application.wo` as a struct and binds every UDF detected by `isCustomFunction()`, while preserving the existing public-only filter for metadata-declared methods and the don't-clobber guard for scope members the spec (or its base class) already provides. Fixes #2790 Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> --- CHANGELOG.md | 1 + vendor/wheels/WheelsTest.cfc | 33 +++++--- .../WheelsTestAutoBindIncludesSpec.cfc | 76 +++++++++++++++++++ 3 files changed, 100 insertions(+), 10 deletions(-) create mode 100644 vendor/wheels/tests/specs/wheelstest/WheelsTestAutoBindIncludesSpec.cfc diff --git a/CHANGELOG.md b/CHANGELOG.md index bc365070e2..f2735de1ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ All historical references to "CFWheels" in this changelog have been preserved fo ### Fixed +- `WheelsTest` auto-bind missed user-defined global helpers added via `include` in `app/global/functions.cfm`. The pseudo-constructor used `getMetaData(application.wo).functions`, which only enumerates methods declared directly on the CFC and skips symbols merged in via `cfinclude`. Specs that called custom helpers (e.g. `can()`, `hasRole()`) had to manually rebind each one in `beforeAll()`. The auto-bind now iterates `application.wo` as a struct and binds every UDF via `isCustomFunction()`, preserving the existing public-only filter for declared methods (#2790) - Linux `.deb` / `.rpm` packages double-nested the framework at `/opt/wheels/module/vendor/wheels/wheels/` instead of `/opt/wheels/module/vendor/wheels/`. `wheels-core-VER.zip` carries a top-level `wheels/` directory that `unzip` preserves; the nfpm `type: tree` rule then copied the entire `build/framework/` tree (wrapper and all) into the destination, leaving `Injector.cfc` one level too deep. Every fresh `wheels new` install on Ubuntu/Fedora then crashed on first request with `could not find component or class with name [wheels.Injector]`, cascading into the cryptic `The key [WO] does not exist.` error in `onError`. The brew formula handles this correctly via `(share/"wheels/framework/wheels").install Dir["*"]`; the Linux nfpm configs now pin `src` at `./build/framework/wheels/` to match. Regression spec at `vendor/wheels/tests/specs/cli/LinuxPackageStagingSpec.cfc` (#2773) - `onError` in the generated app template and demo `public/Application.cfc` now guards `application.wo` with `StructKeyExists(application, "wo")` after the recovery try/catch. When `new wheels.Injector(...)` fails during `onApplicationStart` (e.g. a stale `/wheels` mapping under Lucee Express 7), the original error is preserved via a minimal HTML fallback instead of cascading into the cryptic "The key [WO] does not exist" exception that hit "Your First 15 Minutes" tutorial users on fresh installs diff --git a/vendor/wheels/WheelsTest.cfc b/vendor/wheels/WheelsTest.cfc index 9b261ac67a..b74cd9df53 100644 --- a/vendor/wheels/WheelsTest.cfc +++ b/vendor/wheels/WheelsTest.cfc @@ -10,18 +10,31 @@ component extends="wheels.wheelstest.system.BaseSpec" { // Pseudo-constructor (runs automatically) if (structKeyExists(application, "wo")) { - local.methods = getMetaData(application.wo).functions; - - for (local.method in local.methods) { - // Only add public, non-inherited methods - if (local.method.access eq "public") { - local.methodExists = structKeyExists(variables, local.method.name) || structKeyExists(this, local.method.name); + // Iterate struct keys on application.wo and bind every UDF. This + // catches both methods declared on Global.cfc (visible to + // getMetaData) AND helpers merged in via cfinclude (e.g. + // app/global/functions.cfm), which getMetaData(application.wo).functions + // does NOT enumerate — see #2790. + local.metaIndex = {}; + for (local.fn in getMetaData(application.wo).functions) { + local.metaIndex[local.fn.name] = local.fn.access; + } - if (!local.methodExists) { - variables[local.method.name] = application.wo[local.method.name]; - this[local.method.name] = application.wo[local.method.name]; - } + for (local.key in application.wo) { + if (!isCustomFunction(application.wo[local.key])) { + continue; + } + // For methods present in CFC metadata, keep the existing + // public-only filter; include-injected helpers have no + // access modifier so they're treated as public. + if (structKeyExists(local.metaIndex, local.key) && local.metaIndex[local.key] neq "public") { + continue; + } + if (structKeyExists(variables, local.key) || structKeyExists(this, local.key)) { + continue; } + variables[local.key] = application.wo[local.key]; + this[local.key] = application.wo[local.key]; } } diff --git a/vendor/wheels/tests/specs/wheelstest/WheelsTestAutoBindIncludesSpec.cfc b/vendor/wheels/tests/specs/wheelstest/WheelsTestAutoBindIncludesSpec.cfc new file mode 100644 index 0000000000..9b78596189 --- /dev/null +++ b/vendor/wheels/tests/specs/wheelstest/WheelsTestAutoBindIncludesSpec.cfc @@ -0,0 +1,76 @@ +/** + * Regression: WheelsTest auto-bind misses include-injected helpers (#2790). + * + * `WheelsTest.cfc` uses `getMetaData(application.wo).functions` to discover + * which Wheels globals to bind into the spec's `variables` / `this` scopes. + * That metadata enumerates only methods defined directly on the CFC body, + * NOT symbols merged in via `cfinclude` / `include` (which is how + * `vendor/wheels/Global.cfc` pulls `/app/global/functions.cfm` at the bottom + * of the file). User-defined global helpers therefore worked in controllers / + * views / models but were invisible to test specs — every spec had to + * manually rebind helpers in `beforeAll()`. + * + * These specs simulate the include path by assigning a UDF directly to + * `application.wo` (which is exactly the shape an included function takes: + * a struct key on the component, not a metadata-enumerable function), then + * instantiate a fresh `wheels.WheelsTest` and assert the auto-bind loop + * caught it. + */ +component extends="wheels.WheelsTest" { + + function run() { + + describe("WheelsTest auto-bind", () => { + + describe("helpers attached to application.wo outside of CFC metadata (issue ##2790)", () => { + + it("the bug precondition holds: include-style UDFs are invisible to getMetaData", () => { + var probeName = "$bot2790MetaProbe"; + application.wo[probeName] = function() { + return "metadata-probe"; + }; + try { + var meta = getMetaData(application.wo).functions; + var foundInMeta = false; + for (var fn in meta) { + if (fn.name == probeName) { + foundInMeta = true; + break; + } + } + expect(foundInMeta).toBeFalse(); + expect(structKeyExists(application.wo, probeName)).toBeTrue(); + expect(isCustomFunction(application.wo[probeName])).toBeTrue(); + } finally { + structDelete(application.wo, probeName); + } + }); + + it("auto-binds include-style helpers into a fresh WheelsTest instance", () => { + var probeName = "$bot2790BindProbe"; + application.wo[probeName] = function() { + return "bind-probe"; + }; + try { + var freshSpec = new wheels.WheelsTest(); + expect(structKeyExists(freshSpec, probeName)).toBeTrue(); + var bound = freshSpec[probeName]; + expect(bound()).toBe("bind-probe"); + } finally { + structDelete(application.wo, probeName); + } + }); + + it("still binds methods that ARE in CFC metadata (regression guard for the existing path)", () => { + var freshSpec = new wheels.WheelsTest(); + expect(structKeyExists(freshSpec, "model")).toBeTrue(); + expect(structKeyExists(freshSpec, "urlFor")).toBeTrue(); + }); + + }); + + }); + + } + +} From 34bb41e171ad8402c359498df153dabef26bd225 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 23:32:22 +0000 Subject: [PATCH 2/3] fix(test): address Reviewer A/B consensus findings (round 1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Promote include-injected UDFs from `variables` to `this` in `vendor/wheels/Global.cfc` after `include "/app/global/functions.cfm"`, so the auto-bind loop in `vendor/wheels/WheelsTest.cfc` discovers them uniformly across Lucee, Adobe CF, and BoxLang. Lucee's struct-iteration over a CFC instance surfaces both `this` and `variables` scopes, but Adobe CF only reliably exposes `this`-scope members — without the promotion, the original bug (#2790) would silently persist on Adobe CF even with the new iteration path in WheelsTest.cfc. - Fix the misleading header comment in `vendor/wheels/tests/specs/wheelstest/WheelsTestAutoBindIncludesSpec.cfc`. Bracket-notation assignment from outside writes to `this` scope, not `variables` — so the probe simulates the post-promotion shape, not the raw include shape. Comment now spells this out explicitly. - Add a new `it` case that asserts the probe key is enumerated by `for (key in application.wo)`. Guards the iteration mechanism the auto-bind loop depends on, so failures on any engine where struct- iteration is narrower than expected would fail this spec rather than silently pass-but-not-test downstream. Addresses Reviewer A's cross-engine concern (Adobe CF struct-iteration contract) and Reviewer B's joint recommendation option (b): promote include-injected helpers to `this` so the iteration path is uniform. The accompanying spec correction handles A's "spec injects via wrong scope" finding and B's "misleading header comment" note. Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> --- vendor/wheels/Global.cfc | 16 +++++++ .../WheelsTestAutoBindIncludesSpec.cfc | 46 +++++++++++++++++-- 2 files changed, 57 insertions(+), 5 deletions(-) diff --git a/vendor/wheels/Global.cfc b/vendor/wheels/Global.cfc index 96cb870485..022b09d0a8 100644 --- a/vendor/wheels/Global.cfc +++ b/vendor/wheels/Global.cfc @@ -3852,4 +3852,20 @@ return local.$wheels; // User-defined global functions include "/app/global/functions.cfm"; + // Promote include-injected UDFs from `variables` to `this` so they're + // discoverable via struct-iteration on engines (Adobe CF) where only + // `this`-scope members are reliably enumerable. Declared methods on + // Global.cfc are already in `this` via their `access` modifier and are + // not clobbered by the `structKeyExists(this, ...)` guard. See #2790 + // and the auto-bind loop in `vendor/wheels/WheelsTest.cfc`. + for (local.varKey in variables) { + if (!isCustomFunction(variables[local.varKey])) { + continue; + } + if (structKeyExists(this, local.varKey)) { + continue; + } + this[local.varKey] = variables[local.varKey]; + } + } diff --git a/vendor/wheels/tests/specs/wheelstest/WheelsTestAutoBindIncludesSpec.cfc b/vendor/wheels/tests/specs/wheelstest/WheelsTestAutoBindIncludesSpec.cfc index 9b78596189..8c4d42f7ac 100644 --- a/vendor/wheels/tests/specs/wheelstest/WheelsTestAutoBindIncludesSpec.cfc +++ b/vendor/wheels/tests/specs/wheelstest/WheelsTestAutoBindIncludesSpec.cfc @@ -10,11 +10,23 @@ * views / models but were invisible to test specs — every spec had to * manually rebind helpers in `beforeAll()`. * - * These specs simulate the include path by assigning a UDF directly to - * `application.wo` (which is exactly the shape an included function takes: - * a struct key on the component, not a metadata-enumerable function), then - * instantiate a fresh `wheels.WheelsTest` and assert the auto-bind loop - * caught it. + * Cross-engine note: a bare `include` inside a CFC body lands UDFs in the + * component's `variables` scope, not `this`. Lucee's struct-iteration over + * a CFC instance surfaces both scopes, but Adobe CF's contract only + * reliably exposes `this`-scope members. To make the auto-bind path + * uniform across engines, `vendor/wheels/Global.cfc` promotes include- + * injected UDFs from `variables` to `this` immediately after the include + * runs. These specs simulate that post-promotion shape by assigning the + * probe UDF directly to `application.wo` (bracket-notation assignment + * from outside writes to `this`), then assert that: + * + * - The probe is invisible to `getMetaData(application.wo).functions` + * (the bug precondition the old code missed). + * - The probe is enumerated by `for (key in application.wo)` — the + * iteration mechanism the new auto-bind loop relies on. Failure here + * on any engine means the auto-bind loop will silently miss the helper. + * - The probe lands on a fresh `wheels.WheelsTest` instance and is + * callable. */ component extends="wheels.WheelsTest" { @@ -46,6 +58,30 @@ component extends="wheels.WheelsTest" { } }); + it("for-in iteration over application.wo enumerates the probe key", () => { + // Guards the iteration mechanism the auto-bind loop in + // WheelsTest.cfc relies on. If this fails on any engine + // (notably Adobe CF, where struct-iteration over a CFC + // only reliably exposes this-scope members), the bind + // case below will silently pass-but-not-test. + var probeName = "$bot2790IterProbe"; + application.wo[probeName] = function() { + return "iter-probe"; + }; + try { + var seen = false; + for (var key in application.wo) { + if (key == probeName) { + seen = true; + break; + } + } + expect(seen).toBeTrue(); + } finally { + structDelete(application.wo, probeName); + } + }); + it("auto-binds include-style helpers into a fresh WheelsTest instance", () => { var probeName = "$bot2790BindProbe"; application.wo[probeName] = function() { From 395a2ad76c57caa7e8d5b3b6431dc52f744ea640 Mon Sep 17 00:00:00 2001 From: Peter Amiri Date: Fri, 22 May 2026 07:23:54 -0700 Subject: [PATCH 3/3] fix(test): seed `local` scope before pseudo-constructor for-iterator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The promotion loop added in round 1 of the consensus fixup crashed Lucee 7 with `variable [local] doesn't exist` at Global.cfc:3861 — the test runner couldn't even reach a spec before bailing out. In a CFC pseudo-constructor (component body, not inside a function), the `local` scope is not auto-created. Direct assignment to `local.X = ...` will seed it, but `for (local.X in Y)` tries to read `local` first as the iterator's target parent and fails. WheelsTest.cfc gets away with the same loop shape only because it does `local.metaIndex = {}` earlier in its own pseudo-constructor; Global.cfc had no such seeding line. Add the minimum seeding statement (`local.varKey = "";`) directly above the loop and document the cross-engine reason inline. The loop's filter logic is unchanged. The original review couldn't catch this — the round-1 address-review sandbox lacked a working test runner so the fix went out unverified. Signed-off-by: Peter Amiri --- vendor/wheels/Global.cfc | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/vendor/wheels/Global.cfc b/vendor/wheels/Global.cfc index 022b09d0a8..6b3a2b3657 100644 --- a/vendor/wheels/Global.cfc +++ b/vendor/wheels/Global.cfc @@ -3858,6 +3858,13 @@ return local.$wheels; // Global.cfc are already in `this` via their `access` modifier and are // not clobbered by the `structKeyExists(this, ...)` guard. See #2790 // and the auto-bind loop in `vendor/wheels/WheelsTest.cfc`. + // + // The leading `local.varKey = ""` seeds the `local` scope: Lucee 7's + // pseudo-constructor doesn't auto-create `local` for the iterator + // target of `for (local.X in Y)`, throwing "variable [local] doesn't + // exist" without a prior assignment. The same pattern is used in + // `WheelsTest.cfc` (which seeds `local.metaIndex = {}` before its loop). + local.varKey = ""; for (local.varKey in variables) { if (!isCustomFunction(variables[local.varKey])) { continue;