fix(test): auto-bind include-injected globals into WheelsTest spec scope#2793
Conversation
`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>
Wheels Bot — No doc updatesReviewed this PR's diff and found no docs that need updating (purely internal bug fix — no existing MDX guide, |
There was a problem hiding this comment.
Wheels Bot — Reviewer A
TL;DR: This PR correctly identifies that getMetaData(application.wo).functions silently skips symbols merged in via cfinclude, and the fix of iterating application.wo as a struct to catch all UDFs is the right direction. There is one cross-engine concern — for (local.key in application.wo) may not enumerate variables-scope functions on Adobe CF — and the spec that is supposed to prove the fix works is testing the wrong scope. The findings below should be addressed before merge; they would normally warrant --request-changes but GitHub prevents self-review on bot-authored PRs.
Correctness / Cross-engine
for (key in application.wo) may miss variables-scope functions on Adobe CF
vendor/wheels/Global.cfc:3853 ends with a bare include:
include "/app/global/functions.cfm"A bare include executed in the CFC body (outside any function) puts the declared functions into the component's variables scope, NOT into this. When WheelsTest iterates:
// WheelsTest.cfc:23
for (local.key in application.wo) {
if (!isCustomFunction(application.wo[local.key])) {On Lucee, struct-iteration over a CFC instance exposes both this-scope and callable variables-scope UDFs. On Adobe CF, the struct-iteration contract is narrower: only this-scope members are reliably enumerated. Include-injected UDFs live in variables, not this. If Adobe CF silently skips those keys in the for-in loop, the fix has no effect on Adobe — no error, no improvement, the helper stays unbound exactly as before.
The PR author calls this out themselves in the test-plan note ("CI's compat-matrix.yml should be checked on Adobe 2023/2025 and BoxLang before merge") but leaves it as a post-merge concern. CLAUDE.md §"Before Reporting a Change Complete" says: "Verify Adobe CF fixes locally before pushing — don't iterate via CI". This should be confirmed before merge, not after.
Tests
Spec injects via this scope; real includes land in variables scope
The binding regression case in WheelsTestAutoBindIncludesSpec.cfc:49–62 reads:
application.wo[probeName] = function() {
return "bind-probe";
};Bracket-notation assignment (cfc["key"] = value) writes to the CFC's this scope on both Lucee and Adobe CF. That is NOT the same shape as a function merged in via cfinclude, which lands in variables scope.
The test therefore validates that the loop catches this-scope UDFs — which was already possible through the old metadata path for public declared methods — but does NOT validate that for (local.key in application.wo) discovers variables-scope include-injected functions on any engine.
The first precondition case is correctly formed:
// WheelsTestAutoBindIncludesSpec.cfc:41–44
expect(foundInMeta).toBeFalse();
expect(structKeyExists(application.wo, probeName)).toBeTrue();
expect(isCustomFunction(application.wo[probeName])).toBeTrue();The binding case should match it in fidelity: at minimum, add an assertion that for (key in application.wo) actually enumerates the probe key, so the test fails rather than silently passes-but-does-not-test on Adobe CF. Better still: inject via a mechanism that truly simulates variables-scope placement (e.g. a small fixture include temporarily swapped in, or structural verification that the enumeration path reaches variables-scope keys).
Conventions
The new inline comments in WheelsTest.cfc:13–17 explain a non-obvious cross-engine invariant and are fully justified. No other convention violations found.
Commits
fix(test): auto-bind include-injected globals into WheelsTest spec scope — valid type, subject under 100 chars, sentence-case. Clean.
Docs
CHANGELOG.md [Unreleased] entry is present and accurate. Docs-update items are correctly deferred to bot-update-docs.yml.
Suggested path forward:
- Run
tools/test-matrix.sh adobe2023 sqliteagainst thewheelstestsuite before merge. IfWheelsTestAutoBindIncludesSpecpasses on Adobe 2023, the cross-engine concern is resolved empirically. - If Adobe CF struct-iteration misses
variables-scope keys: consider promoting include-injected helpers tothisscope in Global.cfc immediately after theincludestatement, so the existing loop has access to them uniformly. - Update the binding spec case to add an assertion that the probe key is actually enumerated by
for (key in application.wo), so a failure on Adobe CF would be caught rather than silently ignored.
Wheels Bot — Reviewer B (round 1)A's review is accurate and well-evidenced. Both core findings stand up to scrutiny — the cross-engine concern is real and the test gap is genuine. Converging on changes-needed. SycophancyNone detected. A calls the fix "the right direction" but immediately qualifies with concrete blockers and a non-approving verdict. Not a rubber stamp. False positivesNone detected.
Missed issuesNone significant. One minor clarification the spec's own comment gets wrong: The spec's header comment (lines 13–17) says bracket-notation assignment is "exactly the shape an included function takes." It is not — bracket notation from outside writes to Verdict alignmentA's COMMENTED verdict (GitHub blocks self-review/request-changes on bot-authored PRs) is appropriate — the findings are request-changes-level but the GitHub API limitation is documented. Consistent with the findings. ConvergenceAligned. Both findings are valid:
Joint recommendation: address the spec so it either (a) actually tests the |
- 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>
Wheels Bot — Address Review (round 1)Applied consensus findings from Reviewer A and Reviewer B's convergence (round 1):
Verification note: the local test runner ( The new commit will trigger a fresh Reviewer A run on the updated SHA. Convergence loop continues until reviewers align on |
There was a problem hiding this comment.
Wheels Bot — Reviewer A
TL;DR: Round 1 of the fixup commit correctly addresses both concerns raised in the previous review. The Adobe CF struct-iteration gap is closed by promoting include-injected UDFs from variables to this in Global.cfc before WheelsTest.cfc's loop runs; the spec misleading header comment is corrected and a new iteration-guard test case makes engine failures observable rather than silently passing. One minor test-coverage limitation is documented in the spec itself and is the right trade-off. Would approve but GitHub prevents self-review; this review records a clean pass.
Correctness / Cross-engine
Global.cfc promotion loop — correctly closes the Adobe CF gap
vendor/wheels/Global.cfc (round-1 commit, lines after the include):
for (local.varKey in variables) {
if (!isCustomFunction(variables[local.varKey])) {
continue;
}
if (structKeyExists(this, local.varKey)) {
continue;
}
this[local.varKey] = variables[local.varKey];
}This correctly solves the Adobe CF concern identified in the first review: a bare include at CFC body level lands UDFs in variables, not this, and Adobe CF struct-iteration over a CFC instance only reliably surfaces this-scope members. After promotion, WheelsTest.cfc's for (local.key in application.wo) loop finds the UDFs uniformly on all engines.
Two safety guards verified:
isCustomFunction()filters out non-function variables (strings, structs, etc.) invariablesscope, so no spurious promotions.structKeyExists(this, local.varKey)prevents overwriting the ~122 declared public methods already inthis. Global.cfc has zeroprivatemethods (confirmed with grep), so there is no visibility-escalation risk — no private method can be accidentally surfaced asthis-scope.
The use of local.varKey in the CFC body (pseudo-constructor context) follows the same established pattern as WheelsTest.cfc's own pseudo-constructor (local.metaIndex, local.fn, local.key), which is already proven to work across engines.
Tests
New iteration-guard case closes the silent-failure hole
vendor/wheels/tests/specs/wheelstest/WheelsTestAutoBindIncludesSpec.cfc (round-1 commit) adds an it case asserting that for (var key in application.wo) enumerates the probe key. This is exactly the guard that was missing in the original commit. If struct-iteration over a CFC instance silently skips a this-scope key on any engine, this case fails explicitly rather than letting the bind case give a false pass downstream.
Minor note — spec tests post-promotion shape, not the raw include path
The three probe cases inject via application.wo[probeName] = ... (bracket-notation from outside writes to this), which simulates the post-promotion state rather than a literal cfinclude-injected function. The spec therefore does not directly exercise the for (local.varKey in variables) loop added to Global.cfc. The header comment correctly documents this trade-off: testing the actual cfinclude path portably would require embedding a fixture include file, which adds complexity for marginal gain given how simple and guarded the promotion loop is. Noting for completeness; no action needed.
All four cases have try/finally cleanup, $bot2790* probe name namespacing to avoid key collisions, and clear stated intent. The bracket-notation split in the bind case (var bound = freshSpec[probeName]; expect(bound())...) correctly follows the CLAUDE.md cross-engine rule (Adobe CF 2021/2023 crashes on obj["key"]() inside closures — split into two statements). The probe function returns a bare string literal with no internal calls, so BoxLang's method-reference receiver concern does not apply.
Commits
fix(test): auto-bind include-injected globals into WheelsTest spec scope— valid type, 56-char subject, sentence-case, DCO present. ✓fix(test): address Reviewer A/B consensus findings (round 1)— valid type, 54-char subject, sentence-case, body explains the why in detail, DCO present. ✓
Docs
CHANGELOG.md [Unreleased] entry is present and accurate. Framework docs, AI reference, and CLAUDE.md updates are deferred to bot-update-docs.yml per checklist. Appropriate.
Wheels Bot — Reviewer B (round 1)A's review of the round-1 fixup commit is accurate, well-evidenced, and reaches the right verdict. Every technical claim checked against the actual diff holds up. Converging on approve. SycophancyNone detected. A's "would approve" conclusion is directly supported by line-by-line code analysis. A carried over no lenient hand-waving from the previous round. False positivesNone detected.
Missed issuesNone detected. The iteration-guard case for struct-iteration over application.wo is the genuinely new element and A covers it correctly. One thing A handles implicitly rather than explicitly: the probe assignment to application.wo[probeName] writes to the CFC's this scope (bracket-notation on a CFC from outside), which is distinct from CLAUDE.md cross-engine invariant 2 (application scope itself not accepting function members on Adobe CF). A does not conflate these, which is correct. Verdict alignmentA's COMMENTED verdict with explicit "would approve" language is consistent with the findings and with the GitHub constraint that prevents a bot from self-reviewing its own PR. Appropriate. ConvergenceAligned on approve. Both round-1 findings (Adobe CF struct-iteration gap; misleading spec comment and missing iteration guard) were addressed in the fixup commit, and the new commit introduces no new issues. The CI compat-matrix run on the current SHA remains the authority for Adobe 2023/2025 and BoxLang validation — nothing in the diff contradicts the analysis that the promotion loop should be uniform across engines once include-injected UDFs land in this before WheelsTest iterates. |
…stest-custom-global-helpers-added-via-include Signed-off-by: Peter Amiri <peter@alurium.com> # Conflicts: # CHANGELOG.md
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 <peter@alurium.com>
There was a problem hiding this comment.
Wheels Bot — Reviewer A
TL;DR: Round-2 findings are fully resolved. The single new commit (395a2ad76) adds one line — local.varKey = ""; — immediately before the for (local.varKey in variables) promotion loop in Global.cfc. This is a correct and necessary fix: Lucee 7's CFC pseudo-constructor does not auto-create the local scope, so for (local.X in Y) throws variable [local] doesn't exist without a prior assignment to seed it. The root cause (sandbox lacked a working test runner during round 1) is documented in the commit body. No correctness, cross-engine, or security concerns are present. Would approve, but GitHub prevents self-review on bot-authored PRs.
Correctness
local.varKey = "" seeding — correct and necessary
vendor/wheels/Global.cfc (lines 3861–3870 in the new commit):
local.varKey = "";
for (local.varKey in variables) {
if (!isCustomFunction(variables[local.varKey])) {
continue;
}
if (structKeyExists(this, local.varKey)) {
continue;
}
this[local.varKey] = variables[local.varKey];
}In a CFML CFC pseudo-constructor (component body code running outside any function), local is not an implicit scope — it must be materialised by at least one direct assignment before it can serve as the parent of a for-iterator target. WheelsTest.cfc already handles this correctly with local.metaIndex = {} before its own for (local.fn in ...) loop; Global.cfc had no such seeding line after the round-1 promotion loop was added. The crash was reproducible on Lucee 7 and would have blocked every spec run (Global.cfc compiles before any spec can execute). The seeding assignment is the minimum correct fix; the inline comment correctly documents the cross-engine invariant.
The loop filter logic (isCustomFunction guard + structKeyExists(this, ...) guard) is unchanged from round 1, which the round-2 review already confirmed is correct.
Cross-engine
The seeding pattern is already in use by WheelsTest.cfc's own pseudo-constructor and is therefore validated on Lucee 5/6/7, Adobe CF 2018/2021/2023/2025, and BoxLang. No engine-specific risk introduced by this commit.
Commits
fix(test): seed 'local' scope before pseudo-constructor for-iterator— valid type (fix), 55-char subject, sentence-case, DCO (Signed-off-by: Peter Amiri) present. ✓- Scope
testis a minor label nit (the touched file isvendor/wheels/Global.cfc, framework code) but CLAUDE.md explicitly says scope is unrestricted and optional — no action needed.
Wheels Bot — Reviewer B (round 1)A's review of the round-2 commit is accurate and well-evidenced. The seeding fix is correct, the cross-engine reasoning is sound, and no missed issues are present. Converging on approve. SycophancyNone detected. A's "would approve" is backed by line-level code analysis, not hand-waving. False positivesNone detected.
Missed issuesNone detected. One clarification: A's commit-body quote says the commit adds "one line" but the stat shows 7 insertions — the additional lines are the inline explanatory comment. This is a description simplification, not an error, and the conclusion is unaffected. Verdict alignmentA's COMMENTED verdict with explicit "would approve" is appropriate — GitHub prevents self-review on a bot-authored PR. Consistent with the finding that the fix is minimal, correct, and introduces no new concerns. ConvergenceAligned on approve. The round-2 commit addresses only the unverified crash introduced by the round-1 sandbox limitation. The underlying fix (Global.cfc promotion loop + WheelsTest struct-iteration path + spec coverage) is unchanged from round 1's converged-approve SHA (34bb41e). The new commit adds no behavioral change beyond the seeding line needed to prevent the Lucee 7 crash. CI compat-matrix remains the authority for Adobe 2023/2025 and BoxLang validation. |
WheelsTest's pseudo-constructor usedgetMetaData(application.wo).functionsto discover which Wheels globals to copy into a spec'svariables/thisscope. That metadata enumerates only methods declared on the CFC body and silently skips symbols merged in viacfinclude— which is exactly howvendor/wheels/Global.cfcpulls user helpers fromapp/global/functions.cfmat the very bottom of the file. Net effect: every spec that exercised a custom helper (can(),hasRole(), …) had to manually rebind it inbeforeAll(), and forgetting to do so produced a confusing "function not defined" failure even though the helper worked fine in controllers / views / models.The loop now iterates
application.woas a struct and binds every valueisCustomFunction()confirms is a UDF. The previous public-only filter is preserved for methods that ARE declared on Global.cfc (we still consult metadata to find theiraccessmodifier); include-injected helpers have no access modifier so they're treated as public, which matches how they behave everywhere else in Wheels. The don't-clobber guard for existingvariables/thismembers is unchanged, soWheelsTest.visit()/$testClient()/ inheritedBaseSpecmethods continue to win.Fixes #2790
Test plan
New regression spec
vendor/wheels/tests/specs/wheelstest/WheelsTestAutoBindIncludesSpec.cfcsimulates the include path by attaching a UDF directly toapplication.wo, instantiates a freshwheels.WheelsTest, and asserts the helper lands inthisand is callable. A second case verifies the bug precondition (the probe is invisible togetMetaData(application.wo).functions). A third case is a regression guard for metadata-declared methods (model,urlFor) so we know the existing path still works.Locally on Lucee 7 / SQLite via
tools/test-local.sh-equivalent:wheels.tests.specs.wheelstest— 168 pass, 0 fail, 0 error (was: 1 fail, fixed)wheels.tests.specs.model— 995 pass, 0 fail, 0 errorwheels.tests.specs.controller— cleanwheels.tests.specs.view— cleanCross-engine reviewer note: the fix relies on
isCustomFunction()and struct-key iteration over a CFC instance. Both are used elsewhere in the framework (vendor/wheels/wheelstest/system/{BaseSpec,Assertion,runners/UnitRunner}.cfc,vendor/wheels/tests/specs/mapper/MapperSpec.cfc) so they're known-portable, but CI'scompat-matrix.ymlshould be checked on Adobe 2023/2025 and BoxLang before merge.Checklist
bot-update-docs.yml.ai/wheels/) — handled bybot-update-docs.ymlbot-update-docs.yml