diff --git a/knob.cabal b/knob.cabal
--- a/knob.cabal
+++ b/knob.cabal
@@ -1,15 +1,16 @@
+cabal-version: 2.4
 name: knob
-version: 0.1.1
+version: 0.2
 license: MIT
 license-file: license.txt
 author: John Millikin <jmillikin@gmail.com>
-maintainer: John Millikin <jmillikin@gmail.com>
+maintainer: Naïm Favier <n@monade.li>
 build-type: Simple
-cabal-version: >= 1.6
+tested-with: GHC == 9.0.2, GHC == 9.2.1
 category: System
 stability: experimental
-homepage: https://john-millikin.com/software/knob/
-bug-reports: mailto:jmillikin@gmail.com
+homepage: https://github.com/ncfavier/knob
+bug-reports: https://github.com/ncfavier/knob/issues
 
 synopsis: Memory-backed handles
 description:
@@ -38,22 +39,18 @@
   tests/KnobTests.hs
 
 source-repository head
-  type: bazaar
-  location: https://john-millikin.com/software/knob/
-
-source-repository this
-  type: bazaar
-  location: https://john-millikin.com/branches/knob/0.1/
-  tag: knob_0.1.1
+  type: git
+  location: https://github.com/ncfavier/knob
 
 library
+  default-language: Haskell2010
   hs-source-dirs: lib
-  ghc-options: -Wall -O2
+  ghc-options: -Wall
 
   build-depends:
-      base >= 4.2 && < 5.0
-    , bytestring >= 0.9
-    , transformers >= 0.2
+      base >= 4.15 && < 5
+    , bytestring >= 0.9 && < 0.12
+    , transformers >= 0.2 && < 0.7
 
   exposed-modules:
     Data.Knob
diff --git a/lib/Data/Knob.hs b/lib/Data/Knob.hs
--- a/lib/Data/Knob.hs
+++ b/lib/Data/Knob.hs
@@ -24,15 +24,15 @@
 -- >     bytes <- Data.Knob.getContents knob
 -- >     putStrLn ("Wrote bytes: " ++ show bytes)
 module Data.Knob
-	( Knob
-	, newKnob
-	, Data.Knob.getContents
-	, setContents
-	
-	, newFileHandle
-	, withFileHandle
-	) where
+  ( Knob
+  , newKnob
+  , Data.Knob.getContents
+  , setContents
 
+  , newFileHandle
+  , withFileHandle
+  ) where
+
 import qualified Control.Concurrent.MVar as MVar
 import           Control.Exception (bracket, throwIO)
 import           Control.Monad (when)
@@ -53,106 +53,112 @@
 -- have multiple 'IO.Handle's open to it, each of which behaves like a standard
 -- file handle.
 --
--- Use 'getContents' and 'setContents' to inspect and modify the knob&#8217;s
+-- Use 'Data.Knob.getContents' and 'setContents' to inspect and modify the knob's
 -- byte buffer.
 newtype Knob = Knob (MVar.MVar ByteString)
 
 data Device = Device IO.IOMode (MVar.MVar ByteString) (MVar.MVar Int)
-	deriving (Typeable)
+  deriving (Typeable)
 
 instance IO.IODevice Device where
-	ready _ _ _ = return True
-	close _ = return ()
-	isTerminal _ = return False
-	isSeekable _ = return True
-	
-	seek (Device _ _ var) IO.AbsoluteSeek off = do
-		checkOffset off
-		MVar.modifyMVar_ var (\_ -> return (fromInteger off))
-	
-	seek (Device _ _ var) IO.RelativeSeek off = do
-		MVar.modifyMVar_ var (\old_off -> do
-			let new_off = toInteger old_off + off
-			checkOffset new_off
-			return (fromInteger new_off))
-	
-	seek dev@(Device _ _ off_var) IO.SeekFromEnd off = do
-		MVar.modifyMVar_ off_var (\_ -> do
-			size <- IO.getSize dev
-			let new_off = size + off
-			checkOffset new_off
-			return (fromInteger new_off))
-	
-	tell (Device _ _ var) = fmap toInteger (MVar.readMVar var)
-	getSize (Device _ var _) = do
-		bytes <- MVar.readMVar var
-		return (toInteger (Data.ByteString.length bytes))
-	setSize dev size = setDeviceSize dev size
-	devType _ = return IO.RegularFile
+  ready _ _ _ = return True
+  close _ = return ()
+  isTerminal _ = return False
+  isSeekable _ = return True
 
