Skip to content
File

Blob: src/workerd/jsg/jsg-test.c++

20.9 KB
1// Copyright (c) 2017-2022 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4 
5#include "jsg-test.h"
6 
7namespace workerd::jsg::test {
8namespace {
9 
10// Non-JSG types aren't GC-visitable.
11static_assert(!isGcVisitable<int>());
12static_assert(!isGcVisitable<kj::String>());
13 
14// Various reference types are.
15static_assert(isGcVisitable<Ref<Object>>());
16static_assert(isGcVisitable<kj::Maybe<Ref<Object>>>());
17static_assert(isGcVisitable<Data>());
18static_assert(isGcVisitable<V8Ref<v8::Object>>());
19 
20// Resource types are not directly visitable. Their visitForGc() is private. You should be visiting
21// a Ref<T> pointing at them instead.
22static_assert(!isGcVisitable<Object>());
23static_assert(!isGcVisitable<NumberBox>());
24static_assert(!isGcVisitable<BoxBox>());
25 
26// Any type that defines a public visitForGc() is visitable.
27static_assert(isGcVisitable<TestStruct>());
28static_assert(isGcVisitable<kj::Maybe<TestStruct>>());
29 
30// jsg::Lock is not acceptable as a coroutine param
31static_assert(kj::_::isDisallowedInCoroutine<Lock>());
32static_assert(kj::_::isDisallowedInCoroutine<Lock&>());
33static_assert(kj::_::isDisallowedInCoroutine<Lock*>());
34 
35// ========================================================================================
36 
37V8System v8System;
38class ContextGlobalObject: public Object, public ContextGlobal {};
39 
40struct TestContext: public ContextGlobalObject {
41 JSG_RESOURCE_TYPE(TestContext) {}
42};
43JSG_DECLARE_ISOLATE_TYPE(TestIsolate, TestContext);
44 
45KJ_TEST("hello world") {
46 Evaluator<TestContext, TestIsolate> e(v8System);
47 e.expectEval("'Hello' + ', World!'", "string", "Hello, World!");
48}
49 
50KJ_TEST("throw") {
51 Evaluator<TestContext, TestIsolate> e(v8System);
52 e.expectEval("throw new Error('some error message')", "throws", "Error: some error message");
53}
54 
55KJ_TEST("context type is exposed in the global scope") {
56 Evaluator<TestContext, TestIsolate> e(v8System);
57 e.expectEval("this instanceof TestContext", "boolean", "true");
58}
59 
60// ========================================================================================
61 
62struct InheritContext: public ContextGlobalObject {
63 struct Other: public Object {
64 static jsg::Ref<Other> constructor(jsg::Lock& js) {
65 return js.alloc<Other>();
66 }
67 JSG_RESOURCE_TYPE(Other) {}
68 };
69 
70 Ref<NumberBox> newExtendedAsBase(jsg::Lock& js, double value, kj::String text) {
71 return ExtendedNumberBox::constructor(js, value, kj::mv(text));
72 }
73 
74 JSG_RESOURCE_TYPE(InheritContext) {
75 JSG_NESTED_TYPE(NumberBox);
76 JSG_NESTED_TYPE(Other);
77 JSG_NESTED_TYPE(ExtendedNumberBox);
78 
79 JSG_METHOD(newExtendedAsBase);
80 }
81};
82JSG_DECLARE_ISOLATE_TYPE(
83 InheritIsolate, InheritContext, NumberBox, InheritContext::Other, ExtendedNumberBox);
84 
85KJ_TEST("inheritance") {
86 Evaluator<InheritContext, InheritIsolate> e(v8System);
87 e.expectEval("var n = new ExtendedNumberBox(123, 'foo');\n"
88 "n.increment();\n"
89 "n.getValue()",
90 "number", "124");
91 
92 e.expectEval("var n = new ExtendedNumberBox(123, 'foo');\n"
93 "n.increment();\n"
94 "n.value",
95 "number", "124");
96 
97 e.expectEval("new ExtendedNumberBox(123, 'foo').getText()", "string", "foo");
98 
99 e.expectEval("var n = new ExtendedNumberBox(123, 'foo');\n"
100 "n.setText('bar');\n"
101 "n.text",
102 "string", "bar");
103 
104 e.expectEval("var n = new ExtendedNumberBox(123, 'foo');\n"
105 "n.text = 'bar';\n"
106 "n.getText()",
107 "string", "bar");
108 
109 e.expectEval("new ExtendedNumberBox(123, 'foo') instanceof NumberBox", "boolean", "true");
110 
111 e.expectEval("new ExtendedNumberBox(123, 'foo') instanceof ExtendedNumberBox", "boolean", "true");
112 
113 e.expectEval("new ExtendedNumberBox(123, 'foo') instanceof Other", "boolean", "false");
114 
115 e.expectEval("newExtendedAsBase(123, 'foo') instanceof NumberBox", "boolean", "true");
116 
117 e.expectEval("newExtendedAsBase(123, 'foo') instanceof ExtendedNumberBox", "boolean", "true");
118}
119 
120// ========================================================================================
121 
122struct Utf8Context: public ContextGlobalObject {
123 bool callWithBmpUnicode(Lock& js, jsg::Function<bool(kj::StringPtr)> function) {
124 return function(js, "中国网络");
125 }
126 bool callWithEmojiUnicode(Lock& js, jsg::Function<bool(kj::StringPtr)> function) {
127 return function(js, "😺☁️☄️🐵");
128 }
129 JSG_RESOURCE_TYPE(Utf8Context) {
130 JSG_METHOD(callWithBmpUnicode);
131 JSG_METHOD(callWithEmojiUnicode);
132 }
133};
134JSG_DECLARE_ISOLATE_TYPE(Utf8Isolate, Utf8Context);
135 
136KJ_TEST("utf-8 scripts") {
137 Evaluator<Utf8Context, Utf8Isolate> e(v8System);
138 
139 // BMP unicode.
140 e.expectEval("'中国网络'", "string", "中国网络");
141 
142 // Emoji unicode (including non-BMP characters).
143 e.expectEval("'😺☁️☄️🐵'", "string", "😺☁️☄️🐵");
144 
145 // Go the other way.
146 e.expectEval("callWithBmpUnicode(str => str == '中国网络')", "boolean", "true");
147 e.expectEval("callWithEmojiUnicode(str => str == '😺☁️☄️🐵')", "boolean", "true");
148}
149 
150// ========================================================================================
151 
152struct RefContext: public ContextGlobalObject {
153 Ref<NumberBox> addAndReturnCopy(jsg::Lock& js, NumberBox& box, double value) {
154 auto copy = js.alloc<NumberBox>(box.value);
155 copy->value += value;
156 return copy;
157 }
158 Ref<NumberBox> addAndReturnOwn(Ref<NumberBox> box, double value) {
159 box->value += value;
160 return box;
161 }
162 
163 JSG_RESOURCE_TYPE(RefContext) {
164 JSG_NESTED_TYPE(NumberBox);
165 JSG_METHOD(addAndReturnCopy);
166 JSG_METHOD(addAndReturnOwn);
167 }
168};
169JSG_DECLARE_ISOLATE_TYPE(RefIsolate, RefContext, NumberBox);
170 
171KJ_TEST("Ref") {
172 Evaluator<RefContext, RefIsolate> e(v8System);
173 // addAndReturnCopy() creates a new object and returns it.
174 e.expectEval("var orig = new NumberBox(123);\n"
175 "var result = addAndReturnCopy(orig, 321);\n"
176 "[orig.value, result.value, orig == result].join(', ')",
177 "string", "123, 444, false");
178 
179 // addAndReturnOwn() modifies the original object and returns it by identity.
180 e.expectEval("var orig = new NumberBox(123);\n"
181 "var result = addAndReturnOwn(orig, 321);\n"
182 "[orig.value, result.value, orig == result].join(', ')",
183 "string", "444, 444, true");
184}
185 
186// ========================================================================================
187 
188struct ProtoContext: public ContextGlobalObject {
189 ProtoContext(): contextProperty(kj::str("default-context-property-value")) {}
190 
191 kj::StringPtr getContextProperty() {
192 return contextProperty;
193 }
194 void setContextProperty(kj::String s) {
195 contextProperty = kj::mv(s);
196 }
197 
198 JSG_RESOURCE_TYPE(ProtoContext) {
199 JSG_NESTED_TYPE(NumberBox);
200 JSG_NESTED_TYPE(BoxBox);
201 JSG_NESTED_TYPE(ExtendedNumberBox);
202 JSG_METHOD(getContextProperty);
203 JSG_METHOD(setContextProperty);
204 JSG_INSTANCE_PROPERTY(contextProperty, getContextProperty, setContextProperty);
205 }
206 
207 private:
208 kj::String contextProperty;
209};
210JSG_DECLARE_ISOLATE_TYPE(ProtoIsolate, ProtoContext, NumberBox, BoxBox, ExtendedNumberBox);
211 
212const auto kIllegalInvocation =
213 "TypeError: Illegal invocation: function called with incorrect `this` reference. "
214 "See https://developers.cloudflare.com/workers/observability/errors/#illegal-invocation-errors for details."_kj;
215 
216KJ_TEST("can't invoke builtin methods with alternative 'this'") {
217 Evaluator<ProtoContext, ProtoIsolate> e(v8System);
218 e.expectEval("NumberBox.prototype.getValue.call(123)", "throws", kIllegalInvocation);
219 e.expectEval("NumberBox.prototype.getValue.call(new BoxBox(new NumberBox(123), 123))", "throws",
220 kIllegalInvocation);
221 e.expectEval("getContextProperty.call(new NumberBox(123))", "throws", kIllegalInvocation);
222}
223 
224KJ_TEST("can't use builtin as prototype") {
225 Evaluator<ProtoContext, ProtoIsolate> e(v8System);
226 e.expectEval("function JsType() {}\n"
227 "JsType.prototype = new NumberBox(123);\n"
228 "new JsType().getValue()",
229 "throws", kIllegalInvocation);
230 e.expectEval("function JsType() {}\n"
231 "JsType.prototype = new ExtendedNumberBox(123, 'foo');\n"
232 "new JsType().getValue()",
233 "throws", kIllegalInvocation);
234 e.expectEval("function JsType() {}\n"
235 "JsType.prototype = new NumberBox(123);\n"
236 "new JsType().value",
237 "number", "123");
238 e.expectEval("function JsType() {}\n"
239 "JsType.prototype = new NumberBox(123);\n"
240 "let t = new JsType();\n"
241 "Reflect.get(JsType.prototype, 'value', t)\n",
242 "number", "123");
243 e.expectEval("function JsType() {}\n"
244 "JsType.prototype = new ExtendedNumberBox(123, 'foo');\n"
245 "new JsType().value",
246 "number", "123");
247 e.expectEval("function JsType() {}\n"
248 "JsType.prototype = this;\n"
249 "new JsType().getContextProperty()",
250 "throws", kIllegalInvocation);
251 
252 // For historical reasons, we allow using the global object as a prototype and accessing
253 // properties through a derived object. Our accessor implementations for global object properties
254 // ignore `this` and go directly to the singleton context object, so it doesn't matter.
255 //
256 // (Once upon a time, V8 supported a thing called an "AccessorSignature" which would handle the
257 // type checking, but it didn't work correctly for the global object. V8 later removed
258 // AccessorSignature entirely, forcing us to implement manual type checking. We could totally
259 // make our manual type checking work correctly for global properties, but, again, it doesn't
260 // really matter, and I'd rather not inadvertently break someone.)
261 e.expectEval("function JsType() {}\n"
262 "JsType.prototype = this;\n"
263 "new JsType().contextProperty",
264 "string", "default-context-property-value");
265}
266 
267// ========================================================================================
268 
269struct IcuContext: public ContextGlobalObject {
270 JSG_RESOURCE_TYPE(IcuContext) {}
271};
272JSG_DECLARE_ISOLATE_TYPE(IcuIsolate, IcuContext);
273 
274KJ_TEST("ICU is properly initialized") {
275 Evaluator<IcuContext, IcuIsolate> e(v8System);
276 e.expectEval("function charCodes(str) {"
277 " let result = [];\n"
278 " for (let i = 0; i < str.length; i++) {\n"
279 " result.push(str.charCodeAt(i));\n"
280 " }\n"
281 " return result.join(',');\n"
282 "}"
283 "[ charCodes('\u1E9B\u0323'),\n"
284 " charCodes('\u1E9B\u0323'.normalize('NFC')),\n"
285 " charCodes('\u1E9B\u0323'.normalize('NFD')),\n"
286 " charCodes('\u1E9B\u0323'.normalize('NFKD')),\n"
287 " charCodes('\u1E9B\u0323'.normalize('NFKC')) ].join(' ')",
288 
289 "string", "7835,803 7835,803 383,803,775 115,803,775 7785");
290}
291 
292// ========================================================================================
293 
294KJ_TEST("Uncaught JsExceptionThrown reports stack") {
295 auto exception =
296 KJ_ASSERT_NONNULL(kj::runCatchingExceptions([&]() { throw JsExceptionThrown(); }));
297 KJ_ASSERT(
298 exception.getDescription().startsWith("std::exception: Uncaught JsExceptionThrown\nstack: "),
299 exception.getDescription());
300}
301 
302// TODO(test): Find some way to verify that C++ objects get garbage-collected as expected (hard to
303// test since GC technically does not guarantee that it will collect everything).
304 
305// ========================================================================================
306 
307struct LockLogContext: public ContextGlobalObject {
308 JSG_RESOURCE_TYPE(LockLogContext) {}
309};
310JSG_DECLARE_ISOLATE_TYPE(LockLogIsolate, LockLogContext);
311 
312KJ_TEST("jsg::Lock logWarning") {
313 LockLogIsolate isolate(v8System, kj::heap<IsolateObserver>());
314 bool called = false;
315 isolate.runInLockScope([&](LockLogIsolate::Lock& lock) {
316 lock.setLoggerCallback([&called](jsg::Lock& js, auto message) {
317 KJ_ASSERT(message == "Yes that happened"_kj);
318 called = true;
319 });
320 lock.logWarning("Yes that happened"_kj);
321 KJ_ASSERT(called);
322 });
323}
324 
325// ========================================================================================
326// JSG_CALLABLE Test
327struct CallableContext: public ContextGlobalObject {
328 struct MyCallable: public Object {
329 public:
330 static Ref<MyCallable> constructor(jsg::Lock& js) {
331 return js.alloc<MyCallable>();
332 }
333 
334 bool foo() {
335 return true;
336 }
337 
338 JSG_RESOURCE_TYPE(MyCallable) {
339 JSG_CALLABLE(foo);
340 JSG_METHOD(foo);
341 }
342 };
343 
344 Ref<MyCallable> getCallable(jsg::Lock& js) {
345 return js.alloc<MyCallable>();
346 }
347 
348 JSG_RESOURCE_TYPE(CallableContext) {
349 JSG_METHOD(getCallable);
350 JSG_NESTED_TYPE(MyCallable);
351 }
352};
353JSG_DECLARE_ISOLATE_TYPE(CallableIsolate, CallableContext, CallableContext::MyCallable);
354 
355KJ_TEST("Test JSG_CALLABLE") {
356 Evaluator<CallableContext, CallableIsolate> e(v8System);
357 
358 e.expectEval("let obj = getCallable(); obj.foo();", "boolean", "true");
359 e.expectEval("let obj = getCallable(); obj();", "boolean", "true");
360 
361 e.expectEval("let obj = new MyCallable(); obj();", "boolean", "true");
362 
363 // It's weird, but still accepted.
364 e.expectEval("let obj = getCallable(); new obj();", "boolean", "true");
365}
366 
367// ========================================================================================
368struct InterceptContext: public ContextGlobalObject {
369 struct ProxyImpl: public jsg::Object {
370 static jsg::Ref<ProxyImpl> constructor(jsg::Lock& js) {
371 return js.alloc<ProxyImpl>();
372 }
373 
374 int getBar() {
375 return 123;
376 }
377 
378 // JSG_WILDCARD_PROPERTY implementation
379 kj::Maybe<kj::StringPtr> testGetNamed(jsg::Lock& js, kj::String name) {
380 if (name == "foo") {
381 return "bar"_kj;
382 } else if (name == "abc") {
383 JSG_FAIL_REQUIRE(TypeError, "boom");
384 }
385 return kj::none;
386 }
387 
388 JSG_RESOURCE_TYPE(ProxyImpl) {
389 JSG_READONLY_PROTOTYPE_PROPERTY(bar, getBar);
390 JSG_WILDCARD_PROPERTY(testGetNamed);
391 }
392 };
393 
394 JSG_RESOURCE_TYPE(InterceptContext) {
395 JSG_NESTED_TYPE(ProxyImpl);
396 }
397};
398JSG_DECLARE_ISOLATE_TYPE(InterceptIsolate, InterceptContext, InterceptContext::ProxyImpl);
399 
400KJ_TEST("Named interceptor") {
401 Evaluator<InterceptContext, InterceptIsolate> e(v8System);
402 e.expectEval("p = new ProxyImpl; p.bar", "number", "123");
403 e.expectEval("p = new ProxyImpl; Reflect.has(p, 'foo')", "boolean", "true");
404 e.expectEval("p = new ProxyImpl; p.hasOwnProperty('foo')", "boolean", "false");
405 e.expectEval(
406 "p = new ProxyImpl; Object.getOwnPropertyDescriptor(p, 'foo')", "undefined", "undefined");
407 e.expectEval("p = new ProxyImpl; Reflect.has(p, 'bar')", "boolean", "true");
408 e.expectEval("p = new ProxyImpl; Reflect.has(p, 'baz')", "boolean", "false");
409 e.expectEval("p = new ProxyImpl; p.abc", "throws", "TypeError: boom");
410}
411 
412// ========================================================================================
413struct IsolateUuidContext: public ContextGlobalObject {
414 JSG_RESOURCE_TYPE(IsolateUuidContext) {}
415};
416JSG_DECLARE_ISOLATE_TYPE(IsolateUuidIsolate, IsolateUuidContext);
417 
418KJ_TEST("External memory adjustment") {
419 IsolateUuidIsolate isolate(v8System, kj::heap<IsolateObserver>());
420 isolate.runInLockScope([&](IsolateUuidIsolate::Lock& lock) {
421 // Creating an inner scope to check the case where the adjustment object does not outlive the isolate
422 {
423 // Creating with a specific amount works as expected
424 auto adjuster = lock.getExternalMemoryAdjustment(100);
425 KJ_ASSERT(adjuster.getAmount() == 100);
426 
427 // Adjusting up works as expected
428 adjuster.adjust(10);
429 KJ_ASSERT(adjuster.getAmount() == 110);
430 
431 // Adjusting down works as expected
432 adjuster.adjust(-10);
433 KJ_ASSERT(adjuster.getAmount() == 100);
434 
435 // Setting an explicit value just works
436 adjuster.set(50);
437 KJ_ASSERT(adjuster.getAmount() == 50);
438 
439 // Decrementing by more than the amount will throw an exception
440 try {
441 adjuster.adjust(-200);
442 } catch (...) {
443 auto exc = kj::getCaughtExceptionAsKj();
444 KJ_ASSERT(exc.getDescription() ==
445 "expected amount >= -static_cast<ssize_t>(this->amount) [-200 >= -50]; Memory usage may not be decreased below zero");
446 }
447 
448 KJ_ASSERT(adjuster.getAmount() == 50);
449 
450 adjuster.set(100);
451 auto adjuster2 = kj::mv(adjuster);
452 KJ_ASSERT(adjuster2.getAmount() == 100);
453 
454 // Checking that the amount is zero after the adjuster is moved away would be nice to have,
455 // but we should aim to avoid use-after-move entirely.
456 // KJ_ASSERT(adjuster.getAmount() == 0);
457 }
458 // Note that we are not testing the actual effect on the isolate itself here.
459 // While we have added a getExternalMemory() API to the isolate via a patch in
460 // the internal repo, we have not added that patch to workerd so testing the
461 // specific external memory reported by the isolate is possible but a bit
462 // more cumbersome here.
463 });
464}
465 
466KJ_TEST("External memory adjustment - defered") {
467 kj::Arc<const ExternalMemoryTarget> target;
468 
469 // A memory allocation that will outlive the isolate
470 kj::Array<kj::byte> mem;
471 
472 {
473 IsolateUuidIsolate isolate(v8System, kj::heap<IsolateObserver>());
474 
475 target = isolate.runInLockScope(
476 [&](IsolateUuidIsolate::Lock& lock) { return lock.getExternalMemoryTarget(); });
477 
478 // Adjustment to memory while not holding lock will be applied later
479 auto adjuster1 = target->getAdjustment(1000);
480 KJ_ASSERT(adjuster1.getAmount() == 1000);
481 KJ_ASSERT(target->getPendingMemoryUpdateForTest() == 1000);
482 
483 {
484 // This adjustment has no effect because the adjuster is destroyed before we take the lock again
485 auto adjuster2 = target->getAdjustment(1000);
486 KJ_ASSERT(adjuster2.getAmount() == 1000);
487 KJ_ASSERT(target->getPendingMemoryUpdateForTest() == 2000);
488 }
489 
490 KJ_ASSERT(target->getPendingMemoryUpdateForTest() == 1000);
491 KJ_ASSERT(target->isIsolateAliveForTest());
492 
493 isolate.runInLockScope([&](IsolateUuidIsolate::Lock& lock) {
494 // Once lock is taken, the amount is applied
495 KJ_ASSERT(target->getPendingMemoryUpdateForTest() == 0);
496 
497 // Adjustment made while holding lock applies immediately
498 adjuster1.adjust(-500);
499 KJ_ASSERT(adjuster1.getAmount() == 500);
500 KJ_ASSERT(target->getPendingMemoryUpdateForTest() == 0);
501 KJ_ASSERT(target->isIsolateAliveForTest());
502 });
503 
504 mem = isolate.runInLockScope([&](IsolateUuidIsolate::Lock& lock) {
505 return kj::heapArray<kj::byte>(100).attach(target->getAdjustment(100));
506 });
507 }
508 
509 KJ_ASSERT(!target->isIsolateAliveForTest());
510 
511 // Delete the long-lived array, which will call the adjustment's destructor, to make sure it's safe.
512 mem = nullptr;
513 
514 // Making an adjustment anyway won't do anything but also won't crash
515 auto adjuster3 = target->getAdjustment(500);
516 KJ_ASSERT(target->getPendingMemoryUpdateForTest() == 400);
517}
518 
519KJ_TEST("Memory Allocation Error Propagation") {
520 class MyAllocator final: public v8::ArrayBuffer::Allocator {
521 public:
522 void* Allocate(size_t length) override {
523 return nullptr;
524 }
525 void* AllocateUninitialized(size_t length) override {
526 return nullptr;
527 }
528 void Free(void* data, size_t length) override {}
529 size_t MaxAllocationSize() const override {
530 return 10;
531 }
532 };
533 
534 MyAllocator allocator;
535 v8::Isolate::CreateParams createParams;
536 createParams.constraints.ConfigureDefaults(10, 10);
537 createParams.array_buffer_allocator = &allocator;
538 IsolateUuidIsolate isolate(v8System, kj::heap<IsolateObserver>(), createParams);
539 isolate.runInLockScope([&](IsolateUuidIsolate::Lock& lock) {
540 KJ_EXPECT_THROW_MESSAGE(
541 "Failed to allocate ArrayBuffer backing store", lock.allocBackingStore(100 * 1024));
542 });
543}
544 
545struct MpkContext: public ContextGlobalObject {
546 JSG_RESOURCE_TYPE(MpkContext) {}
547};
548JSG_DECLARE_ISOLATE_TYPE(MpkIsolate, MpkContext);
549KJ_TEST("MemoryProtectionKeyScope") {
550 // In workerd, since V8_ENABLE_SANDBOX is not defined, this test is largely
551 // a non-op, however, when v8 is built with V8_ENABLE_SANDBOX enabled and
552 // the isolate has a memory protection key, this test will (eventually)
553 // verify that the array buffer allocation is writable within the scope.
554 // Essentially once backing stores are protected, and mpk's are enabled,
555 // this shouldn't crash.
556 MpkIsolate isolate(v8System, kj::heap<IsolateObserver>());
557 std::shared_ptr<v8::BackingStore> store;
558 auto mpkScope = isolate.runInLockScope([&](MpkIsolate::Lock& lock) {
559 store = v8::ArrayBuffer::NewBackingStore(lock.v8Isolate, 10);
560 return lock.getMemoryProtectionKeyScope();
561 });
562 bool called = false;
563 // Now that we have our backing store, try writing to it outside the
564 // isolate lock, but within the mpk scope. If mpk's are enabled writing
565 // to the backing store without the mpk scope should segfault, but within
566 // the mpk scope it should succeed.
567 kj::ArrayPtr<kj::byte> bytes(static_cast<kj::byte*>(store->Data()), store->ByteLength());
568 KJ_EXPECT(mpkScope.runWithKey([&] {
569 called = true;
570 bytes.fill(1);
571 return 1;
572 }) == 1);
573 KJ_EXPECT(called);
574}
575 
576} // namespace
577 
578} // namespace workerd::jsg::test