From 22eaff9f996a7a6f34121c116bbc9089a7f76fbd Mon Sep 17 00:00:00 2001 From: robobun Date: Sun, 19 Apr 2026 05:29:17 +0000 Subject: [PATCH] Fix assertion failure when spyOn is used on non-callable indexed property When spyOn(obj, 1) was called on an object where obj[1] was missing or a non-callable value, the mock was stored via putDirectIndex with PropertyAttribute::Accessor set, even though the stored value was a plain JSMockFunction and not a GetterSetter. Any subsequent property lookup on that index hit the PropertySlot::setValue assertion that forbids the Accessor attribute on plain values. Mask out the Accessor attribute when storing the mock at a numeric index, since the indexed path intentionally does not wrap the mock in a GetterSetter. --- src/jsc/bindings/JSMockFunction.cpp | 2 +- test/js/bun/test/mock-fn.test.js | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/jsc/bindings/JSMockFunction.cpp b/src/jsc/bindings/JSMockFunction.cpp index 1c1c7fe70f6..36e1cb3c5df 100644 --- a/src/jsc/bindings/JSMockFunction.cpp +++ b/src/jsc/bindings/JSMockFunction.cpp @@ -1562,7 +1562,7 @@ BUN_DEFINE_HOST_FUNCTION(JSMock__jsSpyOn, (JSC::JSGlobalObject * lexicalGlobalOb mock->spyAttributes |= JSMockFunction::SpyAttributeESModuleNamespace; } else if (auto index = parseIndex(propertyKey)) { // For indexed properties, set the mock directly instead of wrapping in GetterSetter - object->putDirectIndex(globalObject, *index, mock, attributes, PutDirectIndexLikePutDirect); + object->putDirectIndex(globalObject, *index, mock, attributes & ~PropertyAttribute::Accessor, PutDirectIndexLikePutDirect); } else { object->putDirectAccessor(globalObject, propertyKey, JSC::GetterSetter::create(vm, globalObject, mock, mock), attributes); } diff --git a/test/js/bun/test/mock-fn.test.js b/test/js/bun/test/mock-fn.test.js index 7f6a244d980..c20c3b11967 100644 --- a/test/js/bun/test/mock-fn.test.js +++ b/test/js/bun/test/mock-fn.test.js @@ -1011,6 +1011,27 @@ describe("spyOn", () => { expect(arr[14]()).toBe(456); expect(fn).not.toHaveBeenCalled(); }); + + test("spyOn works with non-callable indexed properties", () => { + const obj = {}; + const fn = spyOn(obj, 1); + expect(() => obj[1]).not.toThrow(); + expect(obj[1]).toBe(fn); + expect(Object.getOwnPropertyDescriptor(obj, 1)).toEqual({ + value: fn, + writable: true, + enumerable: true, + configurable: true, + }); + + const obj2 = {}; + obj2[3] = "hello"; + const fn2 = spyOn(obj2, 3); + expect(() => obj2[3]).not.toThrow(); + expect(() => Object.getOwnPropertyDescriptor(obj2, 3)).not.toThrow(); + fn2.mockRestore(); + expect(obj2[3]).toBe("hello"); + }); } // spyOn does not work with getters/setters yet.