From 8e1faf83f42e1ff074a8c43f2da865f9a9c17e87 Mon Sep 17 00:00:00 2001 From: Oisin <10200426+oiisin@users.noreply.github.com> Date: Sat, 24 Oct 2026 17:20:32 +0100 Subject: [PATCH] fix(promise): Check for a promise without indexing the value Promise.isPromise read value.ClassName, which runs the value's __index, so anything handed to Promise code that errors on unknown keys (Cmdr's Registry and Dispatcher, for example) threw instead of being treated as a plain value. It now reads ClassName from the metatable with rawget, which still matches promises from another copy of the package. --- src/promise/src/Shared/Promise.lua | 9 ++++++++- src/promise/src/Shared/Promise.spec.lua | 17 +++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/promise/src/Shared/Promise.lua b/src/promise/src/Shared/Promise.lua index 4af15a465e9..aaec0366182 100644 --- a/src/promise/src/Shared/Promise.lua +++ b/src/promise/src/Shared/Promise.lua @@ -41,7 +41,14 @@ export type Promise = typeof(setmetatable( @return boolean ]=] function Promise.isPromise(value: any): boolean - return type(value) == "table" and value.ClassName == "Promise" + if type(value) ~= "table" then + return false + end + + -- Read the metatable directly: indexing the value runs its __index, and some objects error on + -- unknown keys. Promises from another copy of this package have their own metatable. + local metatable = getmetatable(value) + return type(metatable) == "table" and rawget(metatable, "ClassName") == "Promise" end --[=[ diff --git a/src/promise/src/Shared/Promise.spec.lua b/src/promise/src/Shared/Promise.spec.lua index c5fc988953f..677dc46628a 100644 --- a/src/promise/src/Shared/Promise.spec.lua +++ b/src/promise/src/Shared/Promise.spec.lua @@ -26,6 +26,23 @@ describe("Promise.isPromise", function() expect(Promise.isPromise({})).toEqual(false) expect(Promise.isPromise({ ClassName = "NotAPromise" })).toEqual(false) end) + + it("returns false without indexing objects that error on unknown keys", function() + local strict = setmetatable({}, { + __index = function(_, key) + error(string.format("%s is not a valid member", tostring(key))) + end, + }) + + expect(Promise.isPromise(strict)).toEqual(false) + end) + + it("returns true for a promise from another copy of the package", function() + local OtherPromise = { ClassName = "Promise" } + OtherPromise.__index = OtherPromise + + expect(Promise.isPromise(setmetatable({}, OtherPromise))).toEqual(true) + end) end) describe("Promise.new", function()