Commit 847da074bdd for nodejs

commit 847da074bdd53ea4314edab98b52d0545997b835
Author: James M Snell <jasnell@gmail.com>
Date:   Tue Sep 22 19:45:17 2026 +0000

    stream: fill out stream/iter test coverage

    Signed-off-by: James M Snell <jasnell@gmail.com>
    Assisted-by: Opencode
    PR-URL: https://github.com/nodejs/node/pull/66079
    Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>

diff --git a/lib/internal/streams/iter/classic.js b/lib/internal/streams/iter/classic.js
index ac6e6976657..59eb5be5819 100644
--- a/lib/internal/streams/iter/classic.js
+++ b/lib/internal/streams/iter/classic.js
@@ -631,6 +631,7 @@ function fromWritable(writable, options = kNullPrototype) {
     entry.onAbort = undefined;
   }

+  // Without preserveReason (a clean finish), reason is unused.
   function cleanup(
     reason,
     preserveReason = false,
@@ -639,13 +640,10 @@ function fromWritable(writable, options = kNullPrototype) {
     const pending = drainWaiters;
     drainWaiters = [];
     for (let i = 0; i < pending.length; i++) {
-      if (!preserveReason &&
-          (reason === undefined || reason === null) &&
-          pending[i].close !== undefined) {
-        pending[i].close();
+      if (preserveReason) {
+        pending[i].reject(reason);
       } else {
-        pending[i].reject(
-          preserveReason ? reason : reason ?? new AbortError());
+        pending[i].close();
       }
     }

@@ -654,8 +652,7 @@ function fromWritable(writable, options = kNullPrototype) {
     while (writes.length !== 0) {
       const entry = writes.shift();
       cleanupPendingSignal(entry);
-      entry.reject(
-        preserveReason ? reason : reason ?? new AbortError());
+      entry.reject(preserveReason ? reason : new AbortError());
     }

     if (drainListenerInstalled) {
@@ -773,6 +770,7 @@ function fromWritable(writable, options = kNullPrototype) {
       entry.signal = signal;
       entry.onAbort = () => {
         const index = pendingWrites.indexOf(entry);
+        // Defensive: every dequeue path removes this listener first.
         if (index === -1) return;
         pendingWrites.removeAt(index);
         cleanupPendingSignal(entry);
@@ -792,6 +790,7 @@ function fromWritable(writable, options = kNullPrototype) {
   }

   function finishWithError(reason, keepTerminalListeners = false) {
+    // Defensive: callers already check errored.
     if (errored) return;
     errored = true;
     error = reason;
@@ -834,7 +833,8 @@ function fromWritable(writable, options = kNullPrototype) {
       removeTerminalListeners();
       return;
     }
-    if (finished || (writable.writableFinished ?? false)) {
+    // Duck-typed Writables may finish without emitting 'finish'.
+    if (writable.writableFinished ?? false) {
       onFinish();
       removeTerminalListeners();
       return;
@@ -850,6 +850,7 @@ function fromWritable(writable, options = kNullPrototype) {
   }

   function startEnd() {
+    // Defensive: nothing flushes again once the end has started.
     if (endStarted || pendingEnd === undefined) return;
     endStarted = true;
     const end = pendingEnd;
diff --git a/test/parallel/test-stream-iter-from-writable-lifecycle.js b/test/parallel/test-stream-iter-from-writable-lifecycle.js
new file mode 100644
index 00000000000..cf6732c4184
--- /dev/null
+++ b/test/parallel/test-stream-iter-from-writable-lifecycle.js
@@ -0,0 +1,296 @@
+// Flags: --experimental-stream-iter
+'use strict';
+
+// Tests for fromWritable() error, close, and end edge cases.
+
+const common = require('../common');
+const assert = require('assert');
+const { EventEmitter, once } = require('events');
+const { Writable } = require('stream');
+const { setImmediate } = require('timers/promises');
+const { fromWritable, ondrain } = require('stream/iter');
+
+function createDuckWritable(methods) {
+  const writable = new EventEmitter();
+  writable.write = () => true;
+  writable.end = () => {};
+  writable.destroy = () => {};
+  return Object.assign(writable, methods);
+}
+
+function assertNoTerminalListeners(writable) {
+  assert.strictEqual(writable.listenerCount('error'), 0);
+  assert.strictEqual(writable.listenerCount('finish'), 0);
+  assert.strictEqual(writable.listenerCount('close'), 0);
+}
+
+// A queued write that throws while being flushed on drain rejects on its own;
+// later queued writes are still committed.
+async function testQueuedWriteThrowsDuringFlush() {
+  const reason = new Error('queued write failed');
+  const written = [];
+  let calls = 0;
+  const writable = createDuckWritable({
+    write(chunk) {
+      calls++;
+      if (calls === 1) return false;
+      if (calls === 2) throw reason;
+      written.push(Buffer.from(chunk).toString());
+      return true;
+    },
+  });
+  const writer = fromWritable(writable, { backpressure: 'unbounded' });
+
+  await writer.write('a');
+  const second = writer.write('b');
+  const third = writer.write('c');
+  writable.emit('drain');
+
+  await assert.rejects(second, (error) => error === reason);
+  await third;
+  assert.deepStrictEqual(written, ['c']);
+}
+
+// A queued write that errors the Writable while being flushed rejects with
+// that error, as does every write still queued behind it.
+async function testQueuedWriteErrorsDuringFlush() {
+  const reason = new Error('queued write errored');
+  let calls = 0;
+  const writable = createDuckWritable({
+    write() {
+      calls++;
+      if (calls === 1) return false;
+      writable.emit('error', reason);
+      return true;
+    },
+  });
+  const writer = fromWritable(writable, { backpressure: 'unbounded' });
+
+  await writer.write('a');
+  const second = writer.write('b');
+  const third = writer.write('c');
+  writable.emit('drain');
+
+  await assert.rejects(second, (error) => error === reason);
+  await assert.rejects(third, (error) => error === reason);
+  await assert.rejects(writer.end(), (error) => error === reason);
+  assert.strictEqual(calls, 2);
+  assertNoTerminalListeners(writable);
+}
+
+// A Writable that errors synchronously inside write() rejects the write that
+// triggered it.
+async function testWriteErrorsSynchronously() {
+  for (const method of ['write', 'writev']) {
+    const reason = new Error(`sync ${method} error`);
+    let emitted = false;
+    const writable = createDuckWritable({
+      write() {
+        if (!emitted) {
+          emitted = true;
+          writable.emit('error', reason);
+        }
+        return true;
+      },
+    });
+    const writer = fromWritable(writable);
+    const chunk = method === 'write' ? 'a' : ['a', 'b'];
+    await assert.rejects(writer[method](chunk),
+                         (error) => error === reason);
+    assertNoTerminalListeners(writable);
+  }
+}
+
+async function testWritevThrowRejects() {
+  const reason = new Error('writev failed');
+  const writable = createDuckWritable({
+    write() { throw reason; },
+  });
+  const writer = fromWritable(writable);
+
+  await assert.rejects(writer.writev(['a', 'b']), (error) => error === reason);
+  writer.fail();
+}
+
+async function testEmptyWritev() {
+  const writable = new Writable({ write: common.mustNotCall() });
+  const writer = fromWritable(writable);
+
+  await writer.writev([]);
+  assert.strictEqual(await writer.end(), 0);
+}
+
+// Ending the Writable directly while writes are queued in the adapter rejects
+// them, since they can no longer be committed, and settles drain waiters.
+async function testExternalEndRejectsQueuedWrites() {
+  const callbacks = [];
+  const writable = new Writable({
+    highWaterMark: 1,
+    write(chunk, encoding, cb) { callbacks.push(cb); },
+  });
+  const writer = fromWritable(writable, { backpressure: 'unbounded' });
+
+  await writer.write('a');
+  const queued = writer.write('b');
+  const drained = ondrain(writer);
+  writable.end();
+  callbacks.shift()();
+
+  await assert.rejects(queued, { name: 'AbortError' });
+  assert.strictEqual(await drained, false);
+  assert.strictEqual(callbacks.length, 0);
+}
+
+// A Writable that errored before the adapter was created is detected
+// synchronously; the adapter keeps its listeners until 'close'.
+async function testErroredBeforeAdapterReleasesListenersOnClose() {
+  const reason = new Error('write failed');
+  const writable = new Writable({
+    autoDestroy: false,
+    write(chunk, encoding, cb) { cb(reason); },
+  });
+  writable.on('error', common.mustCall());
+  writable.write('a');
+  await setImmediate();
+
+  const writer = fromWritable(writable);
+  await assert.rejects(writer.write('b'), (error) => error === reason);
+  assert.strictEqual(writable.listenerCount('close'), 1);
+
+  writable.destroy();
+  await setImmediate();
+  assert.strictEqual(writable.listenerCount('close'), 0);
+  assert.strictEqual(writable.listenerCount('finish'), 0);
+}
+
+// Duck-typed Writables may report completion only through writableFinished
+// and 'close', without emitting 'finish'.
+async function testCloseAfterFinishWithoutFinishEvent() {
+  const writable = createDuckWritable({
+    end() {
+      process.nextTick(() => {
+        writable.writableFinished = true;
+        writable.emit('close');
+      });
+    },
+  });
+  const writer = fromWritable(writable);
+
+  await writer.write('ab');
+  assert.strictEqual(await writer.end(), 2);
+  assertNoTerminalListeners(writable);
+}
+
+// A 'close' without any indication of finishing is a premature close.
+async function testDuckPrematureClose() {
+  const writable = createDuckWritable({
+    end() {
+      process.nextTick(() => writable.emit('close'));
+    },
+  });
+  const writer = fromWritable(writable);
+
+  await assert.rejects(writer.end(), { name: 'AbortError' });
+  assertNoTerminalListeners(writable);
+}
+
+async function testAlreadyFinished() {
+  const writable = new Writable({ write(chunk, encoding, cb) { cb(); } });
+  writable.end();
+  await once(writable, 'finish');
+
+  const writer = fromWritable(writable);
+  assert.strictEqual(writer.canWrite, null);
+  await assert.rejects(writer.write('a'),
+                       { code: 'ERR_STREAM_WRITE_AFTER_END' });
+  assert.strictEqual(await writer.end(), 0);
+  assertNoTerminalListeners(writable);
+}
+
+async function testAlreadyDestroyed() {
+  const writable = new Writable({ write: common.mustNotCall() });
+  writable.destroy();
+  await once(writable, 'close');
+
+  const writer = fromWritable(writable);
+  assert.strictEqual(writer.canWrite, null);
+  await assert.rejects(writer.write('a'),
+                       { code: 'ERR_STREAM_WRITE_AFTER_END' });
+  assertNoTerminalListeners(writable);
+}
+
+// When end() and the destroy() that follows both throw, the end() failure
+// is reported and the adapter's listeners are released.
+async function testEndAndDestroyThrow() {
+  const endError = new Error('end failed');
+  const writable = createDuckWritable({
+    end() { throw endError; },
+    destroy() { throw new Error('destroy failed'); },
+  });
+  const writer = fromWritable(writable);
+
+  await assert.rejects(writer.end(), (error) => error === endError);
+  assertNoTerminalListeners(writable);
+}
+
+async function testFailDestroyThrows() {
+  const reason = new Error('fail reason');
+  const destroyError = new Error('destroy failed');
+  const writable = createDuckWritable({
+    destroy() { throw destroyError; },
+  });
+  const writer = fromWritable(writable);
+
+  assert.throws(() => writer.fail(reason), (error) => error === destroyError);
+  assertNoTerminalListeners(writable);
+  await assert.rejects(writer.write('a'), (error) => error === reason);
+}
+
+// An end() with a signal rejects with the Writable's error if ending fails.
+async function testFinalErrorRejectsSignaledEnd() {
+  const reason = new Error('final failed');
+  const writable = new Writable({
+    write(chunk, encoding, cb) { cb(); },
+    final(cb) { cb(reason); },
+  });
+  writable.on('error', common.mustCall());
+  const writer = fromWritable(writable);
+  const { signal } = new AbortController();
+
+  await writer.write('a');
+  await assert.rejects(writer.end({ signal }), (error) => error === reason);
+}
+
+// The signal can be aborted synchronously by the Writable's own end(), after
+// end() has checked it but before it waits on it.
+async function testSignalAbortedByUnderlyingEnd() {
+  const controller = new AbortController();
+  const writable = createDuckWritable({
+    end: common.mustCall(() => controller.abort('stop')),
+  });
+  const writer = fromWritable(writable);
+
+  await assert.rejects(writer.end({ signal: controller.signal }),
+                       (reason) => reason === 'stop');
+  const ending = writer.end();
+  writable.emit('finish');
+  assert.strictEqual(await ending, 0);
+}
+
+Promise.all([
+  testQueuedWriteThrowsDuringFlush(),
+  testQueuedWriteErrorsDuringFlush(),
+  testWriteErrorsSynchronously(),
+  testWritevThrowRejects(),
+  testEmptyWritev(),
+  testExternalEndRejectsQueuedWrites(),
+  testErroredBeforeAdapterReleasesListenersOnClose(),
+  testCloseAfterFinishWithoutFinishEvent(),
+  testDuckPrematureClose(),
+  testAlreadyFinished(),
+  testAlreadyDestroyed(),
+  testEndAndDestroyThrow(),
+  testFailDestroyThrows(),
+  testFinalErrorRejectsSignaledEnd(),
+  testSignalAbortedByUnderlyingEnd(),
+]).then(common.mustCall());
diff --git a/test/parallel/test-stream-iter-readable-interop.js b/test/parallel/test-stream-iter-readable-interop.js
index 5c7b8ed42cd..ba403a74680 100644
--- a/test/parallel/test-stream-iter-readable-interop.js
+++ b/test/parallel/test-stream-iter-readable-interop.js
@@ -621,6 +621,93 @@ async function testAbortWhileReadIsPending() {
   assert.strictEqual(readable.listenerCount('readable'), 0);
 }

+async function testReturnBeforeNext() {
+  const readable = new Readable({ read() {} });
+  const iterator = from(readable)[Symbol.asyncIterator]();
+
+  assert.deepStrictEqual(await iterator.return('stopped'), {
+    value: 'stopped',
+    done: true,
+  });
+  assert.strictEqual(readable.destroyed, true);
+}
+
+async function testThrowBeforeNext() {
+  const readable = new Readable({ read() {} });
+  const iterator = from(readable)[Symbol.asyncIterator]();
+  const reason = new Error('stop');
+
+  await assert.rejects(iterator.throw(reason), (error) => error === reason);
+  assert.strictEqual(readable.destroyed, true);
+}
+
+async function testThrowAfterNext() {
+  const readable = new Readable({ read() {} });
+  readable.push('a');
+  const iterator = from(readable)[Symbol.asyncIterator]();
+  const reason = new Error('stop');
+
+  const first = await iterator.next();
+  assert.strictEqual(Buffer.concat(first.value).toString(), 'a');
+  await assert.rejects(iterator.throw(reason), (error) => error === reason);
+  await setImmediate();
+  assert.strictEqual(readable.destroyed, true);
+  assert.strictEqual(readable.listenerCount('readable'), 0);
+}
+
+async function testThrowWhileReadIsPending() {
+  const readable = new Readable({ read() {} });
+  const iterator = from(readable)[Symbol.asyncIterator]();
+  const reason = new Error('stop');
+  const pending = iterator.next();
+  await setImmediate();
+
+  const thrown = iterator.throw(reason);
+  assert.deepStrictEqual(await pending, { value: undefined, done: true });
+  await assert.rejects(thrown, (error) => error === reason);
+  await setImmediate();
+  assert.strictEqual(readable.destroyed, true);
+  assert.strictEqual(readable.listenerCount('readable'), 0);
+}
+
+// Without autoDestroy, an errored Readable is left for the caller to destroy,
+// but the iterator must still release its listeners.
+async function testErrorWithoutAutoDestroy() {
+  const readable = new Readable({ autoDestroy: false, read() {} });
+  const closeListeners = readable.listenerCount('close');
+  const iterator = from(readable)[Symbol.asyncIterator]();
+  const pending = iterator.next();
+  await setImmediate();
+
+  readable.push(42);
+  await assert.rejects(pending, { code: 'ERR_INVALID_ARG_TYPE' });
+  assert.strictEqual(readable.destroyed, false);
+  assert.strictEqual(readable.listenerCount('readable'), 0);
+  assert.strictEqual(readable.listenerCount('close'), closeListeners);
+  readable.destroy();
+}
+
+// A read() override may return null while data is still buffered; the batch
+// ends early rather than including the null.
+async function testNullReadWithBufferedData() {
+  const readable = new Readable({ objectMode: true, read() {} });
+  readable.push('a');
+  readable.push('b');
+  readable.push(null);
+  const read = readable.read;
+  let calls = 0;
+  readable.read = function(...args) {
+    if (++calls === 2) return null;
+    return read.apply(this, args);
+  };
+
+  const batches = [];
+  for await (const batch of from(readable)) {
+    batches.push(batch.map((chunk) => Buffer.from(chunk).toString()));
+  }
+  assert.deepStrictEqual(batches, [['a'], ['b']]);
+}
+
 // =============================================================================
 // kValidatedSource identity - from() returns same object for validated sources
 // =============================================================================
@@ -672,4 +759,10 @@ Promise.all([
   testAbortSignal(),
   testReturnWhileReadIsPending(),
   testAbortWhileReadIsPending(),
+  testReturnBeforeNext(),
+  testThrowBeforeNext(),
+  testThrowAfterNext(),
+  testThrowWhileReadIsPending(),
+  testErrorWithoutAutoDestroy(),
+  testNullReadWithBufferedData(),
 ]).then(common.mustCall());
diff --git a/test/parallel/test-stream-iter-transform-buffering.js b/test/parallel/test-stream-iter-transform-buffering.js
index 7e90479d70c..b1b813f60b9 100644
--- a/test/parallel/test-stream-iter-transform-buffering.js
+++ b/test/parallel/test-stream-iter-transform-buffering.js
@@ -3,9 +3,9 @@

 const common = require('../common');
 const assert = require('assert');
-const { gzipSync } = require('zlib');
-const { decompressGzip } = require('zlib/iter');
-const { from } = require('stream/iter');
+const { brotliDecompressSync, gunzipSync, gzipSync } = require('zlib');
+const { compressBrotli, compressGzip, decompressGzip } = require('zlib/iter');
+const { bytes, from, pull } = require('stream/iter');

 async function testDecompressionOutputIsBounded() {
   let input = Buffer.alloc(32 * 1024 * 1024, 0x61);
@@ -28,4 +28,88 @@ async function testDecompressionOutputIsBounded() {
   await iterator.return();
 }

-testDecompressionOutputIsBounded().then(common.mustCall());
+// An output buffer smaller than the batch high water mark fills repeatedly
+// before a batch is ready, so the engine is re-entered from the write
+// callback without yielding.
+async function testSmallChunkSizeDecompression() {
+  const input = Buffer.alloc(256 * 1024, 0x61);
+  const output = await bytes(pull(from(gzipSync(input)),
+                                  decompressGzip({ chunkSize: 1024 })));
+  assert.deepStrictEqual(Buffer.from(output), input);
+}
+
+// Deterministic incompressible data. Brotli's buffering decisions depend on
+// content, so random data would make the tests below flaky.
+function incompressible(size) {
+  const buffer = Buffer.allocUnsafe(size);
+  let x = 0x9e3779b9;
+  for (let i = 0; i < size; i++) {
+    x ^= x << 13;
+    x ^= x >>> 17;
+    x ^= x << 5;
+    buffer[i] = x & 0xff;
+  }
+  return buffer;
+}
+
+// Brotli buffers 1MB of incompressible input internally, so finalizing the
+// stream produces more than one batch of output that must be yielded while
+// the finish operation is still in progress.
+async function testLargeFinishOutput() {
+  const input = incompressible(1024 * 1024);
+  const output = await bytes(pull(from(input), compressBrotli()));
+  assert.deepStrictEqual(brotliDecompressSync(output), input);
+}
+
+// Unwrapping the transform function drops the validated fast path, so pull()
+// delivers an explicit null flush signal to the compression transform.
+async function testExplicitFlushSignal() {
+  const input = incompressible(1024 * 1024);
+  const { transform } = compressBrotli();
+  const output = await bytes(pull(from(input), { transform }));
+  assert.deepStrictEqual(brotliDecompressSync(output), input);
+}
+
+// Failures accessing or calling the source iterator's return() method must
+// not replace the transform's successful completion.
+async function testSourceReturnFailuresAreIgnored() {
+  const input = Buffer.from('hello world');
+  function makeSource(returnDescriptor) {
+    let done = false;
+    const iterator = {
+      next() {
+        if (done) return Promise.resolve({ done: true, value: undefined });
+        done = true;
+        return Promise.resolve({ done: false, value: [input] });
+      },
+    };
+    Object.defineProperty(iterator, 'return', returnDescriptor);
+    return { [Symbol.asyncIterator]() { return iterator; } };
+  }
+
+  const sources = [
+    makeSource({ get: common.mustCall(() => { throw new Error('getter'); }) }),
+    makeSource({
+      value: common.mustCall(() => { throw new Error('return'); }),
+    }),
+  ];
+  for (const source of sources) {
+    const { signal } = new AbortController();
+    const batches = [];
+    for await (const batch of compressGzip().transform(source, { signal })) {
+      batches.push(...batch);
+    }
+    assert.deepStrictEqual(gunzipSync(Buffer.concat(batches)), input);
+  }
+}
+
+(async () => {
+  // Run the memory measurement on its own so other tests don't skew it.
+  await testDecompressionOutputIsBounded();
+  await Promise.all([
+    testSmallChunkSizeDecompression(),
+    testLargeFinishOutput(),
+    testExplicitFlushSignal(),
+    testSourceReturnFailuresAreIgnored(),
+  ]);
+})().then(common.mustCall());
diff --git a/test/parallel/test-stream-iter-validation.js b/test/parallel/test-stream-iter-validation.js
index 3f311ec8c46..b130a92ae3d 100644
--- a/test/parallel/test-stream-iter-validation.js
+++ b/test/parallel/test-stream-iter-validation.js
@@ -199,6 +199,18 @@ assert.throws(
   { code: 'ERR_INVALID_ARG_TYPE' },
 );

+// Duck-typed readables must provide each of the required methods.
+{
+  const required = ['read', 'pipe', 'destroy', 'on', 'removeListener'];
+  for (const missing of required) {
+    const duck = {};
+    for (const name of required) {
+      if (name !== missing) duck[name] = () => {};
+    }
+    assert.throws(() => fromReadable(duck), { code: 'ERR_INVALID_ARG_TYPE' });
+  }
+}
+
 // fromWritable Writer options.signal must be AbortSignal
 {
   const writable = new Writable({
diff --git a/test/parallel/test-stream-iter-writable-from.js b/test/parallel/test-stream-iter-writable-from.js
index 12ea80df9fb..e67d75a810e 100644
--- a/test/parallel/test-stream-iter-writable-from.js
+++ b/test/parallel/test-stream-iter-writable-from.js
@@ -589,6 +589,59 @@ async function testDestroyUsesDisposeFallback() {
   assert.strictEqual(disposed, true);
 }

+async function testDestroyUsesAsyncDisposeFallback() {
+  const { promise: disposing, resolve } = Promise.withResolvers();
+  const writable = toWritable({
+    write(chunk) { return Promise.resolve(); },
+    [Symbol.asyncDispose]: common.mustCall(() => disposing),
+  });
+  let closed = false;
+  writable.on('close', () => { closed = true; });
+
+  writable.destroy();
+  await setImmediate();
+  // Destruction completes only once the async dispose settles.
+  assert.strictEqual(closed, false);
+  resolve();
+  await once(writable, 'close');
+}
+
+async function testDestroyWithErrorUsesAsyncDisposeFallback() {
+  const reason = new Error('destroyed');
+  const writable = toWritable({
+    write(chunk) { return Promise.resolve(); },
+    [Symbol.asyncDispose]: common.mustCall(() => Promise.resolve()),
+  });
+
+  writable.destroy(reason);
+  const [error] = await once(writable, 'error');
+  assert.strictEqual(error, reason);
+}
+
+async function testAsyncDisposeRejectionErrorsDestroy() {
+  const reason = new Error('dispose failed');
+  const writable = toWritable({
+    write(chunk) { return Promise.resolve(); },
+    [Symbol.asyncDispose]: common.mustCall(() => Promise.reject(reason)),
+  });
+
+  writable.destroy();
+  const [error] = await once(writable, 'error');
+  assert.strictEqual(error, reason);
+}
+
+async function testDisposeThrowErrorsDestroy() {
+  const reason = new Error('dispose failed');
+  const writable = toWritable({
+    write(chunk) { return Promise.resolve(); },
+    [Symbol.dispose]: common.mustCall(() => { throw reason; }),
+  });
+
+  writable.destroy();
+  const [error] = await once(writable, 'error');
+  assert.strictEqual(error, reason);
+}
+
 // =============================================================================
 // Destroy with error calls fail() when available
 // =============================================================================
@@ -762,6 +815,10 @@ Promise.all([
   testDestroyDelegatesToFail(),
   testDestroyWithoutError(),
   testDestroyUsesDisposeFallback(),
+  testDestroyUsesAsyncDisposeFallback(),
+  testDestroyWithErrorUsesAsyncDisposeFallback(),
+  testAsyncDisposeRejectionErrorsDestroy(),
+  testDisposeThrowErrorsDestroy(),
   testDestroyWithError(),
   testDestroyWithoutFail(),
   testWriteErrorPropagation(),