Skip to content

Commit 7ecbf0c

Browse files
committed
fix(test): keep the catch-block struct unscoped — local. breaks it on BoxLang
The compat matrix for this branch came back +24 tests on all 28 legs, which is correct, and two NEW failures on boxlang for every database: Failed | throws Wheels.JobClassNotFound naming the row and the class | Expected [Wheels.JobClassNotFound] but received [] Failed | throws Wheels.InvalidJobClass when the path resolves to something that is not a job | Expected [Wheels.InvalidJobClass] but received [] Both are mine, and both were caused by the previous commit — the `local.`-scoping convention nit from the bot review. `thrown` is written from inside a catch block, and on BoxLang the catch body runs under a nested `local` that is discarded on exit. Prefixing the struct made `local.thrown.type = e.type` land on that discarded copy instead of mutating the outer struct, so the assertion read an empty type. Cross-engine invariant 11 already covers the scalar case. What it did not say is that the struct workaround it recommends only works when the struct is accessed WITHOUT the prefix — `local.state.flag = true` fails exactly like `local.X = ...`. The prefix is what breaks it, not the assignment shape. Widened the invariant with that, plus a worked example, because `local.`-scoping spec variables IS the house style everywhere else, which makes tidying a catch-using spec to match an easy and completely invisible way to break it. So the original unscoped form was correct and the "nit" was wrong. Reverted for `thrown` only — every other variable in the file stays `local.`-scoped, since those are written from try bodies and are unaffected — with a comment at both sites explaining why, so it does not get tidied back. Worth noting the failure mode: green on Lucee, green on Adobe, wrong only on BoxLang, and silent rather than an error. Nothing local would have caught it. lucee7 + sqlite, full core suite: 4756 pass / 0 fail / 0 error, unchanged. Signed-off-by: Peter Amiri <peter@alurium.com>
1 parent c596188 commit 7ecbf0c

9 files changed

Lines changed: 471 additions & 10 deletions

