diff --git a/LICENSE b/LICENSE
--- a/LICENSE
+++ b/LICENSE
@@ -1,55 +1,34 @@
-# Blue Oak Model License
-
-Version 1.0.0
-
-## Purpose
-
-This license gives everyone as much permission to work with
-this software as possible, while protecting contributors
-from liability.
-
-## Acceptance
-
-In order to receive this license, you must agree to its
-rules.  The rules of this license are both obligations
-under that agreement and conditions to your license.
-You must not do anything with this software that triggers
-a rule that you cannot or will not follow.
-
-## Copyright
-
-Each contributor licenses you to do everything with this
-software that would otherwise infringe that contributor's
-copyright in it.
-
-## Notices
-
-You must ensure that everyone who gets a copy of
-any part of this software from you, with or without
-changes, also gets the text of this license or a link to
-<https://blueoakcouncil.org/license/1.0.0>.
-
-## Excuse
+Copyright (c) 2020 Torsten Schmits
 
-If anyone notifies you in writing that you have not
-complied with [Notices](#notices), you can keep your
-license by taking all practical steps to comply within 30
-days after the notice.  If you do not do so, your license
-ends immediately.
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
+following conditions are met:
 
-## Patent
+  1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
+  disclaimer.
+  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
+  disclaimer in the documentation and/or other materials provided with the distribution.
 
-Each contributor licenses you to do everything with this
-software that would otherwise infringe any patent claims
-they can license or become able to license.
+Subject to the terms and conditions of this license, each copyright holder and contributor hereby grants to those
+receiving rights under this license a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except
+for failure to satisfy the conditions of this license) patent license to make, have made, use, offer to sell, sell,
+import, and otherwise transfer this software, where such license applies only to those patent claims, already acquired
+or hereafter acquired, licensable by such copyright holder or contributor that are necessarily infringed by:
 
-## Reliability
+  (a) their Contribution(s) (the licensed copyrights of copyright holders and non-copyrightable additions of
+  contributors, in source or binary form) alone; or
+  (b) combination of their Contribution(s) with the work of authorship to which such Contribution(s) was added by such
+  copyright holder or contributor, if, at the time the Contribution is added, such addition causes such combination to
+  be necessarily infringed. The patent license shall not apply to any other combinations which include the Contribution.
 
-No contributor can revoke this license.
+Except as expressly stated above, no rights or licenses from any copyright holder or contributor is granted under this
+license, whether expressly, by implication, estoppel or otherwise.
 
-## No Liability
+DISCLAIMER
 
-***As far as the law allows, this software comes as is,
-without any warranty or condition, and no contributor
-will be liable to anyone for any damages related to this
-software or this license, under any kind of legal claim.***
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/lib/Prelude.hs b/lib/Prelude.hs
deleted file mode 100644
--- a/lib/Prelude.hs
+++ /dev/null
@@ -1,5 +0,0 @@
-module Prelude (
-  module Ribosome.PreludeExport
-) where
-
-import Ribosome.PreludeExport
diff --git a/lib/Ribosome/Test/Await.hs b/lib/Ribosome/Test/Await.hs
--- a/lib/Ribosome/Test/Await.hs
+++ b/lib/Ribosome/Test/Await.hs
@@ -1,47 +1,37 @@
 module Ribosome.Test.Await where
 
+import Hedgehog (TestT)
 import Control.Exception (throw)
-import Control.Monad.Error.Class (MonadError, catchError)
-import Control.Monad.IO.Class (MonadIO)
-import Control.Monad.Trans.Class (lift)
-import Control.Monad.Trans.Control (MonadBaseControl)
-import Control.Monad.Trans.Except (ExceptT, runExceptT)
-import Test.Framework.AssertM (AssertM)
+import Control.Monad.Error.Class (MonadError (throwError), catchError)
+import Hedgehog.Internal.Property (mkTestT, runTestT, Failure, Journal)
 
-import Ribosome.Control.Concurrent.Wait (WaitError(Thrown), waitIODef)
+import Ribosome.Control.Concurrent.Wait (WaitError(Thrown, ConditionUnmet, NotStarted), waitIODef)
 
 await ::
-  Show e =>
+  ∀ e a b m .
   MonadError e m =>
   MonadIO m =>
-  MonadFail m =>
   MonadBaseControl IO m =>
-  AssertM m =>
-  (a -> m b) ->
+  (a -> TestT m b) ->
   m a ->
-  m b
+  TestT m b
 await assertion acquire = do
-  r <- waitIODef acquire' check'
-  either failure return r
-  where
-    acquire' = catchError (Right <$> acquire) (return . Left . show)
-    check' (Right a) = Right <$> assertion a
-    check' (Left e) = return (Left e)
-    failure (Thrown e) = throw e
-    failure e = fail $ "await failed with " <> show e
-
-await' ::
-  ∀ a b m.
-  MonadIO m =>
-  MonadFail m =>
-  MonadBaseControl IO m =>
-  AssertM (ExceptT () m) =>
-  (a -> m b) ->
-  m a ->
-  m b
-await' assertion acquire = do
-  r <- runExceptT $ await assertion' (lift acquire)
-  either (const $ fail "internal error") return r
+  lift (waitIODef acquire' check') >>= \case
+    Right a -> pure a
+    Left (ConditionUnmet (Left (err, journal))) ->
+      mkTestT (pure (Left err, journal))
+    Left (ConditionUnmet (Right e)) ->
+      throwError e
+    Left (Thrown e) ->
+      throw e
+    Left NotStarted -> fail "await was not started"
   where
-    assertion' :: a -> ExceptT () m b
-    assertion' = lift . assertion
+    acquire' :: m (Either e a)
+    acquire' =
+      catchError (Right <$> acquire) (pure . Left)
+    check' :: Either e a -> m (Either (Either (Failure, Journal) e) b)
+    check' (Right a) = do
+      (result, journal) <- runTestT (assertion a)
+      pure (mapLeft (Left . (,journal)) result)
+    check' (Left e) = do
+      pure (Left (Right e))
diff --git a/lib/Ribosome/Test/Embed.hs b/lib/Ribosome/Test/Embed.hs
--- a/lib/Ribosome/Test/Embed.hs
+++ b/lib/Ribosome/Test/Embed.hs
@@ -1,15 +1,13 @@
 module Ribosome.Test.Embed where
 
-import Control.Concurrent (forkIO)
-import Control.Monad.IO.Class (MonadIO, liftIO)
-import Control.Monad.Reader (runReaderT)
-import Control.Monad.Trans.Except (runExceptT)
+import Chiasma.Test.Tmux (withProcessWait)
+import Control.Concurrent.Async.Lifted (async, cancel, race)
+import Control.Concurrent.Lifted (fork)
+import Control.Exception.Lifted (bracket, try)
 import Control.Monad.Trans.Resource (runResourceT)
-import Data.Default (Default(def))
-import Data.Foldable (traverse_)
-import Data.Functor (void)
-import Data.Maybe (fromMaybe)
-import GHC.IO.Handle (Handle)
+import qualified Data.Map.Strict as Map (fromList, toList, union)
+import Hedgehog (TestT)
+import Hedgehog.Internal.Property (mkTestT, runTestT)
 import Neovim (Neovim, Object)
 import Neovim.API.Text (vim_command)
 import qualified Neovim.Context.Internal as Internal (
@@ -24,7 +22,7 @@
   transitionTo,
   )
 import Neovim.Main (standalone)
-import Neovim.Plugin (Plugin, startPluginThreads)
+import Neovim.Plugin (Plugin(Plugin), startPluginThreads)
 import Neovim.Plugin.Internal (NeovimPlugin, wrapPlugin)
 import Neovim.RPC.Common (RPCConfig, newRPCConfig)
 import Neovim.RPC.EventHandler (runEventHandler)
@@ -44,15 +42,13 @@
   proc,
   setStdin,
   setStdout,
+  startProcess,
+  stopProcess,
   unsafeProcessHandle,
-  withProcess,
   )
-import UnliftIO.Async (async, cancel, race)
-import UnliftIO.Exception (bracket, tryAny)
-import UnliftIO.MVar (MVar)
-import UnliftIO.STM (putTMVar)
 
 import Ribosome.Api.Option (rtpCat)
+import Ribosome.Control.Exception (tryAny)
 import Ribosome.Control.Monad.Ribo (NvimE)
 import Ribosome.Control.Ribosome (Ribosome(Ribosome), newRibosomeTMVar)
 import qualified Ribosome.Data.ErrorReport as ErrorReport (ErrorReport(..))
@@ -62,12 +58,22 @@
 import Ribosome.System.Time (sleep, sleepW)
 import Ribosome.Test.Orphans ()
 
-type Runner m = TestConfig -> m () -> m ()
+type Runner m = ∀ a . TestConfig -> m a -> m a
 
 newtype Vars =
-  Vars [(Text, Object)]
-  deriving (Eq, Show, Semigroup, Monoid, Default)
+  Vars (Map Text Object)
+  deriving (Eq, Show)
+  deriving newtype (Default, Semigroup, Monoid)
 
+-- |left biased
+varsUnion :: Vars -> Vars -> Vars
+varsUnion (Vars v1) (Vars v2) =
+  Vars (Map.union v1 v2)
+
+varsFromList :: [(Text, Object)] -> Vars
+varsFromList =
+  Vars . Map.fromList
+
 data TestConfig =
   TestConfig {
     tcPluginName :: Text,
@@ -80,23 +86,28 @@
   }
 
 instance Default TestConfig where
-  def = TestConfig "ribosome" "test/f/fixtures/rtp" "test/f/temp/log" 10 def def (Vars [])
+  def = TestConfig "ribosome" "test/u/fixtures/rtp" "test/u/temp/log" 10 def def def
 
 defaultTestConfigWith :: Text -> Vars -> TestConfig
 defaultTestConfigWith name vars =
   def { tcPluginName = name, tcVariables = vars }
 
 defaultTestConfig :: Text -> TestConfig
-defaultTestConfig name = defaultTestConfigWith name (Vars [])
+defaultTestConfig name = defaultTestConfigWith name def
 
 setVars :: ∀ m e. NvimE e m => Vars -> m ()
 setVars (Vars vars) =
-  traverse_ set vars
+  traverse_ set (Map.toList vars)
   where
     set :: (Text, Object) -> m ()
-    set = uncurry vimSetVar
+    set =
+      uncurry vimSetVar
 
-setupPluginEnv :: (MonadIO m, NvimE e m) => TestConfig -> m ()
+setupPluginEnv ::
+  MonadIO m =>
+  NvimE e m =>
+  TestConfig ->
+  m ()
 setupPluginEnv (TestConfig _ rtp _ _ _ _ vars) = do
   absRtp <- liftIO $ makeAbsolute (toString rtp)
   rtpCat (toText absRtp)
@@ -108,9 +119,10 @@
 
 killProcess :: Process i o e -> IO ()
 killProcess prc = do
-  let handle = unsafeProcessHandle prc
-  mayPid <- getPid handle
-  traverse_ killPid mayPid
+  void $ try @_ @SomeException do
+    let handle = unsafeProcessHandle prc
+    mayPid <- getPid handle
+    traverse_ killPid mayPid
 
 testNvimProcessConfig :: TestConfig -> ProcessConfig Handle Handle ()
 testNvimProcessConfig TestConfig {..} =
@@ -119,18 +131,29 @@
     args = fromMaybe defaultArgs tcCmdline
     defaultArgs = ["--embed", "-n", "-u", "NONE", "-i", "NONE"]
 
-startHandlers :: Handle -> Handle -> TestConfig -> Internal.Config RPCConfig -> IO (IO ())
-startHandlers stdoutHandle stdinHandle TestConfig{..} nvimConf = do
-  socketReader <- run runSocketReader stdoutHandle
-  eventHandler <- run runEventHandler stdinHandle
+startHandlers ::
+  MonadIO m =>
+  Handle ->
+  Handle ->
+  TestConfig ->
+  Internal.Config RPCConfig ->
+  m (IO ())
+startHandlers stdoutHandle stdinHandle TestConfig{} nvimConf = do
+  socketReader <- liftIO (run runSocketReader stdoutHandle)
+  eventHandler <- liftIO (run runEventHandler stdinHandle)
   atomically $ putTMVar (Internal.globalFunctionMap nvimConf) (Internal.mkFunctionMap [])
-  let stopEventHandlers = traverse_ cancel [socketReader, eventHandler]
+  let stopEventHandlers = traverse_ @[] cancel [socketReader, eventHandler]
   return stopEventHandlers
   where
     run runner hand = async . void $ runner hand emptyConf
     emptyConf = nvimConf { Internal.pluginSettings = Nothing }
 
-startStdioHandlers :: NvimProc -> TestConfig -> Internal.Config RPCConfig -> IO (IO ())
+startStdioHandlers ::
+  MonadIO m =>
+  NvimProc ->
+  TestConfig ->
+  Internal.Config RPCConfig ->
+  m (IO ())
 startStdioHandlers prc =
   startHandlers (getStdout prc) (getStdin prc)
 
@@ -169,115 +192,160 @@
 shutdownNvim _ prc stopEventHandlers = do
   stopEventHandlers
   killProcess prc
-  -- quitNvim testCfg prc
 
 runTest ::
-  RpcHandler e env m =>
+  MonadIO m =>
+  MonadFail m =>
   ReportError e =>
+  RpcHandler e env n =>
+  MonadBaseControl IO m =>
   TestConfig ->
   Internal.Config env ->
-  m () ->
-  IO ()
+  n a ->
+  m a
 runTest TestConfig{..} testCfg thunk = do
-  result <- race (sleepW tcTimeout) (runNeovimThunk testCfg (runExceptT $ native thunk))
-  case result of
-    Right (Right _) -> return ()
+  race (sleepW tcTimeout) (liftIO (runNeovimThunk testCfg (runExceptT $ native thunk))) >>= \case
+    Right (Right a) -> pure a
     Right (Left e) -> fail . toString . unlines . ErrorReport._log . errorReport $ e
     Left _ -> fail $ "test exceeded timeout of " <> show tcTimeout <> " seconds"
 
 runEmbeddedNvim ::
-  RpcHandler e env m =>
+  MonadIO m =>
+  MonadFail m =>
+  MonadBaseControl IO m =>
+  RpcHandler e env n =>
   ReportError e =>
   TestConfig ->
   env ->
-  m () ->
+  n a ->
   NvimProc ->
-  IO ()
+  m a
 runEmbeddedNvim conf ribo thunk prc = do
-  nvimConf <- Internal.newConfig (pure Nothing) newRPCConfig
+  nvimConf <- liftIO (Internal.newConfig (pure Nothing) newRPCConfig)
   let testCfg = Internal.retypeConfig ribo nvimConf
