From 074ead3fc4aff67d35575a9670b2a3d62df92924 Mon Sep 17 00:00:00 2001 From: Peter Amiri Date: Mon, 3 Aug 2026 22:41:36 -0700 Subject: [PATCH 1/3] fix(job): name the queue row when a persisted jobClass cannot be resolved MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `enqueue()` persists `GetMetadata(this).name` into `wheels_jobs.jobClass`, and the drain re-instantiates with `CreateObject("component", jobRow.jobClass)`. A string produced by engine metadata is stored and later resolved as a component path, so the round trip is only safe if that string keeps the casing of the file on disk — component paths are case-sensitive on Linux and not on macOS or Windows. That is a bug shape which passes in development and fails on a production redeploy, on rows the old instance wrote and the new one drains. I could NOT reproduce a casing drift. Probed on Lucee 7: `GetMetadata().name` comes back canonical (`...jobs.ProbeJob`) even when the component is instantiated through a lowercase path, so Lucee derives it from the file rather than echoing what the caller typed. That explains why this has never bitten — it is a property of the engine, not luck. It is also not a guarantee across the other four engines, and the issue is explicit that it is unverified there. So rather than guess at a normalisation fix for a drift that may not exist, this pins the invariant as a test and improves the failure when it does not hold. JobClassRoundTripSpec asserts, case-sensitively, that the metadata name's last segment equals the .cfc file name; that the name is identical however the component was instantiated (a case-insensitive filesystem resolves both spellings, so an engine that echoed the caller's path would persist whatever casing was typed); and that a job re-instantiates from its own persisted name. Those run on every engine × database leg, so lucee6, adobe2023, adobe2025 and boxlang each answer the open question directly instead of being assumed safe. $instantiateJobClass() replaces the bare CreateObject on both processing paths (Job.$processJob and JobWorker.$executeJob) and throws Wheels.JobClassNotFound naming the class, the queue row id, and the three real causes — casing, rename, delete. The issue's point is that the raw error is `component not found` for a class that plainly exists, which sends investigators to mappings and deployment; the fix is to describe the actual shape of the problem, a string read out of a queue row. It also throws Wheels.InvalidJobClass when the path resolves to a component with no perform(). That narrows but does NOT close the database-string-to-CreateObject shape the issue flags — the real guarantee is still that only $enqueueJob writes that column, and the docstring says so rather than implying the check is a security boundary. JobWorker.$scheduleRetry's CreateObject is deliberately left alone: it is a best-effort backoff lookup already wrapped in a try/catch that falls back to defaults, and it must not start throwing. Red-first, with Job.cfc and JobWorker.cfc reverted: 4734 pass / 2 fail / 1 error. The three invariant specs pass on Lucee 7 — they are meant to, on an engine that holds the invariant. Verification, lucee7 + sqlite, full core suite: develop ab901cff7 4732 pass / 0 fail / 0 error this branch 4737 pass / 0 fail / 0 error Exactly +5, the new specs. Closes #3351 Signed-off-by: Peter Amiri --- changelog.d/3351-jobclass-round-trip.fixed.md | 1 + .../139184C0543CCDB338AEFB643110CB89.cfm | 52 ++++++++++++ .../16CA394252B074478BEBD2B3A9C8EA82.cfm | 52 ++++++++++++ .../30942F4D0BCB6139072EF27C66218715.cfm | 70 +++++++++++++++ .../754BEF48E30B63BC11E518FA73C07785.cfm | 52 ++++++++++++ .../B7681C49C5125395612F24D97559A4C2.cfm | 70 +++++++++++++++ .../C383511C024245809F1F73E0CA52E220.cfm | 70 +++++++++++++++ .../CDCB73D6FD17909D6519A49548FD6769.cfm | 70 +++++++++++++++ vendor/wheels/Job.cfc | 44 +++++++++- vendor/wheels/JobWorker.cfc | 7 +- .../specs/jobs/JobClassRoundTripSpec.cfc | 85 +++++++++++++++++++ 11 files changed, 571 insertions(+), 2 deletions(-) create mode 100644 changelog.d/3351-jobclass-round-trip.fixed.md create mode 100644 public/testbox/system/stubs/139184C0543CCDB338AEFB643110CB89.cfm create mode 100644 public/testbox/system/stubs/16CA394252B074478BEBD2B3A9C8EA82.cfm create mode 100644 public/testbox/system/stubs/30942F4D0BCB6139072EF27C66218715.cfm create mode 100644 public/testbox/system/stubs/754BEF48E30B63BC11E518FA73C07785.cfm create mode 100644 public/testbox/system/stubs/B7681C49C5125395612F24D97559A4C2.cfm create mode 100644 public/testbox/system/stubs/C383511C024245809F1F73E0CA52E220.cfm create mode 100644 public/testbox/system/stubs/CDCB73D6FD17909D6519A49548FD6769.cfm create mode 100644 vendor/wheels/tests/specs/jobs/JobClassRoundTripSpec.cfc diff --git a/changelog.d/3351-jobclass-round-trip.fixed.md b/changelog.d/3351-jobclass-round-trip.fixed.md new file mode 100644 index 0000000000..66ae4194f7 --- /dev/null +++ b/changelog.d/3351-jobclass-round-trip.fixed.md @@ -0,0 +1 @@ +- A background job whose `jobClass` cannot be resolved now throws `Wheels.JobClassNotFound` naming the class, the queue row, and the likely causes, instead of the engine's bare `component not found`. `wheels_jobs.jobClass` is written from `GetMetadata(this).name` on enqueue and resolved as a component path on drain, so the failure appears as "component not found" for a class that plainly exists on disk — which sends people to look at mappings and deployment rather than at the persisted string. Component paths are case-sensitive on Linux but not on macOS or Windows, so a casing mismatch resolves in development and fails on a production redeploy, long after the row was written. A path that resolves to something without a `perform()` method now throws `Wheels.InvalidJobClass` rather than failing later inside job execution. Both processing paths (`Job.$processJob` and `JobWorker.$executeJob`) share the check (#3351) diff --git a/public/testbox/system/stubs/139184C0543CCDB338AEFB643110CB89.cfm b/public/testbox/system/stubs/139184C0543CCDB338AEFB643110CB89.cfm new file mode 100644 index 0000000000..d313ccd44c --- /dev/null +++ b/public/testbox/system/stubs/139184C0543CCDB338AEFB643110CB89.cfm @@ -0,0 +1,52 @@ + + variables[ "closeSSEStream" ] = variables[ "tmp_closeSSEStream_139184C0543CCDB338AEFB643110CB89" ]; + this[ "closeSSEStream" ] = variables[ "tmp_closeSSEStream_139184C0543CCDB338AEFB643110CB89" ]; + + // Clean up + structDelete( variables, "tmp_closeSSEStream_139184C0543CCDB338AEFB643110CB89" ); + structDelete( this, "tmp_closeSSEStream_139184C0543CCDB338AEFB643110CB89" ); + public void function tmp_closeSSEStream_139184C0543CCDB338AEFB643110CB89( + + ) output=true { + + var results = this._mockResults; + var resultsKey = "closeSSEStream"; + var resultsCounter = 0; + var internalCounter = 0; + var resultsLen = 0; + var callbackLen = 0; + var argsHashKey = resultsKey & "|" & this.mockBox.normalizeArguments( arguments ); + var fCallBack = ""; + + // If Method & argument Hash Results, switch the results struct +if (structKeyExists( this._mockArgResults, argsHashKey) ) { + // Check if it is a callback +if (isStruct( this._mockArgResults[ argsHashKey ]) && + structKeyExists( this._mockArgResults[ argsHashKey ], "type" ) && + structKeyExists( this._mockArgResults[ argsHashKey ], "target" ) ) { + fCallBack = this._mockArgResults[ argsHashKey ].target; +} else { + // switch context and key + results = this._mockArgResults; + resultsKey = argsHashKey; + } + } + + // Get the statemachine counter +if (isSimpleValue( fCallBack) ) { + resultsLen = arrayLen( results[ resultsKey ] ); + } + + // Get the callback counter, if it exists +if (structKeyExists( this._mockCallbacks, resultsKey) ) { + callbackLen = arrayLen( this._mockCallbacks[ resultsKey ] ); + } + + // Log the Method Call + this._mockMethodCallCounters[ listFirst( resultsKey, "|" ) ] = this._mockMethodCallCounters[ listFirst( resultsKey, "|" ) ] + 1; + + // Get the CallCounter Reference + internalCounter = this._mockMethodCallCounters[listFirst(resultsKey,"|")]; + arrayAppend( this._mockCallLoggers["closeSSEStream"], arguments ); +} + \ No newline at end of file diff --git a/public/testbox/system/stubs/16CA394252B074478BEBD2B3A9C8EA82.cfm b/public/testbox/system/stubs/16CA394252B074478BEBD2B3A9C8EA82.cfm new file mode 100644 index 0000000000..b7e93ee8ab --- /dev/null +++ b/public/testbox/system/stubs/16CA394252B074478BEBD2B3A9C8EA82.cfm @@ -0,0 +1,52 @@ + + variables[ "sendSSEComment" ] = variables[ "tmp_sendSSEComment_16CA394252B074478BEBD2B3A9C8EA82" ]; + this[ "sendSSEComment" ] = variables[ "tmp_sendSSEComment_16CA394252B074478BEBD2B3A9C8EA82" ]; + + // Clean up + structDelete( variables, "tmp_sendSSEComment_16CA394252B074478BEBD2B3A9C8EA82" ); + structDelete( this, "tmp_sendSSEComment_16CA394252B074478BEBD2B3A9C8EA82" ); + public void function tmp_sendSSEComment_16CA394252B074478BEBD2B3A9C8EA82( + + ) output=true { + + var results = this._mockResults; + var resultsKey = "sendSSEComment"; + var resultsCounter = 0; + var internalCounter = 0; + var resultsLen = 0; + var callbackLen = 0; + var argsHashKey = resultsKey & "|" & this.mockBox.normalizeArguments( arguments ); + var fCallBack = ""; + + // If Method & argument Hash Results, switch the results struct +if (structKeyExists( this._mockArgResults, argsHashKey) ) { + // Check if it is a callback +if (isStruct( this._mockArgResults[ argsHashKey ]) && + structKeyExists( this._mockArgResults[ argsHashKey ], "type" ) && + structKeyExists( this._mockArgResults[ argsHashKey ], "target" ) ) { + fCallBack = this._mockArgResults[ argsHashKey ].target; +} else { + // switch context and key + results = this._mockArgResults; + resultsKey = argsHashKey; + } + } + + // Get the statemachine counter +if (isSimpleValue( fCallBack) ) { + resultsLen = arrayLen( results[ resultsKey ] ); + } + + // Get the callback counter, if it exists +if (structKeyExists( this._mockCallbacks, resultsKey) ) { + callbackLen = arrayLen( this._mockCallbacks[ resultsKey ] ); + } + + // Log the Method Call + this._mockMethodCallCounters[ listFirst( resultsKey, "|" ) ] = this._mockMethodCallCounters[ listFirst( resultsKey, "|" ) ] + 1; + + // Get the CallCounter Reference + internalCounter = this._mockMethodCallCounters[listFirst(resultsKey,"|")]; + arrayAppend( this._mockCallLoggers["sendSSEComment"], arguments ); +} + \ No newline at end of file diff --git a/public/testbox/system/stubs/30942F4D0BCB6139072EF27C66218715.cfm b/public/testbox/system/stubs/30942F4D0BCB6139072EF27C66218715.cfm new file mode 100644 index 0000000000..e0cfd43b5c --- /dev/null +++ b/public/testbox/system/stubs/30942F4D0BCB6139072EF27C66218715.cfm @@ -0,0 +1,70 @@ + + variables[ "checkError" ] = variables[ "tmp_checkError_30942F4D0BCB6139072EF27C66218715" ]; + this[ "checkError" ] = variables[ "tmp_checkError_30942F4D0BCB6139072EF27C66218715" ]; + + // Clean up + structDelete( variables, "tmp_checkError_30942F4D0BCB6139072EF27C66218715" ); + structDelete( this, "tmp_checkError_30942F4D0BCB6139072EF27C66218715" ); + public any function tmp_checkError_30942F4D0BCB6139072EF27C66218715( + + ) output=true { + + var results = this._mockResults; + var resultsKey = "checkError"; + var resultsCounter = 0; + var internalCounter = 0; + var resultsLen = 0; + var callbackLen = 0; + var argsHashKey = resultsKey & "|" & this.mockBox.normalizeArguments( arguments ); + var fCallBack = ""; + + // If Method & argument Hash Results, switch the results struct +if (structKeyExists( this._mockArgResults, argsHashKey) ) { + // Check if it is a callback +if (isStruct( this._mockArgResults[ argsHashKey ]) && + structKeyExists( this._mockArgResults[ argsHashKey ], "type" ) && + structKeyExists( this._mockArgResults[ argsHashKey ], "target" ) ) { + fCallBack = this._mockArgResults[ argsHashKey ].target; +} else { + // switch context and key + results = this._mockArgResults; + resultsKey = argsHashKey; + } + } + + // Get the statemachine counter +if (isSimpleValue( fCallBack) ) { + resultsLen = arrayLen( results[ resultsKey ] ); + } + + // Get the callback counter, if it exists +if (structKeyExists( this._mockCallbacks, resultsKey) ) { + callbackLen = arrayLen( this._mockCallbacks[ resultsKey ] ); + } + + // Log the Method Call + this._mockMethodCallCounters[ listFirst( resultsKey, "|" ) ] = this._mockMethodCallCounters[ listFirst( resultsKey, "|" ) ] + 1; + + // Get the CallCounter Reference + internalCounter = this._mockMethodCallCounters[listFirst(resultsKey,"|")]; + arrayAppend( this._mockCallLoggers["checkError"], arguments ); + + if (resultsLen neq 0) { + if (internalCounter gt resultsLen) { + resultsCounter = internalCounter - ( resultsLen * fix( ( internalCounter - 1 ) / resultsLen ) ); + return results[ resultsKey ][ resultsCounter ]; + } else { + return results[ resultsKey ][ internalCounter ]; + } + } + + if ( callbackLen neq 0 ) { + fCallBack = this._mockCallbacks[ resultsKey ].first(); + return fCallBack( argumentCollection : arguments ); + } + + if ( not isSimpleValue( fCallBack ) ){ + return fCallBack( argumentCollection : arguments ); + } + } + \ No newline at end of file diff --git a/public/testbox/system/stubs/754BEF48E30B63BC11E518FA73C07785.cfm b/public/testbox/system/stubs/754BEF48E30B63BC11E518FA73C07785.cfm new file mode 100644 index 0000000000..2ea84360a3 --- /dev/null +++ b/public/testbox/system/stubs/754BEF48E30B63BC11E518FA73C07785.cfm @@ -0,0 +1,52 @@ + + variables[ "sendSSEEvent" ] = variables[ "tmp_sendSSEEvent_754BEF48E30B63BC11E518FA73C07785" ]; + this[ "sendSSEEvent" ] = variables[ "tmp_sendSSEEvent_754BEF48E30B63BC11E518FA73C07785" ]; + + // Clean up + structDelete( variables, "tmp_sendSSEEvent_754BEF48E30B63BC11E518FA73C07785" ); + structDelete( this, "tmp_sendSSEEvent_754BEF48E30B63BC11E518FA73C07785" ); + public void function tmp_sendSSEEvent_754BEF48E30B63BC11E518FA73C07785( + + ) output=true { + + var results = this._mockResults; + var resultsKey = "sendSSEEvent"; + var resultsCounter = 0; + var internalCounter = 0; + var resultsLen = 0; + var callbackLen = 0; + var argsHashKey = resultsKey & "|" & this.mockBox.normalizeArguments( arguments ); + var fCallBack = ""; + + // If Method & argument Hash Results, switch the results struct +if (structKeyExists( this._mockArgResults, argsHashKey) ) { + // Check if it is a callback +if (isStruct( this._mockArgResults[ argsHashKey ]) && + structKeyExists( this._mockArgResults[ argsHashKey ], "type" ) && + structKeyExists( this._mockArgResults[ argsHashKey ], "target" ) ) { + fCallBack = this._mockArgResults[ argsHashKey ].target; +} else { + // switch context and key + results = this._mockArgResults; + resultsKey = argsHashKey; + } + } + + // Get the statemachine counter +if (isSimpleValue( fCallBack) ) { + resultsLen = arrayLen( results[ resultsKey ] ); + } + + // Get the callback counter, if it exists +if (structKeyExists( this._mockCallbacks, resultsKey) ) { + callbackLen = arrayLen( this._mockCallbacks[ resultsKey ] ); + } + + // Log the Method Call + this._mockMethodCallCounters[ listFirst( resultsKey, "|" ) ] = this._mockMethodCallCounters[ listFirst( resultsKey, "|" ) ] + 1; + + // Get the CallCounter Reference + internalCounter = this._mockMethodCallCounters[listFirst(resultsKey,"|")]; + arrayAppend( this._mockCallLoggers["sendSSEEvent"], arguments ); +} + \ No newline at end of file diff --git a/public/testbox/system/stubs/B7681C49C5125395612F24D97559A4C2.cfm b/public/testbox/system/stubs/B7681C49C5125395612F24D97559A4C2.cfm new file mode 100644 index 0000000000..81f5d1340b --- /dev/null +++ b/public/testbox/system/stubs/B7681C49C5125395612F24D97559A4C2.cfm @@ -0,0 +1,70 @@ + + variables[ "initSSEStream" ] = variables[ "tmp_initSSEStream_B7681C49C5125395612F24D97559A4C2" ]; + this[ "initSSEStream" ] = variables[ "tmp_initSSEStream_B7681C49C5125395612F24D97559A4C2" ]; + + // Clean up + structDelete( variables, "tmp_initSSEStream_B7681C49C5125395612F24D97559A4C2" ); + structDelete( this, "tmp_initSSEStream_B7681C49C5125395612F24D97559A4C2" ); + public any function tmp_initSSEStream_B7681C49C5125395612F24D97559A4C2( + + ) output=true { + + var results = this._mockResults; + var resultsKey = "initSSEStream"; + var resultsCounter = 0; + var internalCounter = 0; + var resultsLen = 0; + var callbackLen = 0; + var argsHashKey = resultsKey & "|" & this.mockBox.normalizeArguments( arguments ); + var fCallBack = ""; + + // If Method & argument Hash Results, switch the results struct +if (structKeyExists( this._mockArgResults, argsHashKey) ) { + // Check if it is a callback +if (isStruct( this._mockArgResults[ argsHashKey ]) && + structKeyExists( this._mockArgResults[ argsHashKey ], "type" ) && + structKeyExists( this._mockArgResults[ argsHashKey ], "target" ) ) { + fCallBack = this._mockArgResults[ argsHashKey ].target; +} else { + // switch context and key + results = this._mockArgResults; + resultsKey = argsHashKey; + } + } + + // Get the statemachine counter +if (isSimpleValue( fCallBack) ) { + resultsLen = arrayLen( results[ resultsKey ] ); + } + + // Get the callback counter, if it exists +if (structKeyExists( this._mockCallbacks, resultsKey) ) { + callbackLen = arrayLen( this._mockCallbacks[ resultsKey ] ); + } + + // Log the Method Call + this._mockMethodCallCounters[ listFirst( resultsKey, "|" ) ] = this._mockMethodCallCounters[ listFirst( resultsKey, "|" ) ] + 1; + + // Get the CallCounter Reference + internalCounter = this._mockMethodCallCounters[listFirst(resultsKey,"|")]; + arrayAppend( this._mockCallLoggers["initSSEStream"], arguments ); + + if (resultsLen neq 0) { + if (internalCounter gt resultsLen) { + resultsCounter = internalCounter - ( resultsLen * fix( ( internalCounter - 1 ) / resultsLen ) ); + return results[ resultsKey ][ resultsCounter ]; + } else { + return results[ resultsKey ][ internalCounter ]; + } + } + + if ( callbackLen neq 0 ) { + fCallBack = this._mockCallbacks[ resultsKey ].first(); + return fCallBack( argumentCollection : arguments ); + } + + if ( not isSimpleValue( fCallBack ) ){ + return fCallBack( argumentCollection : arguments ); + } + } + \ No newline at end of file diff --git a/public/testbox/system/stubs/C383511C024245809F1F73E0CA52E220.cfm b/public/testbox/system/stubs/C383511C024245809F1F73E0CA52E220.cfm new file mode 100644 index 0000000000..968526a2a3 --- /dev/null +++ b/public/testbox/system/stubs/C383511C024245809F1F73E0CA52E220.cfm @@ -0,0 +1,70 @@ + + variables[ "poll" ] = variables[ "tmp_poll_C383511C024245809F1F73E0CA52E220" ]; + this[ "poll" ] = variables[ "tmp_poll_C383511C024245809F1F73E0CA52E220" ]; + + // Clean up + structDelete( variables, "tmp_poll_C383511C024245809F1F73E0CA52E220" ); + structDelete( this, "tmp_poll_C383511C024245809F1F73E0CA52E220" ); + public any function tmp_poll_C383511C024245809F1F73E0CA52E220( + + ) output=true { + + var results = this._mockResults; + var resultsKey = "poll"; + var resultsCounter = 0; + var internalCounter = 0; + var resultsLen = 0; + var callbackLen = 0; + var argsHashKey = resultsKey & "|" & this.mockBox.normalizeArguments( arguments ); + var fCallBack = ""; + + // If Method & argument Hash Results, switch the results struct +if (structKeyExists( this._mockArgResults, argsHashKey) ) { + // Check if it is a callback +if (isStruct( this._mockArgResults[ argsHashKey ]) && + structKeyExists( this._mockArgResults[ argsHashKey ], "type" ) && + structKeyExists( this._mockArgResults[ argsHashKey ], "target" ) ) { + fCallBack = this._mockArgResults[ argsHashKey ].target; +} else { + // switch context and key + results = this._mockArgResults; + resultsKey = argsHashKey; + } + } + + // Get the statemachine counter +if (isSimpleValue( fCallBack) ) { + resultsLen = arrayLen( results[ resultsKey ] ); + } + + // Get the callback counter, if it exists +if (structKeyExists( this._mockCallbacks, resultsKey) ) { + callbackLen = arrayLen( this._mockCallbacks[ resultsKey ] ); + } + + // Log the Method Call + this._mockMethodCallCounters[ listFirst( resultsKey, "|" ) ] = this._mockMethodCallCounters[ listFirst( resultsKey, "|" ) ] + 1; + + // Get the CallCounter Reference + internalCounter = this._mockMethodCallCounters[listFirst(resultsKey,"|")]; + arrayAppend( this._mockCallLoggers["poll"], arguments ); + + if (resultsLen neq 0) { + if (internalCounter gt resultsLen) { + resultsCounter = internalCounter - ( resultsLen * fix( ( internalCounter - 1 ) / resultsLen ) ); + return results[ resultsKey ][ resultsCounter ]; + } else { + return results[ resultsKey ][ internalCounter ]; + } + } + + if ( callbackLen neq 0 ) { + fCallBack = this._mockCallbacks[ resultsKey ].first(); + return fCallBack( argumentCollection : arguments ); + } + + if ( not isSimpleValue( fCallBack ) ){ + return fCallBack( argumentCollection : arguments ); + } + } + \ No newline at end of file diff --git a/public/testbox/system/stubs/CDCB73D6FD17909D6519A49548FD6769.cfm b/public/testbox/system/stubs/CDCB73D6FD17909D6519A49548FD6769.cfm new file mode 100644 index 0000000000..f7f4f1f167 --- /dev/null +++ b/public/testbox/system/stubs/CDCB73D6FD17909D6519A49548FD6769.cfm @@ -0,0 +1,70 @@ + + variables[ "$getChannelEngine" ] = variables[ "tmp_$getChannelEngine_CDCB73D6FD17909D6519A49548FD6769" ]; + this[ "$getChannelEngine" ] = variables[ "tmp_$getChannelEngine_CDCB73D6FD17909D6519A49548FD6769" ]; + + // Clean up + structDelete( variables, "tmp_$getChannelEngine_CDCB73D6FD17909D6519A49548FD6769" ); + structDelete( this, "tmp_$getChannelEngine_CDCB73D6FD17909D6519A49548FD6769" ); + public any function tmp_$getChannelEngine_CDCB73D6FD17909D6519A49548FD6769( + + ) output=true { + + var results = this._mockResults; + var resultsKey = "$getChannelEngine"; + var resultsCounter = 0; + var internalCounter = 0; + var resultsLen = 0; + var callbackLen = 0; + var argsHashKey = resultsKey & "|" & this.mockBox.normalizeArguments( arguments ); + var fCallBack = ""; + + // If Method & argument Hash Results, switch the results struct +if (structKeyExists( this._mockArgResults, argsHashKey) ) { + // Check if it is a callback +if (isStruct( this._mockArgResults[ argsHashKey ]) && + structKeyExists( this._mockArgResults[ argsHashKey ], "type" ) && + structKeyExists( this._mockArgResults[ argsHashKey ], "target" ) ) { + fCallBack = this._mockArgResults[ argsHashKey ].target; +} else { + // switch context and key + results = this._mockArgResults; + resultsKey = argsHashKey; + } + } + + // Get the statemachine counter +if (isSimpleValue( fCallBack) ) { + resultsLen = arrayLen( results[ resultsKey ] ); + } + + // Get the callback counter, if it exists +if (structKeyExists( this._mockCallbacks, resultsKey) ) { + callbackLen = arrayLen( this._mockCallbacks[ resultsKey ] ); + } + + // Log the Method Call + this._mockMethodCallCounters[ listFirst( resultsKey, "|" ) ] = this._mockMethodCallCounters[ listFirst( resultsKey, "|" ) ] + 1; + + // Get the CallCounter Reference + internalCounter = this._mockMethodCallCounters[listFirst(resultsKey,"|")]; + arrayAppend( this._mockCallLoggers["$getChannelEngine"], arguments ); + + if (resultsLen neq 0) { + if (internalCounter gt resultsLen) { + resultsCounter = internalCounter - ( resultsLen * fix( ( internalCounter - 1 ) / resultsLen ) ); + return results[ resultsKey ][ resultsCounter ]; + } else { + return results[ resultsKey ][ internalCounter ]; + } + } + + if ( callbackLen neq 0 ) { + fCallBack = this._mockCallbacks[ resultsKey ].first(); + return fCallBack( argumentCollection : arguments ); + } + + if ( not isSimpleValue( fCallBack ) ){ + return fCallBack( argumentCollection : arguments ); + } + } + \ No newline at end of file diff --git a/vendor/wheels/Job.cfc b/vendor/wheels/Job.cfc index 5d18a25350..6e9f3e93e3 100644 --- a/vendor/wheels/Job.cfc +++ b/vendor/wheels/Job.cfc @@ -268,6 +268,48 @@ component { return local.result; } + /** + * Internal: Turn a persisted `jobClass` string back into a job instance. + * + * `jobClass` is written on enqueue from `GetMetadata(this).name` and read back here as a + * component path, so the round trip depends on that string still resolving — including its + * casing, on a case-sensitive filesystem. Lucee derives the metadata name from the file + * rather than from how the component was instantiated, so it is canonical there; the + * cross-engine guarantee is pinned by JobClassRoundTripSpec rather than assumed. + * + * When it does not resolve, the raw engine error is `component not found` for a class that + * plainly exists on disk, which sends people to look at mappings and deployment. Name the + * real shape of the problem instead: a string read out of a queue row (issue #3351). + * + * @jobClass The component path as persisted in wheels_jobs. + * @jobId The queue row's id, for the error message. Optional. + */ + public any function $instantiateJobClass(required string jobClass, string jobId = "") { + local.rowLabel = Len(arguments.jobId) ? " named by queue row [#arguments.jobId#]" : ""; + try { + local.rv = CreateObject("component", arguments.jobClass); + } catch (any e) { + Throw( + type = "Wheels.JobClassNotFound", + message = "The job class `#arguments.jobClass#`#local.rowLabel# could not be instantiated: #e.message#", + extendedInfo = "This path was persisted to `wheels_jobs.jobClass` when the job was enqueued and is resolved as a component path now. If the file exists, compare its name and directories to the string above CHARACTER BY CHARACTER — component paths are case-sensitive on Linux but not on macOS or Windows, so a casing mismatch resolves in development and fails in production. It also fails if the job class was renamed, moved, or deleted while rows referencing it were still queued." + ); + } + // A job row names something to instantiate and then call perform() on. Anything without + // perform() is not a job, and failing here says so rather than failing later inside the + // job's own execution where it reads as a job bug. Note this narrows but does not close + // the database-string-to-CreateObject shape the issue flags: the actual guarantee is that + // only $enqueueJob writes this column. + if (!StructKeyExists(local.rv, "perform")) { + Throw( + type = "Wheels.InvalidJobClass", + message = "The component `#arguments.jobClass#`#local.rowLabel# is not a job — it has no `perform()` method.", + extendedInfo = "`wheels_jobs.jobClass` must name a component extending `wheels.Job`. Only the framework writes this column; a value that names something else means the row was written by something other than `enqueue()`." + ); + } + return local.rv; + } + /** * Internal: Process a single job row. */ @@ -315,7 +357,7 @@ component { try { // Instantiate and execute the job - local.jobInstance = CreateObject("component", arguments.jobRow.jobClass); + local.jobInstance = $instantiateJobClass(jobClass = arguments.jobRow.jobClass, jobId = arguments.jobRow.id); if (StructKeyExists(local.jobInstance, "baseDelay")) { local.backoffBaseDelay = local.jobInstance.baseDelay; } diff --git a/vendor/wheels/JobWorker.cfc b/vendor/wheels/JobWorker.cfc index 96abbe334e..4f8336f985 100644 --- a/vendor/wheels/JobWorker.cfc +++ b/vendor/wheels/JobWorker.cfc @@ -449,7 +449,12 @@ component { local.hasTenantContext = false; try { - local.jobInstance = CreateObject("component", arguments.jobRow.jobClass); + // Shared with Job.$processJob so both processing paths report an unresolvable + // jobClass the same way (issue #3351) + local.jobInstance = $jobBridge().$instantiateJobClass( + jobClass = arguments.jobRow.jobClass, + jobId = arguments.jobRow.id + ); local.jobData = DeserializeJSON(arguments.jobRow.data); // Restore tenant context if the job was enqueued within a tenant scope and diff --git a/vendor/wheels/tests/specs/jobs/JobClassRoundTripSpec.cfc b/vendor/wheels/tests/specs/jobs/JobClassRoundTripSpec.cfc new file mode 100644 index 0000000000..e2866e8f17 --- /dev/null +++ b/vendor/wheels/tests/specs/jobs/JobClassRoundTripSpec.cfc @@ -0,0 +1,85 @@ +component extends="wheels.WheelsTest" { + + function run() { + + // Issue #3351. `enqueue()` persists `GetMetadata(this).name` into + // `wheels_jobs.jobClass`, and the drain re-instantiates with + // `CreateObject("component", jobRow.jobClass)`. So a string produced by ENGINE + // METADATA is stored and later resolved as a component path, and the round trip is + // only safe if that string keeps the casing of the file on disk — component paths are + // case-sensitive on Linux and not on macOS or Windows, which is exactly the shape of + // bug that passes locally and fails on a production redeploy. + // + // The issue calls the invariant unverified across engines. Rather than guess at a fix, + // these specs assert it. They run on every engine × database leg, so lucee6, lucee7, + // adobe2023, adobe2025 and boxlang each answer the question directly: if any engine + // reports a name that does not match the file, this fails there and names it. + describe("Tests that the persisted jobClass round-trips", () => { + + it("reports a metadata name whose last segment matches the .cfc file name exactly", () => { + job = CreateObject("component", "wheels.tests._assets.jobs.ProbeJob") + meta = GetMetadata(job) + + fileName = ListFirst(ListLast(Replace(meta.path, "\", "/", "all"), "/"), ".") + + // case-sensitive comparison — Compare(), not CompareNoCase() + expect(Compare(ListLast(meta.name, "."), fileName)).toBe(0) + }) + + it("reports the same metadata name however the component was instantiated", () => { + // on a case-insensitive filesystem BOTH of these resolve, so if the engine + // echoed back the path it was handed rather than deriving it from the file, + // the persisted string would carry whatever casing the caller happened to + // type — and that string is what a Linux worker later has to resolve + canonical = CreateObject("component", "wheels.tests._assets.jobs.ProbeJob") + lowercased = CreateObject("component", "wheels.tests._assets.jobs.probejob") + + expect(Compare(GetMetadata(canonical).name, GetMetadata(lowercased).name)).toBe(0) + }) + + it("re-instantiates from its own persisted metadata name", () => { + // the actual enqueue -> drain round trip, without touching the queue table + original = CreateObject("component", "wheels.tests._assets.jobs.ProbeJob") + persisted = GetMetadata(original).name + + revived = (new wheels.Job()).$instantiateJobClass(jobClass = persisted) + + expect(Compare(GetMetadata(revived).name, persisted)).toBe(0) + }) + }) + + describe("Tests that an unresolvable jobClass", () => { + + it("throws Wheels.JobClassNotFound naming the row and the class", () => { + thrown = {type: "", message: ""} + + try { + (new wheels.Job()).$instantiateJobClass(jobClass = "app.jobs.NoSuchJob", jobId = "abc-123") + } catch (any e) { + thrown.type = e.type + thrown.message = e.message + } + + // the raw engine error is "component not found" for a class that plainly + // exists, which points investigators at mappings and deployment + expect(thrown.type).toBe("Wheels.JobClassNotFound") + expect(thrown.message).toInclude("app.jobs.NoSuchJob") + expect(thrown.message).toInclude("abc-123") + }) + + it("throws Wheels.InvalidJobClass when the path resolves to something that is not a job", () => { + thrown = {type: ""} + + try { + // a real component with no perform() + (new wheels.Job()).$instantiateJobClass(jobClass = "wheels.tests._assets.models.Post") + } catch (any e) { + thrown.type = e.type + } + + expect(thrown.type).toBe("Wheels.InvalidJobClass") + }) + }) + } + +} From 9c369c9c40433570dcd094dc51b24e45149c0057 Mon Sep 17 00:00:00 2001 From: Peter Amiri Date: Mon, 3 Aug 2026 23:14:16 -0700 Subject: [PATCH 2/3] fix(test): hoist the job-bridge receiver out of Adobe's parser path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The compat matrix run for this branch reported tests="0" on EVERY database for adobe2023 and adobe2025 while lucee6, lucee7 and boxlang were all clean at +5. That is the compile-error signature: one bad spec file zeroes the entire engine leg, because the core suite compiles via directory="wheels.tests.specs". From the adobe2023 artifact, not inferred: coldfusion.compiler.CFMLParserBase$MissingNameException: Invalid construct: Either argument or name is missing. snippet: describe("Tests that the persisted jobClass round-trips", () => { TEMPLATE: .../specs/jobs/JobClassRoundTripSpec.cfc LINE 17 TYPE SYNTAX JobClassRoundTripSpec called `(new wheels.Job()).$instantiateJobClass(...)` — a parenthesized `new` in RECEIVER position. Adobe rejects it and, as cross-engine invariant 16 warns, blames the enclosing describe() line rather than the offending statement, so it reads like a broken test-block signature. Hoisting the instance to a variable is the fix. That form has 22 existing spec files as precedent — `adapter.$getType(type = "boolean")` in CockroachDBUnitSpec is the same shape, variable receiver with named arguments — and those files compile on the Adobe legs today. CLAUDE.md invariant 16 is widened rather than left to be rediscovered. It documented only the `application`-scope zero-argument form on Adobe 2025; this is a second shape in the same MissingNameException family, it fails on Adobe 2023 as well, and named arguments do not save it because the receiver is what the parser chokes on. Both are now written up as 16a/16b, with the note that `(new X()).method()` is fine in application code that only runs on Lucee — it appears in this file's own Background Jobs examples — and fatal in the core spec suite, which compiles on all five engines. Local Adobe verification was NOT possible: the adobe2023 container will not start on this machine (`runc create failed ... error during container init`, reported as `engine-down` by tools/test-matrix.sh), from both a worktree and the main checkout. So this rests on the artifact root cause plus the in-repo precedent above, and the re-dispatched matrix is the check. lucee7 + sqlite, full core suite: 4737 pass / 0 fail / 0 error, unchanged from before the hoist. Refs #3351 Signed-off-by: Peter Amiri --- CLAUDE.md | 14 +++++++++++++- box.json | 0 .../tests/specs/jobs/JobClassRoundTripSpec.cfc | 16 +++++++++++++--- 3 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 box.json diff --git a/CLAUDE.md b/CLAUDE.md index c6f7beaa3b..7bdf4b7ef1 100755 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -54,7 +54,19 @@ The framework must run on Lucee 5/6/7, Adobe CF 2018/2021/2023/2025, and BoxLang 15. **A parameter named `request` makes the bare `request` token resolve inconsistently on Adobe 2025.** In a function declaring a parameter named `request`, Adobe CF 2025 can resolve bare `request` to the built-in scope in one expression position and to `arguments.request` in another *within the same function* — so a guard written one way cannot protect an access written the other way. `if (StructKeyExists(request, "wheels")) { StructDelete(request.wheels, "tenant"); }` passed the guard and then threw `Element WHEELS is undefined in REQUEST`. Use `IsDefined("request.wheels.tenant")`, which string-resolves the whole dotted path in one evaluation, or assign before use (`if (!StructKeyExists(request, "wheels")) { request.wheels = {}; }` then write) — never mix the two forms. This hits **every middleware component**, because `wheels.middleware.MiddlewareInterface` mandates the signature `handle(required struct request, required any next)`; anti-pattern 11's "never name a parameter after a reserved scope" is unavailable there. Lucee 6/7, BoxLang and Adobe 2023 all resolve consistently, so **local Lucee green and Adobe 2023 smokes do NOT cover this** — only the Adobe 2025 matrix legs catch it, and `compat-matrix.yml` does not run on PRs (weekly cron + `workflow_dispatch`, `continue-on-error: true`). Hit by `TenantResolver.handle()` in [#3338](https://github.com/wheels-dev/wheels/pull/3338). -16. **A zero-argument call through the `application` scope breaks Adobe 2025's parser in statement position.** Inside a closure, `application.wo.$someMethod()` with an **empty** argument list — used as a bare statement or as the whole right-hand side of an assignment — throws at COMPILE time: `coldfusion.compiler.CFMLParserBase$MissingNameException: Invalid construct: Either argument or name is missing` ("When using named parameters to a function, each parameter must have a name"). Adobe appears to parse it as a script-style tag call and demand at least one attribute. This is the `application`-scope sibling of invariant 2. Verified boundaries — each of these compiles, so **do not "fix" them**: +16. **Two receiver shapes break Adobe's parser at COMPILE time with the same `MissingNameException`.** Both throw `coldfusion.compiler.CFMLParserBase$MissingNameException: Invalid construct: Either argument or name is missing` ("When using named parameters to a function, each parameter must have a name"). Adobe appears to parse the construct as a script-style tag call and demand at least one attribute. + + **16a — a parenthesized `new` in receiver position, on EVERY Adobe engine.** `(new wheels.Job()).$someMethod(arg = "x")` fails to compile on Adobe **2023 and 2025**; Lucee 6/7 and BoxLang accept it. The argument list is irrelevant here — named arguments do not save it, because the receiver is what the parser chokes on. Hoist the instance to a variable first: + ```cfm + // WRONG — zeroes out both Adobe legs + revived = (new wheels.Job()).$instantiateJobClass(jobClass = persisted); + // RIGHT — variable receiver; 22 spec files already do this and pass on Adobe + var bridge = new wheels.Job(); + revived = bridge.$instantiateJobClass(jobClass = persisted); + ``` + Note the `(new X()).method()` form appears in this file's own Background Jobs examples and in user-facing docs — it is fine in **application** code that only ever runs on Lucee, and fatal in the **core spec suite**, which compiles on all five engines. Hit by `JobClassRoundTripSpec` in [#3351](https://github.com/wheels-dev/wheels/issues/3351). + + **16b — a zero-argument call through the `application` scope, Adobe 2025.** Inside a closure, `application.wo.$someMethod()` with an **empty** argument list — used as a bare statement or as the whole right-hand side of an assignment — fails the same way. This is the `application`-scope sibling of invariant 2. Verified boundaries — each of these compiles, so **do not "fix" them**: - any argument at all: `application.wo.$get("showErrorInformation")` - nested inside another call: `expect(application.wo.$statusCode()).toBe(418)` (long-standing in `renderingSpec`) - chained further: `application.wo.mapper().resources("posts")` (`RoutePrecedenceSpec`) diff --git a/box.json b/box.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/vendor/wheels/tests/specs/jobs/JobClassRoundTripSpec.cfc b/vendor/wheels/tests/specs/jobs/JobClassRoundTripSpec.cfc index e2866e8f17..51428352de 100644 --- a/vendor/wheels/tests/specs/jobs/JobClassRoundTripSpec.cfc +++ b/vendor/wheels/tests/specs/jobs/JobClassRoundTripSpec.cfc @@ -42,7 +42,13 @@ component extends="wheels.WheelsTest" { original = CreateObject("component", "wheels.tests._assets.jobs.ProbeJob") persisted = GetMetadata(original).name - revived = (new wheels.Job()).$instantiateJobClass(jobClass = persisted) + // Hoisted receiver. A parenthesized `new` in receiver position — `(new X()).m()` + // — is rejected by Adobe's parser with `Invalid construct: Either argument or + // name is missing`, the same MissingNameException family as cross-engine + // invariant 16. Adobe blames the enclosing describe() line and the whole engine + // leg reports tests=0. Caught by the compat matrix; Lucee and BoxLang accept it. + bridge = new wheels.Job() + revived = bridge.$instantiateJobClass(jobClass = persisted) expect(Compare(GetMetadata(revived).name, persisted)).toBe(0) }) @@ -53,8 +59,10 @@ component extends="wheels.WheelsTest" { it("throws Wheels.JobClassNotFound naming the row and the class", () => { thrown = {type: "", message: ""} + bridge = new wheels.Job() + try { - (new wheels.Job()).$instantiateJobClass(jobClass = "app.jobs.NoSuchJob", jobId = "abc-123") + bridge.$instantiateJobClass(jobClass = "app.jobs.NoSuchJob", jobId = "abc-123") } catch (any e) { thrown.type = e.type thrown.message = e.message @@ -70,9 +78,11 @@ component extends="wheels.WheelsTest" { it("throws Wheels.InvalidJobClass when the path resolves to something that is not a job", () => { thrown = {type: ""} + bridge = new wheels.Job() + try { // a real component with no perform() - (new wheels.Job()).$instantiateJobClass(jobClass = "wheels.tests._assets.models.Post") + bridge.$instantiateJobClass(jobClass = "wheels.tests._assets.models.Post") } catch (any e) { thrown.type = e.type } From 576bbbb550d86b858a61421d3f4f7787dcf74afa Mon Sep 17 00:00:00 2001 From: Peter Amiri Date: Mon, 3 Aug 2026 23:40:32 -0700 Subject: [PATCH 3/3] test(job): assert the round-trip property both engine families satisfy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The re-dispatched matrix confirmed the parser fix — adobe2023 compiles again, +5 tests on every database — but surfaced a second, more interesting problem: one new ERROR per database on Adobe. Error | reports the same metadata name however the component was instantiated | Could not find the ColdFusion component or interface wheels.tests._assets.jobs.probejob. Adobe's component resolver is CASE-SENSITIVE independently of the filesystem. A miscased path does not resolve on macOS either, where the filesystem happily would. My spec instantiated through a deliberately lowercase path to prove the engine does not echo the caller's casing back into the persisted name — an assumption that only holds where the miscased path is constructible at all. So the spec was failing on Adobe for a reason *safer* than the one it was testing. Two engine families close the same hole differently: Lucee / BoxLang a miscased path RESOLVES, but the metadata name comes back canonical Adobe a miscased path does not resolve at all — nothing can be persisted because nothing can be constructed The spec now asserts the property both satisfy: a caller's miscasing cannot reach `wheels_jobs.jobClass`. Following cross-engine invariant 11, the flag lives on a struct set in the try rather than a local set in the catch, so it survives on BoxLang. This is the answer to the open question in the issue, which is the reason these specs run on every leg instead of being assumed: the round trip is safe on all five engines, for two different reasons, neither of which was verified before. lucee7 + sqlite, full core suite: 4737 pass / 0 fail / 0 error. Refs #3351 Signed-off-by: Peter Amiri --- box.json | 0 changelog.d/3351-jobclass-round-trip.fixed.md | 1 + .../139184C0543CCDB338AEFB643110CB89.cfm | 52 -------------- .../16CA394252B074478BEBD2B3A9C8EA82.cfm | 52 -------------- .../30942F4D0BCB6139072EF27C66218715.cfm | 70 ------------------- .../754BEF48E30B63BC11E518FA73C07785.cfm | 52 -------------- .../B7681C49C5125395612F24D97559A4C2.cfm | 70 ------------------- .../C383511C024245809F1F73E0CA52E220.cfm | 70 ------------------- .../CDCB73D6FD17909D6519A49548FD6769.cfm | 70 ------------------- .../specs/jobs/JobClassRoundTripSpec.cfc | 41 ++++++++--- 10 files changed, 33 insertions(+), 445 deletions(-) delete mode 100644 box.json delete mode 100644 public/testbox/system/stubs/139184C0543CCDB338AEFB643110CB89.cfm delete mode 100644 public/testbox/system/stubs/16CA394252B074478BEBD2B3A9C8EA82.cfm delete mode 100644 public/testbox/system/stubs/30942F4D0BCB6139072EF27C66218715.cfm delete mode 100644 public/testbox/system/stubs/754BEF48E30B63BC11E518FA73C07785.cfm delete mode 100644 public/testbox/system/stubs/B7681C49C5125395612F24D97559A4C2.cfm delete mode 100644 public/testbox/system/stubs/C383511C024245809F1F73E0CA52E220.cfm delete mode 100644 public/testbox/system/stubs/CDCB73D6FD17909D6519A49548FD6769.cfm diff --git a/box.json b/box.json deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/changelog.d/3351-jobclass-round-trip.fixed.md b/changelog.d/3351-jobclass-round-trip.fixed.md index 66ae4194f7..c55528ef67 100644 --- a/changelog.d/3351-jobclass-round-trip.fixed.md +++ b/changelog.d/3351-jobclass-round-trip.fixed.md @@ -1 +1,2 @@ - A background job whose `jobClass` cannot be resolved now throws `Wheels.JobClassNotFound` naming the class, the queue row, and the likely causes, instead of the engine's bare `component not found`. `wheels_jobs.jobClass` is written from `GetMetadata(this).name` on enqueue and resolved as a component path on drain, so the failure appears as "component not found" for a class that plainly exists on disk — which sends people to look at mappings and deployment rather than at the persisted string. Component paths are case-sensitive on Linux but not on macOS or Windows, so a casing mismatch resolves in development and fails on a production redeploy, long after the row was written. A path that resolves to something without a `perform()` method now throws `Wheels.InvalidJobClass` rather than failing later inside job execution. Both processing paths (`Job.$processJob` and `JobWorker.$executeJob`) share the check (#3351) +- Verified across every engine: the `jobClass` string persisted on enqueue always round-trips. Lucee and BoxLang derive the metadata name from the file, so a miscased path still yields the canonical name; Adobe's component resolver is case-sensitive independently of the filesystem, so a miscased path does not construct at all. Either way a caller's miscasing cannot reach `wheels_jobs.jobClass`. Pinned by `JobClassRoundTripSpec`, which runs on all five engines rather than assuming the invariant (#3351) diff --git a/public/testbox/system/stubs/139184C0543CCDB338AEFB643110CB89.cfm b/public/testbox/system/stubs/139184C0543CCDB338AEFB643110CB89.cfm deleted file mode 100644 index d313ccd44c..0000000000 --- a/public/testbox/system/stubs/139184C0543CCDB338AEFB643110CB89.cfm +++ /dev/null @@ -1,52 +0,0 @@ - - variables[ "closeSSEStream" ] = variables[ "tmp_closeSSEStream_139184C0543CCDB338AEFB643110CB89" ]; - this[ "closeSSEStream" ] = variables[ "tmp_closeSSEStream_139184C0543CCDB338AEFB643110CB89" ]; - - // Clean up - structDelete( variables, "tmp_closeSSEStream_139184C0543CCDB338AEFB643110CB89" ); - structDelete( this, "tmp_closeSSEStream_139184C0543CCDB338AEFB643110CB89" ); - public void function tmp_closeSSEStream_139184C0543CCDB338AEFB643110CB89( - - ) output=true { - - var results = this._mockResults; - var resultsKey = "closeSSEStream"; - var resultsCounter = 0; - var internalCounter = 0; - var resultsLen = 0; - var callbackLen = 0; - var argsHashKey = resultsKey & "|" & this.mockBox.normalizeArguments( arguments ); - var fCallBack = ""; - - // If Method & argument Hash Results, switch the results struct -if (structKeyExists( this._mockArgResults, argsHashKey) ) { - // Check if it is a callback -if (isStruct( this._mockArgResults[ argsHashKey ]) && - structKeyExists( this._mockArgResults[ argsHashKey ], "type" ) && - structKeyExists( this._mockArgResults[ argsHashKey ], "target" ) ) { - fCallBack = this._mockArgResults[ argsHashKey ].target; -} else { - // switch context and key - results = this._mockArgResults; - resultsKey = argsHashKey; - } - } - - // Get the statemachine counter -if (isSimpleValue( fCallBack) ) { - resultsLen = arrayLen( results[ resultsKey ] ); - } - - // Get the callback counter, if it exists -if (structKeyExists( this._mockCallbacks, resultsKey) ) { - callbackLen = arrayLen( this._mockCallbacks[ resultsKey ] ); - } - - // Log the Method Call - this._mockMethodCallCounters[ listFirst( resultsKey, "|" ) ] = this._mockMethodCallCounters[ listFirst( resultsKey, "|" ) ] + 1; - - // Get the CallCounter Reference - internalCounter = this._mockMethodCallCounters[listFirst(resultsKey,"|")]; - arrayAppend( this._mockCallLoggers["closeSSEStream"], arguments ); -} - \ No newline at end of file diff --git a/public/testbox/system/stubs/16CA394252B074478BEBD2B3A9C8EA82.cfm b/public/testbox/system/stubs/16CA394252B074478BEBD2B3A9C8EA82.cfm deleted file mode 100644 index b7e93ee8ab..0000000000 --- a/public/testbox/system/stubs/16CA394252B074478BEBD2B3A9C8EA82.cfm +++ /dev/null @@ -1,52 +0,0 @@ - - variables[ "sendSSEComment" ] = variables[ "tmp_sendSSEComment_16CA394252B074478BEBD2B3A9C8EA82" ]; - this[ "sendSSEComment" ] = variables[ "tmp_sendSSEComment_16CA394252B074478BEBD2B3A9C8EA82" ]; - - // Clean up - structDelete( variables, "tmp_sendSSEComment_16CA394252B074478BEBD2B3A9C8EA82" ); - structDelete( this, "tmp_sendSSEComment_16CA394252B074478BEBD2B3A9C8EA82" ); - public void function tmp_sendSSEComment_16CA394252B074478BEBD2B3A9C8EA82( - - ) output=true { - - var results = this._mockResults; - var resultsKey = "sendSSEComment"; - var resultsCounter = 0; - var internalCounter = 0; - var resultsLen = 0; - var callbackLen = 0; - var argsHashKey = resultsKey & "|" & this.mockBox.normalizeArguments( arguments ); - var fCallBack = ""; - - // If Method & argument Hash Results, switch the results struct -if (structKeyExists( this._mockArgResults, argsHashKey) ) { - // Check if it is a callback -if (isStruct( this._mockArgResults[ argsHashKey ]) && - structKeyExists( this._mockArgResults[ argsHashKey ], "type" ) && - structKeyExists( this._mockArgResults[ argsHashKey ], "target" ) ) { - fCallBack = this._mockArgResults[ argsHashKey ].target; -} else { - // switch context and key - results = this._mockArgResults; - resultsKey = argsHashKey; - } - } - - // Get the statemachine counter -if (isSimpleValue( fCallBack) ) { - resultsLen = arrayLen( results[ resultsKey ] ); - } - - // Get the callback counter, if it exists -if (structKeyExists( this._mockCallbacks, resultsKey) ) { - callbackLen = arrayLen( this._mockCallbacks[ resultsKey ] ); - } - - // Log the Method Call - this._mockMethodCallCounters[ listFirst( resultsKey, "|" ) ] = this._mockMethodCallCounters[ listFirst( resultsKey, "|" ) ] + 1; - - // Get the CallCounter Reference - internalCounter = this._mockMethodCallCounters[listFirst(resultsKey,"|")]; - arrayAppend( this._mockCallLoggers["sendSSEComment"], arguments ); -} - \ No newline at end of file diff --git a/public/testbox/system/stubs/30942F4D0BCB6139072EF27C66218715.cfm b/public/testbox/system/stubs/30942F4D0BCB6139072EF27C66218715.cfm deleted file mode 100644 index e0cfd43b5c..0000000000 --- a/public/testbox/system/stubs/30942F4D0BCB6139072EF27C66218715.cfm +++ /dev/null @@ -1,70 +0,0 @@ - - variables[ "checkError" ] = variables[ "tmp_checkError_30942F4D0BCB6139072EF27C66218715" ]; - this[ "checkError" ] = variables[ "tmp_checkError_30942F4D0BCB6139072EF27C66218715" ]; - - // Clean up - structDelete( variables, "tmp_checkError_30942F4D0BCB6139072EF27C66218715" ); - structDelete( this, "tmp_checkError_30942F4D0BCB6139072EF27C66218715" ); - public any function tmp_checkError_30942F4D0BCB6139072EF27C66218715( - - ) output=true { - - var results = this._mockResults; - var resultsKey = "checkError"; - var resultsCounter = 0; - var internalCounter = 0; - var resultsLen = 0; - var callbackLen = 0; - var argsHashKey = resultsKey & "|" & this.mockBox.normalizeArguments( arguments ); - var fCallBack = ""; - - // If Method & argument Hash Results, switch the results struct -if (structKeyExists( this._mockArgResults, argsHashKey) ) { - // Check if it is a callback -if (isStruct( this._mockArgResults[ argsHashKey ]) && - structKeyExists( this._mockArgResults[ argsHashKey ], "type" ) && - structKeyExists( this._mockArgResults[ argsHashKey ], "target" ) ) { - fCallBack = this._mockArgResults[ argsHashKey ].target; -} else { - // switch context and key - results = this._mockArgResults; - resultsKey = argsHashKey; - } - } - - // Get the statemachine counter -if (isSimpleValue( fCallBack) ) { - resultsLen = arrayLen( results[ resultsKey ] ); - } - - // Get the callback counter, if it exists -if (structKeyExists( this._mockCallbacks, resultsKey) ) { - callbackLen = arrayLen( this._mockCallbacks[ resultsKey ] ); - } - - // Log the Method Call - this._mockMethodCallCounters[ listFirst( resultsKey, "|" ) ] = this._mockMethodCallCounters[ listFirst( resultsKey, "|" ) ] + 1; - - // Get the CallCounter Reference - internalCounter = this._mockMethodCallCounters[listFirst(resultsKey,"|")]; - arrayAppend( this._mockCallLoggers["checkError"], arguments ); - - if (resultsLen neq 0) { - if (internalCounter gt resultsLen) { - resultsCounter = internalCounter - ( resultsLen * fix( ( internalCounter - 1 ) / resultsLen ) ); - return results[ resultsKey ][ resultsCounter ]; - } else { - return results[ resultsKey ][ internalCounter ]; - } - } - - if ( callbackLen neq 0 ) { - fCallBack = this._mockCallbacks[ resultsKey ].first(); - return fCallBack( argumentCollection : arguments ); - } - - if ( not isSimpleValue( fCallBack ) ){ - return fCallBack( argumentCollection : arguments ); - } - } - \ No newline at end of file diff --git a/public/testbox/system/stubs/754BEF48E30B63BC11E518FA73C07785.cfm b/public/testbox/system/stubs/754BEF48E30B63BC11E518FA73C07785.cfm deleted file mode 100644 index 2ea84360a3..0000000000 --- a/public/testbox/system/stubs/754BEF48E30B63BC11E518FA73C07785.cfm +++ /dev/null @@ -1,52 +0,0 @@ - - variables[ "sendSSEEvent" ] = variables[ "tmp_sendSSEEvent_754BEF48E30B63BC11E518FA73C07785" ]; - this[ "sendSSEEvent" ] = variables[ "tmp_sendSSEEvent_754BEF48E30B63BC11E518FA73C07785" ]; - - // Clean up - structDelete( variables, "tmp_sendSSEEvent_754BEF48E30B63BC11E518FA73C07785" ); - structDelete( this, "tmp_sendSSEEvent_754BEF48E30B63BC11E518FA73C07785" ); - public void function tmp_sendSSEEvent_754BEF48E30B63BC11E518FA73C07785( - - ) output=true { - - var results = this._mockResults; - var resultsKey = "sendSSEEvent"; - var resultsCounter = 0; - var internalCounter = 0; - var resultsLen = 0; - var callbackLen = 0; - var argsHashKey = resultsKey & "|" & this.mockBox.normalizeArguments( arguments ); - var fCallBack = ""; - - // If Method & argument Hash Results, switch the results struct -if (structKeyExists( this._mockArgResults, argsHashKey) ) { - // Check if it is a callback -if (isStruct( this._mockArgResults[ argsHashKey ]) && - structKeyExists( this._mockArgResults[ argsHashKey ], "type" ) && - structKeyExists( this._mockArgResults[ argsHashKey ], "target" ) ) { - fCallBack = this._mockArgResults[ argsHashKey ].target; -} else { - // switch context and key - results = this._mockArgResults; - resultsKey = argsHashKey; - } - } - - // Get the statemachine counter -if (isSimpleValue( fCallBack) ) { - resultsLen = arrayLen( results[ resultsKey ] ); - } - - // Get the callback counter, if it exists -if (structKeyExists( this._mockCallbacks, resultsKey) ) { - callbackLen = arrayLen( this._mockCallbacks[ resultsKey ] ); - } - - // Log the Method Call - this._mockMethodCallCounters[ listFirst( resultsKey, "|" ) ] = this._mockMethodCallCounters[ listFirst( resultsKey, "|" ) ] + 1; - - // Get the CallCounter Reference - internalCounter = this._mockMethodCallCounters[listFirst(resultsKey,"|")]; - arrayAppend( this._mockCallLoggers["sendSSEEvent"], arguments ); -} - \ No newline at end of file diff --git a/public/testbox/system/stubs/B7681C49C5125395612F24D97559A4C2.cfm b/public/testbox/system/stubs/B7681C49C5125395612F24D97559A4C2.cfm deleted file mode 100644 index 81f5d1340b..0000000000 --- a/public/testbox/system/stubs/B7681C49C5125395612F24D97559A4C2.cfm +++ /dev/null @@ -1,70 +0,0 @@ - - variables[ "initSSEStream" ] = variables[ "tmp_initSSEStream_B7681C49C5125395612F24D97559A4C2" ]; - this[ "initSSEStream" ] = variables[ "tmp_initSSEStream_B7681C49C5125395612F24D97559A4C2" ]; - - // Clean up - structDelete( variables, "tmp_initSSEStream_B7681C49C5125395612F24D97559A4C2" ); - structDelete( this, "tmp_initSSEStream_B7681C49C5125395612F24D97559A4C2" ); - public any function tmp_initSSEStream_B7681C49C5125395612F24D97559A4C2( - - ) output=true { - - var results = this._mockResults; - var resultsKey = "initSSEStream"; - var resultsCounter = 0; - var internalCounter = 0; - var resultsLen = 0; - var callbackLen = 0; - var argsHashKey = resultsKey & "|" & this.mockBox.normalizeArguments( arguments ); - var fCallBack = ""; - - // If Method & argument Hash Results, switch the results struct -if (structKeyExists( this._mockArgResults, argsHashKey) ) { - // Check if it is a callback -if (isStruct( this._mockArgResults[ argsHashKey ]) && - structKeyExists( this._mockArgResults[ argsHashKey ], "type" ) && - structKeyExists( this._mockArgResults[ argsHashKey ], "target" ) ) { - fCallBack = this._mockArgResults[ argsHashKey ].target; -} else { - // switch context and key - results = this._mockArgResults; - resultsKey = argsHashKey; - } - } - - // Get the statemachine counter -if (isSimpleValue( fCallBack) ) { - resultsLen = arrayLen( results[ resultsKey ] ); - } - - // Get the callback counter, if it exists -if (structKeyExists( this._mockCallbacks, resultsKey) ) { - callbackLen = arrayLen( this._mockCallbacks[ resultsKey ] ); - } - - // Log the Method Call - this._mockMethodCallCounters[ listFirst( resultsKey, "|" ) ] = this._mockMethodCallCounters[ listFirst( resultsKey, "|" ) ] + 1; - - // Get the CallCounter Reference - internalCounter = this._mockMethodCallCounters[listFirst(resultsKey,"|")]; - arrayAppend( this._mockCallLoggers["initSSEStream"], arguments ); - - if (resultsLen neq 0) { - if (internalCounter gt resultsLen) { - resultsCounter = internalCounter - ( resultsLen * fix( ( internalCounter - 1 ) / resultsLen ) ); - return results[ resultsKey ][ resultsCounter ]; - } else { - return results[ resultsKey ][ internalCounter ]; - } - } - - if ( callbackLen neq 0 ) { - fCallBack = this._mockCallbacks[ resultsKey ].first(); - return fCallBack( argumentCollection : arguments ); - } - - if ( not isSimpleValue( fCallBack ) ){ - return fCallBack( argumentCollection : arguments ); - } - } - \ No newline at end of file diff --git a/public/testbox/system/stubs/C383511C024245809F1F73E0CA52E220.cfm b/public/testbox/system/stubs/C383511C024245809F1F73E0CA52E220.cfm deleted file mode 100644 index 968526a2a3..0000000000 --- a/public/testbox/system/stubs/C383511C024245809F1F73E0CA52E220.cfm +++ /dev/null @@ -1,70 +0,0 @@ - - variables[ "poll" ] = variables[ "tmp_poll_C383511C024245809F1F73E0CA52E220" ]; - this[ "poll" ] = variables[ "tmp_poll_C383511C024245809F1F73E0CA52E220" ]; - - // Clean up - structDelete( variables, "tmp_poll_C383511C024245809F1F73E0CA52E220" ); - structDelete( this, "tmp_poll_C383511C024245809F1F73E0CA52E220" ); - public any function tmp_poll_C383511C024245809F1F73E0CA52E220( - - ) output=true { - - var results = this._mockResults; - var resultsKey = "poll"; - var resultsCounter = 0; - var internalCounter = 0; - var resultsLen = 0; - var callbackLen = 0; - var argsHashKey = resultsKey & "|" & this.mockBox.normalizeArguments( arguments ); - var fCallBack = ""; - - // If Method & argument Hash Results, switch the results struct -if (structKeyExists( this._mockArgResults, argsHashKey) ) { - // Check if it is a callback -if (isStruct( this._mockArgResults[ argsHashKey ]) && - structKeyExists( this._mockArgResults[ argsHashKey ], "type" ) && - structKeyExists( this._mockArgResults[ argsHashKey ], "target" ) ) { - fCallBack = this._mockArgResults[ argsHashKey ].target; -} else { - // switch context and key - results = this._mockArgResults; - resultsKey = argsHashKey; - } - } - - // Get the statemachine counter -if (isSimpleValue( fCallBack) ) { - resultsLen = arrayLen( results[ resultsKey ] ); - } - - // Get the callback counter, if it exists -if (structKeyExists( this._mockCallbacks, resultsKey) ) { - callbackLen = arrayLen( this._mockCallbacks[ resultsKey ] ); - } - - // Log the Method Call - this._mockMethodCallCounters[ listFirst( resultsKey, "|" ) ] = this._mockMethodCallCounters[ listFirst( resultsKey, "|" ) ] + 1; - - // Get the CallCounter Reference - internalCounter = this._mockMethodCallCounters[listFirst(resultsKey,"|")]; - arrayAppend( this._mockCallLoggers["poll"], arguments ); - - if (resultsLen neq 0) { - if (internalCounter gt resultsLen) { - resultsCounter = internalCounter - ( resultsLen * fix( ( internalCounter - 1 ) / resultsLen ) ); - return results[ resultsKey ][ resultsCounter ]; - } else { - return results[ resultsKey ][ internalCounter ]; - } - } - - if ( callbackLen neq 0 ) { - fCallBack = this._mockCallbacks[ resultsKey ].first(); - return fCallBack( argumentCollection : arguments ); - } - - if ( not isSimpleValue( fCallBack ) ){ - return fCallBack( argumentCollection : arguments ); - } - } - \ No newline at end of file diff --git a/public/testbox/system/stubs/CDCB73D6FD17909D6519A49548FD6769.cfm b/public/testbox/system/stubs/CDCB73D6FD17909D6519A49548FD6769.cfm deleted file mode 100644 index f7f4f1f167..0000000000 --- a/public/testbox/system/stubs/CDCB73D6FD17909D6519A49548FD6769.cfm +++ /dev/null @@ -1,70 +0,0 @@ - - variables[ "$getChannelEngine" ] = variables[ "tmp_$getChannelEngine_CDCB73D6FD17909D6519A49548FD6769" ]; - this[ "$getChannelEngine" ] = variables[ "tmp_$getChannelEngine_CDCB73D6FD17909D6519A49548FD6769" ]; - - // Clean up - structDelete( variables, "tmp_$getChannelEngine_CDCB73D6FD17909D6519A49548FD6769" ); - structDelete( this, "tmp_$getChannelEngine_CDCB73D6FD17909D6519A49548FD6769" ); - public any function tmp_$getChannelEngine_CDCB73D6FD17909D6519A49548FD6769( - - ) output=true { - - var results = this._mockResults; - var resultsKey = "$getChannelEngine"; - var resultsCounter = 0; - var internalCounter = 0; - var resultsLen = 0; - var callbackLen = 0; - var argsHashKey = resultsKey & "|" & this.mockBox.normalizeArguments( arguments ); - var fCallBack = ""; - - // If Method & argument Hash Results, switch the results struct -if (structKeyExists( this._mockArgResults, argsHashKey) ) { - // Check if it is a callback -if (isStruct( this._mockArgResults[ argsHashKey ]) && - structKeyExists( this._mockArgResults[ argsHashKey ], "type" ) && - structKeyExists( this._mockArgResults[ argsHashKey ], "target" ) ) { - fCallBack = this._mockArgResults[ argsHashKey ].target; -} else { - // switch context and key - results = this._mockArgResults; - resultsKey = argsHashKey; - } - } - - // Get the statemachine counter -if (isSimpleValue( fCallBack) ) { - resultsLen = arrayLen( results[ resultsKey ] ); - } - - // Get the callback counter, if it exists -if (structKeyExists( this._mockCallbacks, resultsKey) ) { - callbackLen = arrayLen( this._mockCallbacks[ resultsKey ] ); - } - - // Log the Method Call - this._mockMethodCallCounters[ listFirst( resultsKey, "|" ) ] = this._mockMethodCallCounters[ listFirst( resultsKey, "|" ) ] + 1; - - // Get the CallCounter Reference - internalCounter = this._mockMethodCallCounters[listFirst(resultsKey,"|")]; - arrayAppend( this._mockCallLoggers["$getChannelEngine"], arguments ); - - if (resultsLen neq 0) { - if (internalCounter gt resultsLen) { - resultsCounter = internalCounter - ( resultsLen * fix( ( internalCounter - 1 ) / resultsLen ) ); - return results[ resultsKey ][ resultsCounter ]; - } else { - return results[ resultsKey ][ internalCounter ]; - } - } - - if ( callbackLen neq 0 ) { - fCallBack = this._mockCallbacks[ resultsKey ].first(); - return fCallBack( argumentCollection : arguments ); - } - - if ( not isSimpleValue( fCallBack ) ){ - return fCallBack( argumentCollection : arguments ); - } - } - \ No newline at end of file diff --git a/vendor/wheels/tests/specs/jobs/JobClassRoundTripSpec.cfc b/vendor/wheels/tests/specs/jobs/JobClassRoundTripSpec.cfc index 51428352de..ee129ee249 100644 --- a/vendor/wheels/tests/specs/jobs/JobClassRoundTripSpec.cfc +++ b/vendor/wheels/tests/specs/jobs/JobClassRoundTripSpec.cfc @@ -26,15 +26,38 @@ component extends="wheels.WheelsTest" { expect(Compare(ListLast(meta.name, "."), fileName)).toBe(0) }) - it("reports the same metadata name however the component was instantiated", () => { - // on a case-insensitive filesystem BOTH of these resolve, so if the engine - // echoed back the path it was handed rather than deriving it from the file, - // the persisted string would carry whatever casing the caller happened to - // type — and that string is what a Linux worker later has to resolve - canonical = CreateObject("component", "wheels.tests._assets.jobs.ProbeJob") - lowercased = CreateObject("component", "wheels.tests._assets.jobs.probejob") - - expect(Compare(GetMetadata(canonical).name, GetMetadata(lowercased).name)).toBe(0) + it("never persists a caller's miscased path", () => { + // The risk is an engine ECHOING BACK the path it was handed instead of + // deriving the name from the file: the persisted string would then carry + // whatever casing the caller happened to type, and that string is what a + // Linux worker later has to resolve. + // + // Engines close that off two different ways, and either is sufficient: + // + // Lucee/BoxLang — a miscased path RESOLVES (the filesystem is + // case-insensitive here) but the metadata name comes back canonical. + // Adobe — a miscased path does not resolve AT ALL. Its component + // resolver is case-sensitive independently of the filesystem, throwing + // "Could not find the ColdFusion component ... probejob". Nothing can be + // persisted because nothing can be constructed. + // + // Asserting only the first would fail on Adobe for a reason that is *safer* + // than the one being tested, so assert the property both satisfy. + canonical = GetMetadata(CreateObject("component", "wheels.tests._assets.jobs.ProbeJob")).name + resolved = {miscasedConstructed = false, name = ""} + + try { + resolved.name = GetMetadata(CreateObject("component", "wheels.tests._assets.jobs.probejob")).name + resolved.miscasedConstructed = true + } catch (any e) { + // case-sensitive resolver — the stronger guarantee + } + + if (resolved.miscasedConstructed) { + expect(Compare(resolved.name, canonical)).toBe(0) + } else { + expect(resolved.name).toBe("") + } }) it("re-instantiates from its own persisted metadata name", () => {