+  seek (Device _ _ var) IO.AbsoluteSeek off = do
+    checkOffset off
+    MVar.modifyMVar var (\_ -> return (fromInteger off, off))
+
+  seek (Device _ _ var) IO.RelativeSeek off = do
+    MVar.modifyMVar var (\old_off -> do
+      let new_off = toInteger old_off + off
+      checkOffset new_off
+      return (fromInteger new_off, new_off))
+
+  seek dev@(Device _ _ off_var) IO.SeekFromEnd off = do
+    MVar.modifyMVar off_var (\_ -> do
+      size <- IO.getSize dev
+      let new_off = size + off
+      checkOffset new_off
+      return (fromInteger new_off, new_off))
+
+  tell (Device _ _ var) = fmap toInteger (MVar.readMVar var)
+  getSize (Device _ var _) = do
+    bytes <- MVar.readMVar var
+    return (toInteger (Data.ByteString.length bytes))
+  setSize dev size = setDeviceSize dev size
+  devType _ = return IO.RegularFile
+
 checkOffset :: Integer -> IO ()
 checkOffset off = when (toInteger (maxBound :: Int) < off) (throwIO err) where
-	err = IO.IOError Nothing IO.InvalidArgument "" "offset > (maxBound :: Int)" Nothing Nothing
+  err = IO.IOError Nothing IO.InvalidArgument "" "offset > (maxBound :: Int)" Nothing Nothing
 
 setDeviceSize :: Device -> Integer -> IO ()
 setDeviceSize (Device mode bytes_var _) size = checkSize >> setBytes where
-	intSize :: Int
-	intSize = fromInteger size
-	
-	checkSize = when (size > toInteger (maxBound :: Int)) $ do
-		throwIO (IO.IOError Nothing IO.InvalidArgument "" "size > (maxBound :: Int)" Nothing Nothing)
-	
-	setBytes = MVar.modifyMVar_ bytes_var $ \bytes -> case mode of
-		IO.ReadMode -> throwIO (IO.IOError Nothing IO.IllegalOperation "" "handle in ReadMode" Nothing Nothing)
-		IO.WriteMode -> return (Data.ByteString.replicate intSize 0)
-		IO.ReadWriteMode -> return (clip bytes)
-		IO.AppendMode -> return (clip bytes)
-	
-	clip bytes = case intSize - Data.ByteString.length bytes of
-		padLen | padLen > 0 -> Data.ByteString.append bytes (Data.ByteString.replicate padLen 0)
-		_ -> Data.ByteString.take intSize bytes
+  intSize :: Int
+  intSize = fromInteger size
 
+  checkSize = when (size > toInteger (maxBound :: Int)) $ do
+    throwIO (IO.IOError Nothing IO.InvalidArgument "" "size > (maxBound :: Int)" Nothing Nothing)
+
+  setBytes = MVar.modifyMVar_ bytes_var $ \bytes -> case mode of
+    IO.ReadMode -> throwIO (IO.IOError Nothing IO.IllegalOperation "" "handle in ReadMode" Nothing Nothing)
+    IO.WriteMode -> return (Data.ByteString.replicate intSize 0)
+    IO.ReadWriteMode -> return (clip bytes)
+    IO.AppendMode -> return (clip bytes)
+
+  clip bytes = case intSize - Data.ByteString.length bytes of
+    padLen | padLen > 0 -> Data.ByteString.append bytes (Data.ByteString.replicate padLen 0)
+    _ -> Data.ByteString.take intSize bytes
+
+instance IO.RawIO Device where
+  read             = error "Raw IO is not implemented for knobs"
+  readNonBlocking  = error "Raw IO is not implemented for knobs"
+  write            = error "Raw IO is not implemented for knobs"
+  writeNonBlocking = error "Raw IO is not implemented for knobs"
+
 instance IO.BufferedIO Device where
-	newBuffer _ = IO.newByteBuffer 4096
-	
-	fillReadBuffer dev buf = do
-		(numRead, newBuf) <- IO.fillReadBuffer0 dev buf
-		return (maybe 0 id numRead, newBuf)
-	
-	fillReadBuffer0 (Device _ bytes_var pos_var) buf = do
-		MVar.withMVar bytes_var $ \bytes -> do
-			MVar.modifyMVar pos_var $ \pos -> do
-				if pos >= Data.ByteString.length bytes
-					then return (pos, (Nothing, buf))
-					else do
-						let chunk = Data.ByteString.take (IO.bufSize buf) (Data.ByteString.drop pos bytes)
-						unsafeUseAsCStringLen chunk $ \(chunkPtr, chunkLen) -> do
-							Foreign.withForeignPtr (IO.bufRaw buf) $ \ptr -> do
-								Foreign.copyArray ptr (Foreign.castPtr chunkPtr) chunkLen
-							return (pos + chunkLen, (Just chunkLen, (buf { IO.bufL = 0, IO.bufR = chunkLen })))
-	
-	flushWriteBuffer (Device _ bytes_var pos_var) buf = do
-		MVar.modifyMVar_ bytes_var $ \bytes -> do
-			MVar.modifyMVar pos_var $ \pos -> do
-				let (before, after) = Data.ByteString.splitAt pos bytes
-				let padding = Data.ByteString.replicate (pos - Data.ByteString.length before) 0
-				
-				let bufStart ptr = Foreign.castPtr (Foreign.plusPtr ptr (IO.bufL buf))
-				let bufLen = IO.bufR buf - IO.bufL buf
-				bufBytes <- Foreign.withForeignPtr (IO.bufRaw buf) (\ptr ->
-					Data.ByteString.packCStringLen (bufStart ptr, bufLen))
-				let newBytes = Data.ByteString.concat [before, padding, bufBytes, Data.ByteString.drop bufLen after]
-				return (pos + bufLen, newBytes)
-		return (buf { IO.bufL = 0, IO.bufR = 0 })
-	
-	flushWriteBuffer0 dev buf = do
-		newBuf <- IO.flushWriteBuffer dev buf
-		return (IO.bufR buf - IO.bufL buf, newBuf)
+  newBuffer _ = IO.newByteBuffer 4096
 