-  bracket (startStdioHandlers prc conf nvimConf) (shutdownNvim testCfg prc) (const $ runTest conf testCfg thunk)
+  bracket (startStdioHandlers prc conf nvimConf) (liftIO . shutdownNvim testCfg prc) (const $ runTest conf testCfg thunk)
 
+withProcessTerm ::
+  MonadIO m =>
+  MonadBaseControl IO m =>
+  ProcessConfig stdin stdout stderr ->
+  (Process stdin stdout stderr -> m a) ->
+  m a
+withProcessTerm config =
+  bracket (startProcess config) (try @_ @SomeException . stopProcess)
+
 runEmbedded ::
-  RpcHandler e env m =>
+  MonadIO m =>
+  MonadFail m =>
+  MonadBaseControl IO m =>
+  RpcHandler e env n =>
   ReportError e =>
   TestConfig ->
   env ->
-  m () ->
-  IO ()
+  n a ->
+  m a
 runEmbedded conf ribo thunk = do
   let pc = testNvimProcessConfig conf
-  withProcess pc $ runEmbeddedNvim conf ribo thunk
+  withProcessWait pc $ runEmbeddedNvim conf ribo thunk
 
 unsafeEmbeddedSpec ::
-  RpcHandler e env m =>
+  MonadIO m =>
+  MonadFail m =>
+  MonadBaseControl IO m =>
+  RpcHandler e env n =>
   ReportError e =>
-  Runner m ->
+  Runner n ->
   TestConfig ->
   env ->
-  m () ->
-  IO ()
+  n a ->
+  m a
 unsafeEmbeddedSpec runner conf s spec =
   runEmbedded conf s $ runner conf spec
 
 unsafeEmbeddedSpecR ::
-  RpcHandler e (Ribosome env) m =>
+  MonadIO m =>
+  MonadFail m =>
   ReportError e =>
-  Runner m ->
+  MonadBaseControl IO m =>
+  RpcHandler e (Ribosome env) n =>
+  Runner n ->
   TestConfig ->
   env ->
-  m () ->
-  IO ()
+  n a ->
+  m a
 unsafeEmbeddedSpecR runner conf env spec = do
   tv <- newRibosomeTMVar env
   let ribo = Ribosome (tcPluginName conf) tv
   unsafeEmbeddedSpec runner conf ribo spec
 
 runPlugin ::
+  MonadIO m =>
+  MonadBaseControl IO m =>
   Handle ->
   Handle ->
   [Neovim () NeovimPlugin] ->
   Internal.Config c ->
-  IO (MVar Internal.StateTransition)
+  m (MVar Internal.StateTransition)
 runPlugin evHandlerHandle sockreaderHandle plugins baseConf = do
-  updateGlobalLogger "Neovim.Plugin" (setLevel ERROR)
-  rpcConf <- newRPCConfig
+  liftIO (updateGlobalLogger "Neovim.Plugin" (setLevel ERROR))
+  rpcConf <- liftIO newRPCConfig
   let conf = Internal.retypeConfig rpcConf baseConf
-  ehTid <- async $ runEventHandler evHandlerHandle conf { Internal.pluginSettings = Nothing }
-  srTid <- async $ runSocketReader sockreaderHandle conf
-  void $ forkIO $ startPluginThreads (Internal.retypeConfig () baseConf) plugins >>= \case
+  ehTid <- (fmap void . async . liftIO) (runEventHandler evHandlerHandle conf { Internal.pluginSettings = Nothing })
+  srTid <- (fmap void . async . liftIO) (runSocketReader sockreaderHandle conf)
+  void $ fork $ liftIO $ startPluginThreads (Internal.retypeConfig () baseConf) plugins >>= \case
     Left e -> do
       putMVar (Internal.transitionTo conf) $ Internal.Failure e
       standalone [ehTid, srTid] conf
     Right (funMapEntries, pluginTids) -> do
       atomically $ putTMVar (Internal.globalFunctionMap conf) (Internal.mkFunctionMap funMapEntries)
       putMVar (Internal.transitionTo conf) Internal.InitSuccess
-      standalone (srTid:ehTid:pluginTids) conf
+      standalone (srTid : ehTid : pluginTids) conf
   return (Internal.transitionTo conf)
 
-runEmbeddedWithPlugin ::
-  RpcHandler e () m =>
+inTestT ::
+  ∀ n m a .
+  TestT n a ->
+  (∀ x . n x -> m x) ->
+  TestT m a
+inTestT ma f =
+  mkTestT (f (runTestT ma))
+
+integrationSpec ::
+  ∀ n m e env a .
+  NvimE e n =>
+  MonadIO n =>
+  MonadIO m =>
+  MonadFail m =>
   ReportError e =>
+  RpcHandler e env n =>
+  MonadBaseControl IO m =>
   TestConfig ->
   Plugin env ->
-  m () ->
-  IO ()
-runEmbeddedWithPlugin conf plugin thunk =
-  withProcess (testNvimProcessConfig conf) run
+  TestT n a ->
+  TestT m a
+integrationSpec conf plugin@(Plugin env _) thunk =
+  inTestT thunk \ na ->
+    withProcessTerm (testNvimProcessConfig conf) (run na)
   where
-    run prc = do
-      nvimConf <- Internal.newConfig (pure Nothing) (pure ())
-      bracket (acquire prc nvimConf) release (const $ runTest conf nvimConf thunk)
+    run :: ∀ x . n x -> Process Handle Handle () -> m x
+    run na prc = do
+      nvimConf <- liftIO (Internal.newConfig (pure Nothing) (pure env))
+      bracket (acquire prc nvimConf) release (const $ runTest conf nvimConf (setupPluginEnv conf *> na))
     acquire prc nvimConf =
-      runPlugin (getStdin prc) (getStdout prc) [wrapPlugin plugin] nvimConf <* sleep 0.5
+      liftIO (runPlugin (getStdin prc) (getStdout prc) [wrapPlugin plugin] nvimConf <* sleep 0.5)
     release transitions =
       tryPutMVar transitions Internal.Quit *> sleep 0.5
 
 integrationSpecDef ::
-  RpcHandler e () m =>
+  NvimE e n =>
+  MonadIO m =>
+  MonadIO n =>
+  MonadFail m =>
   ReportError e =>
+  RpcHandler e env n =>
+  MonadBaseControl IO m =>
   Plugin env ->
-  m () ->
-  IO ()
+  TestT n a ->
+  TestT m a
 integrationSpecDef =
-  runEmbeddedWithPlugin def
+  integrationSpec def
diff --git a/lib/Ribosome/Test/Exists.hs b/lib/Ribosome/Test/Exists.hs
--- a/lib/Ribosome/Test/Exists.hs
+++ b/lib/Ribosome/Test/Exists.hs
@@ -1,8 +1,5 @@
-module Ribosome.Test.Exists(
-  waitForPlugin,
-) where
+module Ribosome.Test.Exists where
 
-import Control.Monad.IO.Class (MonadIO)
 import Data.MessagePack (Object(ObjectInt))
 import Neovim (Neovim, toObject)
 import Neovim.API.Text (vim_call_function)
diff --git a/lib/Ribosome/Test/File.hs b/lib/Ribosome/Test/File.hs
--- a/lib/Ribosome/Test/File.hs
+++ b/lib/Ribosome/Test/File.hs
@@ -1,12 +1,11 @@
 module Ribosome.Test.File where
 
-import Control.Monad.IO.Class (MonadIO, liftIO)
 import qualified Data.ByteString as ByteString (readFile)
 import System.Directory (canonicalizePath, createDirectoryIfMissing, removePathForcibly)
 import System.FilePath ((</>))
 
 testDir :: Text -> IO FilePath
-testDir prefix = canonicalizePath $ "test" </> toString prefix
+testDir = canonicalizePath . toString
 
 -- raises exception if cwd is not the package root so we don't damage anything
 tempDirIO :: Text -> FilePath -> IO FilePath
diff --git a/lib/Ribosome/Test/Input.hs b/lib/Ribosome/Test/Input.hs
--- a/lib/Ribosome/Test/Input.hs
+++ b/lib/Ribosome/Test/Input.hs
@@ -4,6 +4,7 @@
 import Control.Exception.Lifted (bracket)
 
 import Ribosome.Api.Input (syntheticInput)
+import Ribosome.Control.Monad.Ribo (NvimE)
 
 withInput ::
   NvimE e m =>
diff --git a/lib/Ribosome/Test/Orphans.hs b/lib/Ribosome/Test/Orphans.hs
--- a/lib/Ribosome/Test/Orphans.hs
+++ b/lib/Ribosome/Test/Orphans.hs
@@ -2,28 +2,26 @@
 
 module Ribosome.Test.Orphans where
 
-import Control.Monad.IO.Class (liftIO)
-import Control.Monad.IO.Unlift (withRunInIO)
-import Neovim (Neovim)
-import Test.Framework.AssertM (AssertM(..))
+-- import Control.Monad.IO.Unlift (withRunInIO)
+-- import Neovim (Neovim)
 
-import Ribosome.Control.Monad.Ribo (Ribo(Ribo, unRibo))
+-- import Ribosome.Control.Monad.Ribo (Ribo(Ribo, unRibo))
 
-instance AssertM (Neovim e) where
-  genericAssertFailure__ l =
-    liftIO . genericAssertFailure__ l
+-- instance AssertM (Neovim e) where
+--   genericAssertFailure__ l =
+--     liftIO . genericAssertFailure__ l
 
-  genericSubAssert l msg ma =
-    withRunInIO (\f -> genericSubAssert l msg (f ma))
+--   genericSubAssert l msg ma =
+--     withRunInIO (\f -> genericSubAssert l msg (f ma))
 
-instance AssertM (ExceptT e (Neovim s)) where
-  genericAssertFailure__ l = liftIO . genericAssertFailure__ l
+-- instance AssertM (ExceptT e (Neovim s)) where
+--   genericAssertFailure__ l = liftIO . genericAssertFailure__ l
 
-  genericSubAssert l msg =
-    ExceptT . genericSubAssert l msg . runExceptT
+--   genericSubAssert l msg =
+--     ExceptT . genericSubAssert l msg . runExceptT
 
-instance AssertM (Ribo s e) where
-  genericAssertFailure__ l = liftIO . genericAssertFailure__ l
+-- instance AssertM (Ribo s e) where
+--   genericAssertFailure__ l = liftIO . genericAssertFailure__ l
 
