Repository: denoland/deno
Pull request: fix(ext/node): enable test-zlib-invalid-input-memory and test-zlib-un…

  Fixes two previously disabled zlib Node.js compatibility tests.

  - **`parallel/test-zlib-invalid-input-memory.js`** — enabled and passing.
    Root cause was a two-layer problem:
    1. `AsyncResource` in `async_hooks.ts` did not call `emitInit()` in its constructor, so `createHook({ init })` callbacks never fired for `AsyncResource` instances. The test's `init` hook assumed any new async resource was the tracked GC object, and when Deno's streams machinery created `TickObject` resources during error handling, the init callback threw an `AssertionError` from inside a `process.nextTick`, silently swallowing the error event.
    2. In `zlib.js`, `processCallback` skipped calling the Transform write `cb()` when `kError` was set, preventing the stream's destroy sequence from running and thus the error event from being emitted.

  - **`parallel/test-zlib-unused-weak.js`** — enabled and passing. The test passes the V8 flag `--no-concurrent-array-buffer-sweeping` to ensure deterministic GC accounting. Deno's node-compat test runner's `parse_flags` function did not recognise this flag and silently dropped it, so it was never forwarded. Added the flag to `parse_flags` in `mod.rs`.

  - **`abort/test-zlib-invalid-internals-usage.js`** — added to `config.jsonc` as disabled. This test uses `internalBinding('zlib').Zlib` (a Node.js C++ internal) which Deno does not implement and cannot reasonably emulate.

Changed files:
filename: ext/node/polyfills/async_hooks.ts
patch: @@ -8,6 +8,8 @@ import { core, primordials } from "ext:core/mod.js";
 import { validateFunction } from "ext:deno_node/internal/validators.mjs";
 import {
   AsyncHook,
+  emitDestroy as emitDestroyHook,
+  emitInit,
   executionAsyncId as internalExecutionAsyncId,
   newAsyncId,
 } from "ext:deno_node/internal/async_hooks.ts";
@@ -26,6 +28,12 @@ const {
   setAsyncContext,
 } = core;
 
+// FinalizationRegistry to emit the async hook destroy callback when an
+// AsyncResource is garbage collected, matching Node.js behaviour.
+const asyncResourceRegistry = new FinalizationRegistry(
+  (asyncId: number) => emitDestroyHook(asyncId),
+);
+
 export class AsyncResource {
   type: string;
   #snapshot: unknown;
@@ -35,6 +43,12 @@ export class AsyncResource {
     this.type = type;
     this.#snapshot = getAsyncContext();
     this.#asyncId = newAsyncId();
+    // Fire the init hook so that async_hooks.createHook({ init }) callbacks
+    // receive this resource, matching Node.js behaviour.
+    emitInit(this.#asyncId, type, internalExecutionAsyncId(), this);
+    // Register with the FinalizationRegistry so emitDestroy is called when
+    // this object is garbage collected.
+    asyncResourceRegistry.register(this, this.#asyncId);
   }
 
   asyncId() {
@@ -55,7 +69,11 @@ export class AsyncResource {
     }
   }
 
-  emitDestroy() {}
+  emitDestroy() {
+    asyncResourceRegistry.unregister(this);
+    emitDestroyHook(this.#asyncId);
+    return this;
+  }
 
   bind(fn: (...args: unknown[]) => unknown, thisArg) {
     validateFunction(fn, "fn");

filename: ext/node/polyfills/zlib.js
patch: @@ -586,8 +586,9 @@ function processChunk(self, chunk, flushFlag, cb) {
 
   // If the native binding did not call processCallback synchronously
   // (Zlib defers it to match Node.js where compression runs on the libuv
-  // threadpool), schedule it asynchronously.
-  if (handle._callbackPending && !self[kError]) {
+  // threadpool), schedule it asynchronously. Always schedule even on error
+  // so processCallback can call cb() to unblock the Transform write chain.
+  if (handle._callbackPending) {
     process.nextTick(processCallback.bind(handle));
   }
 }
@@ -607,6 +608,15 @@ function processCallback() {
     return;
   }
 
+  if (self[kError]) {
+    // An error occurred during the native write. Call cb() to unblock the
+    // Transform write chain; the error event will be emitted by the nextTick
+    // destroy scheduled in zlibOnError.
+    this.buffer = null;
+    this.cb();
+    return;
+  }
+
   const availOutAfter = state[0];
   const availInAfter = state[1];
 
@@ -660,7 +670,7 @@ function processCallback() {
           self._outOffset, // out_off
           self._chunkSize,
         ); // out_len
-        if (handle._callbackPending && !self[kError]) {
+        if (handle._callbackPending) {
           processCallback.call(this);
         }
       });

filename: tests/node_compat/config.jsonc
patch: @@ -2,6 +2,12 @@
   "tests": {
     "abort/test-addon-register-signal-handler.js": {},
     "abort/test-addon-uv-handle-leak.js": {},
+    "abort/test-zlib-invalid-internals-usage.js": {
+      "darwin": false,
+      "linux": false,
+      "windows": false,
+      "reason": "Tests Node.js internal C++ binding (internalBinding('zlib').Zlib) which is not implemented in Deno"
+    },
     "es-module/test-cjs-prototype-pollution.js": {},
     "es-module/test-esm-assert-strict.mjs": {},
     "es-module/test-esm-child-process-fork-main.mjs": {},
@@ -1651,7 +1657,7 @@
     "parallel/test-zlib-from-gzip.js": {},
     "parallel/test-zlib-from-string.js": {},
     "parallel/test-zlib-invalid-arg-value-brotli-compress.js": {},
-    // "parallel/test-zlib-invalid-input-memory.js": {},
+    "parallel/test-zlib-invalid-input-memory.js": {},
     "parallel/test-zlib-invalid-input.js": {},
     "parallel/test-zlib-kmaxlength-rangeerror.js": {
       "darwin": false,
@@ -1670,7 +1676,7 @@
     "parallel/test-zlib-sync-no-event.js": {},
     "parallel/test-zlib-truncated.js": {},
     "parallel/test-zlib-type-error.js": {},
-    // "parallel/test-zlib-unused-weak.js": {},
+    "parallel/test-zlib-unused-weak.js": {},
     "parallel/test-zlib-unzip-one-byte-chunks.js": {},
     "parallel/test-zlib-write-after-close.js": {},
     "parallel/test-zlib-write-after-end.js": {},

filename: tests/node_compat/mod.rs
patch: @@ -467,6 +467,9 @@ fn parse_flags(source: &str) -> (Vec<String>, Vec<String>) {
           "--expose-gc" => {
             v8_flags.push("--expose-gc".to_string());
           }
+          "--no-concurrent-array-buffer-sweeping" => {
+            v8_flags.push("--no-concurrent-array-buffer-sweeping".to_string());
+          }
           "--no-warnings" => {
             node_options.push("--no-warnings".to_string());
           }