CLAUDE.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,16 @@ The framework must run on Lucee 5/6/7, Adobe CF 2018/2021/2023/2025, and BoxLang
4747
8. **`Left(str, 0)` crashes Lucee 7.** Guard: `len > 0 ? Left(str, len) : ""`.
4848
9. **`toBeInstanceOf("component")` fails on BoxLang** — returns the FQN, not the literal `"component"`. Use `toBeWheelsModel()` for finder results.
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).
50-
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.
50+
11. **Anything written through `local.` 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.
51+
52+
**The struct form only works if you access it WITHOUT the `local.` prefix.** `local.state.flag = true` inside a catch fails exactly like a scalar `local.X = ...` — the nested `local` shadows `local.state`, so the write lands on a discarded copy rather than mutating the outer struct. The prefix is what breaks it, not the assignment shape:
53+
```cfm
54+
var state = {type = ""}; // RIGHT
55+
try { ... } catch (any e) { state.type = e.type; }
56+
local.state = {type = ""}; // WRONG — silently empty after the catch
57+
try { ... } catch (any e) { local.state.type = e.type; }
58+
```
59+
This matters because `local.`-scoping spec variables is the house style everywhere else, so "tidying" a catch-using spec to match is an easy and invisible way to break it. Doing exactly that to `JobClassRoundTripSpec` cost two BoxLang failures on every database (`Expected [Wheels.JobClassNotFound] but received []`) — green on Lucee, caught only by the compat matrix.
5160
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)).
5261
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).
5362
14. **Adobe 2025's JVM rejects member calls on JDK-internal classes (JPMS).** Calling any member on an object whose runtime class lives in an unexported package (`com.sun.*`, `jdk.internal.*`) — e.g. the `com.sun.crypto.provider.PBKDF2KeyImpl` returned by `SecretKeyFactory.generateSecret()` — throws `java.lang.reflect.InaccessibleObjectException` on Adobe 2025 (its reflection layer bulk-`setAccessible`s the concrete class's methods; Lucee, BoxLang, and Adobe ≤2023 tolerate the same call, so **local Adobe 2023 green does NOT cover this**). Route the call through the exported interface's `Method` object instead: `CreateObject("java","java.lang.Class").forName("javax.crypto.SecretKey").getMethod("getEncoded", JavaCast("null","")).invoke(keyObj, JavaCast("null",""))` — `getMethod`/`invoke` treat the null varargs as empty. Hit by `PasswordHasher.$deriveKey()` ([#3300](https://github.com/wheels-dev/wheels/issues/3300)); watch for it with any Java factory API that returns internal implementation types.
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<cfscript>
2+
variables[ "closeSSEStream" ] = variables[ "tmp_closeSSEStream_139184C0543CCDB338AEFB643110CB89" ];
3+
this[ "closeSSEStream" ] = variables[ "tmp_closeSSEStream_139184C0543CCDB338AEFB643110CB89" ];
4+
5+
// Clean up
6+
structDelete( variables, "tmp_closeSSEStream_139184C0543CCDB338AEFB643110CB89" );
7+
structDelete( this, "tmp_closeSSEStream_139184C0543CCDB338AEFB643110CB89" );
8+
public void function tmp_closeSSEStream_139184C0543CCDB338AEFB643110CB89(
9+
10+
) output=true {
11+
12+
var results = this._mockResults;
13+
var resultsKey = "closeSSEStream";
14+
var resultsCounter = 0;
15+
var internalCounter = 0;
16+
var resultsLen = 0;
17+
var callbackLen = 0;
18+
var argsHashKey = resultsKey & "|" & this.mockBox.normalizeArguments( arguments );
19+
var fCallBack = "";
20+
21+
// If Method & argument Hash Results, switch the results struct
22+
if (structKeyExists( this._mockArgResults, argsHashKey) ) {
23+
// Check if it is a callback
24+
if (isStruct( this._mockArgResults[ argsHashKey ]) &&
25+
structKeyExists( this._mockArgResults[ argsHashKey ], "type" ) &&
26+
structKeyExists( this._mockArgResults[ argsHashKey ], "target" ) ) {
27+
fCallBack = this._mockArgResults[ argsHashKey ].target;
28+
} else {
29+
// switch context and key
30+
results = this._mockArgResults;
31+
resultsKey = argsHashKey;
32+
}
33+
}
34+
35+
// Get the statemachine counter
36+
if (isSimpleValue( fCallBack) ) {
37+
resultsLen = arrayLen( results[ resultsKey ] );
38+
}
39+
40+
// Get the callback counter, if it exists
41+
if (structKeyExists( this._mockCallbacks, resultsKey) ) {
42+
callbackLen = arrayLen( this._mockCallbacks[ resultsKey ] );
43+
}
44+
45+
// Log the Method Call
46+
this._mockMethodCallCounters[ listFirst( resultsKey, "|" ) ] = this._mockMethodCallCounters[ listFirst( resultsKey, "|" ) ] + 1;
47+
48+
// Get the CallCounter Reference
49+
internalCounter = this._mockMethodCallCounters[listFirst(resultsKey,"|")];
50+
arrayAppend( this._mockCallLoggers["closeSSEStream"], arguments );
51+
}
52+
</cfscript>
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<cfscript>
2+
variables[ "sendSSEComment" ] = variables[ "tmp_sendSSEComment_16CA394252B074478BEBD2B3A9C8EA82" ];
3+
this[ "sendSSEComment" ] = variables[ "tmp_sendSSEComment_16CA394252B074478BEBD2B3A9C8EA82" ];
4+
5+
// Clean up
6+
structDelete( variables, "tmp_sendSSEComment_16CA394252B074478BEBD2B3A9C8EA82" );
7+
structDelete( this, "tmp_sendSSEComment_16CA394252B074478BEBD2B3A9C8EA82" );
8+
public void function tmp_sendSSEComment_16CA394252B074478BEBD2B3A9C8EA82(
9+
10+
) output=true {
11+
12+
var results = this._mockResults;
13+
var resultsKey = "sendSSEComment";
14+
var resultsCounter = 0;
15+
var internalCounter = 0;
16+
var resultsLen = 0;
17+
var callbackLen = 0;
18+
var argsHashKey = resultsKey & "|" & this.mockBox.normalizeArguments( arguments );
19+
var fCallBack = "";
20+
21+
// If Method & argument Hash Results, switch the results struct
22+
if (structKeyExists( this._mockArgResults, argsHashKey) ) {
23+
// Check if it is a callback
24+
if (isStruct( this._mockArgResults[ argsHashKey ]) &&
25+
structKeyExists( this._mockArgResults[ argsHashKey ], "type" ) &&
26+
structKeyExists( this._mockArgResults[ argsHashKey ], "target" ) ) {
27+
fCallBack = this._mockArgResults[ argsHashKey ].target;
28+
} else {
29+
// switch context and key
30+
results = this._mockArgResults;
31+
resultsKey = argsHashKey;
32+
}
33+
}
34+
35+
// Get the statemachine counter
36+
if (isSimpleValue( fCallBack) ) {
37+
resultsLen = arrayLen( results[ resultsKey ] );
38+
}
39+
40+
// Get the callback counter, if it exists
41+
if (structKeyExists( this._mockCallbacks, resultsKey) ) {
42+
callbackLen = arrayLen( this._mockCallbacks[ resultsKey ] );
43+
}
44+
45+
// Log the Method Call
46+
this._mockMethodCallCounters[ listFirst( resultsKey, "|" ) ] = this._mockMethodCallCounters[ listFirst( resultsKey, "|" ) ] + 1;
47+
48+
// Get the CallCounter Reference
49+
internalCounter = this._mockMethodCallCounters[listFirst(resultsKey,"|")];
50+
arrayAppend( this._mockCallLoggers["sendSSEComment"], arguments );
51+
}
52+
</cfscript>
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<cfscript>
2+
variables[ "checkError" ] = variables[ "tmp_checkError_30942F4D0BCB6139072EF27C66218715" ];
3+
this[ "checkError" ] = variables[ "tmp_checkError_30942F4D0BCB6139072EF27C66218715" ];
4+
5+
// Clean up
6+
structDelete( variables, "tmp_checkError_30942F4D0BCB6139072EF27C66218715" );
7+
structDelete( this, "tmp_checkError_30942F4D0BCB6139072EF27C66218715" );
8+
public any function tmp_checkError_30942F4D0BCB6139072EF27C66218715(
9+
10+
) output=true {
11+
12+
var results = this._mockResults;
13+
var resultsKey = "checkError";
14+
var resultsCounter = 0;
15+
var internalCounter = 0;
16+
var resultsLen = 0;
17+
var callbackLen = 0;
18+
var argsHashKey = resultsKey & "|" & this.mockBox.normalizeArguments( arguments );
19+
var fCallBack = "";
20+
21+
// If Method & argument Hash Results, switch the results struct
22+
if (structKeyExists( this._mockArgResults, argsHashKey) ) {
23+
// Check if it is a callback
24+
if (isStruct( this._mockArgResults[ argsHashKey ]) &&
25+
structKeyExists( this._mockArgResults[ argsHashKey ], "type" ) &&
26+
structKeyExists( this._mockArgResults[ argsHashKey ], "target" ) ) {
27+
fCallBack = this._mockArgResults[ argsHashKey ].target;
28+
} else {
29+
// switch context and key
30+
results = this._mockArgResults;
31+
resultsKey = argsHashKey;
32+
}
33+
}
34+
35+
// Get the statemachine counter
36+
if (isSimpleValue( fCallBack) ) {
37+
resultsLen = arrayLen( results[ resultsKey ] );
38+
}
39+
40+
// Get the callback counter, if it exists
41+
if (structKeyExists( this._mockCallbacks, resultsKey) ) {
42+
callbackLen = arrayLen( this._mockCallbacks[ resultsKey ] );
43+
}
44+
45+
// Log the Method Call
46+
this._mockMethodCallCounters[ listFirst( resultsKey, "|" ) ] = this._mockMethodCallCounters[ listFirst( resultsKey, "|" ) ] + 1;
47+
48+
// Get the CallCounter Reference
49+
internalCounter = this._mockMethodCallCounters[listFirst(resultsKey,"|")];
50+
arrayAppend( this._mockCallLoggers["checkError"], arguments );
51+
52+
if (resultsLen neq 0) {
53+
if (internalCounter gt resultsLen) {
54+
resultsCounter = internalCounter - ( resultsLen * fix( ( internalCounter - 1 ) / resultsLen ) );
55+
return results[ resultsKey ][ resultsCounter ];
56+
} else {
57+
return results[ resultsKey ][ internalCounter ];
58+
}
59+
}
60+
61+
if ( callbackLen neq 0 ) {
62+
fCallBack = this._mockCallbacks[ resultsKey ].first();
63+
return fCallBack( argumentCollection : arguments );
64+
}
65+
66+
if ( not isSimpleValue( fCallBack ) ){
67+
return fCallBack( argumentCollection : arguments );
68+
}
69+
}
70+
</cfscript>
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<cfscript>
2+
variables[ "sendSSEEvent" ] = variables[ "tmp_sendSSEEvent_754BEF48E30B63BC11E518FA73C07785" ];
3+
this[ "sendSSEEvent" ] = variables[ "tmp_sendSSEEvent_754BEF48E30B63BC11E518FA73C07785" ];
4+
5+
// Clean up
6+
structDelete( variables, "tmp_sendSSEEvent_754BEF48E30B63BC11E518FA73C07785" );
7+
structDelete( this, "tmp_sendSSEEvent_754BEF48E30B63BC11E518FA73C07785" );
8+
public void function tmp_sendSSEEvent_754BEF48E30B63BC11E518FA73C07785(
9+
10+
) output=true {
11+
12+
var results = this._mockResults;
13+
var resultsKey = "sendSSEEvent";
14+
var resultsCounter = 0;
15+
var internalCounter = 0;
16+
var resultsLen = 0;
17+
var callbackLen = 0;
18+
var argsHashKey = resultsKey & "|" & this.mockBox.normalizeArguments( arguments );
19+
var fCallBack = "";
20+
21+
// If Method & argument Hash Results, switch the results struct
22+
if (structKeyExists( this._mockArgResults, argsHashKey) ) {
23+
// Check if it is a callback
24+
if (isStruct( this._mockArgResults[ argsHashKey ]) &&
25+
structKeyExists( this._mockArgResults[ argsHashKey ], "type" ) &&
26+
structKeyExists( this._mockArgResults[ argsHashKey ], "target" ) ) {
27+
fCallBack = this._mockArgResults[ argsHashKey ].target;
28+
} else {
29+
// switch context and key
30+
results = this._mockArgResults;
31+
resultsKey = argsHashKey;
32+
}
33+
}
34+
35+
// Get the statemachine counter
36+
if (isSimpleValue( fCallBack) ) {
37+
resultsLen = arrayLen( results[ resultsKey ] );
38+
}
39+
40+
// Get the callback counter, if it exists
41+
if (structKeyExists( this._mockCallbacks, resultsKey) ) {
42+
callbackLen = arrayLen( this._mockCallbacks[ resultsKey ] );
43+
}
44+
45+
// Log the Method Call
46+
this._mockMethodCallCounters[ listFirst( resultsKey, "|" ) ] = this._mockMethodCallCounters[ listFirst( resultsKey, "|" ) ] + 1;
47+
48+
// Get the CallCounter Reference
49+
internalCounter = this._mockMethodCallCounters[listFirst(resultsKey,"|")];
50+
arrayAppend( this._mockCallLoggers["sendSSEEvent"], arguments );
51+
}
52+
</cfscript>
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<cfscript>
2+
variables[ "initSSEStream" ] = variables[ "tmp_initSSEStream_B7681C49C5125395612F24D97559A4C2" ];
3+
this[ "initSSEStream" ] = variables[ "tmp_initSSEStream_B7681C49C5125395612F24D97559A4C2" ];
4+
5+
// Clean up
6+
structDelete( variables, "tmp_initSSEStream_B7681C49C5125395612F24D97559A4C2" );
7+
structDelete( this, "tmp_initSSEStream_B7681C49C5125395612F24D97559A4C2" );
8+
public any function tmp_initSSEStream_B7681C49C5125395612F24D97559A4C2(
9+
10+
) output=true {
11+
12+
var results = this._mockResults;
13+
var resultsKey = "initSSEStream";
14+
var resultsCounter = 0;
15+
var internalCounter = 0;
16+
var resultsLen = 0;
17+
var callbackLen = 0;
18+
var argsHashKey = resultsKey & "|" & this.mockBox.normalizeArguments( arguments );
19+
var fCallBack = "";
20+
21+
// If Method & argument Hash Results, switch the results struct
22+
if (structKeyExists( this._mockArgResults, argsHashKey) ) {
23+
// Check if it is a callback
24+
if (isStruct( this._mockArgResults[ argsHashKey ]) &&
25+
structKeyExists( this._mockArgResults[ argsHashKey ], "type" ) &&
26+
structKeyExists( this._mockArgResults[ argsHashKey ], "target" ) ) {
27+
fCallBack = this._mockArgResults[ argsHashKey ].target;
28+
} else {
29+
// switch context and key
30+
results = this._mockArgResults;
31+
resultsKey = argsHashKey;
32+
}
33+
}
34+
35+
// Get the statemachine counter
36+
if (isSimpleValue( fCallBack) ) {
37+
resultsLen = arrayLen( results[ resultsKey ] );
38+
}
39+
40+
// Get the callback counter, if it exists
41+
if (structKeyExists( this._mockCallbacks, resultsKey) ) {
42+
callbackLen = arrayLen( this._mockCallbacks[ resultsKey ] );
43+
}
44+
45+
// Log the Method Call
46+
this._mockMethodCallCounters[ listFirst( resultsKey, "|" ) ] = this._mockMethodCallCounters[ listFirst( resultsKey, "|" ) ] + 1;
47+
48+
// Get the CallCounter Reference
49+
internalCounter = this._mockMethodCallCounters[listFirst(resultsKey,"|")];
50+
arrayAppend( this._mockCallLoggers["initSSEStream"], arguments );
51+
52+
if (resultsLen neq 0) {
53+
if (internalCounter gt resultsLen) {
54+
resultsCounter = internalCounter - ( resultsLen * fix( ( internalCounter - 1 ) / resultsLen ) );
55+
return results[ resultsKey ][ resultsCounter ];
56+
} else {
57+
return results[ resultsKey ][ internalCounter ];
58+
}
59+
}
60+
61+
if ( callbackLen neq 0 ) {
62+
fCallBack = this._mockCallbacks[ resultsKey ].first();
63+
return fCallBack( argumentCollection : arguments );
64+
}
65+
66+
if ( not isSimpleValue( fCallBack ) ){
67+
return fCallBack( argumentCollection : arguments );
68+
}
69+
}
70+
</cfscript>

0 commit comments

Comments
 (0)