+  fillReadBuffer dev buf = do
+    (numRead, newBuf) <- IO.fillReadBuffer0 dev buf
+    return (maybe 0 id numRead, newBuf)
+
+  fillReadBuffer0 (Device _ bytes_var pos_var) buf = do
+    MVar.withMVar bytes_var $ \bytes -> do
+      MVar.modifyMVar pos_var $ \pos -> do
+        if pos >= Data.ByteString.length bytes
+          then return (pos, (Nothing, buf))
+          else do
+            let chunk = Data.ByteString.take (IO.bufSize buf) (Data.ByteString.drop pos bytes)
+            unsafeUseAsCStringLen chunk $ \(chunkPtr, chunkLen) -> do
+              Foreign.withForeignPtr (IO.bufRaw buf) $ \ptr -> do
+                Foreign.copyArray ptr (Foreign.castPtr chunkPtr) chunkLen
+              return (pos + chunkLen, (Just chunkLen, (buf { IO.bufL = 0, IO.bufR = chunkLen })))
+
+  flushWriteBuffer (Device _ bytes_var pos_var) buf = do
+    MVar.modifyMVar_ bytes_var $ \bytes -> do
+      MVar.modifyMVar pos_var $ \pos -> do
+        let (before, after) = Data.ByteString.splitAt pos bytes
+        let padding = Data.ByteString.replicate (pos - Data.ByteString.length before) 0
+
+        let bufStart ptr = Foreign.castPtr (Foreign.plusPtr ptr (IO.bufL buf))
+        let bufLen = IO.bufR buf - IO.bufL buf
+        bufBytes <- Foreign.withForeignPtr (IO.bufRaw buf) (\ptr ->
+          Data.ByteString.packCStringLen (bufStart ptr, bufLen))
+        let newBytes = Data.ByteString.concat [before, padding, bufBytes, Data.ByteString.drop bufLen after]
+        return (pos + bufLen, newBytes)
+    return (buf { IO.bufL = 0, IO.bufR = 0 })
+
+  flushWriteBuffer0 dev buf = do
+    newBuf <- IO.flushWriteBuffer dev buf
+    return (IO.bufR buf - IO.bufL buf, newBuf)
+
 newKnob :: MonadIO m => ByteString -> m Knob
 newKnob bytes = do
-	var <- liftIO (MVar.newMVar bytes)
-	return (Knob var)
+  var <- liftIO (MVar.newMVar bytes)
+  return (Knob var)
 
 getContents :: MonadIO m => Knob -> m ByteString
 getContents (Knob var) = liftIO (MVar.readMVar var)
@@ -167,11 +173,11 @@
               -> String -- ^ Filename shown in error messages
               -> IO.IOMode -> m IO.Handle
 newFileHandle (Knob var) name mode = liftIO $ do
-	startPosition <- MVar.withMVar var $ \bytes -> return $ case mode of
-		IO.AppendMode -> Data.ByteString.length bytes
-		_ -> 0
-	posVar <- MVar.newMVar startPosition
-	IO.mkFileHandle (Device mode var posVar) name mode Nothing IO.noNewlineTranslation
+  startPosition <- MVar.withMVar var $ \bytes -> return $ case mode of
+    IO.AppendMode -> Data.ByteString.length bytes
+    _ -> 0
+  posVar <- MVar.newMVar startPosition
+  IO.mkFileHandle (Device mode var posVar) name mode Nothing IO.noNewlineTranslation
 
 -- | See 'newFileHandle'.
 withFileHandle :: MonadIO m
diff --git a/tests/KnobTests.hs b/tests/KnobTests.hs
--- a/tests/KnobTests.hs
+++ b/tests/KnobTests.hs
@@ -5,9 +5,9 @@
 --
 -- See license.txt for details
 module Main
-	( tests
-	, main
-	) where
+  ( tests
+  , main
+  ) where
 
 import           Control.Monad.IO.Class (liftIO)
 import qualified Data.ByteString
@@ -24,315 +24,315 @@
 main = Test.Chell.defaultMain tests
 
 tests :: [Suite]
-tests = [test_File, test_Duplex]
+tests = [suite_File, suite_Duplex]
 