-  genericSubAssert l msg =
-    Ribo . ExceptT . genericSubAssert l msg . runExceptT . unRibo
+--   genericSubAssert l msg =
+--     Ribo . ExceptT . genericSubAssert l msg . runExceptT . unRibo
diff --git a/lib/Ribosome/Test/Run.hs b/lib/Ribosome/Test/Run.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Test/Run.hs
@@ -0,0 +1,14 @@
+module Ribosome.Test.Run where
+
+import Hedgehog (TestT, property, test, withTests)
+import Test.Tasty (TestName, TestTree)
+import Test.Tasty.Hedgehog (testProperty)
+
+type UnitTest = TestT IO ()
+
+unitTest ::
+  TestName ->
+  UnitTest ->
+  TestTree
+unitTest desc =
+  testProperty desc . withTests 1 . property . test
diff --git a/lib/Ribosome/Test/Screenshot.hs b/lib/Ribosome/Test/Screenshot.hs
--- a/lib/Ribosome/Test/Screenshot.hs
+++ b/lib/Ribosome/Test/Screenshot.hs
@@ -1,23 +1,19 @@
-{-# OPTIONS_GHC -F -pgmF htfpp #-}
-
 module Ribosome.Test.Screenshot where
 
 import Chiasma.Data.TmuxError (TmuxError)
 import Chiasma.Data.TmuxThunk (TmuxThunk)
 import qualified Chiasma.Test.Screenshot as Chiasma (screenshot)
 import Control.Monad.Catch (MonadMask)
-import Control.Monad.DeepError (MonadDeepError)
 import Control.Monad.Free.Class (MonadFree)
-import Control.Monad.IO.Class (MonadIO)
-import Data.Text (Text)
-import Test.Framework
-import Test.Framework.AssertM (AssertM(..))
+import Hedgehog (TestT, (===))
 
 import Ribosome.Control.Monad.Ribo (MonadRibo, Nvim)
 import Ribosome.Orphans ()
 import Ribosome.Test.Orphans ()
 import Ribosome.Test.Unit (fixture)
 import Ribosome.Tmux.Run (runTmux)
+import Ribosome.System.Time (sleep)
+import Ribosome.Test.Await (await)
 
 screenshot ::
   MonadFree TmuxThunk m =>
@@ -28,23 +24,39 @@
   m (Maybe ([Text], [Text]))
 screenshot name record pane = do
   storage <- fixture "screenshots"
-  Chiasma.screenshot record storage (toString name) pane
+  Chiasma.screenshot record storage name pane
 
 assertScreenshot ::
-  AssertM m =>
   MonadIO m =>
   MonadRibo m =>
   MonadDeepError e TmuxError m =>
+  MonadBaseControl IO m =>
   MonadMask m =>
   Nvim m =>
   Text ->
   Bool ->
   Int ->
-  m ()
-assertScreenshot name record pane =
-  runTmux (screenshot name record pane) >>= check
+  TestT m ()
+assertScreenshot name record pane = do
+  lift (runTmux (screenshot name record pane)) >>= check
   where
     check (Just (current, existing)) =
-      gassertEqual existing current
+      existing === current
     check Nothing =
       return ()
+
+awaitScreenshot ::
+  MonadIO m =>
+  MonadMask m =>
+  MonadRibo m =>
+  MonadBaseControl IO m =>
+  MonadDeepError e TmuxError m =>
+  Nvim m =>
+  Text ->
+  Bool ->
+  Int ->
+  TestT m ()
+awaitScreenshot name True pane =
+  sleep 1 <* lift (runTmux (screenshot name True pane))
+awaitScreenshot name False pane =
+  await (const (assertScreenshot name False pane)) unit
diff --git a/lib/Ribosome/Test/Tmux.hs b/lib/Ribosome/Test/Tmux.hs
--- a/lib/Ribosome/Test/Tmux.hs
+++ b/lib/Ribosome/Test/Tmux.hs
@@ -1,170 +1,203 @@
 module Ribosome.Test.Tmux where
 
+import Hedgehog (TestT)
 import Chiasma.Command.Pane (sendKeys)
 import Chiasma.Data.TmuxError (TmuxError)
 import Chiasma.Data.TmuxId (PaneId(PaneId))
 import Chiasma.Monad.Stream (runTmux)
 import Chiasma.Native.Api (TmuxNative(TmuxNative))
-import Chiasma.Test.Tmux (TmuxTestConf)
+import Chiasma.Test.Tmux (TmuxTestConf, withSystemTempDir)
 import qualified Chiasma.Test.Tmux as Chiasma (tmuxGuiSpec, tmuxSpec, tmuxSpec')
-import Control.Monad.Trans.Except (runExceptT)
+import Control.Exception.Lifted (bracket)
 import Data.DeepPrisms (DeepPrisms)
-import Data.Default (Default(def))
-import Data.Functor (void)
-import qualified Neovim.Context.Internal as Internal (Config, newConfig, retypeConfig)
-import Neovim.RPC.Common (RPCConfig, SocketType(UnixSocket), createHandle, newRPCConfig)
+import qualified Data.Text.IO as Text
+import qualified Neovim.Context.Internal as Internal (
+  StateTransition(Quit),
+  newConfig,
+  retypeConfig,
+  )
+import Neovim.Plugin (Plugin(Plugin))
+import Neovim.Plugin.Internal (wrapPlugin)
+import Neovim.RPC.Common (SocketType(UnixSocket), createHandle, newRPCConfig)
+import System.Directory (doesPathExist)
 import System.FilePath ((</>))
-import UnliftIO (throwString)
-import UnliftIO.Directory (doesPathExist)
-import UnliftIO.Exception (bracket)
-import UnliftIO.Temporary (withTempDirectory)
 
 import Ribosome.Config.Setting (updateSetting)
 import Ribosome.Config.Settings (tmuxSocket)
 import Ribosome.Control.Concurrent.Wait (waitIOPredDef)
-import Ribosome.Control.Monad.Ribo (Ribo)
+import Ribosome.Control.Exception (catchAny, tryAny)
+import Ribosome.Control.Monad.Ribo (NvimE, Ribo)
 import Ribosome.Control.Ribosome (Ribosome(Ribosome), newRibosomeTMVar)
 import Ribosome.Error.Report.Class (ReportError)
+import Ribosome.Msgpack.Encode (toMsgpack)
+import Ribosome.Nvim.Api.IO (vimSetVar)
 import Ribosome.Nvim.Api.RpcCall (RpcError)
 import Ribosome.Plugin.RpcHandler (RpcHandler)
-import Ribosome.Test.Embed (Runner, TestConfig(..), runTest, startHandlers)
+import Ribosome.System.Time (sleep)
+import Ribosome.Test.Embed (
+  Runner,
+  TestConfig(..),
+  inTestT,
+  runPlugin,
+  runTest,
+  startHandlers,
+  testNvimProcessConfig,
+  withProcessTerm,
+  )
 import Ribosome.Test.Orphans ()
-import Ribosome.Test.Unit (tempDir, uSpec)
-
-startSocketHandlers :: FilePath -> TestConfig -> Internal.Config RPCConfig -> IO (IO ())
-startSocketHandlers socket testConfig nvimConf = do
-  handle <- createHandle (UnixSocket socket)
-  startHandlers handle handle testConfig nvimConf
+import Ribosome.Test.Run (UnitTest)
+import Ribosome.Test.Unit (uSpec)
 
-runTmuxNvim ::
-  (RpcHandler e env m, ReportError e) =>
+runSocketNvimHs ::
+  MonadIO m =>
+  MonadFail m =>
+  ReportError e =>
+  RpcHandler e env n =>
+  MonadBaseControl IO m =>
   TestConfig ->
   env ->
-  m () ->
-  FilePath ->
-  IO ()
-runTmuxNvim conf ribo specThunk socket = do
-  nvimConf <- Internal.newConfig (pure Nothing) newRPCConfig
+  n a ->
+  Handle ->
+  m a
+runSocketNvimHs conf ribo specThunk socket = do
+  nvimConf <- liftIO (Internal.newConfig (pure Nothing) newRPCConfig)
   let testCfg = Internal.retypeConfig ribo nvimConf
-  bracket (startSocketHandlers socket conf nvimConf) id (const $ runTest conf testCfg specThunk)
+  bracket (startHandlers socket socket conf nvimConf) liftIO (const $ runTest conf testCfg specThunk)
 
 externalNvimCmdline :: FilePath -> Text
 externalNvimCmdline socket =
   "nvim --listen " <> toText socket <> " -n -u NONE -i NONE"
 
+startNvimInTmux ::
+  TmuxNative ->
+  FilePath ->
+  IO Handle
+startNvimInTmux api temp = do
+  void $ runExceptT @TmuxError $ runTmux api $ sendKeys (PaneId 0) [externalNvimCmdline socket]
+  _ <- waitIOPredDef (pure socket) doesPathExist
+  catchAny err (createHandle (UnixSocket socket))
+  where
+    socket =
+      temp </> "nvim-socket"
+    err (SomeException e) =
+      fail ("startNvimInTmux: createHandle failed: " <> show e)
+
 runGui ::
-  (RpcHandler e env m, ReportError e) =>
+  MonadIO m =>
+  MonadFail m =>
+  ReportError e =>
+  RpcHandler e env n =>
+  MonadBaseControl IO m =>
   TmuxNative ->
   FilePath ->
   TestConfig ->
   env ->
-  m () ->
-  IO ()
-runGui api temp conf ribo specThunk = do
-  void $ runExceptT @TmuxError $ runTmux api $ sendKeys (PaneId 0) [toString $ externalNvimCmdline socket]
-  _ <- waitIOPredDef (pure socket) doesPathExist
-  runTmuxNvim conf ribo specThunk socket
-  where
-    socket = temp </> "nvim-socket"
+  n a ->
+  m a
+runGui api temp conf ribo specThunk =
+  runSocketNvimHs conf ribo specThunk =<< liftIO (startNvimInTmux api temp)
 
 unsafeGuiSpec ::
-  (RpcHandler e env m, ReportError e) =>
+  MonadIO m =>
+  MonadFail m =>
+  ReportError e =>
+  RpcHandler e env n =>
+  MonadBaseControl IO m =>
   TmuxNative ->
   FilePath ->
-  Runner m ->
+  Runner n ->
   TestConfig ->
   env ->
-  m () ->
-  IO ()
+  n a ->
+  m a
 unsafeGuiSpec api temp runner conf s specThunk =
   runGui api temp conf s $ runner conf specThunk
 
 unsafeGuiSpecR ::
-  (RpcHandler e (Ribosome env) m, ReportError e) =>
+  MonadIO m =>
+  MonadFail m =>
+  ReportError e =>
+  MonadBaseControl IO m =>
+  RpcHandler e (Ribosome env) n =>
   TmuxNative ->
   FilePath ->
-  Runner m ->
+  Runner n ->
   TestConfig ->
   env ->
-  m () ->
-  IO ()
+  n a ->
+  m a
 unsafeGuiSpecR api temp runner conf s specThunk = do
   tv <- newRibosomeTMVar s
   let ribo = Ribosome (tcPluginName conf) tv
   unsafeGuiSpec api temp runner conf ribo specThunk
 
 guiSpec ::
-  DeepPrisms e RpcError =>
+  MonadIO m =>
+  MonadFail m =>
   ReportError e =>
+  MonadBaseControl IO m =>
+  DeepPrisms e RpcError =>
   TestConfig ->
   TmuxNative ->
   s ->
-  Ribo s e () ->
-  IO ()
+  Ribo s e a ->
+  m a
 guiSpec conf api env specThunk = do
-  socketDir <- tempDir "tmux-socket"
-  withTempDirectory socketDir "spec" run
+  withSystemTempDir run
   where
     run tempdir =
       unsafeGuiSpecR api tempdir uSpec conf env specThunk
 
 withTmux ::
   DeepPrisms e RpcError =>
-  Ribo s e () ->
+  Ribo s e a ->
   TmuxNative ->
-  Ribo s e ()
+  Ribo s e a
 withTmux thunk (TmuxNative (Just socket)) =
   updateSetting tmuxSocket socket *> thunk
 withTmux _ _ =
-  throwString "no socket in test tmux"
+  throwText "no socket in test tmux"
 
 tmuxSpec ::
-  Show s =>
   DeepPrisms e RpcError =>
   ReportError e =>
-  Default s =>
   TestConfig ->
   s ->
-  Ribo s e () ->
-  IO ()
+  TestT (Ribo s e) () ->
+  UnitTest
 tmuxSpec conf env specThunk =
-  Chiasma.tmuxSpec run
-  where
-    run api = guiSpec conf api env (withTmux specThunk api)
+  inTestT specThunk \ th ->
+    Chiasma.tmuxSpec \ api -> guiSpec conf api env (withTmux th api)
 
 tmuxSpec' ::
-  Show s =>
   DeepPrisms e RpcError =>
   ReportError e =>
-  Default s =>
   TmuxTestConf ->
   TestConfig ->
   s ->
-  Ribo s e () ->
-  IO ()
+  TestT (Ribo s e) () ->
+  UnitTest
 tmuxSpec' tmuxConf conf env specThunk =
-  Chiasma.tmuxSpec' tmuxConf run
-  where
-    run api = guiSpec conf api env (withTmux specThunk api)
+  inTestT specThunk \ th ->
+    Chiasma.tmuxSpec' tmuxConf \ api ->
+      guiSpec conf api env (withTmux th api)
 
 tmuxSpecDef ::
-  Show s =>
   DeepPrisms e RpcError =>
   ReportError e =>
   Default s =>
-  Ribo s e () ->
-  IO ()
+  TestT (Ribo s e) () ->
+  UnitTest
 tmuxSpecDef =
   tmuxSpec def def
 
 tmuxGuiSpec ::
   DeepPrisms e RpcError =>
   ReportError e =>
-  Default s =>
   TestConfig ->
   s ->
   Ribo s e () ->
-  IO ()
+  UnitTest
 tmuxGuiSpec conf env specThunk =
   Chiasma.tmuxGuiSpec run
   where
@@ -175,6 +208,64 @@
   ReportError e =>
   Default s =>
   Ribo s e () ->
-  IO ()
+  UnitTest
 tmuxGuiSpecDef =
   tmuxGuiSpec def def
+
+withTmuxInt ::
+  NvimE e m =>
+  MonadIO m =>
+  Text ->
+  m () ->
+  TmuxNative ->
+  m ()
+withTmuxInt name thunk (TmuxNative (Just socket)) = do
+  () <- vimSetVar (name <> "_tmux_socket") (toMsgpack socket)
+  thunk
+withTmuxInt _ _ _ =
+  throwText "no socket in test tmux"
+
+runTmuxWithPlugin ::
+  MonadIO m =>
+  MonadFail m =>
+  ReportError e =>
+  RpcHandler e env n =>
+  MonadBaseControl IO m =>
+  TmuxNative ->
+  TestConfig ->
+  Plugin env ->
+  n () ->
+  m ()
+runTmuxWithPlugin api conf plugin@(Plugin env _) thunk = do
+  withSystemTempDir runProc
+  where
+    runProc temp =
+      either logError pure =<< (tryAny (withProcessTerm (testNvimProcessConfig conf) (run temp)))
+    run temp prc = do
+      nvimConf <- liftIO (Internal.newConfig (pure Nothing) (pure env))
+      bracket (acquire prc nvimConf temp) release (const $ runTest conf nvimConf thunk)
+    acquire _ nvimConf temp = do
+      socket <- liftIO (startNvimInTmux api temp)
+      runPlugin socket socket [wrapPlugin plugin] nvimConf <* sleep 0.5
+    release transitions =
+      tryPutMVar transitions Internal.Quit *> sleep 0.5
+    logError (SomeException e) =
+      liftIO (Text.hPutStr stderr ("runTmuxWithPlugin: nvim process failed with: " <> show e))
+
+tmuxIntegrationSpecDef ::
+  NvimE e n =>
+  MonadIO m =>
+  MonadIO n =>
+  MonadFail m =>
+  ReportError e =>
+  RpcHandler e env n =>
+  MonadBaseControl IO m =>
+  Text ->
+  Plugin env ->
+  n () ->
+  m ()
+tmuxIntegrationSpecDef name plugin specThunk =
+  Chiasma.tmuxGuiSpec run
+  where
+    run api =
+      runTmuxWithPlugin api def plugin (withTmuxInt name specThunk api)
diff --git a/lib/Ribosome/Test/Ui.hs b/lib/Ribosome/Test/Ui.hs
--- a/lib/Ribosome/Test/Ui.hs
+++ b/lib/Ribosome/Test/Ui.hs
@@ -1,9 +1,6 @@
-{-# OPTIONS_GHC -F -pgmF htfpp #-}
-
 module Ribosome.Test.Ui where
 
-import Test.Framework
-
+import Hedgehog (TestT, (===))
 import Ribosome.Api.Window (currentCursor, cursor)
 import Ribosome.Control.Monad.Ribo (NvimE)
 import Ribosome.Nvim.Api.Data (Window)
@@ -11,28 +8,25 @@
 
 windowCountIs ::
   NvimE e m =>
-  AssertM m =>
   Int ->
-  m ()
+  TestT m ()
 windowCountIs count = do
   wins <- nvimListWins
-  gassertEqual count (length wins)
+  count === (length wins)
 
 cursorIs ::
   NvimE e m =>
-  AssertM m =>
   Int ->
   Int ->
   Window ->
-  m ()
+  TestT m ()
 cursorIs line col =
-  gassertEqual (line, col) <=< cursor
+  ((line, col) ===) <=< lift . cursor
 
 currentCursorIs ::
   NvimE e m =>
-  AssertM m =>
   Int ->
   Int ->
-  m ()
+  TestT m ()
 currentCursorIs line col =
-  gassertEqual (line, col) =<< currentCursor
+  ((line, col) ===) =<< lift currentCursor
diff --git a/lib/Ribosome/Test/Unit.hs b/lib/Ribosome/Test/Unit.hs
--- a/lib/Ribosome/Test/Unit.hs
+++ b/lib/Ribosome/Test/Unit.hs
@@ -1,9 +1,11 @@
 module Ribosome.Test.Unit where
 
-import Control.Monad.IO.Class (MonadIO)
-import Data.Default (def)
+import Control.Exception.Lifted (bracket_)
+import Hedgehog (TestT)
+import Hedgehog.Internal.Property (mkTestT, runTestT)
 import System.FilePath (takeDirectory, takeFileName, (</>))
-import System.Log.Logger (Priority(DEBUG), setLevel, updateGlobalLogger)
+import System.Log ()
+import System.Log.Logger (Priority(DEBUG, WARNING), setLevel, updateGlobalLogger)
 
 import Ribosome.Control.Monad.Ribo (MonadRibo, NvimE, pluginName)
 import Ribosome.Control.Ribosome (Ribosome)
@@ -14,7 +16,7 @@
 import Ribosome.Test.Orphans ()
 
 uPrefix :: Text
-uPrefix = "u"
+uPrefix = "test"
 
 uSpec :: (MonadIO m, NvimE e m) => Runner m
 uSpec conf spec = do
@@ -22,26 +24,44 @@
   spec
 
 unitSpec ::
-  (RpcHandler e (Ribosome env) m, ReportError e, MonadIO m, NvimE e' m) =>
+  MonadIO n =>
+  MonadIO m =>
+  NvimE e' n =>
+  MonadFail m =>
+  ReportError e =>
+  MonadBaseControl IO m =>
+  RpcHandler e (Ribosome env) n =>
   TestConfig ->
   env ->
-  m () ->
-  IO ()
-unitSpec =
-  unsafeEmbeddedSpecR uSpec
+  TestT n a ->
+  TestT m a
+unitSpec cfg env t = do
+  mkTestT (unsafeEmbeddedSpecR uSpec cfg env (runTestT t))
 
 unitSpecDef ::
-  (RpcHandler e (Ribosome env) m, ReportError e, MonadIO m, NvimE e' m) =>
+  MonadIO n =>
+  MonadIO m =>
+  NvimE e' n =>
+  MonadFail m =>
+  ReportError e =>
+  MonadBaseControl IO m =>
+  RpcHandler e (Ribosome env) n =>
   env ->
-  m () ->
-  IO ()
+  TestT n a ->
+  TestT m a
 unitSpecDef =
   unitSpec def
 
 unitSpecDef' ::
-  (RpcHandler e (Ribosome ()) m, ReportError e, MonadIO m, NvimE e' m) =>
-  m () ->
-  IO ()
+  MonadIO n =>
+  MonadIO m =>
+  NvimE e' n =>
+  MonadFail m =>
+  ReportError e =>
+  MonadBaseControl IO m =>
+  RpcHandler e (Ribosome ()) n =>
+  TestT n a ->
+  TestT m a
 unitSpecDef' =
   unitSpecDef ()
 
@@ -61,15 +81,19 @@
 
 withLogAs ::
   MonadIO m =>
+  MonadBaseControl IO m =>
   Text ->
   m a ->
   m a
-withLogAs name thunk = do
-  liftIO $ updateGlobalLogger (toString name) (setLevel DEBUG)
-  thunk
+withLogAs name =
+  bracket_ (logLevel DEBUG) (logLevel WARNING)
+  where
+    logLevel =
+      liftIO . updateGlobalLogger (toString name) . setLevel
 
 withLog ::
   MonadRibo m =>
+  MonadBaseControl IO m =>
   m a ->
   m a
 withLog thunk =
diff --git a/readme.md b/readme.md
new file mode 100644
--- /dev/null
+++ b/readme.md
@@ -0,0 +1,91 @@
+# About
+
+*ribosome* is an extension framework for [nvim-hs], a [Haskell] library that
+provides a [Neovim] plugin engine.
+
+*ribosome*'s structure is similar to that of vanilla [nvim-hs] plugins and is
+intended to provide a more comfortable API on top of it.
+
+# Basic Plugin
+
+*ribosome* comes with a companion plugin manager, [chromatin], but you can
+start plugins regularly with `jobstart()`.
+
+First, add `ribosome` to your `myplugin.cabal`'s dependencies.
+
+Your project should have an executable that looks like this:
+
+```haskell
+import Neovim (neovim, plugins, defaultConfig)
+import MyPlugin.Plugin (plugin)
+
+main :: IO ()
+main =
+  neovim defaultConfig {plugins = [plugin]}
+```
+
+In `MyPlugin.Plugin`, you should write a plugin definition like this:
+
+```haskell
+module MyPlugin.Plugin where
+
+import Data.Default.Class (Default(def))
+import Neovim (Neovim, NeovimPlugin, Plugin, wrapPlugin)
+import Neovim.Context.Internal (Config(customConfig), asks')
+import Ribosome.Api.Echo (echom)
+import Ribosome.Control.Monad.Ribo (MonadRibo, NvimE, Ribo)
+import Ribosome.Control.Ribosome (Ribosome, newRibosome)
+import Ribosome.Error.Report (reportError)
+import Ribosome.Internal.IO (retypeNeovim)
+import Ribosome.Plugin (RpcDef, autocmd, cmd, riboPlugin, rpcHandler)
+import System.Log.Logger (Priority(ERROR), setLevel, updateGlobalLogger)
+
+handleError :: Text -> Ribo () Text ()
+handleError err =
+  echom err
+
+hello :: NvimE e m => MonadRibo m => m ()
+hello ::
+  echom "hello"
+
+bufEnter :: NvimE e m => MonadRibo m => m ()
+bufEnter ::
+  echom "BufEnter"
+
+rpcHandlers :: [[RpcDef (Ribo () Error)]]
+rpcHandlers =
+  [
+    $(rpcHandler (cmd []) 'hello),
+    $(rpcHandler (autocmd "BufEnter") 'bufEnter)
+  ]
+
+plugin' :: Ribosome () -> Plugin (Ribosome ())
+plugin' env =
+  riboPlugin "myplugin" env rpcHandlers def handleError def
+
+initialize :: Neovim e (Ribosome Env)
+initialize = do
+  liftIO $ updateGlobalLogger "Neovim.Plugin" (setLevel ERROR)
+  ribo <- newRibosome "myplugin" def
+  retypeNeovim (const ribo) (asks' customConfig)
+
+plugin :: Neovim e NeovimPlugin
+plugin =
+  wrapPlugin . plugin' =<< initialize
+```
+
+Follow the instructions for the bootstrapping vim config in the documentation
+for [chromatin].
+Now you can execute the command `:Hello`.
+
+For more inspiration, check out some example projects: [proteome], [myo],
+[uracil].
+
+[nvim-hs]: https://github.com/neovimhaskell/nvim-hs
+[Haskell]: https://www.haskell.org
+[proteome]: https://github.com/tek/proteome
+[myo]: https://github.com/tek/myo
+[uracil]: https://github.com/tek/uracil
+[chromatin]: https://github.com/tek/chromatin
+[nvim-hs]: https://github.com/neovimhaskell/nvim-hs
+[Neovim]: https://github.com/neovim/neovim
diff --git a/ribosome-test.cabal b/ribosome-test.cabal
--- a/ribosome-test.cabal
+++ b/ribosome-test.cabal
@@ -1,75 +1,281 @@
-cabal-version: 1.12
-name: ribosome-test
-version: 0.3.0.1
-license: OtherLicense
-license-file: LICENSE
-copyright: 2019 Torsten Schmits
-maintainer: tek@tryp.io
-author: Torsten Schmits
-homepage: https://github.com/tek/ribosome-hs#readme
-bug-reports: https://github.com/tek/ribosome-hs/issues
-synopsis: test helpers for ribosome
-description:
-    Please see the README on GitHub at <https://github.com/tek/ribosome-hs>
-category: Neovim
-build-type: Simple
+cabal-version: 2.2
 
+-- This file has been generated from package.yaml by hpack version 0.34.4.
+--
+-- see: https://github.com/sol/hpack
+--
+-- hash: d204ea32a519247a24754414a535360674e998d19dd95ea48b7fd7d7ab1044dc
+
+name:           ribosome-test
+version:        0.4.0.0
+synopsis:       test helpers for ribosome
+description:    Please see the README on GitHub at <https://github.com/tek/ribosome>
+category:       Neovim
+homepage:       https://github.com/tek/ribosome#readme
+bug-reports:    https://github.com/tek/ribosome/issues
+author:         Torsten Schmits
+maintainer:     tek@tryp.io
+copyright:      2021 Torsten Schmits
+license:        BSD-2-Clause-Patent
+license-file:   LICENSE
+build-type:     Simple
+extra-source-files:
+    readme.md
+
 source-repository head
-    type: git
-    location: https://github.com/tek/ribosome-hs
+  type: git
+  location: https://github.com/tek/ribosome
 
 library
-    exposed-modules:
-        Ribosome.Test.Await
-        Ribosome.Test.Embed
-        Ribosome.Test.Exists
-        Ribosome.Test.File
-        Ribosome.Test.Functional
-        Ribosome.Test.Input
-        Ribosome.Test.Orphans
-        Ribosome.Test.Screenshot
-        Ribosome.Test.Tmux
-        Ribosome.Test.Ui
-        Ribosome.Test.Unit
-    hs-source-dirs: lib
-    other-modules:
-        Prelude
-    default-language: Haskell2010
-    default-extensions: AutoDeriveTypeable BangPatterns BinaryLiterals
-                        ConstraintKinds DataKinds DefaultSignatures DeriveDataTypeable
-                        DeriveFoldable DeriveFunctor DeriveGeneric DeriveTraversable
-                        DoAndIfThenElse EmptyDataDecls ExistentialQuantification
-                        FlexibleContexts FlexibleInstances FunctionalDependencies GADTs
-                        GeneralizedNewtypeDeriving InstanceSigs KindSignatures LambdaCase
-                        MonadFailDesugaring MultiParamTypeClasses MultiWayIf NamedFieldPuns
-                        OverloadedStrings PartialTypeSignatures PatternGuards PolyKinds
-                        RankNTypes RecordWildCards ScopedTypeVariables StandaloneDeriving
-                        TupleSections TypeApplications TypeFamilies TypeSynonymInstances
-                        UnicodeSyntax ViewPatterns
-    build-depends:
-        HTF >=0.13.2.5 && <0.14,
-        base-noprelude >=4.7 && <5,
-        bytestring >=0.10.8.2 && <0.11,
-        chiasma >=0.1.0.0 && <0.2,
-        cornea >=0.2.2.0 && <0.3,
-        data-default >=0.7.1.1 && <0.8,
-        directory >=1.3.1.5 && <1.4,
-        exceptions >=0.10.0 && <0.11,
-        filepath >=1.4.2 && <1.5,
-        free >=5.0.2 && <5.1,
-        hslogger >=1.2.12 && <1.3,
-        lifted-base >=0.2.3.12 && <0.3,
-        messagepack >=0.5.4 && <0.6,
-        monad-control >=1.0.2.3 && <1.1,
-        mtl >=2.2.2 && <2.3,
-        nvim-hs >=2.1.0.0 && <2.2,
-        process >=1.6.3.0 && <1.7,
-        relude >=0.1.1 && <0.2,
-        resourcet >=1.2.2 && <1.3,
-        ribosome >=0.3.0.1 && <0.4,
-        text >=1.2.3.1 && <1.3,
-        transformers >=0.5.5.0 && <0.6,
-        typed-process >=0.2.3.0 && <0.3,
-        unix >=2.7.2.2 && <2.8,
-        unliftio >=0.2.9.0 && <0.3,
-        unliftio-core >=0.1.2.0 && <0.2
+  exposed-modules:
+      Ribosome.Test.Await
+      Ribosome.Test.Embed
+      Ribosome.Test.Exists
+      Ribosome.Test.File
+      Ribosome.Test.Functional
+      Ribosome.Test.Input
+      Ribosome.Test.Orphans
+      Ribosome.Test.Run
+      Ribosome.Test.Screenshot
+      Ribosome.Test.Tmux
+      Ribosome.Test.Ui
+      Ribosome.Test.Unit
+  other-modules:
+      Paths_ribosome_test
+  autogen-modules:
+      Paths_ribosome_test
+  hs-source-dirs:
+      lib
+  default-extensions:
+      AllowAmbiguousTypes
+      ApplicativeDo
+      BangPatterns
+      BinaryLiterals
+      BlockArguments
+      ConstraintKinds
+      DataKinds
+      DefaultSignatures
+      DeriveAnyClass
+      DeriveDataTypeable
+      DeriveFoldable
+      DeriveFunctor
+      DeriveGeneric
+      DeriveLift
+      DeriveTraversable
+      DerivingStrategies
+      DisambiguateRecordFields
+      DoAndIfThenElse
+      DuplicateRecordFields
+      EmptyDataDecls
+      ExistentialQuantification
+      FlexibleContexts
+      FlexibleInstances
+      FunctionalDependencies
+      GADTs
+      GeneralizedNewtypeDeriving
+      InstanceSigs
+      KindSignatures
+      LambdaCase
+      LiberalTypeSynonyms
+      MultiParamTypeClasses
+      MultiWayIf
+      NamedFieldPuns
+      OverloadedStrings
+      OverloadedLists
+      PackageImports
+      PartialTypeSignatures
+      PatternGuards
+      PatternSynonyms
+      PolyKinds
+      QuantifiedConstraints
+      QuasiQuotes
+      RankNTypes
+      RecordWildCards
+      RecursiveDo
+      ScopedTypeVariables
+      StandaloneDeriving
+      TemplateHaskell
+      TupleSections
+      TypeApplications
+      TypeFamilies
+      TypeFamilyDependencies
+      TypeOperators
+      TypeSynonymInstances
+      UndecidableInstances
+      UnicodeSyntax
+      ViewPatterns
+  ghc-options: -Wall -Wredundant-constraints -Wsimplifiable-class-constraints
+  build-depends:
+      aeson
+    , base ==4.*
+    , bytestring
+    , chiasma
+    , composition
+    , composition-extra
+    , conduit
+    , containers
+    , cornea
+    , data-default
+    , directory
+    , either
+    , exceptions
+    , filepath
+    , free
+    , hedgehog
+    , hslogger
+    , lens
+    , lifted-async
+    , lifted-base
+    , messagepack
+    , monad-control
+    , mtl
+    , nvim-hs
+    , path
+    , path-io
+    , prettyprinter
+    , prettyprinter-ansi-terminal
+    , process
+    , relude >=0.7 && <1.2
+    , resourcet
+    , ribosome
+    , tasty
+    , tasty-hedgehog
+    , template-haskell
+    , text
+    , transformers
+    , typed-process
+    , unix
+    , unliftio
+  mixins:
+      base hiding (Prelude)
+    , ribosome hiding (Ribosome.Prelude)
+    , ribosome (Ribosome.Prelude as Prelude)
+  default-language: Haskell2010
+
+test-suite ribosome-unit
+  type: exitcode-stdio-1.0
+  main-is: Main.hs
+  other-modules:
+      Ribosome.Test.AutocmdTest
+      Ribosome.Test.MappingTest
+      Ribosome.Test.MenuTest
+      Ribosome.Test.MsgpackTest
+      Ribosome.Test.NvimMenuTest
+      Ribosome.Test.PromptTest
+      Ribosome.Test.RpcTest
+      Ribosome.Test.ScratchTest
+      Ribosome.Test.SettingTest
+      Ribosome.Test.SyntaxTest
+      Ribosome.Test.THTest
+      Ribosome.Test.WatcherTest
+      Ribosome.Test.WindowTest
+      TestError
+      Paths_ribosome_test
+  hs-source-dirs:
+      test
+  default-extensions:
+      AllowAmbiguousTypes
+      ApplicativeDo
+      BangPatterns
+      BinaryLiterals
+      BlockArguments
+      ConstraintKinds
+      DataKinds
+      DefaultSignatures
+      DeriveAnyClass
+      DeriveDataTypeable
+      DeriveFoldable
+      DeriveFunctor
+      DeriveGeneric
+      DeriveLift
+      DeriveTraversable
+      DerivingStrategies
+      DisambiguateRecordFields
+      DoAndIfThenElse
+      DuplicateRecordFields
+      EmptyDataDecls
+      ExistentialQuantification
+      FlexibleContexts
+      FlexibleInstances
+      FunctionalDependencies
+      GADTs
+      GeneralizedNewtypeDeriving
+      InstanceSigs
+      KindSignatures
+      LambdaCase
+      LiberalTypeSynonyms
+      MultiParamTypeClasses
+      MultiWayIf
+      NamedFieldPuns
+      OverloadedStrings
+      OverloadedLists
+      PackageImports
+      PartialTypeSignatures
+      PatternGuards
+      PatternSynonyms
+      PolyKinds
+      QuantifiedConstraints
+      QuasiQuotes
+      RankNTypes
+      RecordWildCards
+      RecursiveDo
+      ScopedTypeVariables
+      StandaloneDeriving
+      TemplateHaskell
+      TupleSections
+      TypeApplications
+      TypeFamilies
+      TypeFamilyDependencies
+      TypeOperators
+      TypeSynonymInstances
+      UndecidableInstances
+      UnicodeSyntax
+      ViewPatterns
+  ghc-options: -Wall -Wredundant-constraints -Wsimplifiable-class-constraints -threaded -rtsopts -with-rtsopts=-N
+  build-depends:
+      aeson
+    , base ==4.*
+    , bytestring
+    , chiasma
+    , composition
+    , composition-extra
+    , conduit
+    , containers
+    , cornea
+    , data-default
+    , directory
+    , either
+    , exceptions
+    , filepath
+    , free
+    , hedgehog
+    , hslogger
+    , lens
+    , lifted-async
+    , lifted-base
+    , messagepack
+    , monad-control
+    , mtl
+    , nvim-hs
+    , path
+    , path-io
+    , prettyprinter
+    , prettyprinter-ansi-terminal
+    , process
+    , relude >=0.7 && <1.2
+    , resourcet
+    , ribosome
+    , ribosome-test
+    , tasty
+    , tasty-hedgehog
+    , template-haskell
+    , text
+    , transformers
+    , typed-process
+    , unix
+    , unliftio
+  mixins:
+      base hiding (Prelude)
+    , ribosome hiding (Ribosome.Prelude)
+    , ribosome (Ribosome.Prelude as Prelude)
+  default-language: Haskell2010
diff --git a/test/Main.hs b/test/Main.hs
new file mode 100644
--- /dev/null
+++ b/test/Main.hs
@@ -0,0 +1,42 @@
+module Main where
+
+import Ribosome.Test.AutocmdTest (test_autocmd)
+import Ribosome.Test.MappingTest (test_mapping)
+import Ribosome.Test.MenuTest (test_menu)
+import Ribosome.Test.MsgpackTest (test_msgpack)
+import Ribosome.Test.NvimMenuTest (test_nvimMenu)
+import Ribosome.Test.PromptTest (test_promptSet)
+import Ribosome.Test.RpcTest (test_rpc)
+import Ribosome.Test.Run (unitTest)
+import Ribosome.Test.ScratchTest (test_regularScratch, test_floatScratch)
+import Ribosome.Test.SettingTest (test_settingSuccess, test_settingFail)
+-- import Ribosome.Test.SyntaxTest (test_syntax)
+import Ribosome.Test.THTest ()
+import Ribosome.Test.WatcherTest (test_varWatcher)
+import Ribosome.Test.WindowTest (test_findMainWindowCreate, test_findMainWindowExisting)
+import Test.Tasty (TestTree, defaultMain, testGroup)
+
+tests :: TestTree
+tests =
+  testGroup "all" [
+    unitTest "autocmd handler" test_autocmd,
+    unitTest "mapping for scratch buffer" test_mapping,
+    test_menu,
+    test_msgpack,
+    test_nvimMenu,
+    unitTest "set the prompt text" test_promptSet,
+    unitTest "rpc handlers" test_rpc,
+    unitTest "scratch buffer" test_regularScratch,
+    unitTest "floating scratch buffer" test_floatScratch,
+    unitTest "successfully read a setting" test_settingSuccess,
+    unitTest "read an unset setting" test_settingFail,
+    -- TODO flaky test
+    -- unitTest "syntax" test_syntax,
+    unitTest "variable watcher" test_varWatcher,
+    unitTest "find the existing main window" test_findMainWindowExisting,
+    unitTest "create the main window" test_findMainWindowCreate
+  ]
+
+main :: IO ()
+main =
+  defaultMain tests
diff --git a/test/Ribosome/Test/AutocmdTest.hs b/test/Ribosome/Test/AutocmdTest.hs
new file mode 100644
--- /dev/null
+++ b/test/Ribosome/Test/AutocmdTest.hs
@@ -0,0 +1,49 @@
+module Ribosome.Test.AutocmdTest where
+
+import Hedgehog ((===))
+import Neovim (Plugin(..))
+import TestError (RiboTest, handleTestError)
+
+import Ribosome.Control.Monad.Ribo (NvimE)
+import Ribosome.Control.Ribosome (Ribosome, newRibosome)
+import Ribosome.Msgpack.Encode (MsgpackEncode(toMsgpack))
+import Ribosome.Nvim.Api.IO (vimCommand, vimGetVar, vimSetVar)
+import Ribosome.Orphans ()
+import Ribosome.Plugin (autocmd, riboPlugin, rpcHandler)
+import Ribosome.Test.Await (await)
+import Ribosome.Test.Embed (integrationSpecDef)
+import Ribosome.Test.Orphans ()
+import Ribosome.Test.Run (UnitTest)
+
+varName :: Text
+varName =
+  "result"
+
+result :: Int
+result =
+  5
+
+testAuto ::
+  NvimE e m =>
+  m ()
+testAuto =
+  vimSetVar varName (toMsgpack result)
+
+$(return [])
+
+autocmdPlugin :: IO (Plugin (Ribosome ()))
+autocmdPlugin = do
+  env <- newRibosome "test" ()
+  return $ riboPlugin "test" env funcs [] handleTestError def
+  where
+    funcs = [$(rpcHandler (autocmd "BufWritePre") 'testAuto)]
+
+autocmdSpec :: RiboTest ()
+autocmdSpec = do
+  () <- vimCommand "doautocmd BufWritePre"
+  await (result ===) (vimGetVar varName)
+
+test_autocmd :: UnitTest
+test_autocmd = do
+  plug <- liftIO autocmdPlugin
+  integrationSpecDef plug autocmdSpec
diff --git a/test/Ribosome/Test/MappingTest.hs b/test/Ribosome/Test/MappingTest.hs
new file mode 100644
--- /dev/null
+++ b/test/Ribosome/Test/MappingTest.hs
@@ -0,0 +1,71 @@
+module Ribosome.Test.MappingTest where
+
+import qualified Data.Map.Strict as Map (empty)
+import Hedgehog ((===))
+import Neovim (Plugin(..))
+import TestError (RiboTest, handleTestError)
+
+import Ribosome.Api.Buffer (currentBufferContent)
+import Ribosome.Control.Monad.Ribo (MonadRibo, NvimE)
+import Ribosome.Control.Ribosome (Ribosome, newRibosome)
+import Ribosome.Data.Mapping (Mapping(Mapping), MappingIdent(MappingIdent))
+import Ribosome.Data.ScratchOptions (ScratchOptions(ScratchOptions))
+import Ribosome.Msgpack.Encode (MsgpackEncode(toMsgpack))
+import Ribosome.Msgpack.Error (DecodeError)
+import Ribosome.Nvim.Api.IO (nvimFeedkeys, vimCallFunction, vimGetVar, vimSetVar)
+import Ribosome.Plugin (riboPlugin, rpcHandlerDef)
+import Ribosome.Plugin.Mapping (MappingHandler, mappingHandler)
+import Ribosome.Scratch (showInScratch)
+import Ribosome.Test.Await (await)
+import Ribosome.Test.Embed (integrationSpecDef)
+import Ribosome.Test.Orphans ()
+import Ribosome.Test.Run (UnitTest)
+
+target :: [Text]
+target = ["line 1", "line 2"]
+
+go :: NvimE e m => m ()
+go =
+  vimSetVar "number" (toMsgpack (13 :: Int))
+
+mapping :: Mapping
+mapping =
+  Mapping (MappingIdent "go") "a" "n" False True
+
+mapHandler :: NvimE e m => MappingHandler m
+mapHandler =
+  mappingHandler "go" go
+
+setupMappingScratch ::
+  NvimE e m =>
+  MonadRibo m =>
+  MonadBaseControl IO m =>
+  MonadDeepError e DecodeError m =>
+  m ()
+setupMappingScratch = do
+  _ <- showInScratch target options
+  return ()
+  where
+    options =
+      ScratchOptions False True False True True True False Nothing Nothing Nothing [] [mapping] "buffi"
+
+$(return [])
+
+mappingPlugin :: IO (Plugin (Ribosome ()))
+mappingPlugin = do
+  env <- newRibosome "mapping" ()
+  return $ riboPlugin "mapping" env funcs [mapHandler] handleTestError Map.empty
+  where
+    funcs = [$(rpcHandlerDef 'setupMappingScratch)]
+
+mappingSpec :: RiboTest ()
+mappingSpec = do
+  () <- vimCallFunction "SetupMappingScratch" []
+  await (target ===) currentBufferContent
+  nvimFeedkeys "a" "x" False
+  await ((13 :: Int) ===) (vimGetVar "number")
+
+test_mapping :: UnitTest
+test_mapping = do
+  plug <- liftIO mappingPlugin
+  integrationSpecDef plug mappingSpec
diff --git a/test/Ribosome/Test/MenuTest.hs b/test/Ribosome/Test/MenuTest.hs
new file mode 100644
--- /dev/null
+++ b/test/Ribosome/Test/MenuTest.hs
@@ -0,0 +1,324 @@
+module Ribosome.Test.MenuTest where
+
+import Conduit (ConduitT, yield, yieldMany)
+import Control.Concurrent.MVar.Lifted (modifyMVar_)
+import Control.Lens (view)
+import Control.Monad.Trans.Resource (ResourceT, runResourceT)
+import qualified Data.Map.Strict as Map (fromList)
+import Hedgehog ((===))
+import Test.Tasty (TestTree, testGroup)
+
+import Ribosome.Control.StrictRibosome (StrictRibosome)
+import Ribosome.Menu.Action (menuContinue, menuExecute, menuQuit)
+import Ribosome.Menu.Data.FilteredMenuItem (FilteredMenuItem(FilteredMenuItem))
+import qualified Ribosome.Menu.Data.FilteredMenuItem as FilteredMenuItem (item)
+import Ribosome.Menu.Data.Menu (Menu(Menu), MenuFilter(MenuFilter))
+import qualified Ribosome.Menu.Data.Menu as Menu (items)
+import Ribosome.Menu.Data.MenuAction (MenuAction)
+import Ribosome.Menu.Data.MenuConfig (MenuConfig(MenuConfig))
+import Ribosome.Menu.Data.MenuConsumer (MenuConsumer(MenuConsumer))
+import Ribosome.Menu.Data.MenuConsumerAction (MenuConsumerAction)
+import qualified Ribosome.Menu.Data.MenuEvent as MenuEvent (MenuEvent(..))
+import Ribosome.Menu.Data.MenuItem (MenuItem(MenuItem), simpleMenuItem)
+import qualified Ribosome.Menu.Data.MenuItem as MenuItem (MenuItem(_text), text)
+import Ribosome.Menu.Data.MenuRenderEvent (MenuRenderEvent)
+import qualified Ribosome.Menu.Data.MenuRenderEvent as MenuRenderEvent (MenuRenderEvent(..))
+import Ribosome.Menu.Data.MenuUpdate (MenuUpdate(MenuUpdate))
+import Ribosome.Menu.Prompt.Data.Prompt (Prompt(Prompt))
+import Ribosome.Menu.Prompt.Data.PromptConfig (PromptConfig(PromptConfig), PromptFlag(StartInsert))
+import Ribosome.Menu.Prompt.Data.PromptEvent (PromptEvent)
+import qualified Ribosome.Menu.Prompt.Data.PromptEvent as PromptEvent (PromptEvent(..))
+import qualified Ribosome.Menu.Prompt.Data.PromptState as PromptState (PromptState(..))
+import Ribosome.Menu.Prompt.Run (basicTransition, noPromptRenderer)
+import Ribosome.Menu.Run (runMenu)
+import Ribosome.Menu.Simple (
+  basicMenu,
+  defaultMenu,
+  deleteByFilteredIndex,
+  fuzzyMenuItemMatcher,
+  markedMenuItems,
+  markedMenuItemsOnly,
+  selectedMenuItem,
+  simpleMenu,
+  )
+import Ribosome.System.Time (sleep)
+import Ribosome.Test.Run (UnitTest, unitTest)
+
+promptInput ::
+  MonadIO m =>
+  [Text] ->
+  ConduitT () PromptEvent m ()
+promptInput chars = do
+  lift $ sleep 0.1
+  yieldMany (PromptEvent.Character <$> chars)
+
+menuItems ::
+  Monad m =>
+  [Text] ->
+  ConduitT () [MenuItem Text] m ()
+menuItems =
+  yield . fmap (simpleMenuItem "name")
+
+storePrompt ::
+  MonadBaseControl IO m =>
+  MVar [Prompt] ->
+  MenuUpdate m a Text ->
+  m (MenuConsumerAction m a, Menu Text)
+storePrompt prompts (MenuUpdate event menu) =
+  check event
+  where
+    check (MenuEvent.PromptChange prompt) =
+      store prompt
+    check (MenuEvent.Mapping _ prompt) =
+      store prompt
+    check (MenuEvent.Quit _) =
+      menuQuit menu
+    check _ =
+      menuContinue menu
+    store prompt =
+      modifyMVar_ prompts (return . (prompt :)) *> menuContinue menu
+
+render ::
+  MonadIO m =>
+  MonadBaseControl IO m =>
+  MVar [[FilteredMenuItem Text]] ->
+  MenuRenderEvent m a Text ->
+  m ()
+render varItems (MenuRenderEvent.Render _ (Menu _ items _ _ _ _)) = do
+  modifyMVar_ varItems (return . (items :))
+  sleep 0.01
+render _ (MenuRenderEvent.Quit _) =
+  return ()
+
+type TestM = StateT (StrictRibosome ()) (ResourceT IO)
+
+menuTest ::
+  (MenuUpdate TestM a Text -> TestM (MenuAction TestM a, Menu Text)) ->
+  [Text] ->
+  [Text] ->
+  IO [[FilteredMenuItem Text]]
+menuTest handler items chars = do
+  itemsVar <- newMVar []
+  void $ runResourceT $ runMenu (conf itemsVar) `execStateT` def
+  readMVar itemsVar
+  where
+    conf itemsVar =
+      MenuConfig (menuItems items) (MenuConsumer handler) (render itemsVar) promptConfig def
+    promptConfig =
+      PromptConfig (promptInput chars) basicTransition noPromptRenderer [StartInsert]
+
+promptTest :: [Text] -> [Text] -> IO ([[FilteredMenuItem Text]], [Prompt])
+promptTest items chars = do
+  prompts <- newMVar []
+  itemsResult <- menuTest (basicMenu fuzzyMenuItemMatcher (storePrompt prompts)) items chars
+  (itemsResult,) <$> readMVar prompts
+
+promptsTarget1 :: [Prompt]
+promptsTarget1 =
+  uncurry one' <$> [
+    (1, PromptState.Insert),
+    (0, PromptState.Normal),
+    (0, PromptState.Normal),
+    (1, PromptState.Insert)
+    ]
+  where
+    one' c s = Prompt c s "i"
+
+items1 :: [Text]
+items1 =
+  [
+    "i1",
+    "j1",
+    "i2",
+    "i3",
+    "j2",
+    "i4"
+    ]
+
+chars1 :: [Text]
+chars1 =
+  [
+    "i",
+    "esc",
+    "k",
+    "a",
+    "2"
+    ]
+
+itemsTarget1 :: [[MenuItem Text]]
+itemsTarget1 =
+  [
+    item <$> ["i2"],
+    item <$> ["i1", "i2", "i3", "i4"]
+    ]
+  where
+    item =
+      simpleMenuItem "name"
+
+test_pureMenuModeChange :: UnitTest
+test_pureMenuModeChange = do
+  (items, prompts) <- liftIO (promptTest items1 chars1)
+  itemsTarget1 === (view FilteredMenuItem.item <$$> take 2 items)
+  promptsTarget1 === (take 4 $ reverse prompts)
+
+chars2 :: [Text]
+chars2 =
+  ["l", "o", "n", "g", "-", "i", "t", "e", "m"]
+
+items2 :: [Text]
+items2 =
+  [
+    "long",
+    "short",
+    "long-item",
+    "longitem"
+    ]
+
+itemsTarget :: [MenuItem Text]
+itemsTarget =
+  [simpleMenuItem "name" "long-item"]
+
+test_pureMenuFilter :: UnitTest
+test_pureMenuFilter = do
+  items <- liftIO (fst <$> promptTest items2 chars2)
+  [itemsTarget] === (view FilteredMenuItem.item <$$> take 1 items)
+
+chars3 :: [Text]
+chars3 =
+  ["i", "esc", "cr"]
+
+items3 :: [Text]
+items3 =
+  [
+    "item1",
+    "item2"
+    ]
+
+exec ::
+  MonadIO m =>
+  MVar [Text] ->
+  Menu Text ->
+  Prompt ->
+  m (MenuConsumerAction m a, Menu Text)
+exec var m@(Menu _ items _ _ _ _) _ =
+  swapMVar var (view (FilteredMenuItem.item . MenuItem.text) <$> items) *> menuQuit m
+
+test_pureMenuExecute :: UnitTest
+test_pureMenuExecute = do
+  var <- newMVar []
+  _ <- liftIO (menuTest (simpleMenu (Map.fromList [("cr", exec var)])) items3 chars3)
+  (items3 ===) =<< readMVar var
+
+charsMulti :: [Text]
+charsMulti =
+  ["esc", "k", "k", "space", "space", "space", "space", "j", "space", "cr"]
+
+itemsMulti :: [Text]
+itemsMulti =
+  [
+    "item1",
+    "item2",
+    "item3",
+    "item4",
+    "item5",
+    "item6"
+    ]
+
+execMulti ::
+  MonadIO m =>
+  MVar (Maybe (NonEmpty Text)) ->
+  Menu Text ->
+  Prompt ->
+  m (MenuConsumerAction m a, Menu Text)
+execMulti var m _ =
+  swapMVar var (MenuItem._text <$$> markedMenuItems m) *> menuQuit m
+
+test_menuMultiMark :: UnitTest
+test_menuMultiMark = do
+  var <- newMVar Nothing
+  _ <- liftIO (menuTest (defaultMenu (Map.fromList [("cr", execMulti var)])) itemsMulti charsMulti)
+  (Just ("item3" :| ["item4", "item5"]) ===) =<< readMVar var
+
+charsToggle :: [Text]
+charsToggle =
+  ["esc", "k", "space", "*", "i", "a", "b", "cr"]
+
+itemsToggle :: [Text]
+itemsToggle =
+  [
+    "a",
+    "ab",
+    "abc"
+    ]
+
+execToggle ::
+  MonadIO m =>
+  MVar (Maybe (NonEmpty Text)) ->
+  Menu Text ->
+  Prompt ->
+  m (MenuConsumerAction m a, Menu Text)
+execToggle var m _ =
+  swapMVar var (MenuItem._text <$$> markedMenuItemsOnly m) *> menuQuit m
+
+test_menuToggle :: UnitTest
+test_menuToggle = do
+  var <- newMVar Nothing
+  _ <- liftIO (menuTest (defaultMenu (Map.fromList [("cr", execToggle var)])) itemsToggle charsToggle)
+  (Nothing ===) =<< readMVar var
+
+charsExecuteThunk :: [Text]
+charsExecuteThunk =
+  ["esc", "cr", "k", "cr", "esc"]
+
+itemsExecuteThunk :: [Text]
+itemsExecuteThunk =
+  [
+    "a",
+    "b"
+    ]
+
+execExecuteThunk ::
+  MonadIO m =>
+  MonadBaseControl IO m =>
+  MVar [Text] ->
+  Menu Text ->
+  Prompt ->
+  m (MenuConsumerAction m a, Menu Text)
+execExecuteThunk var m _ =
+  menuExecute (modifyMVar_ var prepend) m
+  where
+    prepend a =
+      pure $ a ++ maybeToList (MenuItem._text <$> selectedMenuItem m)
+
+test_menuExecuteThunk :: UnitTest
+test_menuExecuteThunk = do
+  var <- newMVar []
+  _ <- liftIO (menuTest (defaultMenu (Map.fromList [("cr", execExecuteThunk var)])) itemsExecuteThunk charsExecuteThunk)
+  (["a", "b"] ===) =<< readMVar var
+
+test_menuDeleteByFilteredIndex :: UnitTest
+test_menuDeleteByFilteredIndex =
+  (target ===) . fmap (view MenuItem.text) . view Menu.items . deleteByFilteredIndex [1, 2] $ menu
+  where
+    target =
+      ["1", "2", "3", "5", "7", "8"]
+    menu =
+      Menu items filtered 0 [] (MenuFilter "") Nothing
+    items =
+      simpleMenuItem () <$> ["1", "2", "3", "4", "5", "6", "7", "8"]
+    filtered =
+      uncurry FilteredMenuItem . second menuItem <$> [(1, "2"), (3, "4"), (5, "6"), (7, "8")]
+    menuItem t =
+      MenuItem () t t
+
+test_menu :: TestTree
+test_menu =
+  testGroup "menu" [
+    unitTest "change mode" test_pureMenuModeChange,
+    unitTest "filter items" test_pureMenuFilter,
+    unitTest "execute an action" test_pureMenuExecute,
+    unitTest "mark multiple items" test_menuMultiMark,
+    unitTest "toggle marked items" test_menuToggle,
+    unitTest "execute a thunk action" test_menuExecuteThunk,
+    unitTest "delete by filtered index" test_menuDeleteByFilteredIndex
+    ]
diff --git a/test/Ribosome/Test/MsgpackTest.hs b/test/Ribosome/Test/MsgpackTest.hs
new file mode 100644
--- /dev/null
+++ b/test/Ribosome/Test/MsgpackTest.hs
@@ -0,0 +1,170 @@
+module Ribosome.Test.MsgpackTest where
+
+import qualified Data.Map.Strict as Map (fromList)
+import Data.MessagePack (Object(..))
+import Data.Text.Prettyprint.Doc (Doc, defaultLayoutOptions, layoutPretty)
+import Data.Text.Prettyprint.Doc.Render.Terminal (AnsiStyle, renderStrict)
+import Hedgehog ((===))
+import Path (Abs, Dir, File, Path, Rel, absfile, relfile)
+import Test.Tasty (TestTree, testGroup)
+
+import Ribosome.Msgpack.Decode (MsgpackDecode(..), fromMsgpack)
+import Ribosome.Msgpack.Encode (MsgpackEncode(..))
+import qualified Ribosome.Msgpack.Util as Util (string)
+import Ribosome.Test.Run (UnitTest, unitTest)
+
+newtype NT =
+  NT Text
+  deriving (Eq, Show, Generic)
+  deriving newtype (MsgpackEncode, MsgpackDecode)
+
+data Blob =
+  Blob {
+    key4 :: [[Int]],
+    key5 :: Map Int Text,
+    key6 :: NT,
+    key7 :: (Int, String)
+  }
+  deriving (Eq, Show, Generic, MsgpackEncode, MsgpackDecode)
+
+data Prod =
+  Prod Text Int
+  deriving (Eq, Show, Generic, MsgpackEncode, MsgpackDecode)
+
+data Dat =
+  Dat {
+    key1 :: Blob,
+    key2 :: Bool,
+    key3 :: Maybe Prod
+  }
+  deriving (Eq, Show, Generic, MsgpackEncode, MsgpackDecode)
+
+dat :: Dat
+dat = Dat (Blob [[1, 2], [3]] (Map.fromList [(1, "1"), (2, "2")]) (NT "nt") (91, "pair")) False (Just $ Prod "dat" 27)
+
+os :: String -> Object
+os = Util.string
+
+i :: Int64 -> Object
+i = ObjectInt
+
+encodedBlob :: Object
+encodedBlob =
+  ObjectMap $ Map.fromList [
+    (os "key4", ObjectArray [ObjectArray [i 1, i 2], ObjectArray [i 3]]),
+    (os "key5", ObjectMap $ Map.fromList [(i 1, os "1"), (i 2, os "2")]),
+    (os "key6", os "nt"),
+    (os "key7", ObjectArray [ObjectInt 91, ObjectString "pair"])
+    ]
+
+encoded :: Object
+encoded =
+  ObjectMap $ Map.fromList [
+    (os "key1", encodedBlob),
+    (os "key2", ObjectBool False),
+    (ObjectString "key3", ObjectArray [os "dat", i 27])
+    ]
+
+test_encode :: UnitTest
+test_encode =
+  encoded === toMsgpack dat
+
+doc2Text :: Either (Doc AnsiStyle) a -> Either Text a
+doc2Text =
+  mapLeft (renderStrict . layoutPretty defaultLayoutOptions)
+
+decodeSubject :: Object
+decodeSubject =
+  ObjectMap $ Map.fromList [
+    (os "key1", encodedBlob),
+    (os "key2", ObjectBool False),
+    (ObjectBinary "key3", ObjectArray [os "dat", i 27])
+    ]
+
+test_decodeBasic :: UnitTest
+test_decodeBasic =
+  Right dat === doc2Text (fromMsgpack decodeSubject)
+
+data Nope =
+  Nope {
+    present :: Int,
+    absent :: Maybe Int
+  }
+  deriving (Eq, Show, Generic, MsgpackDecode)
+
+encodedNope :: Object
+encodedNope =
+  ObjectMap $ Map.fromList [(os "present", i 5)]
+
+test_maybeMissing :: UnitTest
+test_maybeMissing =
+  Right (Nope 5 Nothing) === doc2Text (fromMsgpack encodedNope)
+
+test_decodeEither :: UnitTest
+test_decodeEither =
+  Right (Left "text" :: Either Text Int) === doc2Text (fromMsgpack (ObjectBinary "text"))
+
+data ST =
+  STL {
+    stName :: Text,
+    stCount :: Int
+  }
+  |
+  STR {
+    stName :: Text,
+    stDesc :: Text
+  }
+  deriving (Eq, Show, Generic, MsgpackEncode, MsgpackDecode)
+
+sumName :: Text
+sumName =
+  "sumName"
+
+sumCount :: Int
+sumCount =
+  1313
+
+encodedSum :: Object
+encodedSum =
+  ObjectMap $ Map.fromList [
+    (toMsgpack @Text "stName", toMsgpack sumName),
+    (toMsgpack @Text "stCount", toMsgpack sumCount)
+    ]
+
+test_encodeSum :: UnitTest
+test_encodeSum =
+  encodedSum === toMsgpack (STL sumName sumCount)
+
+test_decodeSum :: UnitTest
+test_decodeSum =
+  Right (STL sumName sumCount) === doc2Text (fromMsgpack encodedSum)
+
+encodedPath :: Object
+encodedPath =
+  ObjectString "/path/to/file"
+
+test_decodePath :: UnitTest
+test_decodePath =
+  Right [absfile|/path/to/file|] === doc2Text (fromMsgpack @(Path Abs File) encodedPath)
+
+test_failDecodePath :: UnitTest
+test_failDecodePath =
+  Left "InvalidRelDir \"/path/to/file\"" === doc2Text (fromMsgpack @(Path Rel Dir) encodedPath)
+
+test_encodePath :: UnitTest
+test_encodePath =
+  ObjectString "path/to/file" === toMsgpack [relfile|path/to/file|]
+
+test_msgpack :: TestTree
+test_msgpack =
+  testGroup "msgpack" [
+    unitTest "encode" test_encode,
+    unitTest "decode" test_decodeBasic,
+    unitTest "decode Nothing" test_maybeMissing,
+    unitTest "decode Right" test_decodeEither,
+    unitTest "encode sum type" test_encodeSum,
+    unitTest "decode sum type" test_decodeSum,
+    unitTest "decode Path" test_decodePath,
+    unitTest "decode invalid Path" test_failDecodePath,
+    unitTest "encode Path" test_encodePath
+  ]
diff --git a/test/Ribosome/Test/NvimMenuTest.hs b/test/Ribosome/Test/NvimMenuTest.hs
new file mode 100644
--- /dev/null
+++ b/test/Ribosome/Test/NvimMenuTest.hs
@@ -0,0 +1,164 @@
+module Ribosome.Test.NvimMenuTest where
+
+import Conduit (ConduitT, yield, yieldMany)
+import Control.Concurrent.Lifted (fork, killThread)
+import Control.Exception.Lifted (bracket)
+import Control.Lens (element, (^?))
+import qualified Data.Map.Strict as Map (empty, fromList)
+import Hedgehog ((===))
+import Test.Tasty (TestTree, testGroup)
+import TestError (RiboTest, TestError)
+
+import Ribosome.Api.Input (syntheticInput)
+import Ribosome.Control.Monad.Ribo (Ribo)
+import Ribosome.Menu.Action (menuReturn)
+import qualified Ribosome.Menu.Data.FilteredMenuItem as FilteredMenuItem (item)
+import Ribosome.Menu.Data.Menu (Menu(Menu))
+import Ribosome.Menu.Data.MenuConsumerAction (MenuConsumerAction)
+import Ribosome.Menu.Data.MenuItem (MenuItem, simpleMenuItem)
+import qualified Ribosome.Menu.Data.MenuItem as MenuItem (text)
+import Ribosome.Menu.Data.MenuResult (MenuResult)
+import qualified Ribosome.Menu.Data.MenuResult as MenuResult (MenuResult(..))
+import Ribosome.Menu.Prompt.Data.Prompt (Prompt(Prompt))
+import Ribosome.Menu.Prompt.Data.PromptConfig (PromptConfig(PromptConfig), PromptFlag(StartInsert))
+import Ribosome.Menu.Prompt.Data.PromptEvent (PromptEvent)
+import qualified Ribosome.Menu.Prompt.Data.PromptEvent as PromptEvent (PromptEvent(..))
+import Ribosome.Menu.Prompt.Nvim (getCharC, nvimPromptRenderer)
+import Ribosome.Menu.Prompt.Run (basicTransition)
+import Ribosome.Menu.Run (nvimMenu)
+import Ribosome.Menu.Simple (Mappings, defaultMenu)
+import Ribosome.Nvim.Api.IO (vimGetWindows)
+import Ribosome.System.Time (sleep)
+import Ribosome.Test.Run (UnitTest, unitTest)
+import Ribosome.Test.Tmux (tmuxSpecDef)
+
+promptInput ::
+  MonadIO m =>
+  [Text] ->
+  ConduitT () PromptEvent m ()
+promptInput chars' = do
+  lift $ sleep 0.1
+  yieldMany (PromptEvent.Character <$> chars')
+
+menuItems ::
+  Monad m =>
+  [Text] ->
+  ConduitT () [MenuItem Text] m ()
+menuItems =
+  yield . fmap (simpleMenuItem "name")
+
+chars :: [Text]
+chars =
+  ["i", "t", "e", "esc", "k", "k", "k", "cr"]
+
+items :: [Text]
+items =
+  ("item" <>) . show <$> [(1 :: Int)..8]
+
+exec ::
+  MonadIO m =>
+  Menu Text ->
+  Prompt ->
+  m (MenuConsumerAction m (Maybe Text), Menu Text)
+exec m@(Menu _ items' selected _ _ _) _ =
+  menuReturn item m
+  where
+    item =
+      items' ^? element selected . FilteredMenuItem.item . MenuItem.text
+
+promptConfig ::
+  ConduitT () PromptEvent (Ribo () TestError) () ->
+  PromptConfig (Ribo () TestError)
+promptConfig source =
+  PromptConfig source basicTransition nvimPromptRenderer [StartInsert]
+
+runNvimMenu ::
+  Mappings (Ribo () TestError) a Text ->
+  ConduitT () PromptEvent (Ribo () TestError) () ->
+  Ribo () TestError (MenuResult a)
+runNvimMenu maps source =
+  nvimMenu def (menuItems items) (defaultMenu maps) (promptConfig source) Nothing
+
+mappings :: Mappings (Ribo () TestError) (Maybe Text) Text
+mappings =
+  Map.fromList [("cr", exec)]
+
+nvimMenuSpec ::
+  ConduitT () PromptEvent (Ribo () TestError) () ->
+  RiboTest ()
+nvimMenuSpec =
+  (MenuResult.Return (Just "item4") ===) <=< lift . runNvimMenu mappings
+
+nvimMenuPureSpec :: RiboTest ()
+nvimMenuPureSpec =
+  nvimMenuSpec (promptInput chars)
+
+test_nvimMenuPure :: UnitTest
+test_nvimMenuPure =
+  tmuxSpecDef nvimMenuPureSpec
+
+nativeChars :: [Text]
+nativeChars =
+  ["i", "t", "e", "<esc>", "k", "<c-k>", "k", "<cr>"]
+
+nvimMenuNativeSpec :: RiboTest ()
+nvimMenuNativeSpec =
+  bracket (fork input) killThread (const $ nvimMenuSpec (getCharC 0.1))
+  where
+    input =
+      syntheticInput (Just 0.2) nativeChars
+
+test_nvimMenuNative :: UnitTest
+test_nvimMenuNative =
+  tmuxSpecDef nvimMenuNativeSpec
+
+nvimMenuInterruptSpec :: RiboTest ()
+nvimMenuInterruptSpec = do
+  (MenuResult.Aborted ===) =<< spec
+  (1 ===) =<< length <$> vimGetWindows
+  where
+    spec :: RiboTest (MenuResult ())
+    spec =
+      lift (bracket (fork input) killThread (const run))
+    run =
+      nvimMenu def (menuItems items) (defaultMenu Map.empty) (promptConfig (getCharC 0.1)) Nothing
+    input =
+      syntheticInput (Just 0.2) ["<c-c>", "<cr>"]
+
+test_nvimMenuInterrupt :: UnitTest
+test_nvimMenuInterrupt =
+  tmuxSpecDef nvimMenuInterruptSpec
+
+returnPrompt ::
+  MonadIO m =>
+  Menu Text ->
+  Prompt ->
+  m (MenuConsumerAction m Text, Menu Text)
+returnPrompt m (Prompt _ _ text) =
+  menuReturn text m
+
+navChars :: [Text]
+navChars =
+  ["i", "t", "e", "m", "1", "<bs>", "<esc>", "h", "h", "h", "h", "h", "x", "a", "o", "<cr>"]
+
+nvimMenuNavSpec :: RiboTest ()
+nvimMenuNavSpec =
+  (MenuResult.Return "toem" ===) =<< lift run
+  where
+    run =
+      bracket (fork input) killThread (const $ runNvimMenu (Map.fromList [("cr", returnPrompt)]) (getCharC 0.1))
+    input =
+      syntheticInput (Just 0.2) navChars
+
+test_nvimMenuNav :: UnitTest
+test_nvimMenuNav =
+  tmuxSpecDef nvimMenuNavSpec
+
+test_nvimMenu :: TestTree
+test_nvimMenu =
+  testGroup "nvim menu" [
+    unitTest "pure" test_nvimMenuPure,
+    unitTest "native" test_nvimMenuNative,
+    unitTest "interrupt" test_nvimMenuInterrupt,
+    unitTest "navigation" test_nvimMenuNav
+  ]
diff --git a/test/Ribosome/Test/PromptTest.hs b/test/Ribosome/Test/PromptTest.hs
new file mode 100644
--- /dev/null
+++ b/test/Ribosome/Test/PromptTest.hs
@@ -0,0 +1,39 @@
+module Ribosome.Test.PromptTest where
+
+import Conduit (awaitForever, evalStateC, runConduit, yield, (.|))
+import qualified Data.Conduit.Combinators as Conduit (last)
+import Hedgehog (TestT, (===))
+import TestError (TestError)
+
+import Ribosome.Control.Monad.Ribo (Ribo)
+import Ribosome.Menu.Prompt.Data.Prompt (Prompt(Prompt))
+import Ribosome.Menu.Prompt.Data.PromptConfig (PromptConfig(PromptConfig), PromptFlag(StartInsert))
+import Ribosome.Menu.Prompt.Data.PromptConsumed (PromptConsumed(Yes))
+import Ribosome.Menu.Prompt.Data.PromptConsumerUpdate (PromptConsumerUpdate(PromptConsumerUpdate))
+import qualified Ribosome.Menu.Prompt.Data.PromptEvent as PromptEvent (PromptEvent(..))
+import qualified Ribosome.Menu.Prompt.Data.PromptState as PromptState (PromptState(..))
+import Ribosome.Menu.Prompt.Run (basicTransition, noPromptRenderer, processPromptEvent)
+import Ribosome.Test.Run (UnitTest)
+import Ribosome.Test.Unit (unitSpecDef')
+
+promptSetSpec :: TestT (Ribo () TestError) ()
+promptSetSpec = do
+  update <- runConduit $ yield event .| exec .| Conduit.last
+  Just target === update
+  where
+    exec =
+      evalStateC initialPrompt (awaitForever (processPromptEvent config))
+    initialPrompt =
+      Prompt 1 PromptState.Normal "abc"
+    config =
+      PromptConfig (return ()) basicTransition noPromptRenderer [StartInsert]
+    event =
+      PromptEvent.Set (Prompt 10 PromptState.Normal text)
+    text =
+      "12345678"
+    target =
+      PromptConsumerUpdate event (Prompt 8 PromptState.Normal text) Yes
+
+test_promptSet :: UnitTest
+test_promptSet =
+  unitSpecDef' promptSetSpec
diff --git a/test/Ribosome/Test/RpcTest.hs b/test/Ribosome/Test/RpcTest.hs
new file mode 100644
--- /dev/null
+++ b/test/Ribosome/Test/RpcTest.hs
@@ -0,0 +1,154 @@
+module Ribosome.Test.RpcTest where
+
+import Data.DeepPrisms (DeepPrisms)
+import qualified Data.List as List (lines)
+import qualified Data.Map.Strict as Map (empty)
+import Hedgehog ((===))
+import Language.Haskell.TH hiding (reportError)
+import Neovim (CommandArguments, Plugin(..))
+import System.Log.Logger (Priority(DEBUG), setLevel, updateGlobalLogger)
+import TestError (RiboTest, TestError)
+
+import Ribosome.Api.Variable (setVar)
+import Ribosome.Control.Monad.Ribo (MonadRibo, NvimE, Ribo)
+import Ribosome.Control.Ribosome (Ribosome, newRibosome)
+import Ribosome.Error.Report (reportError)
+import Ribosome.Nvim.Api.IO (vimCommand, vimGetVar)
+import Ribosome.Nvim.Api.RpcCall (RpcError)
+import Ribosome.Plugin (RpcDef, cmd, riboPlugin, rpcHandler, sync)
+import Ribosome.System.Time (sleep)
+import Ribosome.Test.Embed (integrationSpecDef)
+import Ribosome.Test.Run (UnitTest)
+
+handleError ::
+  DeepPrisms e RpcError =>
+  TestError ->
+  Ribo s e ()
+handleError =
+  reportError "test"
+
+target :: Int
+target =
+  13
+
+resultVar :: Text
+resultVar =
+  "test_result"
+
+handler ::
+  NvimE e m =>
+  m Int
+handler = do
+  setVar resultVar target
+  return target
+
+handlerCmdCmdArgs ::
+  NvimE e m =>
+  CommandArguments ->
+  Text ->
+  Text ->
+  Text ->
+  m Int
+handlerCmdCmdArgs _ _ _ _ =
+  handler
+
+handlerCmdNoCmdArgs ::
+  NvimE e m =>
+  Text ->
+  Text ->
+  Text ->
+  m Int
+handlerCmdNoCmdArgs _ _ _ =
+  handler
+
+handlerCmdNoArgs ::
+  NvimE e m =>
+  m Int
+handlerCmdNoArgs =
+  handler
+
+handlerCmdOneArg ::
+  NvimE e m =>
+  Text ->
+  m Int
+handlerCmdOneArg _ =
+  handler
+
+handlerCmdMaybeArg ::
+  NvimE e m =>
+  Maybe Text ->
+  m Int
+handlerCmdMaybeArg _ =
+  handler
+
+handlerCmdListArg ::
+  NvimE e m =>
+  [Text] ->
+  m Int
+handlerCmdListArg _ =
+  handler
+
+$(return [])
+
+handlers ::
+  MonadRibo m =>
+  NvimE e m =>
+  [[RpcDef m]]
+handlers =
+  [
+    $(rpcHandler (sync . cmd []) 'handlerCmdCmdArgs),
+    $(rpcHandler (sync . cmd []) 'handlerCmdNoCmdArgs),
+    $(rpcHandler (sync . cmd []) 'handlerCmdNoArgs),
+    $(rpcHandler (sync . cmd []) 'handlerCmdOneArg),
+    $(rpcHandler (sync . cmd []) 'handlerCmdMaybeArg),
+    $(rpcHandler (sync . cmd []) 'handlerCmdListArg)
+    ]
+
+plugin :: IO (Plugin (Ribosome ()))
+plugin = do
+  ribo <- newRibosome "test" def
+  pure $ riboPlugin "test" ribo handlers [] handleError Map.empty
+
+successCommand ::
+  Text ->
+  RiboTest ()
+successCommand cmd' = do
+  setVar resultVar (0 :: Int)
+  vimCommand cmd'
+  (target ===) =<< vimGetVar resultVar
+
+failureCommand ::
+  Text ->
+  RiboTest ()
+failureCommand cmd' = do
+  lift (setVar resultVar (0 :: Int))
+  catchAt recoverRpc (vimCommand cmd')
+  ((0 :: Int) ===) =<< vimGetVar resultVar
+  where
+    recoverRpc (_ :: RpcError) =
+      return ()
+
+rpcSpec :: RiboTest ()
+rpcSpec = do
+  successCommand "HandlerCmdCmdArgs a b c"
+  successCommand "HandlerCmdNoCmdArgs a b c"
+  successCommand "HandlerCmdNoArgs"
+  successCommand "HandlerCmdOneArg a"
+  successCommand "HandlerCmdMaybeArg a"
+  successCommand "HandlerCmdMaybeArg"
+  successCommand "HandlerCmdListArg"
+  successCommand "HandlerCmdListArg a b c"
+  failureCommand "HandlerCmdCmdArgs a b"
+  failureCommand "HandlerCmdNoCmdArgs a b"
+  failureCommand "HandlerCmdNoArgs a"
+  sleep 1
+
+test_rpc :: UnitTest
+test_rpc = do
+  liftIO $ updateGlobalLogger "test" (setLevel DEBUG)
+  plug <- liftIO plugin
+  when debug $ traverse_ putStrLn . List.lines $ $(stringE . pprint =<< rpcHandler (sync . cmd []) 'handlerCmdListArg)
+  integrationSpecDef plug rpcSpec
+  where
+    debug =
+      False
diff --git a/test/Ribosome/Test/ScratchTest.hs b/test/Ribosome/Test/ScratchTest.hs
new file mode 100644
--- /dev/null
+++ b/test/Ribosome/Test/ScratchTest.hs
@@ -0,0 +1,95 @@
+module Ribosome.Test.ScratchTest where
+
+import qualified Data.Map.Strict as Map (toList)
+import Hedgehog ((===))
+import Neovim (Plugin(..))
+import TestError (RiboTest, handleTestError)
+
+import Ribosome.Api.Buffer (currentBufferContent)
+import Ribosome.Control.Monad.Ribo (MonadRibo, NvimE, pluginInternalL)
+import Ribosome.Control.Ribosome (Ribosome, newRibosome)
+import qualified Ribosome.Control.Ribosome as Ribosome (scratch)
+import Ribosome.Data.FloatOptions (FloatOptions(FloatOptions), FloatRelative(Cursor))
+import Ribosome.Data.ScratchOptions (ScratchOptions(ScratchOptions))
+import Ribosome.Msgpack.Error (DecodeError)
+import Ribosome.Nvim.Api.IO (vimCallFunction, vimCommand)
+import Ribosome.Plugin (riboPlugin, rpcHandler, rpcHandlerDef, sync)
+import Ribosome.Scratch (showInScratch)
+import Ribosome.Test.Await (await)
+import Ribosome.Test.Embed (integrationSpecDef)
+import Ribosome.Test.Run (UnitTest)
+
+target :: [Text]
+target = ["line 1", "line 2"]
+
+name :: Text
+name =
+  "buffi"
+
+makeScratch ::
+  NvimE e m =>
+  MonadRibo m =>
+  MonadBaseControl IO m =>
+  MonadDeepError e DecodeError m =>
+  m ()
+makeScratch =
+  void $ showInScratch target (ScratchOptions False True False True True True False Nothing Nothing Nothing [] [] name)
+
+floatOptions :: FloatOptions
+floatOptions =
+  FloatOptions Cursor 30 2 1 1 True def Nothing
+
+makeFloatScratch ::
+  NvimE e m =>
+  MonadRibo m =>
+  MonadBaseControl IO m =>
+  MonadDeepError e DecodeError m =>
+  m ()
+makeFloatScratch =
+  void $ showInScratch target options
+  where
+    options =
+      ScratchOptions False True False True True True False (Just floatOptions) Nothing (Just 0) [] [] name
+
+scratchCount ::
+  MonadRibo m =>
+  m Int
+scratchCount =
+  length . Map.toList <$> pluginInternalL Ribosome.scratch
+
+$(return [])
+
+scratchPlugin :: IO (Plugin (Ribosome ()))
+scratchPlugin = do
+  env <- newRibosome "test" ()
+  return $ riboPlugin "test" env funcs [] handleTestError def
+  where
+    funcs = [$(rpcHandlerDef 'makeScratch), $(rpcHandlerDef 'makeFloatScratch), $(rpcHandler sync 'scratchCount)]
+
+scratchSpec :: Text -> RiboTest ()
+scratchSpec fun = do
+  () <- vimCallFunction fun []
+  await ((1 :: Int) ===) scratches
+  await (target ===) currentBufferContent
+  vimCommand "bdelete"
+  await (0 ===) scratches
+  where
+    scratches = vimCallFunction "ScratchCount" []
+
+regularScratchSpec :: RiboTest ()
+regularScratchSpec =
+  scratchSpec "MakeScratch"
+
+test_regularScratch :: UnitTest
+test_regularScratch = do
+  plug <- liftIO scratchPlugin
+  integrationSpecDef plug regularScratchSpec
+
+floatScratchSpec :: RiboTest ()
+floatScratchSpec =
+  scratchSpec "MakeFloatScratch"
+
+test_floatScratch :: UnitTest
+test_floatScratch = do
+  plug <- liftIO scratchPlugin
+  integrationSpecDef plug floatScratchSpec
diff --git a/test/Ribosome/Test/SettingTest.hs b/test/Ribosome/Test/SettingTest.hs
new file mode 100644
--- /dev/null
+++ b/test/Ribosome/Test/SettingTest.hs
@@ -0,0 +1,51 @@
+module Ribosome.Test.SettingTest where
+
+import Data.Either.Combinators (swapEither)
+import Hedgehog (TestT, evalEither, (===))
+
+import Ribosome.Config.Setting (setting, updateSetting)
+import Ribosome.Control.Monad.Ribo (Ribo)
+import Ribosome.Data.Setting (Setting(Setting))
+import Ribosome.Data.SettingError (SettingError)
+import Ribosome.Error.Report.Class (ReportError(..))
+import Ribosome.Nvim.Api.RpcCall (RpcError)
+import Ribosome.Test.Run (UnitTest)
+import Ribosome.Test.Unit (unitSpec)
+
+data SettingSpecError =
+  Sett SettingError
+  |
+  Rpc RpcError
+  deriving Show
+
+instance ReportError SettingSpecError where
+  errorReport = undefined
+
+deepPrisms ''SettingSpecError
+
+sett :: Setting Int
+sett = Setting "name" True Nothing
+
+settingSuccessSpec :: TestT (Ribo s SettingSpecError) ()
+settingSuccessSpec = do
+  updateSetting sett 5
+  r <- lift (setting sett)
+  5 === r
+
+test_settingSuccess :: UnitTest
+test_settingSuccess =
+  unitSpec def () settingSuccessSpec
+
+settingFailSpec :: TestT (Ribo s SettingSpecError) ()
+settingFailSpec = do
+  ea <- lift (catchAt catch $ Right <$> result)
+  void $ evalEither (swapEither ea)
+  where
+    result :: Ribo s SettingSpecError Int
+    result = setting sett
+    catch :: SettingError -> Ribo s SettingSpecError (Either SettingError Int)
+    catch = return . Left
+
+test_settingFail :: UnitTest
+test_settingFail =
+  unitSpec def () settingFailSpec
diff --git a/test/Ribosome/Test/SyntaxTest.hs b/test/Ribosome/Test/SyntaxTest.hs
new file mode 100644
--- /dev/null
+++ b/test/Ribosome/Test/SyntaxTest.hs
@@ -0,0 +1,48 @@
+module Ribosome.Test.SyntaxTest where
+
+import TestError (TestError)
+import Hedgehog (TestT)
+import Chiasma.Data.TmuxError (TmuxError)
+import Chiasma.Test.Tmux (TmuxTestConf(..))
+
+import Ribosome.Api.Buffer (setCurrentBufferContent)
+import Ribosome.Api.Syntax (executeSyntax)
+import Ribosome.Control.Monad.Ribo (Ribo)
+import Ribosome.Data.Syntax (
+  Syntax(Syntax),
+  syntaxHighlight,
+  syntaxMatch,
+  )
+import Ribosome.Error.Report.Class (ReportError(..))
+import Ribosome.System.Time (sleep)
+import Ribosome.Test.Embed (defaultTestConfig)
+import Ribosome.Test.Run (UnitTest)
+import Ribosome.Test.Screenshot (awaitScreenshot)
+import Ribosome.Test.Tmux (tmuxSpec')
+
+data SyntaxSpecError =
+  Test TestError
+  |
+  Tmux TmuxError
+
+deepPrisms ''SyntaxSpecError
+
+instance ReportError SyntaxSpecError where
+  errorReport _ =
+    undefined
+
+syntax :: Syntax
+syntax =
+  Syntax [syntaxMatch "TestColons" "::"] [syntaxHighlight "TestColons"
+    [("cterm", "reverse"), ("ctermfg", "1"), ("gui", "reverse"), ("guifg", "#dc322f")]] []
+
+syntaxSpec :: TestT (Ribo () SyntaxSpecError) ()
+syntaxSpec = do
+  lift (setCurrentBufferContent ["function :: String -> Int", "function _ = 5"])
+  _ <- lift (executeSyntax syntax)
+  sleep 1
+  awaitScreenshot "syntax" False 0
+
+test_syntax :: UnitTest
+test_syntax =
+  tmuxSpec' def { ttcWidth = 300, ttcHeight = 51, ttcGui = False } (defaultTestConfig "syntax") def syntaxSpec
diff --git a/test/Ribosome/Test/THTest.hs b/test/Ribosome/Test/THTest.hs
new file mode 100644
--- /dev/null
+++ b/test/Ribosome/Test/THTest.hs
@@ -0,0 +1,45 @@
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+{-# OPTIONS_GHC -Wno-unused-imports #-}
+{-# OPTIONS_GHC -Wno-unused-top-binds #-}
+{- HLINT ignore -}
+
+module Ribosome.Test.THTest where
+
+import Data.Aeson (FromJSON)
+-- import Data.Default (def)
+import GHC.Generics (Generic)
+import Language.Haskell.TH
+import Neovim (Plugin(..))
+-- import TestError (handleTestError)
+
+-- import Ribosome.Control.Ribosome (Ribosome, newRibosome)
+import Ribosome.Control.Ribosome (Ribosome, newRibosome)
+import Ribosome.Msgpack.Decode (MsgpackDecode)
+import Ribosome.Msgpack.Encode (MsgpackEncode)
+import Ribosome.Plugin
+import Ribosome.Test.Run (UnitTest)
+
+data Par =
+  Par {
+    parA :: Int,
+    parB :: Int
+  }
+  deriving (Eq, Show, Generic, MsgpackDecode, MsgpackEncode, FromJSON)
+
+handler :: Text -> m ()
+handler =
+  undefined
+
+$(return [])
+
+plugin' :: IO (Plugin (Ribosome Int))
+plugin' = do
+  -- ribo <- newRibosome ("test" :: Text) 1
+  undefined
+  -- return $ riboPlugin "test" ribo [$(rpcHandler (cmd []) 'handler)] [] handleTestError def
+
+test_plug :: UnitTest
+test_plug = do
+  return ()
+  -- _ <- plugin'
+  -- traverse_ putStrLn $ lines $(stringE . pprint =<< rpcHandler (sync . cmd []) 'handler)
diff --git a/test/Ribosome/Test/WatcherTest.hs b/test/Ribosome/Test/WatcherTest.hs
new file mode 100644
--- /dev/null
+++ b/test/Ribosome/Test/WatcherTest.hs
@@ -0,0 +1,46 @@
+module Ribosome.Test.WatcherTest where
+
+import qualified Data.Map.Strict as Map (singleton)
+import Data.MessagePack (Object)
+import Hedgehog ((===))
+import Neovim (Plugin(..))
+import TestError (RiboTest, handleTestError)
+
+import Ribosome.Api.Autocmd (doautocmd)
+import Ribosome.Api.Variable (setVar)
+import Ribosome.Control.Monad.Ribo (NvimE)
+import Ribosome.Control.Ribosome (Ribosome, newRibosome)
+import Ribosome.Nvim.Api.IO (vimGetVar)
+import Ribosome.Plugin (riboPlugin)
+import Ribosome.Test.Await (await)
+import Ribosome.Test.Embed (integrationSpecDef)
+import Ribosome.Test.Orphans ()
+import Ribosome.Test.Run (UnitTest)
+
+changed :: NvimE e m => Object -> m ()
+changed _ =
+  setVar "number" =<< ((+) (1 :: Int)) <$> vimGetVar "number"
+
+varWatcherPlugin :: IO (Plugin (Ribosome ()))
+varWatcherPlugin = do
+  env <- newRibosome "varwatcher" ()
+  return $ riboPlugin "varwatcher" env [] [] handleTestError (Map.singleton "trigger" changed)
+
+varWatcherSpec :: RiboTest ()
+varWatcherSpec = do
+  setVar "number" (10 :: Int)
+  setVar "trigger" (5 :: Int)
+  await ((10 :: Int) ===) (vimGetVar "number")
+  await ((5 :: Int) ===) (vimGetVar "trigger")
+  doautocmd True "CmdlineLeave"
+  doautocmd True "CmdlineLeave"
+  await ((11 :: Int) ===) (vimGetVar "number")
+  setVar "trigger" (6 :: Int)
+  await ((6 :: Int) ===) (vimGetVar "trigger")
+  doautocmd True "CmdlineLeave"
+  await ((12 :: Int) ===) (vimGetVar "number")
+
+test_varWatcher :: UnitTest
+test_varWatcher = do
+  plug <- liftIO varWatcherPlugin
+  integrationSpecDef plug varWatcherSpec
diff --git a/test/Ribosome/Test/WindowTest.hs b/test/Ribosome/Test/WindowTest.hs
new file mode 100644
--- /dev/null
+++ b/test/Ribosome/Test/WindowTest.hs
@@ -0,0 +1,43 @@
+module Ribosome.Test.WindowTest where
+
+import Hedgehog ((===))
+import TestError (RiboTest)
+
+import Ribosome.Api.Window (ensureMainWindow)
+import Ribosome.Msgpack.Encode (toMsgpack)
+import Ribosome.Nvim.Api.Data (Window)
+import Ribosome.Nvim.Api.IO (bufferSetOption, vimCommand, vimGetCurrentBuffer, vimGetCurrentWindow, vimGetWindows)
+import Ribosome.Test.Run (UnitTest)
+import Ribosome.Test.Unit (unitSpecDef')
+
+setCurrentNofile :: RiboTest ()
+setCurrentNofile = do
+  buf <- vimGetCurrentBuffer
+  bufferSetOption buf "buftype" (toMsgpack ("nofile" :: Text))
+
+createNofile :: RiboTest Window
+createNofile = do
+  initialWindow <- vimGetCurrentWindow
+  vimCommand "new"
+  setCurrentNofile
+  return initialWindow
+
+findMainWindowExistingSpec :: RiboTest ()
+findMainWindowExistingSpec = do
+  initialWindow <- createNofile
+  (initialWindow ===) =<< ensureMainWindow
+
+test_findMainWindowExisting :: UnitTest
+test_findMainWindowExisting =
+  unitSpecDef' findMainWindowExistingSpec
+
+findMainWindowCreateSpec :: RiboTest ()
+findMainWindowCreateSpec = do
+  setCurrentNofile
+  void createNofile
+  void ensureMainWindow
+  (3 ===) =<< length <$> vimGetWindows
+
+test_findMainWindowCreate :: UnitTest
+test_findMainWindowCreate =
+  unitSpecDef' findMainWindowCreateSpec
diff --git a/test/TestError.hs b/test/TestError.hs
new file mode 100644
--- /dev/null
+++ b/test/TestError.hs
@@ -0,0 +1,28 @@
+module TestError where
+
+import Hedgehog (TestT)
+import Ribosome.Control.Monad.Ribo (Nvim, NvimE, Ribo)
+import Ribosome.Data.Mapping (MappingError)
+import Ribosome.Error.Report.Class (ReportError(..))
+import Ribosome.Log (showError)
+import Ribosome.Msgpack.Error (DecodeError)
+import Ribosome.Nvim.Api.RpcCall (RpcError)
+
+data TestError =
+  Rpc RpcError
+  |
+  Decode DecodeError
+  |
+  Mapping MappingError
+  deriving (Show, Generic, ReportError)
+
+deepPrisms ''TestError
+
+handleTestError :: TestError -> Ribo s TestError ()
+handleTestError =
+  showError "error in test:"
+
+type RiboTest a = TestT (Ribo () TestError) a
+type RiboT a = Ribo () TestError a
+
+instance (Nvim m, MonadDeepError TestError RpcError m) => NvimE TestError (TestT m) where
