From a9d2bf1bd611b74f02bca0fe646caa215bb6cba6 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Mon, 27 Jul 2026 11:21:14 +0000 Subject: [PATCH 1/2] [JSC] Implement Promise.allKeyed and Promise.allSettledKeyed behind usePromiseAllKeyed Adds the Stage 3 await-dictionary proposal (tc39/proposal-await-dictionary) as JS builtins on the Promise constructor, gated by the new usePromiseAllKeyed option (default off). Both methods take an object, enumerate its own enumerable keys via [[OwnPropertyKeys]] (strings then symbols, one trap call), resolve each value through C.resolve, and resolve the capability with a null-prototype object keyed in enumeration order. allSettledKeyed wraps each settlement as {status, value}/{status, reason}. To get spec-correct [[OwnPropertyKeys]] semantics from a JS builtin, a new @Object.@ownKeys private slot is added that calls ownPropertyKeys(StringsAndSymbols, Include), matching Reflect.ownKeys. test262: maps the await-dictionary feature to usePromiseAllKeyed. One vendored test (allSettledKeyed/result-property-descriptors.js) is synced to the upstream fix for tc39/test262#5084. 89/89 tests pass. JSTests/stress/promise-allKeyed.js covers the basics plus symbol keys, non-enumerable and inherited skipping, ordering, rejections, and C.resolve validation. --- JSTests/stress/promise-allKeyed.js | 179 ++++++++++++++++++ JSTests/test262/config.yaml | 2 +- .../result-property-descriptors.js | 17 +- Source/JavaScriptCore/builtins/BuiltinNames.h | 1 + .../builtins/PromiseConstructor.js | 157 +++++++++++++++ .../runtime/JSPromiseConstructor.cpp | 6 + .../runtime/ObjectConstructor.cpp | 11 ++ .../Preferences/UnifiedWebPreferences.yaml | 13 ++ 8 files changed, 379 insertions(+), 7 deletions(-) create mode 100644 JSTests/stress/promise-allKeyed.js diff --git a/JSTests/stress/promise-allKeyed.js b/JSTests/stress/promise-allKeyed.js new file mode 100644 index 0000000000000..038d1a349e5c5 --- /dev/null +++ b/JSTests/stress/promise-allKeyed.js @@ -0,0 +1,179 @@ +//@ requireOptions("--usePromiseAllKeyed=1") + +function shouldBe(actual, expected) { + if (actual !== expected) + throw new Error(`expected ${expected} but got ${actual}`); +} + +function shouldBeArray(actual, expected) { + shouldBe(actual.length, expected.length); + for (let i = 0; i < expected.length; ++i) + shouldBe(actual[i], expected[i]); +} + +function notReached() { + throw new Error("should not reach here"); +} + +shouldBe(typeof Promise.allKeyed, "function"); +shouldBe(Promise.allKeyed.length, 1); +shouldBe(Promise.allKeyed.name, "allKeyed"); +shouldBe(typeof Promise.allSettledKeyed, "function"); +shouldBe(Promise.allSettledKeyed.length, 1); +shouldBe(Promise.allSettledKeyed.name, "allSettledKeyed"); + +let allKeyedDesc = Object.getOwnPropertyDescriptor(Promise, "allKeyed"); +shouldBe(allKeyedDesc.writable, true); +shouldBe(allKeyedDesc.enumerable, false); +shouldBe(allKeyedDesc.configurable, true); + +async function test() { + // Basic: resolves to a null-prototype object with the same keys. + { + let result = await Promise.allKeyed({ a: 1, b: Promise.resolve(2), c: Promise.resolve(3) }); + shouldBe(Object.getPrototypeOf(result), null); + shouldBeArray(Object.keys(result), ["a", "b", "c"]); + shouldBe(result.a, 1); + shouldBe(result.b, 2); + shouldBe(result.c, 3); + } + + // Empty object resolves to an empty null-prototype object. + { + let result = await Promise.allKeyed({}); + shouldBe(Object.getPrototypeOf(result), null); + shouldBe(Reflect.ownKeys(result).length, 0); + } + + // Non-object argument rejects with TypeError. + for (let value of [undefined, null, 42, "str", true, Symbol()]) { + try { + await Promise.allKeyed(value); + notReached(); + } catch (e) { + shouldBe(e instanceof TypeError, true); + } + } + + // Rejection propagates for allKeyed. + { + let err = new Error("boom"); + try { + await Promise.allKeyed({ a: Promise.resolve(1), b: Promise.reject(err) }); + notReached(); + } catch (e) { + shouldBe(e, err); + } + } + + // Key order follows own-property-key order, not settlement order. + { + let resolveA, resolveB, resolveC; + let input = { + a: new Promise(r => { resolveA = r; }), + b: new Promise(r => { resolveB = r; }), + c: new Promise(r => { resolveC = r; }), + }; + let combined = Promise.allKeyed(input); + resolveC("C"); + resolveA("A"); + resolveB("B"); + let result = await combined; + shouldBeArray(Object.keys(result), ["a", "b", "c"]); + shouldBe(result.a, "A"); + shouldBe(result.b, "B"); + shouldBe(result.c, "C"); + } + + // Symbol keys are included; non-enumerable keys are skipped. + { + let sym = Symbol("s"); + let hidden = Symbol("hidden"); + let input = { str: Promise.resolve(1) }; + input[sym] = Promise.resolve(2); + Object.defineProperty(input, "nonenum", { enumerable: false, value: Promise.resolve(3) }); + Object.defineProperty(input, hidden, { enumerable: false, value: Promise.resolve(4) }); + let result = await Promise.allKeyed(input); + let keys = Reflect.ownKeys(result); + shouldBe(keys.length, 2); + shouldBe(keys[0], "str"); + shouldBe(keys[1], sym); + shouldBe(result.str, 1); + shouldBe(result[sym], 2); + shouldBe(Object.prototype.hasOwnProperty.call(result, "nonenum"), false); + shouldBe(Object.prototype.hasOwnProperty.call(result, hidden), false); + } + + // Inherited properties are ignored. + { + let proto = { inherited: Promise.resolve("nope") }; + let input = Object.create(proto); + input.own = Promise.resolve("yes"); + let result = await Promise.allKeyed(input); + shouldBeArray(Object.keys(result), ["own"]); + shouldBe(result.own, "yes"); + shouldBe(Object.prototype.hasOwnProperty.call(result, "inherited"), false); + } + + // allSettledKeyed: fulfilled/rejected entries. + { + let err = new Error("rej"); + let result = await Promise.allSettledKeyed({ + ok: Promise.resolve(1), + bad: Promise.reject(err), + plain: 2, + }); + shouldBe(Object.getPrototypeOf(result), null); + shouldBeArray(Object.keys(result), ["ok", "bad", "plain"]); + shouldBe(result.ok.status, "fulfilled"); + shouldBe(result.ok.value, 1); + shouldBe(result.bad.status, "rejected"); + shouldBe(result.bad.reason, err); + shouldBe(result.plain.status, "fulfilled"); + shouldBe(result.plain.value, 2); + } + + // allSettledKeyed: empty object. + { + let result = await Promise.allSettledKeyed({}); + shouldBe(Object.getPrototypeOf(result), null); + shouldBe(Reflect.ownKeys(result).length, 0); + } + + // allSettledKeyed: non-object rejects. + try { + await Promise.allSettledKeyed(null); + notReached(); + } catch (e) { + shouldBe(e instanceof TypeError, true); + } + + // Non-constructor this throws synchronously (from NewPromiseCapability). + { + let threw = false; + try { + Promise.allKeyed.call(eval); + } catch (e) { + threw = e instanceof TypeError; + } + shouldBe(threw, true); + } + + // C.resolve not callable rejects. + { + class Sub extends Promise {} + Sub.resolve = null; + try { + await Sub.allKeyed({ a: 1 }); + notReached(); + } catch (e) { + shouldBe(e instanceof TypeError, true); + } + } +} + +test().then( + () => {}, + e => { print(e.stack || e); $vm.abort(); } +); +drainMicrotasks(); diff --git a/JSTests/test262/config.yaml b/JSTests/test262/config.yaml index 0ee3b59c39e52..4020f1c03f48c 100644 --- a/JSTests/test262/config.yaml +++ b/JSTests/test262/config.yaml @@ -10,6 +10,7 @@ flags: iterator-sequencing: useIteratorSequencing explicit-resource-management: useExplicitResourceManagement import-defer: useImportDefer + await-dictionary: usePromiseAllKeyed skip: features: - callable-boundary-realms @@ -18,7 +19,6 @@ skip: - source-phase-imports - joint-iteration - Intl.Era-monthcode - - await-dictionary - import-bytes - immutable-arraybuffer - error-stack-accessor diff --git a/JSTests/test262/test/built-ins/Promise/allSettledKeyed/result-property-descriptors.js b/JSTests/test262/test/built-ins/Promise/allSettledKeyed/result-property-descriptors.js index d7c6fdf0457e7..fdff67ea5118c 100644 --- a/JSTests/test262/test/built-ins/Promise/allSettledKeyed/result-property-descriptors.js +++ b/JSTests/test262/test/built-ins/Promise/allSettledKeyed/result-property-descriptors.js @@ -39,38 +39,43 @@ asyncTest(function() { assert.sameValue(Object.getPrototypeOf(result.fulfilled), Object.prototype, "fulfilled entry prototype"); assert.sameValue(Object.getPrototypeOf(result.rejected), Object.prototype, "rejected entry prototype"); + // Capture the properties eagerly because `verifyProperty` will delete them + // as part of the `configurable` check. + var fulfilledEntry = result.fulfilled; + var rejectedEntry = result.rejected; + verifyProperty(result, "fulfilled", { - value: result.fulfilled, + value: fulfilledEntry, writable: true, enumerable: true, configurable: true }); verifyProperty(result, "rejected", { - value: result.rejected, + value: rejectedEntry, writable: true, enumerable: true, configurable: true }); - verifyProperty(result.fulfilled, "status", { + verifyProperty(fulfilledEntry, "status", { value: "fulfilled", writable: true, enumerable: true, configurable: true }); - verifyProperty(result.fulfilled, "value", { + verifyProperty(fulfilledEntry, "value", { value: 1, writable: true, enumerable: true, configurable: true }); - verifyProperty(result.rejected, "status", { + verifyProperty(rejectedEntry, "status", { value: "rejected", writable: true, enumerable: true, configurable: true }); - verifyProperty(result.rejected, "reason", { + verifyProperty(rejectedEntry, "reason", { value: error, writable: true, enumerable: true, diff --git a/Source/JavaScriptCore/builtins/BuiltinNames.h b/Source/JavaScriptCore/builtins/BuiltinNames.h index 758638919cc99..562c9f8deb070 100644 --- a/Source/JavaScriptCore/builtins/BuiltinNames.h +++ b/Source/JavaScriptCore/builtins/BuiltinNames.h @@ -214,6 +214,7 @@ namespace JSC { macro(getOwnPropertyDescriptor) \ macro(getOwnPropertyNames) \ macro(getOwnPropertySymbols) \ + macro(ownKeys) \ macro(hasOwn) \ macro(indexOf) \ macro(pop) \ diff --git a/Source/JavaScriptCore/builtins/PromiseConstructor.js b/Source/JavaScriptCore/builtins/PromiseConstructor.js index fcdf4d6784ad7..060b2e0ed08dd 100644 --- a/Source/JavaScriptCore/builtins/PromiseConstructor.js +++ b/Source/JavaScriptCore/builtins/PromiseConstructor.js @@ -45,6 +45,163 @@ function try(callback /*, ...args */) return promiseCapability.promise; } +// https://tc39.es/proposal-await-dictionary/#sec-promise.allkeyed +function allKeyed(promises) +{ + "use strict"; + + var promiseCapability = @newPromiseCapability(this); + var promiseResolve; + try { + promiseResolve = this.resolve; + if (!@isCallable(promiseResolve)) + @throwTypeError("Promise resolve is not a function"); + if (!@isObject(promises)) + @throwTypeError("Promise.allKeyed requires that its argument is an object"); + } catch (error) { + promiseCapability.reject.@call(@undefined, error); + return promiseCapability.promise; + } + + var keys = []; + var values = []; + var remainingElementsCount = 1; + var index = 0; + + function newResolveElement(currentIndex) + { + var alreadyCalled = false; + return (argument) => { + if (alreadyCalled) + return @undefined; + alreadyCalled = true; + @putByValDirect(values, currentIndex, argument); + if (!--remainingElementsCount) { + var result = @Object.@create(null); + for (var i = 0; i < keys.length; ++i) + @putByValDirect(result, keys[i], values[i]); + promiseCapability.resolve.@call(@undefined, result); + } + return @undefined; + }; + } + + try { + var allKeys = @Object.@ownKeys(promises); + for (var k = 0, length = allKeys.length; k < length; ++k) { + var key = allKeys[k]; + var desc = @Object.@getOwnPropertyDescriptor(promises, key); + if (desc === @undefined || !desc.enumerable) + continue; + var value = promises[key]; + @putByValDirect(keys, index, key); + @putByValDirect(values, index, @undefined); + var nextPromise = promiseResolve.@call(this, value); + var resolveElement = newResolveElement(index); + ++remainingElementsCount; + nextPromise.then(resolveElement, promiseCapability.reject); + ++index; + } + if (!--remainingElementsCount) { + var result = @Object.@create(null); + for (var i = 0; i < keys.length; ++i) + @putByValDirect(result, keys[i], values[i]); + promiseCapability.resolve.@call(@undefined, result); + } + } catch (error) { + promiseCapability.reject.@call(@undefined, error); + } + + return promiseCapability.promise; +} + +// https://tc39.es/proposal-await-dictionary/#sec-promise.allsettledkeyed +function allSettledKeyed(promises) +{ + "use strict"; + + var promiseCapability = @newPromiseCapability(this); + var promiseResolve; + try { + promiseResolve = this.resolve; + if (!@isCallable(promiseResolve)) + @throwTypeError("Promise resolve is not a function"); + if (!@isObject(promises)) + @throwTypeError("Promise.allSettledKeyed requires that its argument is an object"); + } catch (error) { + promiseCapability.reject.@call(@undefined, error); + return promiseCapability.promise; + } + + var keys = []; + var values = []; + var remainingElementsCount = 1; + var index = 0; + + function newResolveRejectElements(currentIndex) + { + var alreadyCalled = false; + return [ + (0, (argument) => { + if (alreadyCalled) + return @undefined; + alreadyCalled = true; + var entry = { status: "fulfilled", value: argument }; + @putByValDirect(values, currentIndex, entry); + if (!--remainingElementsCount) { + var result = @Object.@create(null); + for (var i = 0; i < keys.length; ++i) + @putByValDirect(result, keys[i], values[i]); + promiseCapability.resolve.@call(@undefined, result); + } + return @undefined; + }), + (0, (argument) => { + if (alreadyCalled) + return @undefined; + alreadyCalled = true; + var entry = { status: "rejected", reason: argument }; + @putByValDirect(values, currentIndex, entry); + if (!--remainingElementsCount) { + var result = @Object.@create(null); + for (var i = 0; i < keys.length; ++i) + @putByValDirect(result, keys[i], values[i]); + promiseCapability.resolve.@call(@undefined, result); + } + return @undefined; + }), + ]; + } + + try { + var allKeys = @Object.@ownKeys(promises); + for (var k = 0, length = allKeys.length; k < length; ++k) { + var key = allKeys[k]; + var desc = @Object.@getOwnPropertyDescriptor(promises, key); + if (desc === @undefined || !desc.enumerable) + continue; + var value = promises[key]; + @putByValDirect(keys, index, key); + @putByValDirect(values, index, @undefined); + var nextPromise = promiseResolve.@call(this, value); + var elements = newResolveRejectElements(index); + ++remainingElementsCount; + nextPromise.then(elements[0], elements[1]); + ++index; + } + if (!--remainingElementsCount) { + var result = @Object.@create(null); + for (var i = 0; i < keys.length; ++i) + @putByValDirect(result, keys[i], values[i]); + promiseCapability.resolve.@call(@undefined, result); + } + } catch (error) { + promiseCapability.reject.@call(@undefined, error); + } + + return promiseCapability.promise; +} + @nakedConstructor function Promise(executor) { diff --git a/Source/JavaScriptCore/runtime/JSPromiseConstructor.cpp b/Source/JavaScriptCore/runtime/JSPromiseConstructor.cpp index 6d818dcb2c218..4235282539beb 100644 --- a/Source/JavaScriptCore/runtime/JSPromiseConstructor.cpp +++ b/Source/JavaScriptCore/runtime/JSPromiseConstructor.cpp @@ -111,6 +111,12 @@ void JSPromiseConstructor::finishCreation(VM& vm, JSPromisePrototype* promisePro if (Options::usePromiseIsPromise()) JSC_NATIVE_FUNCTION_WITHOUT_TRANSITION("isPromise"_s, promiseConstructorFuncIsPromise, static_cast(PropertyAttribute::DontEnum), 1, ImplementationVisibility::Public); + + if (Options::usePromiseAllKeyed()) { + // https://tc39.es/proposal-await-dictionary/ + JSC_BUILTIN_FUNCTION_WITHOUT_TRANSITION("allKeyed"_s, promiseConstructorAllKeyedCodeGenerator, static_cast(PropertyAttribute::DontEnum)); + JSC_BUILTIN_FUNCTION_WITHOUT_TRANSITION("allSettledKeyed"_s, promiseConstructorAllSettledKeyedCodeGenerator, static_cast(PropertyAttribute::DontEnum)); + } } #if USE(BUN_JSC_ADDITIONS) diff --git a/Source/JavaScriptCore/runtime/ObjectConstructor.cpp b/Source/JavaScriptCore/runtime/ObjectConstructor.cpp index 676609f45f725..2f5bb06f3caad 100644 --- a/Source/JavaScriptCore/runtime/ObjectConstructor.cpp +++ b/Source/JavaScriptCore/runtime/ObjectConstructor.cpp @@ -49,6 +49,7 @@ static JSC_DECLARE_HOST_FUNCTION(objectConstructorIsSealed); static JSC_DECLARE_HOST_FUNCTION(objectConstructorIsFrozen); static JSC_DECLARE_HOST_FUNCTION(objectConstructorIsExtensible); static JSC_DECLARE_HOST_FUNCTION(objectConstructorHasOwn); +static JSC_DECLARE_HOST_FUNCTION(objectConstructorOwnKeys); } @@ -105,6 +106,7 @@ void ObjectConstructor::finishCreation(VM& vm, JSGlobalObject* globalObject, Obj JSC_NATIVE_FUNCTION_WITHOUT_TRANSITION(vm.propertyNames->builtinNames().getOwnPropertyDescriptorPrivateName(), objectConstructorGetOwnPropertyDescriptor, static_cast(PropertyAttribute::DontEnum), 2, ImplementationVisibility::Public); JSC_NATIVE_FUNCTION_WITHOUT_TRANSITION(vm.propertyNames->builtinNames().getOwnPropertyNamesPrivateName(), objectConstructorGetOwnPropertyNames, static_cast(PropertyAttribute::DontEnum), 1, ImplementationVisibility::Public); JSC_NATIVE_FUNCTION_WITHOUT_TRANSITION(vm.propertyNames->builtinNames().getOwnPropertySymbolsPrivateName(), objectConstructorGetOwnPropertySymbols, static_cast(PropertyAttribute::DontEnum), 1, ImplementationVisibility::Public); + JSC_NATIVE_FUNCTION_WITHOUT_TRANSITION(vm.propertyNames->builtinNames().ownKeysPrivateName(), objectConstructorOwnKeys, static_cast(PropertyAttribute::DontEnum), 1, ImplementationVisibility::Public); JSC_NATIVE_FUNCTION_WITHOUT_TRANSITION(vm.propertyNames->builtinNames().keysPrivateName(), objectConstructorKeys, static_cast(PropertyAttribute::DontEnum), 1, ImplementationVisibility::Public); JSC_NATIVE_FUNCTION_WITHOUT_TRANSITION(vm.propertyNames->builtinNames().definePropertyPrivateName(), objectConstructorDefineProperty, static_cast(PropertyAttribute::DontEnum), 3, ImplementationVisibility::Public); JSC_NATIVE_FUNCTION_WITHOUT_TRANSITION(vm.propertyNames->builtinNames().createPrivateName(), objectConstructorCreate, static_cast(PropertyAttribute::DontEnum), 2, ImplementationVisibility::Public); @@ -265,6 +267,15 @@ JSC_DEFINE_HOST_FUNCTION(objectConstructorGetOwnPropertySymbols, (JSGlobalObject RELEASE_AND_RETURN(scope, JSValue::encode(ownPropertyKeys(globalObject, object, PropertyNameMode::Symbols, DontEnumPropertiesMode::Include))); } +JSC_DEFINE_HOST_FUNCTION(objectConstructorOwnKeys, (JSGlobalObject* globalObject, CallFrame* callFrame)) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + JSObject* object = callFrame->argument(0).toObject(globalObject); + RETURN_IF_EXCEPTION(scope, encodedJSValue()); + RELEASE_AND_RETURN(scope, JSValue::encode(ownPropertyKeys(globalObject, object, PropertyNameMode::StringsAndSymbols, DontEnumPropertiesMode::Include))); +} + JSC_DEFINE_HOST_FUNCTION(objectConstructorKeys, (JSGlobalObject* globalObject, CallFrame* callFrame)) { VM& vm = globalObject->vm(); diff --git a/Source/WTF/Scripts/Preferences/UnifiedWebPreferences.yaml b/Source/WTF/Scripts/Preferences/UnifiedWebPreferences.yaml index 54df5b0f66c1a..ef94dbaf155f5 100644 --- a/Source/WTF/Scripts/Preferences/UnifiedWebPreferences.yaml +++ b/Source/WTF/Scripts/Preferences/UnifiedWebPreferences.yaml @@ -6897,6 +6897,19 @@ ProcessSwapOnCrossSiteNavigationEnabled: "PLATFORM(PLAYSTATION)": false default: true +PromiseAllKeyedEnabled: + type: bool + status: testable + category: javascript + humanReadableName: "Promise.allKeyed and Promise.allSettledKeyed" + humanReadableDescription: "Expose the Promise.allKeyed and Promise.allSettledKeyed methods." + webcoreBinding: none + jscOptionName: usePromiseAllKeyed + sharedPreferenceForWebProcess: true + defaultValue: + WebKit: + default: false + PromiseIsPromiseEnabled: type: bool status: testable From 0bea00d046fdfda7ccbe8a91c8dcc109a4f9f99e Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Mon, 27 Jul 2026 12:53:30 +0000 Subject: [PATCH 2/2] JSTests: pin Proxy trap-call counts for Promise.allKeyed / allSettledKeyed Asserts that [[OwnPropertyKeys]] is called exactly once on a Proxy input and that per-key [[GetOwnProperty]]/[[Get]] fire in the spec order (all own keys for gOPD, only enumerable keys for Get). A refactor to @getOwnPropertyNames + @getOwnPropertySymbols would fail the ownKeysCalls === 1 check. Mirrors JSTests/stress/object-spread.js. --- JSTests/stress/promise-allKeyed.js | 36 ++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/JSTests/stress/promise-allKeyed.js b/JSTests/stress/promise-allKeyed.js index 038d1a349e5c5..297a260d3e646 100644 --- a/JSTests/stress/promise-allKeyed.js +++ b/JSTests/stress/promise-allKeyed.js @@ -170,6 +170,42 @@ async function test() { shouldBe(e instanceof TypeError, true); } } + + // Spec step ordering: PerformPromiseAllKeyed does a single [[OwnPropertyKeys]] + // call, then per-key [[GetOwnProperty]] and [[Get]] only for enumerable keys. + for (let fn of [Promise.allKeyed, Promise.allSettledKeyed]) { + let s0 = Symbol("s0"); + let s1 = Symbol("s1"); + + let ownKeysCalls = 0; + let gopdCalls = []; + let getCalls = []; + + let source = new Proxy({ a: 0, b: 1, c: 2, [s0]: 3, [s1]: 4 }, { + ownKeys: t => { + ++ownKeysCalls; + return Reflect.ownKeys(t); + }, + getOwnPropertyDescriptor: (t, key) => { + gopdCalls.push(key); + let desc = Reflect.getOwnPropertyDescriptor(t, key); + if (key === "b" || key === s0) + desc.enumerable = false; + return desc; + }, + get: (t, key, receiver) => { + getCalls.push(key); + return Reflect.get(t, key, receiver); + }, + }); + + let result = await fn.call(Promise, source); + + shouldBe(ownKeysCalls, 1); + shouldBeArray(gopdCalls, ["a", "b", "c", s0, s1]); + shouldBeArray(getCalls, ["a", "c", s1]); + shouldBeArray(Reflect.ownKeys(result), ["a", "c", s1]); + } } test().then(