-test_File :: Suite
-test_File = suite "file"
-	[ suite "read"
-		[ test_ReadFromStart
-		, test_ReadFromOffset
-		, test_ReadToEOF
-		, test_ReadPastEOF
-		]
-	, suite "write"
-		[ test_WriteFromStart
-		, test_WriteFromOffset
-		, test_WritePastEOF
-		, test_WriteAppended
-		]
-	, suite "seek"
-		[ test_SeekAbsolute
-		, test_SeekRelative
-		, test_SeekFromEnd
-		, test_SeekBeyondMaxInt
-		]
-	, suite "setSize"
-		[ test_SetSize_Read
-		, test_SetSize_Write
-		, test_SetSize_ReadWrite
-		, test_SetSize_Append
-		]
-	
-	, test_Ready
-	, test_Close
-	, test_SetContents
-	, test_WithFileHandle
-	]
+suite_File :: Suite
+suite_File = suite "file" $ concatMap suiteTests
+  [ suite "read"
+    [ test_ReadFromStart
+    , test_ReadFromOffset
+    , test_ReadToEOF
+    , test_ReadPastEOF
+    ]
+  , suite "write"
+    [ test_WriteFromStart
+    , test_WriteFromOffset
+    , test_WritePastEOF
+    , test_WriteAppended
+    ]
+  , suite "seek"
+    [ test_SeekAbsolute
+    , test_SeekRelative
+    , test_SeekFromEnd
+    , test_SeekBeyondMaxInt
+    ]
+  , suite "setSize"
+    [ test_SetSize_Read
+    , test_SetSize_Write
+    , test_SetSize_ReadWrite
+    , test_SetSize_Append
+    ]
+  ]
+  ++ [ test_Ready
+     , test_Close
+     , test_SetContents
+     , test_WithFileHandle
+     ]
 
-test_ReadFromStart :: Suite
+test_ReadFromStart :: Test
 test_ReadFromStart = assertions "from-start" $ do
-	k <- newKnob "abcde"
-	h <- newFileHandle k "foo.txt" ReadMode
-	
-	bytes <- liftIO $ Data.ByteString.hGet h 3
-	$expect (equal bytes "abc")
-	
-	off <- liftIO $ hTell h
-	$expect (equal off 3)
+  k <- newKnob "abcde"
+  h <- newFileHandle k "foo.txt" ReadMode
 
-test_ReadFromOffset :: Suite
+  bytes <- liftIO $ Data.ByteString.hGet h 3
+  $expect (equal bytes "abc")
+
+  off <- liftIO $ hTell h
+  $expect (equal off 3)
+
+test_ReadFromOffset :: Test
 test_ReadFromOffset = assertions "from-offset" $ do
-	k <- newKnob "abcde"
-	h <- newFileHandle k "foo.txt" ReadMode
-	
-	liftIO $ hSeek h AbsoluteSeek 1
-	bytes <- liftIO $ Data.ByteString.hGet h 3
-	$expect (equal bytes "bcd")
-	
-	off <- liftIO $ hTell h
-	$expect (equal off 4)
+  k <- newKnob "abcde"
+  h <- newFileHandle k "foo.txt" ReadMode
 
-test_ReadToEOF :: Suite
+  liftIO $ hSeek h AbsoluteSeek 1
+  bytes <- liftIO $ Data.ByteString.hGet h 3
+  $expect (equal bytes "bcd")
+
+  off <- liftIO $ hTell h
+  $expect (equal off 4)
+
+test_ReadToEOF :: Test
 test_ReadToEOF = assertions "to-eof" $ do
-	k <- newKnob "abcde"
-	h <- newFileHandle k "foo.txt" ReadMode
-	
-	bytes <- liftIO $ Data.ByteString.hGet h 10
-	$expect (equal bytes "abcde")
-	
-	off <- liftIO $ hTell h
-	$expect (equal off 5)
+  k <- newKnob "abcde"
+  h <- newFileHandle k "foo.txt" ReadMode
 
-test_ReadPastEOF :: Suite
+  bytes <- liftIO $ Data.ByteString.hGet h 10
+  $expect (equal bytes "abcde")
+
+  off <- liftIO $ hTell h
+  $expect (equal off 5)
+
+test_ReadPastEOF :: Test
 test_ReadPastEOF = assertions "past-eof" $ do
-	k <- newKnob "abcde"
-	h <- newFileHandle k "foo.txt" ReadMode
-	
-	liftIO $ hSeek h AbsoluteSeek 10
-	bytes <- liftIO $ Data.ByteString.hGet h 10
-	$expect (equal bytes "")
-	
-	off <- liftIO $ hTell h
-	$expect (equal off 10)
+  k <- newKnob "abcde"
+  h <- newFileHandle k "foo.txt" ReadMode
 
-test_WriteFromStart :: Suite
+  liftIO $ hSeek h AbsoluteSeek 10
+  bytes <- liftIO $ Data.ByteString.hGet h 10
+  $expect (equal bytes "")
+
+  off <- liftIO $ hTell h
+  $expect (equal off 10)
+
+test_WriteFromStart :: Test
 test_WriteFromStart = assertions "from-start" $ do
