diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,16 @@
 # Revision history for fs-sim
 
+## 0.3.1.0 -- 2024-12-10
+
+### Non-breaking
+
+* Expose `openHandles` for testing.
+
+### Patch
+
+* Make `genInfinite` generate truly infinite streams.
+* The shrinker for `Errors` now truly shrinks towards empty errors.
+
 ## 0.3.0.1 -- 2024-10-02
 
 ### Patch
diff --git a/fs-sim.cabal b/fs-sim.cabal
--- a/fs-sim.cabal
+++ b/fs-sim.cabal
@@ -1,6 +1,6 @@
 cabal-version:   3.0
 name:            fs-sim
-version:         0.3.0.1
+version:         0.3.1.0
 synopsis:        Simulated file systems
 description:     Simulated file systems.
 license:         Apache-2.0
diff --git a/src/System/FS/Sim/Error.hs b/src/System/FS/Sim/Error.hs
--- a/src/System/FS/Sim/Error.hs
+++ b/src/System/FS/Sim/Error.hs
@@ -452,30 +452,32 @@
 instance Arbitrary Errors where
   arbitrary = genErrors True True
 
-  shrink err@($(fields 'Errors)) = concatMap (filter (not . allNull))
-      [ (\s' -> err { dumpStateE = s' })                <$> Stream.shrinkStream dumpStateE
-      , (\s' -> err { hOpenE = s' })                    <$> Stream.shrinkStream hOpenE
-      , (\s' -> err { hCloseE = s' })                   <$> Stream.shrinkStream hCloseE
-      , (\s' -> err { hSeekE = s' })                    <$> Stream.shrinkStream hSeekE
-      , (\s' -> err { hGetSomeE = s' })                 <$> Stream.shrinkStream hGetSomeE
-      , (\s' -> err { hGetSomeAtE = s' })               <$> Stream.shrinkStream hGetSomeAtE
-      , (\s' -> err { hPutSomeE = s' })                 <$> Stream.shrinkStream hPutSomeE
-      , (\s' -> err { hTruncateE = s' })                <$> Stream.shrinkStream hTruncateE
-      , (\s' -> err { hGetSizeE = s' })                 <$> Stream.shrinkStream hGetSizeE
-      , (\s' -> err { createDirectoryE = s' })          <$> Stream.shrinkStream createDirectoryE
-      , (\s' -> err { createDirectoryIfMissingE = s' }) <$> Stream.shrinkStream createDirectoryIfMissingE
-      , (\s' -> err { listDirectoryE = s' })            <$> Stream.shrinkStream listDirectoryE
-      , (\s' -> err { doesDirectoryExistE = s' })       <$> Stream.shrinkStream doesDirectoryExistE
-      , (\s' -> err { doesFileExistE = s' })            <$> Stream.shrinkStream doesFileExistE
-      , (\s' -> err { removeDirectoryRecursiveE = s' }) <$> Stream.shrinkStream removeDirectoryRecursiveE
-      , (\s' -> err { removeFileE = s' })               <$> Stream.shrinkStream removeFileE
-      , (\s' -> err { renameFileE = s' })               <$> Stream.shrinkStream renameFileE
-        -- File I\/O with user-supplied buffers
-      , (\s' -> err { hGetBufSomeE = s' })   <$> Stream.shrinkStream hGetBufSomeE
-      , (\s' -> err { hGetBufSomeAtE = s' }) <$> Stream.shrinkStream hGetBufSomeAtE
-      , (\s' -> err { hPutBufSomeE = s' })   <$> Stream.shrinkStream hPutBufSomeE
-      , (\s' -> err { hPutBufSomeAtE = s' }) <$> Stream.shrinkStream hPutBufSomeAtE
-      ]
+  shrink err@($(fields 'Errors))
+    | allNull err = []
+    | otherwise = emptyErrors : concatMap (filter (not . allNull))
+        [ (\s' -> err { dumpStateE = s' })                <$> Stream.shrinkStream dumpStateE
+        , (\s' -> err { hOpenE = s' })                    <$> Stream.shrinkStream hOpenE
+        , (\s' -> err { hCloseE = s' })                   <$> Stream.shrinkStream hCloseE
+        , (\s' -> err { hSeekE = s' })                    <$> Stream.shrinkStream hSeekE
+        , (\s' -> err { hGetSomeE = s' })                 <$> Stream.shrinkStream hGetSomeE
+        , (\s' -> err { hGetSomeAtE = s' })               <$> Stream.shrinkStream hGetSomeAtE
+        , (\s' -> err { hPutSomeE = s' })                 <$> Stream.shrinkStream hPutSomeE
+        , (\s' -> err { hTruncateE = s' })                <$> Stream.shrinkStream hTruncateE
+        , (\s' -> err { hGetSizeE = s' })                 <$> Stream.shrinkStream hGetSizeE
+        , (\s' -> err { createDirectoryE = s' })          <$> Stream.shrinkStream createDirectoryE
+        , (\s' -> err { createDirectoryIfMissingE = s' }) <$> Stream.shrinkStream createDirectoryIfMissingE
+        , (\s' -> err { listDirectoryE = s' })            <$> Stream.shrinkStream listDirectoryE
+        , (\s' -> err { doesDirectoryExistE = s' })       <$> Stream.shrinkStream doesDirectoryExistE
+        , (\s' -> err { doesFileExistE = s' })            <$> Stream.shrinkStream doesFileExistE
+        , (\s' -> err { removeDirectoryRecursiveE = s' }) <$> Stream.shrinkStream removeDirectoryRecursiveE
+        , (\s' -> err { removeFileE = s' })               <$> Stream.shrinkStream removeFileE
+        , (\s' -> err { renameFileE = s' })               <$> Stream.shrinkStream renameFileE
+          -- File I\/O with user-supplied buffers
+        , (\s' -> err { hGetBufSomeE = s' })   <$> Stream.shrinkStream hGetBufSomeE
+        , (\s' -> err { hGetBufSomeAtE = s' }) <$> Stream.shrinkStream hGetBufSomeAtE
+        , (\s' -> err { hPutBufSomeE = s' })   <$> Stream.shrinkStream hPutBufSomeE
+        , (\s' -> err { hPutBufSomeAtE = s' }) <$> Stream.shrinkStream hPutBufSomeAtE
+        ]
 
 {-------------------------------------------------------------------------------
   Simulate Errors monad
diff --git a/src/System/FS/Sim/MockFS.hs b/src/System/FS/Sim/MockFS.hs
--- a/src/System/FS/Sim/MockFS.hs
+++ b/src/System/FS/Sim/MockFS.hs
@@ -22,6 +22,7 @@
   , example
   , handleIsOpen
   , numOpenHandles
+  , openHandles
   , pretty
     -- * Debugging
   , dumpState
diff --git a/src/System/FS/Sim/Stream.hs b/src/System/FS/Sim/Stream.hs
--- a/src/System/FS/Sim/Stream.hs
+++ b/src/System/FS/Sim/Stream.hs
@@ -165,4 +165,4 @@
 genInfinite ::
      Gen (Maybe a)  -- ^ Tip: use 'genMaybe'.
   -> Gen (Stream a)
-genInfinite gen = Stream Infinite <$> QC.listOf gen
+genInfinite gen = Stream Infinite <$> QC.infiniteListOf gen
diff --git a/test/Test/System/FS/Sim/Error.hs b/test/Test/System/FS/Sim/Error.hs
--- a/test/Test/System/FS/Sim/Error.hs
+++ b/test/Test/System/FS/Sim/Error.hs
@@ -140,6 +140,15 @@
                 MockFS.fromBuffer mba 0 (fromIntegral $ BS.length bs) >>=
                   maybe (error "fromOutput: should not fail") pure
         in  propGetterGetsAll hGetBufSomeAtC get fromOutput p bs
+
+    -- Generators and shrinkers
+
+    , testProperty "prop_regression_shrinkErrors"
+        prop_regression_shrinkErrors
+    , testProperty "prop_regression_shrinkNonEmptyErrors"
+        prop_regression_shrinkNonEmptyErrors
+    , testProperty "prop_regression_shrinkEmptyErrors"
+        prop_regression_shrinkEmptyErrors
     ]
 
 instance Arbitrary BS.ByteString where
@@ -242,3 +251,29 @@
       , hGetBufSomeE = errStream
       , hGetBufSomeAtE = errStream
       }
+
+{-------------------------------------------------------------------------------
+  Generators and shrinkers
+-------------------------------------------------------------------------------}
+
+-- | See fs-sim#84
+prop_regression_shrinkErrors :: Errors -> Property
+prop_regression_shrinkErrors _errs = expectFailure $
+    property False
+
+-- | See fs-sim#84
+prop_regression_shrinkNonEmptyErrors :: Errors -> Property
+prop_regression_shrinkNonEmptyErrors errs = expectFailure $
+    not (allNull errs) ==> property False
+
+newtype EmptyErrors = EmptyErrors Errors
+  deriving Show
+
+instance Arbitrary EmptyErrors where
+  arbitrary = EmptyErrors <$> oneof [ pure emptyErrors ]
+  shrink (EmptyErrors errs) = EmptyErrors <$> shrink errs
+
+-- | See fs-sim#84
+prop_regression_shrinkEmptyErrors :: EmptyErrors -> Property
+prop_regression_shrinkEmptyErrors (EmptyErrors errs) = expectFailure $
+    allNull errs ==> property False
