Repository navigation
napi: apply ToObject coercion to element/property-name/prototype APIs - #34133
Conversation
|
Warning Review limit reached
Next review available in: 34 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (3)
WalkthroughN-API object-taking functions now share exception-aware ChangesN-API object coercion
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
There was a problem hiding this comment.
I didn't find any bugs, but this is a behavioral change across seven N-API entry points (ToObject coercion, pending-exception semantics, enum validation, and the new napi_key_keep_numbers rewrite loop) in native C++/Rust — worth a human look.
What was reviewed:
NAPI_CHECK_TO_OBJECTmacro: exception left pending viaRETURN_IF_EXCEPTIONon the preamble throw scope, matching Node's contract.napi_delete_element: delete now runs unconditionally (was previously skipped whenresult == NULL); exception check added afterdeletePropertyByIndex.napi_key_keep_numbersloop:asString(key)->value()hasNAPI_RETURN_IF_EXCEPTION;parseIndexbounds match V8's array-index range.- Rust
napi_get_prototype:to_objectresult is discarded only for the throw side effect;get_prototypeon primitives synthesizes the wrapper prototype without allocation.
Extended reasoning...
Overview
This PR aligns seven N-API functions (napi_set_element, napi_has_element, napi_get_element, napi_delete_element, napi_get_property_names, napi_get_all_property_names, napi_get_prototype) with Node's CHECK_TO_OBJECT contract: primitives are coerced via ToObject and succeed, while null/undefined return napi_object_expected with a pending TypeError. It also adds enum validation to napi_get_all_property_names, implements napi_key_keep_numbers (previously ignored), and fixes napi_delete_element to actually perform the delete when result == NULL. Changes span src/jsc/bindings/napi.cpp (~70 lines), src/runtime/napi/napi_body.rs (~7 lines), plus a new C++ standalone test and a checkSameOutput test case.
Security risks
None identified. This is Node.js compatibility work on argument coercion and status codes; no auth, crypto, or untrusted-input parsing paths are involved. The new parseIndex loop operates on JSC-generated property-name strings, not user-controlled data in a security-relevant sense.
Level of scrutiny
Medium-high. N-API is the FFI surface used by every native addon; changing status codes and whether exceptions are left pending is exactly the kind of thing portable addons feature-detect against. The changes look correct and are validated byte-for-byte against Node via checkSameOutput, but this is not a mechanical or config-level change — it introduces a new macro, a new post-processing loop over property keys, and alters error-path semantics at six C++ call sites plus one Rust site.
Other factors
- The bug hunter found no issues; one style-only candidate (comment length) was ruled out.
- Test coverage is thorough: a dedicated C++ standalone test prints status/pending for every affected call and is diffed against Node, with additional spot-check assertions so
USE_SYSTEM_BUN=1fails. - The
napi_delete_elementchange is a real behavioral fix (delete was silently skipped whenresultwas null) bundled into this PR. - The PR description notes tests were deferred to CI ("no test proof · Platform-specific test(s) that do not run on this machine"), so CI green is the actual verification signal.
Given the scope (native code, seven public API entry points, new exception-handling macro, behavioral semantics change), this exceeds the bar for auto-approval and should get a human review even though no defects were found.
8e92bdf to
86acfff
Compare
| return env.invalid_arg(); | ||
| } | ||
| if !object.is_object() { | ||
| // Node's CHECK_TO_OBJECT: ToObject throws on null/undefined; leave the |
There was a problem hiding this comment.
is this correct? this looks wrong?
There was a problem hiding this comment.
Yes, this is correct. The previous !object.is_object() gate rejected every primitive with napi_object_expected and no pending exception, which is the bug: Node coerces primitives via ToObject and only fails on null/undefined.
object.get_prototype(...) calls JSC__JSValue__getPrototype which is JSValue::getPrototype (JSObjectInlines.h:83):
ALWAYS_INLINE JSValue JSValue::getPrototype(JSGlobalObject* globalObject) const
{
if (isObject())
return asObject(asCell())->getPrototype(globalObject);
return synthesizePrototype(globalObject);
}synthesizePrototype returns numberPrototype() for numbers, stringPrototype() for strings, etc., without allocating a wrapper object, and only throws on null/undefined. So letting primitives fall through to get_prototype produces exactly what Node's CHECK_TO_OBJECT + GetPrototype() does, without the intermediate Number/String wrapper allocation.
The is_undefined_or_null() branch handles the remaining case: to_object() throws the TypeError (left pending, matching Node) and we return napi_object_expected.
The new test confirms byte-for-byte parity with Node:
get_prototype(number): status=0 pending=0
get_prototype(number) is Number.prototype=1
get_prototype(string): status=0 pending=0
get_prototype(bool): status=0 pending=0
get_prototype(null): status=2 pending=1
get_prototype(undefined): status=2 pending=1
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/runtime/napi/napi_body.rs`:
- Around line 876-882: Update the prototype lookup flow around the visible
null/undefined handling and get_prototype call to first check whether env has a
stashed pending exception, returning napi_pending_exception immediately when
present. Perform this guard before to_object or get_prototype so prior
napi_throw* errors are preserved and cannot reach proxy traps or be replaced
with napi_object_expected.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 7283eafb-16c3-453f-aa4f-ff7923a73c7d
📒 Files selected for processing (4)
src/jsc/bindings/napi.cppsrc/runtime/napi/napi_body.rstest/napi/napi-app/standalone_tests.cpptest/napi/napi.test.ts
napi_set_element, napi_has_element, napi_get_element, napi_delete_element,
napi_get_property_names, napi_get_all_property_names, and napi_get_prototype
all checked getObject()/is_object() on their target and rejected primitives
with napi_array_expected or napi_object_expected (no pending exception).
Node's CHECK_TO_OBJECT instead runs ToObject: strings/numbers/booleans are
coerced (so napi_get_element("abc", 1) succeeds with "b") and only
null/undefined fail with napi_object_expected plus a pending TypeError.
napi_get_all_property_names additionally accepted any integer for its
key_mode/key_conversion enum arguments and ignored key_conversion entirely,
so napi_key_keep_numbers still returned index keys as strings.
Add a NAPI_CHECK_TO_OBJECT macro mirroring Node's semantics and apply it at
every affected site; validate the two enum arguments; convert canonical
array-index string keys back to numbers when key_conversion is
napi_key_keep_numbers. napi_delete_element now also performs the delete
when result is null, matching Node.
napi_set_property, napi_get_property, napi_has_property, napi_delete_property, napi_has_own_property, and the three napi_*_named_property variants already coerced primitives via toObject but returned napi_pending_exception (10) on null/undefined instead of Node's napi_object_expected (2). Node routes all of these through the same CHECK_TO_OBJECT macro; switch them to the new helper so the whole class matches.
napi_throw stashes the exception on NapiEnv::m_pendingException, not on the VM, and bun's NAPI_PREAMBLE only checks the VM throw scope. The functions that previously called toObject() followed it with NAPI_RETURN_IF_EXCEPTION, which checks both; the new macro only checked the scope, so napi_get_named_property/napi_set_named_property kept running (and invoked the accessor) when an env-level exception was already pending. Restore the env-level check inside the macro so callers bail before any side effect, matching Node's NAPI_PREAMBLE ordering.
376dd51 to
7721611
Compare
There was a problem hiding this comment.
Additional findings (outside current diff — PR may have been updated during review):
-
🟡
src/jsc/bindings/napi.cpp:1830-1839— The new enum checks forkey_mode/key_conversionrun beforeNAPI_CHECK_TO_OBJECT, but Node'snapi_get_all_property_namesrunsCHECK_TO_OBJECTfirst and validates the enums after. Sonapi_get_all_property_names(env, jsnull, (napi_key_collection_mode)99, ...)returnsstatus=1 pending=0in Bun vsstatus=2 pending=1in Node. Trivial reorder — move the twoNAPI_RETURN_EARLY_IF_FALSEchecks belowNAPI_CHECK_TO_OBJECT— and the test doesn't exercise this combination socheckSameOutputcurrently misses it.Extended reasoning...
What the issue is
Node's
napi_get_all_property_names(js_native_api_v8.cc, ~lines 1085-1136) validates arguments in this order:NAPI_PREAMBLE(env); CHECK_ARG(env, result); CHECK_TO_OBJECT(env, context, obj, object); // ← ToObject FIRST // ... filter setup ... switch (key_mode) { ... default: return napi_set_last_error(env, napi_invalid_arg); } switch (key_conversion) { ... default: return napi_set_last_error(env, napi_invalid_arg); }
This PR places the two new
NAPI_RETURN_EARLY_IF_FALSEenum checks at lines 1830-1835, beforeNAPI_CHECK_TO_OBJECTat line 1839. So the enum validation runs first and short-circuits beforeToObjectever sees the target.Code path that triggers it
napi_value r; napi_status s = napi_get_all_property_names( env, v_null, (napi_key_collection_mode)99, // out-of-range napi_key_all_properties, napi_key_numbers_to_strings, &r);
- Node:
CHECK_TO_OBJECTruns first →ToObject(null)throws → returnsnapi_object_expected(2) with a pendingTypeError. - Bun (this PR):
NAPI_RETURN_EARLY_IF_FALSE(env, key_mode == ... || ..., napi_invalid_arg)fires first → returnsnapi_invalid_arg(1) with no pending exception.
Why the test doesn't catch it
test_napi_object_coercionexercises the two failure modes independently:get_all_property_names(key_mode=99)uses a validobjget_all_property_names(null)uses valid enum arguments
The (null object + bad enum) combination is never sent through
checkSameOutput, so byte-for-byte parity is never asserted for it.Impact
Minimal — the trigger requires an addon to pass both
null/undefinedand an out-of-range C enum constant simultaneously. Enum arguments are compile-time constants in real addons, so this is a double-programmer-error scenario with essentially zero real-world likelihood. It's flagged only because it's exactly the class of status-code-ordering divergence this PR set out to eliminate, and the fix is a two-line reorder.Fix
Move the two enum checks after
NAPI_CHECK_TO_OBJECT, matching Node's order:NAPI_PREAMBLE(env); NAPI_CHECK_ARG(env, result); NAPI_CHECK_ARG(env, objectNapi); auto globalObject = toJS(env); auto objectValue = toJS(objectNapi); NAPI_CHECK_TO_OBJECT(env, globalObject, object, objectValue); NAPI_RETURN_EARLY_IF_FALSE(env, key_mode == napi_key_include_prototypes || key_mode == napi_key_own_only, napi_invalid_arg); NAPI_RETURN_EARLY_IF_FALSE(env, key_conversion == napi_key_keep_numbers || key_conversion == napi_key_numbers_to_strings, napi_invalid_arg);
Optionally add one line to the fixture so
checkSameOutputlocks it in:report_status(env, "get_all_property_names(null,key_mode=99)", napi_get_all_property_names(env, v_null, static_cast<napi_key_collection_mode>(99), napi_key_all_properties, napi_key_numbers_to_strings, &out));
Step-by-step proof
- Call
napi_get_all_property_names(env, v_null, (napi_key_collection_mode)99, napi_key_all_properties, napi_key_numbers_to_strings, &out). NAPI_PREAMBLEpasses (no pending exception).NAPI_CHECK_ARG(env, result)passes (&outnon-null).NAPI_CHECK_ARG(env, objectNapi)passes (v_nullis a non-nullnapi_valuehandle wrapping JSnull).- Line 1830:
key_mode == napi_key_include_prototypes || key_mode == napi_key_own_only→99 == 0 || 99 == 1→false→return napi_set_last_error(env, napi_invalid_arg). Status = 1, pending = 0. - In Node, step 3 doesn't exist yet;
CHECK_TO_OBJECTruns onv_null→ V8'sToObjectthrowsTypeError: Cannot convert undefined or null to object→RETURN_STATUS_IF_FALSE(..., napi_object_expected). Status = 2, pending = 1. - Observable divergence:
{status: 1, pending: 0}vs{status: 2, pending: 1}.
- Node:
…type napi_get_all_property_names: Node runs CHECK_TO_OBJECT before validating key_mode/key_conversion, so a null target with an out-of-range enum returns napi_object_expected with a pending TypeError, not napi_invalid_arg. Reorder the checks and cover the combination in the test. napi_get_prototype: add a NapiEnv::has_pending_exception() FFI shim and bail with napi_pending_exception before to_object(null/undefined), mirroring the guard in NAPI_CHECK_TO_OBJECT so a prior napi_throw value is not shadowed by the fresh VM TypeError.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/jsc/bindings/napi.cpp (1)
2477-2492: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winAdd a
napi_delete_element(..., nullptr)test case
The existing coverage only callsnapi_delete_elementwith a non-nullbool*, so the regression where deletion was skipped whenresultwasNULLstill isn’t exercised.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/jsc/bindings/napi.cpp` around lines 2477 - 2492, Add a test for napi_delete_element that passes nullptr as the result argument, then verify the targeted element is actually deleted and the call succeeds. Reuse the existing napi_delete_element test setup and assertions, changing only the result-pointer scenario needed to cover the null-output path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@test/napi/napi.test.ts`:
- Around line 552-585: Update the subprocess assertions in the
“napi_fatal_exception triggers uncaughtException for non-Error values” test so
stderr is checked only when exitCode is nonzero, matching the file’s existing
subprocess patterns; preserve the current stdout payload and successful
exit-code expectations.
---
Outside diff comments:
In `@src/jsc/bindings/napi.cpp`:
- Around line 2477-2492: Add a test for napi_delete_element that passes nullptr
as the result argument, then verify the targeted element is actually deleted and
the call succeeds. Reuse the existing napi_delete_element test setup and
assertions, changing only the result-pointer scenario needed to cover the
null-output path.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: bcde39e2-927c-4d10-9a37-f9d80c0d74f2
📒 Files selected for processing (6)
src/jsc/bindings/napi.cppsrc/runtime/napi/napi_body.rstest/napi/napi-app/js_test_helpers.cpptest/napi/napi-app/module.jstest/napi/napi-app/standalone_tests.cpptest/napi/napi.test.ts
There was a problem hiding this comment.
Caution
Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/jsc/bindings/napi.cpp (1)
2477-2492: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winAdd a
napi_delete_element(..., nullptr)test case
The existing coverage only callsnapi_delete_elementwith a non-nullbool*, so the regression where deletion was skipped whenresultwasNULLstill isn’t exercised.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/jsc/bindings/napi.cpp` around lines 2477 - 2492, Add a test for napi_delete_element that passes nullptr as the result argument, then verify the targeted element is actually deleted and the call succeeds. Reuse the existing napi_delete_element test setup and assertions, changing only the result-pointer scenario needed to cover the null-output path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@test/napi/napi.test.ts`:
- Around line 552-585: Update the subprocess assertions in the
“napi_fatal_exception triggers uncaughtException for non-Error values” test so
stderr is checked only when exitCode is nonzero, matching the file’s existing
subprocess patterns; preserve the current stdout payload and successful
exit-code expectations.
---
Outside diff comments:
In `@src/jsc/bindings/napi.cpp`:
- Around line 2477-2492: Add a test for napi_delete_element that passes nullptr
as the result argument, then verify the targeted element is actually deleted and
the call succeeds. Reuse the existing napi_delete_element test setup and
assertions, changing only the result-pointer scenario needed to cover the
null-output path.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: bcde39e2-927c-4d10-9a37-f9d80c0d74f2
📒 Files selected for processing (6)
src/jsc/bindings/napi.cppsrc/runtime/napi/napi_body.rstest/napi/napi-app/js_test_helpers.cpptest/napi/napi-app/module.jstest/napi/napi-app/standalone_tests.cpptest/napi/napi.test.ts
🛑 Comments failed to post (1)
test/napi/napi.test.ts (1)
552-585: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Unconditional
expect(stderr).toBe("")risks flakiness on ASAN/debug lanes.This departs from the file's other subprocess-assertion patterns (gating on
exitCode !== 0, or checking specific PASS/FAIL substrings). Prefer only assertingstderrwhen the run actually failed.🧪 Proposed fix
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - expect(stderr).toBe(""); expect(JSON.parse(stdout.trim())).toEqual({ statuses: [0, 0, 0, 0], caught: ["addon says: something fatal", 42, { plain: "object" }, "Error: real error"], }); + if (exitCode !== 0) { + expect(stderr).toBe(""); + } expect(exitCode).toBe(0);Based on learnings, "avoid unconditionally asserting expect(stderr).toBe(""). In ASAN/debug builds, benign warnings may be emitted to stderr" and the established convention to assert
stderronly whenexitCode !== 0.📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); expect(JSON.parse(stdout.trim())).toEqual({ statuses: [0, 0, 0, 0], caught: ["addon says: something fatal", 42, { plain: "object" }, "Error: real error"], }); if (exitCode !== 0) { expect(stderr).toBe(""); } expect(exitCode).toBe(0);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@test/napi/napi.test.ts` around lines 552 - 585, Update the subprocess assertions in the “napi_fatal_exception triggers uncaughtException for non-Error values” test so stderr is checked only when exitCode is nonzero, matching the file’s existing subprocess patterns; preserve the current stdout payload and successful exit-code expectations.Source: Learnings
|
Re the CodeRabbit out-of-line findings:
|
|
CI on 7d9d546 (build 72827): Diff is ready for review. |
…plicates napi_instanceof: take #34129's version (uses NAPI_PREAMBLE, which now gates on hasPendingException via this branch's change). NAPI_CHECK_TO_OBJECT: drop the per-macro hasPendingException check #34133 added (all callers use NAPI_PREAMBLE, which now covers it). Remove duplicate NapiEnv__hasPendingException export and NapiEnv::has_pending_exception method that both branches added independently; keep this branch's version which also checks the VM scope. napi_get_prototype: drop #34133's inline has_pending_exception check since preamble! already covers it.
What does this PR do?
The N-API object-operating family diverged from Node's
CHECK_TO_OBJECTcontract across two groups of entry points.Group 1 (element / property-name / prototype):
napi_set_element,napi_has_element,napi_get_element,napi_delete_element,napi_get_property_names,napi_get_all_property_names,napi_get_prototype. These calledgetObject()/is_object()and rejected every primitive withnapi_array_expectedornapi_object_expectedand no pending exception.Group 2 (by-key property siblings):
napi_set_property,napi_get_property,napi_has_property,napi_delete_property,napi_has_own_property,napi_set_named_property,napi_get_named_property,napi_has_named_property. These already coerced primitives viatoObjectbut returnednapi_pending_exception(10) instead of Node'snapi_object_expected(2) onnull/undefined.Portable addons feature-detect against these status codes, and
napi_array_expectedis a status Node uses only fornapi_get_array_length.Cause
Node runs
ToObject(context)(itsCHECK_TO_OBJECTmacro, js_native_api_v8.cc) on the target in every one of these entry points, which coerces strings/numbers/booleans/symbols to their wrapper object and only throws onnull/undefined; when it throws, the status isnapi_object_expectedand theTypeErroris left pending.napi_get_all_property_namesadditionally never validated itskey_mode/key_conversionenum arguments (Node rejects out-of-range values withnapi_invalid_arg) and never readkey_conversionat all, sonapi_key_keep_numbersstill returned array-index keys asJSStrings.Fix
NAPI_CHECK_TO_OBJECT(env, globalObject, result, src)macro that mirrors Node's semantics:JSValue::toObject()to coerce primitives, and on exception returnnapi_object_expectedwith theTypeErrorstill pending. Apply it at all fourteen C++ sites above.napi_get_prototype(Rust): replace theis_object()gate with anis_undefined_or_null()gate that throws theTypeErrorviato_object()and returnsnapi_object_expected; other primitives fall through toJSValue::getPrototype(), which already synthesizes the correct prototype (synthesizePrototype) without allocating a wrapper.napi_get_all_property_names: validatekey_modeandkey_conversion; whenkey_conversion == napi_key_keep_numbers, walk the result array and replace canonical array-index strings withjsNumbervalues (JSC'sownPropertyKeys/allPropertyKeysalways emit string keys).napi_delete_elementnow performs the delete even whenresult == NULL, matching Node (previously the delete itself was skipped).Verification
test/napi/napi-app/standalone_tests.cppgainstest_napi_object_coercion, which printsstatus/pendingfor every call above plus thekey_conversiontypeof check. Thenapi.test.tscase runs it throughcheckSameOutput, so Bun's output is asserted byte-for-byte against Node's, and additionally spot-checks the high-signal lines so the assertion fails underUSE_SYSTEM_BUN=1even if the Node comparison happened to produce matching (wrong) output.Before / after
Node's upstream
js-native-api/test_object,test_array, andtest_propertiessuites continue to pass.no test proof · iteration 1 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/napi/napi.test.ts