-	k <- newKnob ""
-	h <- newFileHandle k "foo.txt" WriteMode
-	liftIO $ hSetBuffering h NoBuffering
-	
-	liftIO $ Data.ByteString.hPut h "abcde"
-	bytes <- Data.Knob.getContents k
-	$expect (equal bytes "abcde")
+  k <- newKnob ""
+  h <- newFileHandle k "foo.txt" WriteMode
+  liftIO $ hSetBuffering h NoBuffering
 
-test_WriteFromOffset :: Suite
+  liftIO $ Data.ByteString.hPut h "abcde"
+  bytes <- Data.Knob.getContents k
+  $expect (equal bytes "abcde")
+
+test_WriteFromOffset :: Test
 test_WriteFromOffset = assertions "from-offset" $ do
-	k <- newKnob ""
-	h <- newFileHandle k "foo.txt" WriteMode
-	liftIO $ hSetBuffering h NoBuffering
-	
-	liftIO $ Data.ByteString.hPut h "abcde"
-	liftIO $ hSeek h AbsoluteSeek 2
-	liftIO $ Data.ByteString.hPut h "abcde"
-	
-	bytes <- Data.Knob.getContents k
-	$expect (equal bytes "ababcde")
+  k <- newKnob ""
+  h <- newFileHandle k "foo.txt" WriteMode
+  liftIO $ hSetBuffering h NoBuffering
 
-test_WritePastEOF :: Suite
+  liftIO $ Data.ByteString.hPut h "abcde"
+  liftIO $ hSeek h AbsoluteSeek 2
+  liftIO $ Data.ByteString.hPut h "abcde"
+
+  bytes <- Data.Knob.getContents k
+  $expect (equal bytes "ababcde")
+
+test_WritePastEOF :: Test
 test_WritePastEOF = assertions "past-eof" $ do
-	k <- newKnob ""
-	h <- newFileHandle k "foo.txt" WriteMode
-	liftIO $ hSetBuffering h NoBuffering
-	
-	liftIO $ hSeek h AbsoluteSeek 2
-	liftIO $ Data.ByteString.hPut h "abcde"
-	bytes <- Data.Knob.getContents k
-	$expect (equal bytes "\0\0abcde")
+  k <- newKnob ""
+  h <- newFileHandle k "foo.txt" WriteMode
+  liftIO $ hSetBuffering h NoBuffering
 
-test_WriteAppended :: Suite
+  liftIO $ hSeek h AbsoluteSeek 2
+  liftIO $ Data.ByteString.hPut h "abcde"
+  bytes <- Data.Knob.getContents k
+  $expect (equal bytes "\0\0abcde")
+
+test_WriteAppended :: Test
 test_WriteAppended = assertions "appended" $ do
-	k <- newKnob "foo"
-	h <- newFileHandle k "foo.txt" AppendMode
-	liftIO $ hSetBuffering h NoBuffering
-	
-	liftIO $ Data.ByteString.hPut h "bar"
-	bytes <- Data.Knob.getContents k
-	$expect (equal bytes "foobar")
+  k <- newKnob "foo"
+  h <- newFileHandle k "foo.txt" AppendMode
+  liftIO $ hSetBuffering h NoBuffering
 
-test_SeekAbsolute :: Suite
+  liftIO $ Data.ByteString.hPut h "bar"
+  bytes <- Data.Knob.getContents k
+  $expect (equal bytes "foobar")
+
+test_SeekAbsolute :: Test
 test_SeekAbsolute = assertions "absolute" $ do
-	k <- newKnob ""
-	h <- newFileHandle k "foo.txt" ReadMode
-	
-	before <- liftIO $ hTell h
-	liftIO $ hSeek h AbsoluteSeek 2
-	after <- liftIO $ hTell h
-	
-	$expect (equal before 0)
-	$expect (equal after 2)
+  k <- newKnob ""
+  h <- newFileHandle k "foo.txt" ReadMode
 
-test_SeekRelative :: Suite
+  before <- liftIO $ hTell h
+  liftIO $ hSeek h AbsoluteSeek 2
+  after <- liftIO $ hTell h
+
+  $expect (equal before 0)
+  $expect (equal after 2)
+
+test_SeekRelative :: Test
 test_SeekRelative = assertions "relative" $ do
-	k <- newKnob ""
-	h <- newFileHandle k "foo.txt" ReadMode
-	
-	before <- liftIO $ hTell h
-	liftIO $ hSeek h RelativeSeek 2
-	after1 <- liftIO $ hTell h
-	liftIO $ hSeek h RelativeSeek 2
-	after2 <- liftIO $ hTell h
-	
-	$expect (equal before 0)
-	$expect (equal after1 2)
-	$expect (equal after2 4)
+  k <- newKnob ""
+  h <- newFileHandle k "foo.txt" ReadMode
 
-test_SeekFromEnd :: Suite
+  before <- liftIO $ hTell h
+  liftIO $ hSeek h RelativeSeek 2
+  after1 <- liftIO $ hTell h
+  liftIO $ hSeek h RelativeSeek 2
+  after2 <- liftIO $ hTell h
+
+  $expect (equal before 0)
+  $expect (equal after1 2)
+  $expect (equal after2 4)
+
+test_SeekFromEnd :: Test
 test_SeekFromEnd = assertions "from-end" $ do
-	k <- newKnob "abcde"
-	h <- newFileHandle k "foo.txt" ReadMode
-	
-	before <- liftIO $ hTell h
-	liftIO $ hSeek h SeekFromEnd (- 2)
-	after <- liftIO $ hTell h
-	
-	$expect (equal before 0)
-	$expect (equal after 3)
+  k <- newKnob "abcde"
+  h <- newFileHandle k "foo.txt" ReadMode
 
-test_SeekBeyondMaxInt :: Suite
+  before <- liftIO $ hTell h
+  liftIO $ hSeek h SeekFromEnd (- 2)
+  after <- liftIO $ hTell h
+
+  $expect (equal before 0)
+  $expect (equal after 3)
+
+test_SeekBeyondMaxInt :: Test
 test_SeekBeyondMaxInt = assertions "beyond-max-int" $ do
-	k <- newKnob "abcde"
-	h <- newFileHandle k "foo.txt" ReadMode
-	
-	let intPlusOne = toInteger (maxBound :: Int) + 1
-	
-	$expect $ throwsEq
-		(GHC.IOError (Just h) GHC.InvalidArgument "hSeek" "offset > (maxBound :: Int)" Nothing (Just "foo.txt"))
-		(hSeek h AbsoluteSeek intPlusOne)
-	$expect $ throwsEq
-		(GHC.IOError (Just h) GHC.InvalidArgument "hSeek" "offset > (maxBound :: Int)" Nothing (Just "foo.txt"))
-		(hSeek h RelativeSeek intPlusOne)
-	
-	-- testing this with real contents is difficult/impossible on a
-	-- 64-bit system, so use an unsafe function to corrupt the knob's
-	-- internal buffer first.
-	hugeBytes <- liftIO (unsafePackCStringLen (nullPtr, maxBound))
-	liftIO $ hSeek h AbsoluteSeek (intPlusOne - 1)
-	setContents k hugeBytes
-	$expect $ throwsEq
-		(GHC.IOError (Just h) GHC.InvalidArgument "hSeek" "offset > (maxBound :: Int)" Nothing (Just "foo.txt"))
-		(hSeek h SeekFromEnd 2)
+  k <- newKnob "abcde"
+  h <- newFileHandle k "foo.txt" ReadMode
 
-test_Ready :: Suite
+  let intPlusOne = toInteger (maxBound :: Int) + 1
+
+  $expect $ throwsEq
+    (GHC.IOError (Just h) GHC.InvalidArgument "hSeek" "offset > (maxBound :: Int)" Nothing (Just "foo.txt"))
+    (hSeek h AbsoluteSeek intPlusOne)
+  $expect $ throwsEq
+    (GHC.IOError (Just h) GHC.InvalidArgument "hSeek" "offset > (maxBound :: Int)" Nothing (Just "foo.txt"))
+    (hSeek h RelativeSeek intPlusOne)
+
+  -- testing this with real contents is difficult/impossible on a
+  -- 64-bit system, so use an unsafe function to corrupt the knob's
+  -- internal buffer first.
+  hugeBytes <- liftIO (unsafePackCStringLen (nullPtr, maxBound))
+  liftIO $ hSeek h AbsoluteSeek (intPlusOne - 1)
+  setContents k hugeBytes
+  $expect $ throwsEq
+    (GHC.IOError (Just h) GHC.InvalidArgument "hSeek" "offset > (maxBound :: Int)" Nothing (Just "foo.txt"))
+    (hSeek h SeekFromEnd 2)
+
+test_Ready :: Test
 test_Ready = assertions "ready" $ do
-	k <- newKnob "abcde"
-	h <- newFileHandle k "foo.txt" ReadMode
-	
-	ready <- liftIO $ hReady h
-	$expect ready
-	
-	_ <- liftIO $ Data.ByteString.hGet h 10
-	$expect $ throwsEq
-		(GHC.IOError (Just h) GHC.EOF "hWaitForInput" "" Nothing (Just "foo.txt"))
-		(hReady h)
+  k <- newKnob "abcde"
+  h <- newFileHandle k "foo.txt" ReadMode
 
-test_Close :: Suite
+  ready <- liftIO $ hReady h
+  $expect ready
+
+  _ <- liftIO $ Data.ByteString.hGet h 10
+  $expect $ throwsEq
+    (GHC.IOError (Just h) GHC.EOF "hWaitForInput" "" Nothing (Just "foo.txt"))
+    (hReady h)
+
+test_Close :: Test
 test_Close = assertions "close" $ do
-	k <- newKnob "abcde"
-	h <- newFileHandle k "foo.txt" ReadMode
-	
-	liftIO $ hClose h
-	$expect $ throwsEq
-		(GHC.IOError (Just h) GHC.IllegalOperation "hGetBuf" "handle is closed" Nothing (Just "foo.txt"))
-		(Data.ByteString.hGet h 1)
-	$expect $ throwsEq
-		(GHC.IOError (Just h) GHC.IllegalOperation "hWaitForInput" "handle is closed" Nothing (Just "foo.txt"))
-		(hReady h)
+  k <- newKnob "abcde"
+  h <- newFileHandle k "foo.txt" ReadMode
 
-test_SetSize_Read :: Suite
+  liftIO $ hClose h
+  $expect $ throwsEq
+    (GHC.IOError (Just h) GHC.IllegalOperation "hGetBuf" "handle is closed" Nothing (Just "foo.txt"))
+    (Data.ByteString.hGet h 1)
+  $expect $ throwsEq
+    (GHC.IOError (Just h) GHC.IllegalOperation "hWaitForInput" "handle is closed" Nothing (Just "foo.txt"))
+    (hReady h)
+
+test_SetSize_Read :: Test
 test_SetSize_Read = assertions "ReadMode" $ do
-	k <- newKnob "abcde"
-	h <- newFileHandle k "foo.txt" ReadMode
-	
-	let intPlusOne = toInteger (maxBound :: Int) + 1
-	$expect $ throwsEq
-		(GHC.IOError (Just h) GHC.InvalidArgument "hSetFileSize" "size > (maxBound :: Int)" Nothing (Just "foo.txt"))
-		(hSetFileSize h intPlusOne)
-	
-	$expect $ throwsEq
-		(GHC.IOError (Just h) GHC.IllegalOperation "hSetFileSize" "handle in ReadMode" Nothing (Just "foo.txt"))
-		(hSetFileSize h 2)
+  k <- newKnob "abcde"
+  h <- newFileHandle k "foo.txt" ReadMode
 
-test_SetSize_Write :: Suite
+  let intPlusOne = toInteger (maxBound :: Int) + 1
+  $expect $ throwsEq
+    (GHC.IOError (Just h) GHC.InvalidArgument "hSetFileSize" "size > (maxBound :: Int)" Nothing (Just "foo.txt"))
+    (hSetFileSize h intPlusOne)
+
+  $expect $ throwsEq
+    (GHC.IOError (Just h) GHC.IllegalOperation "hSetFileSize" "handle in ReadMode" Nothing (Just "foo.txt"))
+    (hSetFileSize h 2)
+
+test_SetSize_Write :: Test
 test_SetSize_Write = assertions "WriteMode" $ do
-	k <- newKnob "abcde"
-	h <- newFileHandle k "foo.txt" WriteMode
-	
-	let intPlusOne = toInteger (maxBound :: Int) + 1
-	$expect $ throwsEq
-		(GHC.IOError (Just h) GHC.InvalidArgument "hSetFileSize" "size > (maxBound :: Int)" Nothing (Just "foo.txt"))
-		(hSetFileSize h intPlusOne)
-	
-	-- Resets contents to all NULL, regardless of offset
-	liftIO $ hSeek h AbsoluteSeek 2
-	liftIO $ hSetFileSize h 4
-	
-	bytes <- Data.Knob.getContents k
-	$expect (equal bytes "\0\0\0\0")
+  k <- newKnob "abcde"
+  h <- newFileHandle k "foo.txt" WriteMode
 
-test_SetSize_ReadWrite :: Suite
+  let intPlusOne = toInteger (maxBound :: Int) + 1
+  $expect $ throwsEq
+    (GHC.IOError (Just h) GHC.InvalidArgument "hSetFileSize" "size > (maxBound :: Int)" Nothing (Just "foo.txt"))
+    (hSetFileSize h intPlusOne)
+
+  -- Resets contents to all NULL, regardless of offset
+  liftIO $ hSeek h AbsoluteSeek 2
+  liftIO $ hSetFileSize h 4
+
+  bytes <- Data.Knob.getContents k
+  $expect (equal bytes "\0\0\0\0")
+
+test_SetSize_ReadWrite :: Test
 test_SetSize_ReadWrite = assertions "ReadWriteMode" $ do
-	k <- newKnob "abcde"
-	h <- newFileHandle k "foo.txt" ReadWriteMode
-	
-	let intPlusOne = toInteger (maxBound :: Int) + 1
-	$expect $ throwsEq
-		(GHC.IOError (Just h) GHC.InvalidArgument "hSetFileSize" "size > (maxBound :: Int)" Nothing (Just "foo.txt"))
-		(hSetFileSize h intPlusOne)
-	
-	-- Truncates contents, regardless of offset
-	do
-		liftIO $ hSeek h AbsoluteSeek 2
-		liftIO $ hSetFileSize h 4
-		bytes <- Data.Knob.getContents k
-		$expect (equal bytes "abcd")
-	
-	do
-		liftIO $ hSetFileSize h 6
-		bytes <- Data.Knob.getContents k
-		$expect (equal bytes "abcd\0\0")
+  k <- newKnob "abcde"
+  h <- newFileHandle k "foo.txt" ReadWriteMode
 
-test_SetSize_Append :: Suite
+  let intPlusOne = toInteger (maxBound :: Int) + 1
+  $expect $ throwsEq
+    (GHC.IOError (Just h) GHC.InvalidArgument "hSetFileSize" "size > (maxBound :: Int)" Nothing (Just "foo.txt"))
+    (hSetFileSize h intPlusOne)
+
+  -- Truncates contents, regardless of offset
+  do
+    liftIO $ hSeek h AbsoluteSeek 2
+    liftIO $ hSetFileSize h 4
+    bytes <- Data.Knob.getContents k
+    $expect (equal bytes "abcd")
+
+  do
+    liftIO $ hSetFileSize h 6
+    bytes <- Data.Knob.getContents k
+    $expect (equal bytes "abcd\0\0")
+
+test_SetSize_Append :: Test
 test_SetSize_Append = assertions "AppendMode" $ do
-	k <- newKnob "abcde"
-	h <- newFileHandle k "foo.txt" AppendMode
-	
-	let intPlusOne = toInteger (maxBound :: Int) + 1
-	$expect $ throwsEq
-		(GHC.IOError (Just h) GHC.InvalidArgument "hSetFileSize" "size > (maxBound :: Int)" Nothing (Just "foo.txt"))
-		(hSetFileSize h intPlusOne)
-	
-	do
-		liftIO $ hSetFileSize h 4
-		bytes <- Data.Knob.getContents k
-		$expect (equal bytes "abcd")
-	
-	do
-		liftIO $ hSetFileSize h 6
-		bytes <- Data.Knob.getContents k
-		$expect (equal bytes "abcd\0\0")
+  k <- newKnob "abcde"
+  h <- newFileHandle k "foo.txt" AppendMode
 
-test_SetContents :: Suite
+  let intPlusOne = toInteger (maxBound :: Int) + 1
+  $expect $ throwsEq
+    (GHC.IOError (Just h) GHC.InvalidArgument "hSetFileSize" "size > (maxBound :: Int)" Nothing (Just "foo.txt"))
+    (hSetFileSize h intPlusOne)
+
+  do
+    liftIO $ hSetFileSize h 4
+    bytes <- Data.Knob.getContents k
+    $expect (equal bytes "abcd")
+
+  do
+    liftIO $ hSetFileSize h 6
+    bytes <- Data.Knob.getContents k
+    $expect (equal bytes "abcd\0\0")
+
+test_SetContents :: Test
 test_SetContents = assertions "setContents" $ do
-	k <- newKnob "abcde"
-	before <- Data.Knob.getContents k
-	setContents k "foo"
-	after <- Data.Knob.getContents k
-	
-	$expect (equal before "abcde")
-	$expect (equal after "foo")
+  k <- newKnob "abcde"
+  before <- Data.Knob.getContents k
+  setContents k "foo"
+  after <- Data.Knob.getContents k
 
-test_WithFileHandle :: Suite
+  $expect (equal before "abcde")
+  $expect (equal after "foo")
+
+test_WithFileHandle :: Test
 test_WithFileHandle = assertions "withFileHandle" $ do
-	k <- newKnob ""
-	h <- withFileHandle k "test.txt" WriteMode $ \h -> do
-		Data.ByteString.hPut h "abcde"
-		return h
-	
-	bytes <- Data.Knob.getContents k
-	$expect (equal bytes "abcde")
-	
-	closed <- liftIO $ hIsClosed h
-	$expect closed
+  k <- newKnob ""
+  h <- withFileHandle k "test.txt" WriteMode $ \h -> do
+    Data.ByteString.hPut h "abcde"
+    return h
 
-test_Duplex :: Suite
-test_Duplex = suite "duplex" []
+  bytes <- Data.Knob.getContents k
+  $expect (equal bytes "abcde")
+
+  closed <- liftIO $ hIsClosed h
+  $expect closed
+
+suite_Duplex :: Suite
+suite_Duplex = suite "duplex" []
diff --git a/tests/knob-tests.cabal b/tests/knob-tests.cabal
--- a/tests/knob-tests.cabal
+++ b/tests/knob-tests.cabal
@@ -1,7 +1,7 @@
 name: knob-tests
 version: 0
 build-type: Simple
-cabal-version: >= 1.6
+cabal-version: >= 1.8
 
 flag coverage
   default: False
@@ -11,12 +11,13 @@
   main-is: KnobTests.hs
   ghc-options: -Wall
   hs-source-dirs: ../lib,.
+  other-modules: Data.Knob
 
   if flag(coverage)
     ghc-options: -fhpc
 
   build-depends:
-      base >= 4.2 && < 5.0
-    , bytestring >= 0.9 && < 0.10
-    , chell >= 0.2 && < 0.3
-    , transformers >= 0.2 && < 0.3
+      base >= 4.15 && < 5
+    , bytestring >= 0.9 && < 0.12
+    , chell >= 0.2 && < 0.6
+    , transformers >= 0.2 && < 0.7
