diff --git a/LICENSE b/LICENSE
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2020 Torsten Schmits
+Copyright (c) 2022 Torsten Schmits
 
 Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
 following conditions are met:
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/lib/Ribosome/Handler.hs b/lib/Ribosome/Handler.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Handler.hs
@@ -0,0 +1,21 @@
+module Ribosome.Handler (
+  module Ribosome.Host.Data.RpcType,
+  module Ribosome.Host.Handler,
+) where
+
+import Ribosome.Host.Data.RpcType (
+  AutocmdEvents (..),
+  AutocmdGroup (..),
+  AutocmdOptions (..),
+  AutocmdPatterns (..),
+  CommandCompletion (..),
+  CommandOptions (..),
+  CompleteStyle (..),
+  RpcType (..),
+  )
+import Ribosome.Host.Handler (
+  complete,
+  completeBuiltin,
+  completeCustom,
+  completeWith,
+  )
diff --git a/lib/Ribosome/Test.hs b/lib/Ribosome/Test.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Test.hs
@@ -0,0 +1,164 @@
+module Ribosome.Test (
+  -- * Introduction
+  -- $intro
+
+  -- * Embedded testing
+  -- $embed
+
+  -- * tmux testing
+  -- $tmux
+
+  -- * Embedded test API
+  testPlugin,
+  testEmbed,
+  testPluginEmbed,
+  runEmbedTest,
+  runTest,
+  testPluginConf,
+  testPlugin_,
+  testEmbedConf,
+  testEmbed_,
+  testEmbedLevel,
+  testEmbedLevel_,
+  testEmbedDebug,
+  testEmbedDebug_,
+  testEmbedTrace,
+  testEmbedTrace_,
+  runTestConf,
+  runTestLogConf,
+  EmbedStackWith,
+  EmbedStack,
+  EmbedHandlerStack,
+  TestEffects,
+  TestConfig (TestConfig),
+  TmuxTestConfig (TmuxTestConfig),
+  -- * Error handling
+  module Ribosome.Test.Error,
+  module Ribosome.Host.Data.Report,
+  -- * Assertions for Neovim UI elements
+  windowCountIs,
+  cursorIs,
+  currentCursorIs,
+  awaitScreenshot,
+  -- * Assertions that are made repeatedly until the succeed
+  assertWait,
+  assertWaitFor,
+) where
+
+import Ribosome.Host.Data.Report (resumeReportFail, stopReportToFail)
+import Ribosome.Test.Data.TestConfig (TestConfig (TestConfig), TmuxTestConfig (TmuxTestConfig))
+import Ribosome.Test.Embed (
+  EmbedHandlerStack,
+  EmbedStack,
+  EmbedStackWith,
+  TestEffects,
+  runEmbedTest,
+  runTest,
+  runTestConf,
+  runTestLogConf,
+  testEmbed,
+  testEmbedConf,
+  testEmbedLevel,
+  testEmbedLevel_,
+  testEmbedTrace,
+  testEmbedDebug,
+  testEmbedTrace_,
+  testEmbedDebug_,
+  testEmbed_,
+  testPlugin,
+  testPluginConf,
+  testPluginEmbed,
+  testPlugin_,
+  )
+import Ribosome.Test.Error
+import Ribosome.Test.Screenshot (awaitScreenshot)
+import Ribosome.Test.Ui
+import Ribosome.Test.Wait
+
+-- $intro
+-- This is the test library for the "Ribosome" Neoivm plugin framework.
+--
+-- Three different test environments are available:
+--
+-- - "Ribosome.Test.Embed" runs Neovim as a subprocess and connects over stdio
+--
+-- - "Ribosome.Test.EmbedTmux" is like "Ribosome.Test.Embed", but provides a tmux server
+--
+-- - "Ribosome.Test.SocketTmux" runs Neovim in a window in a fresh tmux server, either headless in a pseudo terminal
+-- or in an @xterm@ instance
+--
+-- This module reexports "Ribosome.Test.Embed".
+
+-- $embed
+-- Running a test against an embedded Neovim process is the simplest approach that is suited for unit testing plugin
+-- logic where the integration with Neovim startup isn't important.
+--
+-- Handlers can be registered in Neovim and triggered via RPC API functions like 'nvimCallFunction' and 'nvimCommand'.
+-- Most of the time this is only interesting if a handler has complex parameters and you want to test that they are
+-- decoded correctly, or that the handler is triggered properly by an autocmd.
+-- In more basic cases, where only the interaction with Neovim from within the handler is relevant, it can simply be run
+-- directly.
+--
+-- > import Polysemy.Test
+-- > import Ribosome
+-- > import Ribosome.Api
+-- > import Ribosome.Test
+-- >
+-- > store ::
+-- >   Member (Rpc !! RpcError) r =>
+-- >   Args ->
+-- >   Handler r ()
+-- > store (Args msg) =
+-- >   ignoreRpcError do
+-- >     nvimSetVar "message" msg
+-- >
+-- > test_direct :: UnitTest
+-- > test_direct =
+-- >   testEmbed_ do
+-- >     store "test directly"
+-- >     assertEq "test directly" =<< nvimGetVar @Text "message"
+-- >
+-- > test_rpc :: UnitTest
+-- > test_rpc =
+-- >   testPlugin_ [rpcCommand "Store" Sync store] do
+-- >     nvimCommand "Store test RPC"
+-- >     assertEq "test RPC" =<< nvimGetVar @Text "message"
+--
+-- See [Ribosome.Test.Embed]("Ribosome.Test.Embed") for more options.
+
+-- $tmux
+-- It is possible to run a standalone Neovim instance to test against.
+-- This is useful to observe the UI's behaviour for debugging purposes, but might also be desired to test a feature in
+-- the full environment that is used in production.
+--
+-- Ribosome provides a testing mode that starts a terminal with a tmux server, in which Neovim is executed as a regular
+-- shell process.
+-- Variants of this that run tmux in a pseudo terminal that is not rendered, or simply run a tmux server for use in an
+-- embedded test, are also available.
+--
+-- In the terminal case, the test connects the plugin over a socket.
+-- It is possible to take \"screenshots\" (capturing the tmux pane running Neovim) that are automatically stored in the
+-- @fixtures@ directory of the test suite and compared to previous recordings on subsequent runs, as in this example
+-- that runs tmux in a terminal and tests some syntax rules:
+--
+-- > import Polysemy.Test
+-- > import Ribosome.Api
+-- > import Ribosome.Syntax
+-- > import Ribosome.Test
+-- > import Ribosome.Test.SocketTmux
+-- >
+-- > syntax :: Syntax
+-- > syntax =
+-- >   Syntax [syntaxMatch "TestColons" "::"] [
+-- >     syntaxHighlight "TestColons" [("cterm", "reverse"), ("ctermfg", "1"), ("gui", "reverse"), ("guifg", "#dc322f")]
+-- >   ] []
+-- >
+-- > test_syntax :: UnitTest
+-- > test_syntax =
+-- >   testSocketTmuxGui do
+-- >     setCurrentBufferContent ["function :: String -> Int", "function _ = 5"]
+-- >     _ <- executeSyntax syntax
+-- >     awaitScreenshot False "syntax" 0
+-- >
+-- > See [Ribosome.Test.SocketTmux]("Ribosome.Test.SocketTmux") and [Ribosome.Test.EmbedTmux]("Ribosome.Test.EmbedTmux")
+-- > for more options.
diff --git a/lib/Ribosome/Test/Await.hs b/lib/Ribosome/Test/Await.hs
deleted file mode 100644
--- a/lib/Ribosome/Test/Await.hs
+++ /dev/null
@@ -1,37 +0,0 @@
-module Ribosome.Test.Await where
-
-import Hedgehog (TestT)
-import Control.Exception (throw)
-import Control.Monad.Error.Class (MonadError (throwError), catchError)
-import Hedgehog.Internal.Property (mkTestT, runTestT, Failure, Journal)
-
-import Ribosome.Control.Concurrent.Wait (WaitError(Thrown, ConditionUnmet, NotStarted), waitIODef)
-
-await ::
-  ∀ e a b m .
-  MonadError e m =>
-  MonadIO m =>
-  MonadBaseControl IO m =>
-  (a -> TestT m b) ->
-  m a ->
-  TestT m b
-await assertion acquire = do
-  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
-    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/Data/TestConfig.hs b/lib/Ribosome/Test/Data/TestConfig.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Test/Data/TestConfig.hs
@@ -0,0 +1,28 @@
+module Ribosome.Test.Data.TestConfig where
+
+import qualified Chiasma.Test.Data.TmuxTestConfig as Chiasma
+
+import Ribosome.Data.PluginConfig (PluginConfig (PluginConfig))
+import Ribosome.Host.Data.HostConfig (HostConfig (HostConfig), dataLogConc)
+
+data TestConfig =
+  TestConfig {
+    freeze :: Bool,
+    plugin :: PluginConfig ()
+  }
+  deriving stock (Eq, Show, Generic)
+
+instance Default TestConfig where
+  def =
+    TestConfig False (PluginConfig "test" (HostConfig def { dataLogConc = False }) unit)
+
+data TmuxTestConfig =
+  TmuxTestConfig {
+    core :: TestConfig,
+    tmux :: Chiasma.TmuxTestConfig
+  }
+  deriving stock (Eq, Show, Generic)
+
+instance Default TmuxTestConfig where
+  def =
+    TmuxTestConfig def def { Chiasma.gui = False }
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,351 +1,278 @@
-module Ribosome.Test.Embed where
-
-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 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 (
-  Config,
-  Neovim(Neovim),
-  StateTransition(Failure, InitSuccess, Quit),
-  globalFunctionMap,
-  mkFunctionMap,
-  newConfig,
-  pluginSettings,
-  retypeConfig,
-  transitionTo,
-  )
-import Neovim.Main (standalone)
-import Neovim.Plugin (Plugin(Plugin), startPluginThreads)
-import Neovim.Plugin.Internal (NeovimPlugin, wrapPlugin)
-import Neovim.RPC.Common (RPCConfig, newRPCConfig)
-import Neovim.RPC.EventHandler (runEventHandler)
-import Neovim.RPC.SocketReader (runSocketReader)
-import System.Directory (makeAbsolute)
-import System.Exit (ExitCode)
-import System.Log.Logger (Priority(ERROR), setLevel, updateGlobalLogger)
-import qualified System.Posix.Signals as Signal (killProcess, signalProcess)
-import System.Process (getPid)
-import System.Process.Typed (
-  Process,
-  ProcessConfig,
-  createPipe,
-  getExitCode,
-  getStdin,
-  getStdout,
-  proc,
-  setStdin,
-  setStdout,
-  startProcess,
-  stopProcess,
-  unsafeProcessHandle,
-  )
-
-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(..))
-import Ribosome.Error.Report.Class (ReportError(errorReport))
-import Ribosome.Nvim.Api.IO (vimSetVar)
-import Ribosome.Plugin.RpcHandler (RpcHandler(native))
-import Ribosome.System.Time (sleep, sleepW)
-import Ribosome.Test.Orphans ()
-
-type Runner m = ∀ a . TestConfig -> m a -> m a
+module Ribosome.Test.Embed (
+  -- * Embedded Neovim testing
+  -- $intro
 
-newtype Vars =
-  Vars (Map Text Object)
-  deriving (Eq, Show)
-  deriving newtype (Default, Semigroup, Monoid)
+  testPlugin,
+  testEmbed,
+  testPluginEmbed,
+  runEmbedTest,
+  runTest,
+  testPluginConf,
+  testPlugin_,
+  testEmbedConf,
+  testEmbed_,
+  testEmbedLevel,
+  testEmbedLevel_,
+  testEmbedTrace,
+  testEmbedDebug,
+  testEmbedTrace_,
+  testEmbedDebug_,
+  runTestConf,
+  runTestLogConf,
+  EmbedStackWith,
+  EmbedStack,
+  EmbedHandlerStack,
+  TestEffects,
+) where
 
--- |left biased
-varsUnion :: Vars -> Vars -> Vars
-varsUnion (Vars v1) (Vars v2) =
-  Vars (Map.union v1 v2)
+import Log (Severity (Debug, Trace))
+import Polysemy.Test (TestError, UnitTest)
 
-varsFromList :: [(Text, Object)] -> Vars
-varsFromList =
-  Vars . Map.fromList
+import Ribosome.Data.PluginConfig (PluginConfig (PluginConfig))
+import Ribosome.Data.PluginName (PluginName)
+import Ribosome.Data.SettingError (SettingError)
+import Ribosome.Effect.Scratch (Scratch)
+import Ribosome.Effect.Settings (Settings)
+import Ribosome.Embed (HandlerEffects, embedPlugin, interpretPluginEmbed)
+import Ribosome.Host.Data.BootError (BootError)
+import Ribosome.Host.Data.HostConfig (setStderr)
+import Ribosome.Host.Data.Report (Report)
+import Ribosome.Host.Data.RpcError (RpcError)
+import Ribosome.Host.Data.RpcHandler (RpcHandler)
+import Ribosome.Host.Effect.Rpc (Rpc)
+import Ribosome.Host.Error (resumeBootError)
+import Ribosome.Host.Interpret (HigherOrder)
+import Ribosome.Host.Interpreter.Handlers (withHandlers)
+import Ribosome.Host.Interpreter.Host (HostDeps)
+import qualified Ribosome.Host.Test.Data.TestConfig as Host
+import qualified Ribosome.Host.Test.Run as Host
+import Ribosome.Host.Test.Run (TestConfStack, TestStack, runUnitTest)
+import Ribosome.Plugin.Builtin (BuiltinHandlersDeps)
+import Ribosome.Test.Data.TestConfig (TestConfig (TestConfig))
+import Ribosome.Test.Error (testError, testHandler)
+import Ribosome.Test.Log (testLogLevel)
 
-data TestConfig =
-  TestConfig {
-    tcPluginName :: Text,
-    tcExtraRtp :: Text,
-    tcLogPath :: FilePath,
-    tcTimeout :: Word,
-    tcCmdline :: Maybe [Text],
-    tcCmdArgs :: [Text],
-    tcVariables :: Vars
-  }
+-- $intro
+-- The function 'testPluginEmbed' starts an embedded Neovim subprocess and a Ribosome main loop, then executes the
+-- supplied 'Sem'.
+--
+-- This can be interpreted into a "Hedgehog" 'Hedgehog.TestT' by using the functions 'runEmbedTest' and 'runTest'.
+--
+-- The functions 'testPluginConf' and 'testPlugin' run a full Ribosome plugin with RPC handlers and extra effects in
+-- addition to the above.
+-- This can be used to test calling RPC handlers from Neovim, which usually shouldn't be necessary but may be helpful
+-- for some edge cases.
+--
+-- The functions 'testEmbedConf' and 'testEmbed' run tests with extra effects, but no handlers.
+-- This is the most advisable way to test plugins, running handlers directly as Haskell functions instead of routing
+-- them through Neovim, in particular for those that don't have any parameters.
 
-instance Default TestConfig where
-  def = TestConfig "ribosome" "test/u/fixtures/rtp" "test/u/temp/log" 10 def def def
+-- |The extra effects that tests are expected to use, related to errors.
+--
+-- The plugin effects 'Scratch', 'Settings' and 'Rpc' are allowed without 'Resume', causing tests to terminate
+-- immediately if one of these effects is used and throws an error.
+--
+-- Additionally, the two core errors, 'LogReport' and 'RpcError' are executed directly via 'Stop'.
+type TestEffects =
+  [
+    Stop Report,
+    Stop RpcError,
+    Scratch,
+    Settings,
+    Rpc
+  ]
 
-defaultTestConfigWith :: Text -> Vars -> TestConfig
-defaultTestConfigWith name vars =
-  def { tcPluginName = name, tcVariables = vars }
+-- |The full test stack below test effects and extra effects.
+type EmbedHandlerStack =
+  HandlerEffects ++ Reader PluginName : TestStack
 
-defaultTestConfig :: Text -> TestConfig
-defaultTestConfig name = defaultTestConfigWith name def
+-- |The full test stack with additional effects.
+type EmbedStackWith r =
+  TestEffects ++ r ++ EmbedHandlerStack
 
-setVars :: ∀ m e. NvimE e m => Vars -> m ()
-setVars (Vars vars) =
-  traverse_ set (Map.toList vars)
-  where
-    set :: (Text, Object) -> m ()
-    set =
-      uncurry vimSetVar
+-- |The full test stack with no additional effects.
+type EmbedStack =
+  EmbedStackWith '[]
 
-setupPluginEnv ::
-  MonadIO m =>
-  NvimE e m =>
+-- |Interpret the basic test effects without 'IO' related effects.
+runTestLogConf ::
+  Members [Error BootError, Resource, Race, Async, Embed IO] r =>
   TestConfig ->
-  m ()
-setupPluginEnv (TestConfig _ rtp _ _ _ _ vars) = do
-  absRtp <- liftIO $ makeAbsolute (toString rtp)
-  rtpCat (toText absRtp)
-  setVars vars
-
-killPid :: Integral a => a -> IO ()
-killPid =
-  void . tryAny . Signal.signalProcess Signal.killProcess . fromIntegral
-
-killProcess :: Process i o e -> IO ()
-killProcess prc = do
-  void $ try @_ @SomeException do
-    let handle = unsafeProcessHandle prc
-    mayPid <- getPid handle
-    traverse_ killPid mayPid
-
-testNvimProcessConfig :: TestConfig -> ProcessConfig Handle Handle ()
-testNvimProcessConfig TestConfig {..} =
-  setStdin createPipe . setStdout createPipe . proc "nvim" . fmap toString $ args <> tcCmdArgs
-  where
-    args = fromMaybe defaultArgs tcCmdline
-    defaultArgs = ["--embed", "-n", "-u", "NONE", "-i", "NONE"]
+  InterpretersFor (Reader PluginName : TestConfStack) r
+runTestLogConf (TestConfig freezeTime (PluginConfig name conf _)) =
+  Host.runTestLogConf (Host.TestConfig freezeTime conf) .
+  runReader name
 
-startHandlers ::
-  MonadIO m =>
-  Handle ->
-  Handle ->
+-- |Run the basic test effects as a "Hedgehog" test.
+runTestConf ::
+  HasCallStack =>
   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]
-  return stopEventHandlers
-  where
-    run runner hand = async . void $ runner hand emptyConf
-    emptyConf = nvimConf { Internal.pluginSettings = Nothing }
+  Sem (Reader PluginName : TestStack) () ->
+  UnitTest
+runTestConf conf =
+  runUnitTest .
+  runTestLogConf conf
 
-startStdioHandlers ::
-  MonadIO m =>
-  NvimProc ->
+-- |Run the plugin stack and the test stack, using the supplied config.
+runEmbedTest ::
+  HasCallStack =>
   TestConfig ->
-  Internal.Config RPCConfig ->
-  m (IO ())
-startStdioHandlers prc =
-  startHandlers (getStdout prc) (getStdin prc)
-
-runNeovimThunk :: Internal.Config e -> Neovim e a -> IO a
-runNeovimThunk cfg (Internal.Neovim thunk) =
-  runReaderT (runResourceT thunk) cfg
+  Sem EmbedHandlerStack () ->
+  UnitTest
+runEmbedTest conf =
+  runTestConf conf .
+  interpretPluginEmbed
 
-type NvimProc = Process Handle Handle ()
+-- |Run the plugin stack and the test stack, using the default config.
+runTest ::
+  HasCallStack =>
+  Sem EmbedHandlerStack () ->
+  UnitTest
+runTest =
+  runEmbedTest def
 
-waitQuit :: NvimProc -> IO (Maybe ExitCode)
-waitQuit prc =
-  wait 30
-  where
-    wait :: Int -> IO (Maybe ExitCode)
-    wait 0 = return Nothing
-    wait count = do
-      code <- getExitCode prc
-      case code of
-        Just a -> return $ Just a
-        Nothing -> do
-          sleep 0.1
-          wait $ count - 1
+-- |Run the test plugin effects, 'TestEffects', and start an embedded Neovim subprocess.
+testPluginEmbed ::
+  Members (HostDeps er) r =>
+  Members BuiltinHandlersDeps r =>
+  Members [Settings !! SettingError, Error TestError] r =>
+  InterpretersFor TestEffects r
+testPluginEmbed =
+  embedPlugin .
+  resumeBootError @Rpc .
+  resumeBootError @Settings .
+  resumeBootError @Scratch .
+  testError .
+  testHandler .
+  insertAt @4
 
-quitNvim :: Internal.Config e -> NvimProc -> IO ()
-quitNvim testCfg prc = do
-  quitThread <- async $ runNeovimThunk testCfg quit
-  result <- waitQuit prc
-  case result of
-    Just _ -> return ()
-    Nothing -> killProcess prc
-  cancel quitThread
-  where
-    quit = vim_command "qall!"
+-- |Run a full plugin test, using extra effects and RPC handlers.
+testPluginConf ::
+  ∀ r .
+  HasCallStack =>
+  HigherOrder r EmbedHandlerStack =>
+  TestConfig ->
+  InterpretersFor r EmbedHandlerStack ->
+  [RpcHandler (r ++ EmbedHandlerStack)] ->
+  Sem (EmbedStackWith r) () ->
+  UnitTest
+testPluginConf conf effs handlers =
+  runEmbedTest conf .
+  effs .
+  withHandlers handlers .
+  testPluginEmbed
 
-shutdownNvim :: Internal.Config e -> NvimProc -> IO () -> IO ()
-shutdownNvim _ prc stopEventHandlers = do
-  stopEventHandlers
-  killProcess prc
+-- |Run a full plugin test, using extra effects and RPC handlers.
+testPlugin ::
+  ∀ r .
+  HasCallStack =>
+  HigherOrder r EmbedHandlerStack =>
+  InterpretersFor r EmbedHandlerStack ->
+  [RpcHandler (r ++ EmbedHandlerStack)] ->
+  Sem (EmbedStackWith r) () ->
+  UnitTest
+testPlugin =
+  testPluginConf @r def
 
-runTest ::
-  MonadIO m =>
-  MonadFail m =>
-  ReportError e =>
-  RpcHandler e env n =>
-  MonadBaseControl IO m =>
-  TestConfig ->
-  Internal.Config env ->
-  n a ->
-  m a
-runTest TestConfig{..} testCfg thunk = do
-  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"
+-- |Run a plugin test with RPC handlers.
+testPlugin_ ::
+  HasCallStack =>
+  [RpcHandler EmbedHandlerStack] ->
+  Sem EmbedStack () ->
+  UnitTest
+testPlugin_ =
+  testPlugin @'[] id
 
-runEmbeddedNvim ::
-  MonadIO m =>
-  MonadFail m =>
-  MonadBaseControl IO m =>
-  RpcHandler e env n =>
-  ReportError e =>
+-- |Run a plugin test with extra effects but no RPC handlers.
+testEmbedConf ::
+  ∀ r .
+  HasCallStack =>
+  HigherOrder r EmbedHandlerStack =>
   TestConfig ->
-  env ->
-  n a ->
-  NvimProc ->
-  m a
-runEmbeddedNvim conf ribo thunk prc = do
-  nvimConf <- liftIO (Internal.newConfig (pure Nothing) newRPCConfig)
-  let testCfg = Internal.retypeConfig ribo nvimConf
-  bracket (startStdioHandlers prc conf nvimConf) (liftIO . shutdownNvim testCfg prc) (const $ runTest conf testCfg thunk)
+  InterpretersFor r EmbedHandlerStack ->
+  Sem (EmbedStackWith r) () ->
+  UnitTest
+testEmbedConf conf effs =
+  testPluginConf @r conf effs mempty
 
-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)
+-- |Run a plugin test with extra effects but no RPC handlers.
+testEmbed ::
+  ∀ r .
+  HasCallStack =>
+  HigherOrder r EmbedHandlerStack =>
+  InterpretersFor r EmbedHandlerStack ->
+  Sem (EmbedStackWith r) () ->
+  UnitTest
+testEmbed =
+  testEmbedConf @r def
 
-runEmbedded ::
-  MonadIO m =>
-  MonadFail m =>
-  MonadBaseControl IO m =>
-  RpcHandler e env n =>
-  ReportError e =>
-  TestConfig ->
-  env ->
-  n a ->
-  m a
-runEmbedded conf ribo thunk = do
-  let pc = testNvimProcessConfig conf
-  withProcessWait pc $ runEmbeddedNvim conf ribo thunk
+-- |Run a plugin test with extra effects but no RPC handlers.
+--
+-- Takes a log level, for which the default is to only print critical errors.
+testEmbedLevel ::
+  ∀ r .
+  HasCallStack =>
+  HigherOrder r EmbedHandlerStack =>
+  Severity ->
+  InterpretersFor r EmbedHandlerStack ->
+  Sem (EmbedStackWith r) () ->
+  UnitTest
+testEmbedLevel level =
+  testEmbedConf @r (def & #plugin . #host %~ setStderr level)
 
-unsafeEmbeddedSpec ::
-  MonadIO m =>
-  MonadFail m =>
-  MonadBaseControl IO m =>
-  RpcHandler e env n =>
-  ReportError e =>
-  Runner n ->
-  TestConfig ->
-  env ->
-  n a ->
-  m a
-unsafeEmbeddedSpec runner conf s spec =
-  runEmbedded conf s $ runner conf spec
+-- |Run a plugin test with extra effects but no RPC handlers at the 'Debug' log level.
+testEmbedDebug ::
+  ∀ r .
+  HasCallStack =>
+  HigherOrder r EmbedHandlerStack =>
+  InterpretersFor r EmbedHandlerStack ->
+  Sem (EmbedStackWith r) () ->
+  UnitTest
+testEmbedDebug effs =
+  testLogLevel Debug \ conf -> testEmbedConf @r conf effs
 
-unsafeEmbeddedSpecR ::
-  MonadIO m =>
-  MonadFail m =>
-  ReportError e =>
-  MonadBaseControl IO m =>
-  RpcHandler e (Ribosome env) n =>
-  Runner n ->
-  TestConfig ->
-  env ->
-  n a ->
-  m a
-unsafeEmbeddedSpecR runner conf env spec = do
-  tv <- newRibosomeTMVar env
-  let ribo = Ribosome (tcPluginName conf) tv
-  unsafeEmbeddedSpec runner conf ribo spec
+-- |Run a plugin test with extra effects but no RPC handlers at the 'Trace' log level for debugging RPC traffic.
+testEmbedTrace ::
+  ∀ r .
+  HasCallStack =>
+  HigherOrder r EmbedHandlerStack =>
+  InterpretersFor r EmbedHandlerStack ->
+  Sem (EmbedStackWith r) () ->
+  UnitTest
+testEmbedTrace effs =
+  testLogLevel Trace \ conf -> testEmbedConf @r conf effs
 
-runPlugin ::
-  MonadIO m =>
-  MonadBaseControl IO m =>
-  Handle ->
-  Handle ->
-  [Neovim () NeovimPlugin] ->
-  Internal.Config c ->
-  m (MVar Internal.StateTransition)
-runPlugin evHandlerHandle sockreaderHandle plugins baseConf = do
-  liftIO (updateGlobalLogger "Neovim.Plugin" (setLevel ERROR))
-  rpcConf <- liftIO newRPCConfig
-  let conf = Internal.retypeConfig rpcConf baseConf
-  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
-  return (Internal.transitionTo conf)
+-- |Run a plugin test without extra effects and RPC handlers.
+testEmbed_ ::
+  HasCallStack =>
+  Sem EmbedStack () ->
+  UnitTest
+testEmbed_ =
+  testPlugin_ mempty
 
-inTestT ::
-  ∀ n m a .
-  TestT n a ->
-  (∀ x . n x -> m x) ->
-  TestT m a
-inTestT ma f =
-  mkTestT (f (runTestT ma))
+-- |Run a plugin test without extra effects and RPC handlers.
+--
+-- Takes a log level, for which the default is to only print critical errors.
+testEmbedLevel_ ::
+  HasCallStack =>
+  Severity ->
+  Sem EmbedStack () ->
+  UnitTest
+testEmbedLevel_ level =
+  testEmbedConf @'[] (def & #plugin . #host %~ setStderr level) id
 
-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 ->
-  TestT n a ->
-  TestT m a
-integrationSpec conf plugin@(Plugin env _) thunk =
-  inTestT thunk \ na ->
-    withProcessTerm (testNvimProcessConfig conf) (run na)
-  where
-    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 =
-      liftIO (runPlugin (getStdin prc) (getStdout prc) [wrapPlugin plugin] nvimConf <* sleep 0.5)
-    release transitions =
-      tryPutMVar transitions Internal.Quit *> sleep 0.5
+-- |Run a plugin test without extra effects and RPC handlers at the 'Debug' log level.
+testEmbedDebug_ ::
+  HasCallStack =>
+  Sem EmbedStack () ->
+  UnitTest
+testEmbedDebug_ =
+  testEmbedLevel_ Debug
 
-integrationSpecDef ::
-  NvimE e n =>
-  MonadIO m =>
-  MonadIO n =>
-  MonadFail m =>
-  ReportError e =>
-  RpcHandler e env n =>
-  MonadBaseControl IO m =>
-  Plugin env ->
-  TestT n a ->
-  TestT m a
-integrationSpecDef =
-  integrationSpec def
+-- |Run a plugin test without extra effects and RPC handlers at the 'Trace' log level for debugging RPC traffic.
+testEmbedTrace_ ::
+  HasCallStack =>
+  Sem EmbedStack () ->
+  UnitTest
+testEmbedTrace_ =
+  testEmbedLevel_ Trace
diff --git a/lib/Ribosome/Test/EmbedTmux.hs b/lib/Ribosome/Test/EmbedTmux.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Test/EmbedTmux.hs
@@ -0,0 +1,83 @@
+module Ribosome.Test.EmbedTmux where
+
+import Polysemy.Test (UnitTest)
+
+import Ribosome.Embed (HandlerEffects, interpretPluginEmbed)
+import Ribosome.Host.Data.RpcHandler (RpcHandler)
+import Ribosome.Host.Interpreter.Handlers (withHandlers)
+import Ribosome.Test.Data.TestConfig (TmuxTestConfig)
+import Ribosome.Test.Embed (TestEffects, testPluginEmbed)
+import Ribosome.Test.TmuxCommon (TmuxStack, runTmuxNvim)
+
+type HandlerStack =
+  HandlerEffects ++ TmuxStack
+
+type EmbedTmuxWith r =
+  TestEffects ++ r ++ HandlerStack
+
+type EmbedTmux =
+  EmbedTmuxWith '[]
+
+runEmbedTmuxTestConf ::
+  HasCallStack =>
+  TmuxTestConfig ->
+  Sem HandlerStack () ->
+  UnitTest
+runEmbedTmuxTestConf conf =
+  runTmuxNvim conf .
+  interpretPluginEmbed
+
+runEmbedTmuxTest ::
+  HasCallStack =>
+  Sem HandlerStack () ->
+  UnitTest
+runEmbedTmuxTest =
+  runEmbedTmuxTestConf def
+
+runEmbedTmuxGuiTest ::
+  HasCallStack =>
+  Sem HandlerStack () ->
+  UnitTest
+runEmbedTmuxGuiTest =
+  runEmbedTmuxTestConf (def & #tmux . #gui .~ True)
+
+testPluginEmbedTmuxConf ::
+  ∀ r .
+  HasCallStack =>
+  Members HandlerStack (r ++ HandlerStack) =>
+  TmuxTestConfig ->
+  InterpretersFor r HandlerStack ->
+  [RpcHandler (r ++ HandlerStack)] ->
+  Sem (EmbedTmuxWith r) () ->
+  UnitTest
+testPluginEmbedTmuxConf conf effs handlers =
+  runEmbedTmuxTestConf conf .
+  effs .
+  withHandlers handlers .
+  testPluginEmbed
+
+testPluginEmbedTmux ::
+  ∀ r .
+  HasCallStack =>
+  Members HandlerStack (r ++ HandlerStack) =>
+  InterpretersFor r HandlerStack ->
+  [RpcHandler (r ++ HandlerStack)] ->
+  Sem (EmbedTmuxWith r) () ->
+  UnitTest
+testPluginEmbedTmux =
+  testPluginEmbedTmuxConf @r def
+
+testPluginEmbedTmux_ ::
+  HasCallStack =>
+  [RpcHandler HandlerStack] ->
+  Sem EmbedTmux () ->
+  UnitTest
+testPluginEmbedTmux_ =
+  testPluginEmbedTmux @'[] id
+
+testEmbedTmux ::
+  HasCallStack =>
+  Sem EmbedTmux () ->
+  UnitTest
+testEmbedTmux =
+  testPluginEmbedTmux_ mempty
diff --git a/lib/Ribosome/Test/Error.hs b/lib/Ribosome/Test/Error.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Test/Error.hs
@@ -0,0 +1,47 @@
+{-# options_haddock not-home #-}
+
+module Ribosome.Test.Error where
+
+import Polysemy.Test (TestError (TestError))
+
+import Ribosome.Host.Data.Report (Reportable, mapReport, reportMessages)
+import Ribosome.Host.Data.RpcHandler (Handler)
+
+-- |Resume an effect and convert its error from @'Stop' err@ to @'Error' 'TestError'@.
+resumeTestError ::
+  ∀ eff err r .
+  Show err =>
+  Members [eff !! err, Error TestError] r =>
+  InterpreterFor eff r
+resumeTestError =
+  resumeHoistError (TestError . show)
+
+-- |Run a 'Handler', converting the @'Stop' 'Report'@ at its head to @'Error' 'TestError'@.
+testHandler ::
+  Member (Error TestError) r =>
+  Handler r a ->
+  Sem r a
+testHandler =
+  stopToErrorWith (TestError . reportMessages)
+
+-- |Run a 'Handler' in a new thread and return an action that waits for the thread to terminate when sequenced.
+-- Converts the @'Stop' 'Report'@ at its head to @'Error' 'TestError'@ when it is awaited.
+testHandlerAsync ::
+  Members [Error TestError, Async] r =>
+  Handler r a ->
+  Sem r (Sem r a)
+testHandlerAsync h = do
+  thread <- async do
+    runStop h
+  pure do
+    testHandler . stopEither =<< note (TestError "async handler didn't produce result") =<< await thread
+
+-- |Interpret @'Stop' err@ to @'Error' 'TestError'@ by using @err@'s instance of 'Reportable'.
+testError ::
+  ∀ err r a .
+  Reportable err =>
+  Member (Error TestError) r =>
+  Sem (Stop err : r) a ->
+  Sem r a
+testError =
+  testHandler . mapReport . raiseUnder
diff --git a/lib/Ribosome/Test/Examples/Example1.hs b/lib/Ribosome/Test/Examples/Example1.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Test/Examples/Example1.hs
@@ -0,0 +1,30 @@
+{-# options_haddock prune, hide #-}
+
+-- |An example for the docs.
+module Ribosome.Test.Examples.Example1 where
+
+import Polysemy.Test
+
+import Ribosome
+import Ribosome.Api
+import Ribosome.Test
+
+store ::
+  Member (Rpc !! RpcError) r =>
+  Args ->
+  Handler r ()
+store (Args msg) =
+  ignoreRpcError do
+    nvimSetVar "message" msg
+
+test_direct :: UnitTest
+test_direct =
+  testEmbed_ do
+    store "test directly"
+    assertEq "test directly" =<< nvimGetVar @Text "message"
+
+test_rpc :: UnitTest
+test_rpc =
+  testPlugin_ [rpcCommand "Store" Sync store] do
+    nvimCommand "Store test RPC"
+    assertEq "test RPC" =<< nvimGetVar @Text "message"
diff --git a/lib/Ribosome/Test/Exists.hs b/lib/Ribosome/Test/Exists.hs
deleted file mode 100644
--- a/lib/Ribosome/Test/Exists.hs
+++ /dev/null
@@ -1,44 +0,0 @@
-module Ribosome.Test.Exists where
-
-import Data.MessagePack (Object(ObjectInt))
-import Neovim (Neovim, toObject)
-import Neovim.API.Text (vim_call_function)
-import UnliftIO.Exception (tryAny)
-
-import Ribosome.Data.Text (capitalize)
-import Ribosome.System.Time (epochSeconds, sleep)
-
-retry ::
-  MonadIO m =>
-  MonadFail m =>
-  Double ->
-  Int ->
-  m a ->
-  (a -> m (Either Text b)) ->
-  m b
-retry interval timeout thunk check = do
-  start <- epochSeconds
-  step start
-  where
-    step start = do
-      result <- thunk
-      checked <- check result
-      recurse start checked
-    recurse _ (Right b) = return b
-    recurse start (Left e) = do
-      current <- epochSeconds
-      if (current - start) < timeout
-      then do
-        sleep interval
-        step start
-      else fail (toString e)
-
-waitForPlugin :: Text -> Double -> Int -> Neovim env ()
-waitForPlugin name interval timeout =
-  retry interval timeout thunk check
-  where
-    thunk = tryAny $ vim_call_function "exists" $ fromList [toObject $ "*" <> capitalize name <> "Poll"]
-    check (Right (ObjectInt 1)) = return $ Right ()
-    check (Right a) = return $ Left $ errormsg <> "weird return type " <> show a
-    check (Left e) = return $ Left $ errormsg <> show e
-    errormsg = "plugin didn't start within " <> show timeout <> " seconds: "
diff --git a/lib/Ribosome/Test/File.hs b/lib/Ribosome/Test/File.hs
deleted file mode 100644
--- a/lib/Ribosome/Test/File.hs
+++ /dev/null
@@ -1,41 +0,0 @@
-module Ribosome.Test.File where
-
-import qualified Data.ByteString as ByteString (readFile)
-import System.Directory (canonicalizePath, createDirectoryIfMissing, removePathForcibly)
-import System.FilePath ((</>))
-
-testDir :: Text -> IO FilePath
-testDir = canonicalizePath . toString
-
--- raises exception if cwd is not the package root so we don't damage anything
-tempDirIO :: Text -> FilePath -> IO FilePath
-tempDirIO prefix path = do
-  base <- testDir prefix
-  let dir = base </> "temp"
-  removePathForcibly dir
-  createDirectoryIfMissing False dir
-  let absPath = dir </> path
-  createDirectoryIfMissing True absPath
-  return absPath
-
-tempDir :: MonadIO m => Text -> FilePath -> m FilePath
-tempDir prefix path =
-  liftIO $ tempDirIO prefix path
-
-fixture ::
-  MonadIO m =>
-  Text ->
-  FilePath ->
-  m FilePath
-fixture prefix path = do
-  base <- liftIO $ testDir prefix
-  return $ base </> "fixtures" </> path
-
-fixtureContent ::
-  MonadIO m =>
-  Text ->
-  FilePath ->
-  m Text
-fixtureContent prefix subPath = do
-  path <- fixture prefix subPath
-  decodeUtf8 <$> liftIO (ByteString.readFile path)
diff --git a/lib/Ribosome/Test/Functional.hs b/lib/Ribosome/Test/Functional.hs
deleted file mode 100644
--- a/lib/Ribosome/Test/Functional.hs
+++ /dev/null
@@ -1,74 +0,0 @@
-module Ribosome.Test.Functional where
-
--- import Control.Exception (finally)
--- import Control.Monad (when)
--- import Control.Monad.IO.Class
--- import Data.Foldable (traverse_)
--- import Neovim (Neovim, vim_command')
--- import System.Console.ANSI (Color(Green), ColorIntensity(Dull), ConsoleLayer(Foreground), SGR(SetColor, Reset), setSGR)
--- import System.Directory (createDirectoryIfMissing, doesFileExist, getCurrentDirectory, makeAbsolute, removePathForcibly)
--- import System.FilePath (takeDirectory, takeFileName, (</>))
-
--- import Ribosome.Control.Ribo (Ribo)
--- import Ribosome.Test.Embed (TestConfig(..), setupPluginEnv, unsafeEmbeddedSpec)
--- import Ribosome.Test.Exists (waitForPlugin)
--- import qualified Ribosome.Test.File as F (fixture, tempDir)
-
--- jobstart :: MonadIO f => Text -> f Text
--- jobstart cmd = do
---   dir <- liftIO getCurrentDirectory
---   return $ "call jobstart('" <> cmd <> "', { 'rpc': v:true, 'cwd': '" <> dir <> "' })"
-
--- logFile :: TestConfig -> IO FilePath
--- logFile TestConfig{..} = makeAbsolute $ tcLogPath <> "-spec"
-
--- startPlugin :: TestConfig -> Neovim env ()
--- startPlugin tc@TestConfig{..} = do
---   absLogPath <- liftIO $ makeAbsolute tcLogPath
---   absLogFile <- liftIO $ logFile tc
---   liftIO $ createDirectoryIfMissing True (takeDirectory absLogPath)
---   liftIO $ removePathForcibly absLogFile
---   setupPluginEnv tc
---   cmd <- jobstart $ "stack run -- -l " <> absLogFile <> " -v INFO"
---   vim_command' cmd
---   waitForPlugin tcPluginName 0.1 3
-
--- fSpec :: TestConfig -> Neovim env () -> Neovim env ()
--- fSpec conf spec = startPlugin conf >> spec
-
--- showLog' :: Text -> IO ()
--- showLog' output = do
---   putStrLn ""
---   setSGR [SetColor Foreground Dull Green]
---   putStrLn "plugin output:"
---   setSGR [Reset]
---   traverse_ putStrLn (lines output)
---   putStrLn ""
-
--- showLog :: TestConfig -> IO ()
--- showLog conf = do
---   file <- logFile conf
---   exists <- doesFileExist file
---   when exists $ do
---     output <- readFile file
---     case output of
---       [] -> return ()
---       o -> showLog' o
-
--- functionalSpec :: TestConfig -> Ribo () () -> IO ()
--- functionalSpec conf spec =
---   finally (unsafeEmbeddedSpec fSpec conf () spec) (showLog conf)
-
--- fPrefix :: Text
--- fPrefix = "f"
-
--- tempDir :: FilePath -> Neovim e FilePath
--- tempDir = F.tempDir fPrefix
-
--- tempFile :: FilePath -> Neovim e FilePath
--- tempFile file = do
---   absDir <- tempDir $ takeDirectory file
---   return $ absDir </> takeFileName file
-
--- fixture :: MonadIO m => FilePath -> m FilePath
--- fixture = F.fixture fPrefix
diff --git a/lib/Ribosome/Test/Input.hs b/lib/Ribosome/Test/Input.hs
deleted file mode 100644
--- a/lib/Ribosome/Test/Input.hs
+++ /dev/null
@@ -1,21 +0,0 @@
-module Ribosome.Test.Input where
-
-import Control.Concurrent.Lifted (fork, killThread)
-import Control.Exception.Lifted (bracket)
-
-import Ribosome.Api.Input (syntheticInput)
-import Ribosome.Control.Monad.Ribo (NvimE)
-
-withInput ::
-  NvimE e m =>
-  MonadIO m =>
-  MonadBaseControl IO m =>
-  Maybe Double ->
-  [Text] ->
-  m a ->
-  m a
-withInput interval chars thunk =
-  bracket (fork input) killThread (const thunk)
-  where
-    input =
-      syntheticInput interval chars
diff --git a/lib/Ribosome/Test/Log.hs b/lib/Ribosome/Test/Log.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Test/Log.hs
@@ -0,0 +1,22 @@
+module Ribosome.Test.Log where
+
+import Log (Severity)
+
+import Ribosome.Test.Data.TestConfig (TestConfig)
+
+testLogLevelConf ::
+  HasCallStack =>
+  Severity ->
+  (TestConfig -> a) ->
+  TestConfig ->
+  a
+testLogLevelConf =
+  undefined
+
+testLogLevel ::
+  HasCallStack =>
+  Severity ->
+  (TestConfig -> a) ->
+  a
+testLogLevel level f =
+  testLogLevelConf level f def
diff --git a/lib/Ribosome/Test/Orphans.hs b/lib/Ribosome/Test/Orphans.hs
deleted file mode 100644
--- a/lib/Ribosome/Test/Orphans.hs
+++ /dev/null
@@ -1,27 +0,0 @@
-{-# OPTIONS_GHC -fno-warn-orphans #-}
-
-module Ribosome.Test.Orphans where
-
--- import Control.Monad.IO.Unlift (withRunInIO)
--- import Neovim (Neovim)
-
--- import Ribosome.Control.Monad.Ribo (Ribo(Ribo, unRibo))
-
--- instance AssertM (Neovim e) where
---   genericAssertFailure__ l =
---     liftIO . genericAssertFailure__ l
-
---   genericSubAssert l msg ma =
---     withRunInIO (\f -> genericSubAssert l msg (f ma))
-
--- instance AssertM (ExceptT e (Neovim s)) where
---   genericAssertFailure__ l = liftIO . genericAssertFailure__ l
-
---   genericSubAssert l msg =
---     ExceptT . genericSubAssert l msg . runExceptT
-
--- instance AssertM (Ribo s e) where
---   genericAssertFailure__ l = liftIO . genericAssertFailure__ l
-
---   genericSubAssert l msg =
---     Ribo . ExceptT . genericSubAssert l msg . runExceptT . unRibo
diff --git a/lib/Ribosome/Test/Run.hs b/lib/Ribosome/Test/Run.hs
deleted file mode 100644
--- a/lib/Ribosome/Test/Run.hs
+++ /dev/null
@@ -1,14 +0,0 @@
-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,62 +1,95 @@
 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.Free.Class (MonadFree)
-import Hedgehog (TestT, (===))
+import Chiasma.Data.CodecError (CodecError)
+import Chiasma.Effect.Codec (NativeCommandCodecE)
+import Chiasma.Effect.TmuxClient (NativeTmux)
+import qualified Chiasma.Test.Screenshot as Chiasma
+import Chiasma.Tmux (withTmux)
+import Chiasma.TmuxApi (Tmux)
+import Control.Lens.Regex.Text (group, regex)
+import Exon (exon)
+import Hedgehog.Internal.Property (Failure)
+import qualified Log
+import Path (reldir)
+import Polysemy.Chronos (ChronosTime)
+import qualified Polysemy.Test as Test
+import Polysemy.Test (Hedgehog, Test, TestError (TestError), (===))
+import Prelude hiding (group)
+import qualified Time
+import Time (Seconds (Seconds))
 
-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)
+import Ribosome.Host.Effect.Log (StderrLog, stderrLog)
+import Ribosome.Test.Wait (assertWait)
 
+-- |Nvim appears to add random whitespace sequences, optionally interspersed with color codes, to empty lines.
+-- This remotes that noise from lines starting with `~\ESC[39m` or `\ESC[94m~\ESC[39m`.
+sanitize :: Text -> Text
+sanitize =
+  [regex|(\x{1b}\[94m)?~((\s|\x{1b}\[94m|\x{1b}\[39m)*)$|] . group 1 .~ ""
+
 screenshot ::
-  MonadFree TmuxThunk m =>
-  MonadIO m =>
-  Text ->
+  Members [Tmux, Test, Error TestError, Embed IO] r =>
   Bool ->
+  Bool ->
+  Text ->
   Int ->
-  m (Maybe ([Text], [Text]))
-screenshot name record pane = do
-  storage <- fixture "screenshots"
-  Chiasma.screenshot record storage name pane
+  Sem r (Maybe ([Text], [Text]))
+screenshot record sane name pane = do
+  storage <- Test.fixturePath [reldir|screenshots|]
+  mapError TestError (Chiasma.screenshotSanitized (if sane then sanitize else id) record storage name pane)
 
 assertScreenshot ::
-  MonadIO m =>
-  MonadRibo m =>
-  MonadDeepError e TmuxError m =>
-  MonadBaseControl IO m =>
-  MonadMask m =>
-  Nvim m =>
-  Text ->
+  HasCallStack =>
+  Members [NativeTmux, NativeCommandCodecE, Stop CodecError] r =>
+  Members [Hedgehog IO, Test, Error TestError, Error Failure, ChronosTime, Race, Embed IO] r =>
   Bool ->
+  Text ->
   Int ->
-  TestT m ()
-assertScreenshot name record pane = do
-  lift (runTmux (screenshot name record pane)) >>= check
+  Sem r ()
+assertScreenshot sane name pane =
+  withFrozenCallStack $ withTmux $ restop do
+    assertWait (screenshot False sane name pane) (traverse_ check)
   where
-    check (Just (current, existing)) =
+    check (current, existing) =
       existing === current
-    check Nothing =
-      return ()
 
-awaitScreenshot ::
-  MonadIO m =>
-  MonadMask m =>
-  MonadRibo m =>
-  MonadBaseControl IO m =>
-  MonadDeepError e TmuxError m =>
-  Nvim m =>
+updateScreeshot ::
+  HasCallStack =>
+  Members [NativeTmux, NativeCommandCodecE, Stop CodecError] r =>
+  Members [Hedgehog IO, Test, Error TestError, Error Failure, ChronosTime, StderrLog, Race, Embed IO] r =>
+  Bool ->
   Text ->
+  Int ->
+  Sem r ()
+updateScreeshot sane name pane =
+  withTmux $ restop do
+    stderrLog (Log.info [exon|Waiting for one second before storing new screenshot for '#{name}'|])
+    Time.sleep (Seconds 1)
+    void (screenshot sane True name pane)
+
+awaitScreenshot' ::
+  HasCallStack =>
+  Members [NativeTmux, NativeCommandCodecE, Stop CodecError] r =>
+  Members [Hedgehog IO, Test, Error TestError, Error Failure, ChronosTime, StderrLog, Race, Embed IO] r =>
   Bool ->
+  Bool ->
+  Text ->
   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
+  Sem r ()
+awaitScreenshot' = \case
+  True ->
+    updateScreeshot
+  False ->
+    withFrozenCallStack assertScreenshot
+
+awaitScreenshot ::
+  HasCallStack =>
+  Members [NativeTmux, NativeCommandCodecE, Stop CodecError] r =>
+  Members [Hedgehog IO, Test, Error TestError, Error Failure, ChronosTime, StderrLog, Race, Embed IO] r =>
+  Bool ->
+  Text ->
+  Int ->
+  Sem r ()
+awaitScreenshot record =
+  withFrozenCallStack do
+    awaitScreenshot' record True
diff --git a/lib/Ribosome/Test/SocketTmux.hs b/lib/Ribosome/Test/SocketTmux.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Test/SocketTmux.hs
@@ -0,0 +1,172 @@
+-- |Socket tmux tests start a tmux server, then run a regular Neovim session in a pane and connect to Neovim over a
+-- socket.
+module Ribosome.Test.SocketTmux where
+
+import Chiasma.Command.Pane (sendKeys)
+import Chiasma.Data.CodecError (CodecError)
+import Chiasma.Data.SendKeysParams (Key (Lit))
+import Chiasma.Effect.Codec (NativeCommandCodecE)
+import Chiasma.Effect.TmuxApi (Tmux)
+import Chiasma.Effect.TmuxClient (NativeTmux)
+import Chiasma.Tmux (withTmux)
+import Exon (exon)
+import Hedgehog.Internal.Property (Failure)
+import Path (Abs, File, Path, reldir, relfile, (</>))
+import Path.IO (doesPathExist)
+import Polysemy.Chronos (ChronosTime)
+import qualified Polysemy.Test as Test
+import Polysemy.Test (Hedgehog, Test, UnitTest, assert)
+
+import Ribosome.Host.Data.NvimSocket (NvimSocket (NvimSocket))
+import Ribosome.Host.Data.RpcHandler (RpcHandler)
+import Ribosome.Host.Interpreter.Handlers (withHandlers)
+import Ribosome.Host.Path (pathText)
+import Ribosome.Socket (SocketHandlerEffects, interpretPluginSocket)
+import Ribosome.Test.Data.TestConfig (TmuxTestConfig)
+import Ribosome.Test.Embed (TestEffects, testPluginEmbed)
+import Ribosome.Test.TmuxCommon (TmuxStack, runTmuxNvim)
+import Ribosome.Test.Wait (assertWait)
+import Ribosome.Data.CustomConfig (CustomConfig (CustomConfig))
+
+-- |The stack of internal effects for socket tmux tests.
+type TmuxHandlerStack =
+  SocketHandlerEffects ++ Reader (CustomConfig ()) : Reader NvimSocket : TmuxStack
+
+-- |The socket tmux test stack with additional effects.
+type SocketTmuxWith r =
+  TestEffects ++ r ++ TmuxHandlerStack
+
+-- |The socket tmux test stack with no additional effects.
+type SocketTmux =
+  SocketTmuxWith '[]
+
+nvimCmdline :: Path Abs File -> Text
+nvimCmdline socket =
+  [exon|nvim --listen #{pathText socket} -n -u NONE -i NONE --clean|]
+
+-- |Create a socket for Neovim to listen on and run a 'Reader' with it.
+withSocketTmuxNvim ::
+  Members [Test, Hedgehog IO, ChronosTime, Error Failure, Race, Embed IO] r =>
+  Members [NativeTmux, NativeCommandCodecE, Stop CodecError] r =>
+  InterpreterFor (Reader NvimSocket) r
+withSocketTmuxNvim sem = do
+  dir <- Test.tempDir [reldir|tmux-test|]
+  let socket = dir </> [relfile|nvim-socket|]
+  withTmux do
+    restop @CodecError @Tmux $ sendKeys 0 [Lit (nvimCmdline socket)]
+  assertWait (pure socket) (assert <=< doesPathExist)
+  runReader (NvimSocket socket) sem
+
+-- |Run the tmux test stack.
+runSocketTmuxTestConf ::
+  HasCallStack =>
+  TmuxTestConfig ->
+  Sem TmuxHandlerStack () ->
+  UnitTest
+runSocketTmuxTestConf conf =
+  runTmuxNvim conf .
+  withSocketTmuxNvim .
+  runReader (CustomConfig ()) .
+  interpretPluginSocket
+
+-- |Run the tmux test stack, using a pty to host tmux.
+runSocketTmuxTest ::
+  HasCallStack =>
+  Sem TmuxHandlerStack () ->
+  UnitTest
+runSocketTmuxTest =
+  runSocketTmuxTestConf def
+
+-- |Run the tmux test stack, using a terminal to tmux tmux.
+runSocketTmuxGuiTest ::
+  HasCallStack =>
+  Sem TmuxHandlerStack () ->
+  UnitTest
+runSocketTmuxGuiTest =
+  runSocketTmuxTestConf (def & #tmux . #gui .~ True)
+
+-- |Run a plugin test against a Neovim process running in a fresh tmux session, connected over a socket.
+testPluginSocketTmuxConf ::
+  ∀ r .
+  HasCallStack =>
+  Members TmuxHandlerStack (r ++ TmuxHandlerStack) =>
+  -- |Regular test config combined with tmux config
+  TmuxTestConfig ->
+  -- |Interpreter for custom effects
+  InterpretersFor r TmuxHandlerStack ->
+  -- |RPC handlers
+  [RpcHandler (r ++ TmuxHandlerStack)] ->
+  Sem (SocketTmuxWith r) () ->
+  UnitTest
+testPluginSocketTmuxConf conf effs handlers =
+  runSocketTmuxTestConf conf .
+  effs .
+  withHandlers handlers .
+  testPluginEmbed
+
+-- |Run a plugin test against a Neovim process running in a fresh tmux session, connected over a socket.
+testPluginSocketTmux ::
+  ∀ r .
+  HasCallStack =>
+  Members TmuxHandlerStack (r ++ TmuxHandlerStack) =>
+  -- |Interpreter for custom effects
+  InterpretersFor r TmuxHandlerStack ->
+  -- |RPC handlers
+  [RpcHandler (r ++ TmuxHandlerStack)] ->
+  Sem (SocketTmuxWith r) () ->
+  UnitTest
+testPluginSocketTmux =
+  testPluginSocketTmuxConf @r def
+
+-- |Run a plugin test against a Neovim process running in a fresh tmux session, connected over a socket.
+testHandlersSocketTmux ::
+  HasCallStack =>
+  -- |RPC handlers
+  [RpcHandler TmuxHandlerStack] ->
+  Sem SocketTmux () ->
+  UnitTest
+testHandlersSocketTmux =
+  testPluginSocketTmux @'[] id
+
+-- |Run a plugin test against a Neovim process running in a fresh tmux session, connected over a socket.
+testSocketTmux ::
+  HasCallStack =>
+  Sem SocketTmux () ->
+  UnitTest
+testSocketTmux =
+  testHandlersSocketTmux mempty
+
+-- |Run a plugin test against a Neovim process running in a fresh tmux session displayed in a terminal, connected over a
+-- socket.
+testPluginSocketTmuxGui ::
+  ∀ r .
+  HasCallStack =>
+  Members TmuxHandlerStack (r ++ TmuxHandlerStack) =>
+  -- |Interpreter for custom effects
+  InterpretersFor r TmuxHandlerStack ->
+  -- |RPC handlers
+  [RpcHandler (r ++ TmuxHandlerStack)] ->
+  Sem (SocketTmuxWith r) () ->
+  UnitTest
+testPluginSocketTmuxGui =
+  testPluginSocketTmuxConf @r (def & #tmux . #gui .~ True)
+
+-- |Run a plugin test against a Neovim process running in a fresh tmux session displayed in a terminal, connected over a
+-- socket.
+testHandlersSocketTmuxGui ::
+  HasCallStack =>
+  -- |RPC handlers
+  [RpcHandler TmuxHandlerStack] ->
+  Sem SocketTmux () ->
+  UnitTest
+testHandlersSocketTmuxGui =
+  testPluginSocketTmuxGui @'[] id
+
+-- |Run a plugin test against a Neovim process running in a fresh tmux session displayed in a terminal, connected over a
+-- socket.
+testSocketTmuxGui ::
+  HasCallStack =>
+  Sem SocketTmux () ->
+  UnitTest
+testSocketTmuxGui =
+  testHandlersSocketTmuxGui mempty
diff --git a/lib/Ribosome/Test/Tmux.hs b/lib/Ribosome/Test/Tmux.hs
deleted file mode 100644
--- a/lib/Ribosome/Test/Tmux.hs
+++ /dev/null
@@ -1,271 +0,0 @@
-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, withSystemTempDir)
-import qualified Chiasma.Test.Tmux as Chiasma (tmuxGuiSpec, tmuxSpec, tmuxSpec')
-import Control.Exception.Lifted (bracket)
-import Data.DeepPrisms (DeepPrisms)
-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 Ribosome.Config.Setting (updateSetting)
-import Ribosome.Config.Settings (tmuxSocket)
-import Ribosome.Control.Concurrent.Wait (waitIOPredDef)
-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.System.Time (sleep)
-import Ribosome.Test.Embed (
-  Runner,
-  TestConfig(..),
-  inTestT,
-  runPlugin,
-  runTest,
-  startHandlers,
-  testNvimProcessConfig,
-  withProcessTerm,
-  )
-import Ribosome.Test.Orphans ()
-import Ribosome.Test.Run (UnitTest)
-import Ribosome.Test.Unit (uSpec)
-
-runSocketNvimHs ::
-  MonadIO m =>
-  MonadFail m =>
-  ReportError e =>
-  RpcHandler e env n =>
-  MonadBaseControl IO m =>
-  TestConfig ->
-  env ->
-  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 (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 ::
-  MonadIO m =>
-  MonadFail m =>
-  ReportError e =>
-  RpcHandler e env n =>
-  MonadBaseControl IO m =>
-  TmuxNative ->
-  FilePath ->
-  TestConfig ->
-  env ->
-  n a ->
-  m a
-runGui api temp conf ribo specThunk =
-  runSocketNvimHs conf ribo specThunk =<< liftIO (startNvimInTmux api temp)
-
-unsafeGuiSpec ::
-  MonadIO m =>
-  MonadFail m =>
-  ReportError e =>
-  RpcHandler e env n =>
-  MonadBaseControl IO m =>
-  TmuxNative ->
-  FilePath ->
-  Runner n ->
-  TestConfig ->
-  env ->
-  n a ->
-  m a
-unsafeGuiSpec api temp runner conf s specThunk =
-  runGui api temp conf s $ runner conf specThunk
-
-unsafeGuiSpecR ::
-  MonadIO m =>
-  MonadFail m =>
-  ReportError e =>
-  MonadBaseControl IO m =>
-  RpcHandler e (Ribosome env) n =>
-  TmuxNative ->
-  FilePath ->
-  Runner n ->
-  TestConfig ->
-  env ->
-  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 ::
-  MonadIO m =>
-  MonadFail m =>
-  ReportError e =>
-  MonadBaseControl IO m =>
-  DeepPrisms e RpcError =>
-  TestConfig ->
-  TmuxNative ->
-  s ->
-  Ribo s e a ->
-  m a
-guiSpec conf api env specThunk = do
-  withSystemTempDir run
-  where
-    run tempdir =
-      unsafeGuiSpecR api tempdir uSpec conf env specThunk
-
-withTmux ::
-  DeepPrisms e RpcError =>
-  Ribo s e a ->
-  TmuxNative ->
-  Ribo s e a
-withTmux thunk (TmuxNative (Just socket)) =
-  updateSetting tmuxSocket socket *> thunk
-withTmux _ _ =
-  throwText "no socket in test tmux"
-
-tmuxSpec ::
-  DeepPrisms e RpcError =>
-  ReportError e =>
-  TestConfig ->
-  s ->
-  TestT (Ribo s e) () ->
-  UnitTest
-tmuxSpec conf env specThunk =
-  inTestT specThunk \ th ->
-    Chiasma.tmuxSpec \ api -> guiSpec conf api env (withTmux th api)
-
-tmuxSpec' ::
-  DeepPrisms e RpcError =>
-  ReportError e =>
-  TmuxTestConf ->
-  TestConfig ->
-  s ->
-  TestT (Ribo s e) () ->
-  UnitTest
-tmuxSpec' tmuxConf conf env specThunk =
-  inTestT specThunk \ th ->
-    Chiasma.tmuxSpec' tmuxConf \ api ->
-      guiSpec conf api env (withTmux th api)
-
-tmuxSpecDef ::
-  DeepPrisms e RpcError =>
-  ReportError e =>
-  Default s =>
-  TestT (Ribo s e) () ->
-  UnitTest
-tmuxSpecDef =
-  tmuxSpec def def
-
-tmuxGuiSpec ::
-  DeepPrisms e RpcError =>
-  ReportError e =>
-  TestConfig ->
-  s ->
-  Ribo s e () ->
-  UnitTest
-tmuxGuiSpec conf env specThunk =
-  Chiasma.tmuxGuiSpec run
-  where
-    run api = guiSpec conf api env (withTmux specThunk api)
-
-tmuxGuiSpecDef ::
-  DeepPrisms e RpcError =>
-  ReportError e =>
-  Default s =>
-  Ribo s e () ->
-  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/TmuxCommon.hs b/lib/Ribosome/Test/TmuxCommon.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Test/TmuxCommon.hs
@@ -0,0 +1,63 @@
+module Ribosome.Test.TmuxCommon where
+
+import Chiasma.Data.CodecError (CodecError)
+import Chiasma.Data.RenderError (RenderError)
+import Chiasma.Data.TmuxError (TmuxError)
+import Chiasma.Effect.TmuxClient (NativeTmux)
+import qualified Chiasma.Test.Data.TmuxTestConfig as Chiasma
+import Chiasma.Test.Tmux (TestTmuxEffects, withSystemTempDir, withTestTmux)
+import Polysemy.Test (UnitTest)
+
+import Ribosome.Data.PluginName (PluginName)
+import Ribosome.Host.Data.BootError (BootError (BootError))
+import Ribosome.Host.Test.Run (TestStack)
+import Ribosome.Test.Data.TestConfig (TmuxTestConfig (TmuxTestConfig))
+import Ribosome.Test.Embed (runTestConf)
+
+type TmuxErrors =
+  [
+    Stop CodecError,
+    Error CodecError,
+    Stop RenderError,
+    Error RenderError,
+    Stop TmuxError,
+    Error TmuxError,
+    Error Text
+  ]
+
+type TmuxBaseStack =
+  TestTmuxEffects ++ TmuxErrors
+
+type TmuxStack =
+  NativeTmux : TmuxBaseStack ++ Reader PluginName : TestStack
+
+interpretTmuxErrors ::
+  Member (Error BootError) r =>
+  InterpretersFor TmuxErrors r
+interpretTmuxErrors =
+  mapError BootError .
+  mapError @TmuxError (BootError . show) .
+  stopToError .
+  mapError @RenderError (BootError . show) .
+  stopToError .
+  mapError @CodecError (BootError . show) .
+  stopToError
+
+withTmuxTest ::
+  Members TestStack r =>
+  Chiasma.TmuxTestConfig ->
+  InterpretersFor TmuxBaseStack r
+withTmuxTest conf =
+  interpretTmuxErrors .
+  withSystemTempDir .
+  withTestTmux conf
+
+runTmuxNvim ::
+  HasCallStack =>
+  TmuxTestConfig ->
+  Sem TmuxStack () ->
+  UnitTest
+runTmuxNvim (TmuxTestConfig conf tmuxConf) =
+  runTestConf conf .
+  withTmuxTest tmuxConf .
+  restop @TmuxError @NativeTmux
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,32 +1,40 @@
+-- |Assertions for Neovim UI elements
 module Ribosome.Test.Ui where
 
-import Hedgehog (TestT, (===))
+import Polysemy.Test (Hedgehog, assertEq, (===))
+
 import Ribosome.Api.Window (currentCursor, cursor)
-import Ribosome.Control.Monad.Ribo (NvimE)
-import Ribosome.Nvim.Api.Data (Window)
-import Ribosome.Nvim.Api.IO (nvimListWins)
+import Ribosome.Host.Api.Data (Window)
+import Ribosome.Host.Api.Effect (nvimListWins)
+import Ribosome.Host.Effect.Rpc (Rpc)
 
+-- |Assert the number of windows.
 windowCountIs ::
-  NvimE e m =>
+  Monad m =>
+  Members [Rpc, Hedgehog m] r =>
   Int ->
-  TestT m ()
+  Sem r ()
 windowCountIs count = do
   wins <- nvimListWins
-  count === (length wins)
+  count === length wins
 
+-- |Assert the cursor position in a window.
 cursorIs ::
-  NvimE e m =>
+  Monad m =>
+  Members [Rpc, Hedgehog m] r =>
   Int ->
   Int ->
   Window ->
-  TestT m ()
+  Sem r ()
 cursorIs line col =
-  ((line, col) ===) <=< lift . cursor
+  assertEq (line, col) <=< cursor
 
+-- |Assert the cursor position in the current window.
 currentCursorIs ::
-  NvimE e m =>
+  Monad m =>
+  Members [Rpc, Hedgehog m] r =>
   Int ->
   Int ->
-  TestT m ()
+  Sem r ()
 currentCursorIs line col =
-  ((line, col) ===) =<< lift currentCursor
+  assertEq (line, col) =<< currentCursor
diff --git a/lib/Ribosome/Test/Unit.hs b/lib/Ribosome/Test/Unit.hs
deleted file mode 100644
--- a/lib/Ribosome/Test/Unit.hs
+++ /dev/null
@@ -1,100 +0,0 @@
-module Ribosome.Test.Unit where
-
-import Control.Exception.Lifted (bracket_)
-import Hedgehog (TestT)
-import Hedgehog.Internal.Property (mkTestT, runTestT)
-import System.FilePath (takeDirectory, takeFileName, (</>))
-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)
-import Ribosome.Error.Report.Class (ReportError)
-import Ribosome.Plugin.RpcHandler (RpcHandler)
-import Ribosome.Test.Embed (Runner, TestConfig(..), setupPluginEnv, unsafeEmbeddedSpecR)
-import qualified Ribosome.Test.File as F (fixture, fixtureContent, tempDir)
-import Ribosome.Test.Orphans ()
-
-uPrefix :: Text
-uPrefix = "test"
-
-uSpec :: (MonadIO m, NvimE e m) => Runner m
-uSpec conf spec = do
-  setupPluginEnv conf
-  spec
-
-unitSpec ::
-  MonadIO n =>
-  MonadIO m =>
-  NvimE e' n =>
-  MonadFail m =>
-  ReportError e =>
-  MonadBaseControl IO m =>
-  RpcHandler e (Ribosome env) n =>
-  TestConfig ->
-  env ->
-  TestT n a ->
-  TestT m a
-unitSpec cfg env t = do
-  mkTestT (unsafeEmbeddedSpecR uSpec cfg env (runTestT t))
-
-unitSpecDef ::
-  MonadIO n =>
-  MonadIO m =>
-  NvimE e' n =>
-  MonadFail m =>
-  ReportError e =>
-  MonadBaseControl IO m =>
-  RpcHandler e (Ribosome env) n =>
-  env ->
-  TestT n a ->
-  TestT m a
-unitSpecDef =
-  unitSpec def
-
-unitSpecDef' ::
-  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 ()
-
-tempDir :: MonadIO m => FilePath -> m FilePath
-tempDir = F.tempDir uPrefix
-
-tempFile :: MonadIO m => FilePath -> m FilePath
-tempFile file = do
-  absDir <- tempDir $ takeDirectory file
-  return $ absDir </> takeFileName file
-
-fixture :: MonadIO m => FilePath -> m FilePath
-fixture = F.fixture uPrefix
-
-fixtureContent :: MonadIO m => FilePath -> m Text
-fixtureContent = F.fixtureContent uPrefix
-
-withLogAs ::
-  MonadIO m =>
-  MonadBaseControl IO m =>
-  Text ->
-  m a ->
-  m a
-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 =
-  (`withLogAs` thunk) =<< pluginName
diff --git a/lib/Ribosome/Test/Wait.hs b/lib/Ribosome/Test/Wait.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Test/Wait.hs
@@ -0,0 +1,46 @@
+module Ribosome.Test.Wait where
+
+import Hedgehog.Internal.Property (Failure, failWith, liftTest, mkTest)
+import qualified Conc
+import Conc (interpretAtomic)
+import Polysemy.Test (Hedgehog, liftH)
+import qualified Polysemy.Time as Time
+import Polysemy.Time (MilliSeconds (MilliSeconds), Seconds (Seconds))
+
+assertWaitFor ::
+  Monad m =>
+  HasCallStack =>
+  Members [Hedgehog m, Time t d, Race, Error Failure, Embed IO] r =>
+  TimeUnit t1 =>
+  TimeUnit t2 =>
+  t1 ->
+  t2 ->
+  Sem r a ->
+  (a -> Sem r b) ->
+  Sem r b
+assertWaitFor timeout interval acquire test =
+  withFrozenCallStack do
+    interpretAtomic Nothing do
+      Conc.timeout_ timeoutError timeout spin
+  where
+    spin = do
+      a <- raise acquire
+      catch (raise (test a)) \ e -> do
+        atomicPut (Just e)
+        Time.sleep interval
+        spin
+    timeoutError =
+      atomicGet >>= liftH . \case
+        Just e -> liftTest (mkTest (Left e, mempty))
+        Nothing -> failWith Nothing "timed out before an assertion was made"
+
+assertWait ::
+  Monad m =>
+  HasCallStack =>
+  Members [Hedgehog m, Time t d, Race, Error Failure, Embed IO] r =>
+  Sem r a ->
+  (a -> Sem r b) ->
+  Sem r b
+assertWait acquire test =
+  withFrozenCallStack do
+    assertWaitFor (Seconds 3) (MilliSeconds 100) acquire test
diff --git a/readme.md b/readme.md
deleted file mode 100644
--- a/readme.md
+++ /dev/null
@@ -1,91 +0,0 @@
-# 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,49 +1,36 @@
 cabal-version: 2.2
 
--- This file has been generated from package.yaml by hpack version 0.34.4.
+-- This file has been generated from package.yaml by hpack version 0.34.6.
 --
 -- 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>
+version:        0.9.9.9
+synopsis:       Test tools for Ribosome
+description:    See https://hackage.haskell.org/package/ribosome-test/docs/Ribosome-Test.html
 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
+maintainer:     hackage@tryp.io
+copyright:      2022 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
-
 library
   exposed-modules:
-      Ribosome.Test.Await
+      Ribosome.Handler
+      Ribosome.Test
+      Ribosome.Test.Data.TestConfig
       Ribosome.Test.Embed
-      Ribosome.Test.Exists
-      Ribosome.Test.File
-      Ribosome.Test.Functional
-      Ribosome.Test.Input
-      Ribosome.Test.Orphans
-      Ribosome.Test.Run
+      Ribosome.Test.EmbedTmux
+      Ribosome.Test.Error
+      Ribosome.Test.Examples.Example1
+      Ribosome.Test.Log
       Ribosome.Test.Screenshot
-      Ribosome.Test.Tmux
+      Ribosome.Test.SocketTmux
+      Ribosome.Test.TmuxCommon
       Ribosome.Test.Ui
-      Ribosome.Test.Unit
-  other-modules:
-      Paths_ribosome_test
-  autogen-modules:
-      Paths_ribosome_test
+      Ribosome.Test.Wait
   hs-source-dirs:
       lib
   default-extensions:
@@ -63,9 +50,11 @@
       DeriveLift
       DeriveTraversable
       DerivingStrategies
+      DerivingVia
       DisambiguateRecordFields
       DoAndIfThenElse
       DuplicateRecordFields
+      EmptyCase
       EmptyDataDecls
       ExistentialQuantification
       FlexibleContexts
@@ -80,8 +69,9 @@
       MultiParamTypeClasses
       MultiWayIf
       NamedFieldPuns
-      OverloadedStrings
+      OverloadedLabels
       OverloadedLists
+      OverloadedStrings
       PackageImports
       PartialTypeSignatures
       PatternGuards
@@ -92,6 +82,7 @@
       RankNTypes
       RecordWildCards
       RecursiveDo
+      RoleAnnotations
       ScopedTypeVariables
       StandaloneDeriving
       TemplateHaskell
@@ -104,73 +95,41 @@
       UndecidableInstances
       UnicodeSyntax
       ViewPatterns
-  ghc-options: -Wall -Wredundant-constraints -Wsimplifiable-class-constraints
+      StandaloneKindSignatures
+      OverloadedLabels
+  ghc-options: -Wall -Wredundant-constraints -Wincomplete-uni-patterns -Wmissing-deriving-strategies -Widentities -Wunused-packages -fplugin=Polysemy.Plugin
   build-depends:
-      aeson
-    , base ==4.*
-    , bytestring
+      base >=4.12 && <5
     , chiasma
-    , composition
-    , composition-extra
-    , conduit
-    , containers
-    , cornea
-    , data-default
-    , directory
-    , either
-    , exceptions
-    , filepath
-    , free
+    , chiasma-test
+    , exon
     , hedgehog
-    , hslogger
-    , lens
-    , lifted-async
-    , lifted-base
-    , messagepack
-    , monad-control
-    , mtl
-    , nvim-hs
+    , lens-regex-pcre
     , path
     , path-io
-    , prettyprinter
-    , prettyprinter-ansi-terminal
-    , process
-    , relude >=0.7 && <1.2
-    , resourcet
+    , polysemy
+    , polysemy-chronos
+    , polysemy-plugin
+    , polysemy-test
+    , prelate >=0.1
     , ribosome
-    , tasty
-    , tasty-hedgehog
-    , template-haskell
-    , text
-    , transformers
-    , typed-process
-    , unix
-    , unliftio
+    , ribosome-host
+    , ribosome-host-test
   mixins:
       base hiding (Prelude)
-    , ribosome hiding (Ribosome.Prelude)
-    , ribosome (Ribosome.Prelude as Prelude)
+    , prelate (Prelate as Prelude)
+    , prelate hiding (Prelate)
   default-language: Haskell2010
 
-test-suite ribosome-unit
+test-suite ribosome-test-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.EmbedTmuxTest
+      Ribosome.Test.ReportTest
+      Ribosome.Test.SocketTmuxTest
       Ribosome.Test.SyntaxTest
-      Ribosome.Test.THTest
-      Ribosome.Test.WatcherTest
-      Ribosome.Test.WindowTest
-      TestError
-      Paths_ribosome_test
+      Skip
   hs-source-dirs:
       test
   default-extensions:
@@ -190,9 +149,11 @@
       DeriveLift
       DeriveTraversable
       DerivingStrategies
+      DerivingVia
       DisambiguateRecordFields
       DoAndIfThenElse
       DuplicateRecordFields
+      EmptyCase
       EmptyDataDecls
       ExistentialQuantification
       FlexibleContexts
@@ -207,8 +168,9 @@
       MultiParamTypeClasses
       MultiWayIf
       NamedFieldPuns
-      OverloadedStrings
+      OverloadedLabels
       OverloadedLists
+      OverloadedStrings
       PackageImports
       PartialTypeSignatures
       PatternGuards
@@ -219,6 +181,7 @@
       RankNTypes
       RecordWildCards
       RecursiveDo
+      RoleAnnotations
       ScopedTypeVariables
       StandaloneDeriving
       TemplateHaskell
@@ -231,51 +194,21 @@
       UndecidableInstances
       UnicodeSyntax
       ViewPatterns
-  ghc-options: -Wall -Wredundant-constraints -Wsimplifiable-class-constraints -threaded -rtsopts -with-rtsopts=-N
+      StandaloneKindSignatures
+      OverloadedLabels
+  ghc-options: -Wall -Wredundant-constraints -Wincomplete-uni-patterns -Wmissing-deriving-strategies -Widentities -Wunused-packages -fplugin=Polysemy.Plugin -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
+      base >=4.12 && <5
+    , polysemy
+    , polysemy-plugin
+    , polysemy-test
+    , prelate >=0.1
     , ribosome
+    , ribosome-host
     , 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)
+    , prelate (Prelate as Prelude)
+    , prelate hiding (Prelate)
   default-language: Haskell2010
diff --git a/test/Main.hs b/test/Main.hs
--- a/test/Main.hs
+++ b/test/Main.hs
@@ -1,40 +1,20 @@
 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 Polysemy.Test (unitTest)
+import Ribosome.Test.EmbedTmuxTest (test_embedTmux)
+import Ribosome.Test.ReportTest (test_report)
+import Ribosome.Test.SocketTmuxTest (test_socketTmux)
+import Ribosome.Test.SyntaxTest (test_syntax)
+import Skip (skipUnlessX)
 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
+  testGroup "ribosome" [
+    unitTest "socket tmux" (skipUnlessX test_socketTmux),
+    unitTest "embed tmux" (skipUnlessX test_embedTmux),
+    unitTest "syntax" (skipUnlessX test_syntax),
+    unitTest "report" (skipUnlessX test_report)
   ]
 
 main :: IO ()
diff --git a/test/Ribosome/Test/AutocmdTest.hs b/test/Ribosome/Test/AutocmdTest.hs
deleted file mode 100644
--- a/test/Ribosome/Test/AutocmdTest.hs
+++ /dev/null
@@ -1,49 +0,0 @@
-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/EmbedTmuxTest.hs b/test/Ribosome/Test/EmbedTmuxTest.hs
new file mode 100644
--- /dev/null
+++ b/test/Ribosome/Test/EmbedTmuxTest.hs
@@ -0,0 +1,12 @@
+module Ribosome.Test.EmbedTmuxTest where
+
+import Polysemy.Test (UnitTest, assert)
+
+import Ribosome.Host.Api.Effect (nvimGetVar, nvimSetVar)
+import Ribosome.Test.EmbedTmux (testEmbedTmux)
+
+test_embedTmux :: UnitTest
+test_embedTmux =
+  testEmbedTmux do
+    nvimSetVar "test" True
+    assert =<< nvimGetVar "test"
diff --git a/test/Ribosome/Test/MappingTest.hs b/test/Ribosome/Test/MappingTest.hs
deleted file mode 100644
--- a/test/Ribosome/Test/MappingTest.hs
+++ /dev/null
@@ -1,71 +0,0 @@
-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
deleted file mode 100644
--- a/test/Ribosome/Test/MenuTest.hs
+++ /dev/null
@@ -1,324 +0,0 @@
-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
deleted file mode 100644
--- a/test/Ribosome/Test/MsgpackTest.hs
+++ /dev/null
@@ -1,170 +0,0 @@
-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
deleted file mode 100644
--- a/test/Ribosome/Test/NvimMenuTest.hs
+++ /dev/null
@@ -1,164 +0,0 @@
-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
deleted file mode 100644
--- a/test/Ribosome/Test/PromptTest.hs
+++ /dev/null
@@ -1,39 +0,0 @@
-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/ReportTest.hs b/test/Ribosome/Test/ReportTest.hs
new file mode 100644
--- /dev/null
+++ b/test/Ribosome/Test/ReportTest.hs
@@ -0,0 +1,37 @@
+module Ribosome.Test.ReportTest where
+
+import qualified Log
+import Log (Severity (Error, Info))
+import Polysemy.Test (UnitTest)
+
+import Ribosome.Host.Api.Effect (nvimCommand, nvimInput)
+import Ribosome.Host.Data.Execution (Execution (Sync))
+import Ribosome.Host.Data.Report (Report (Report))
+import Ribosome.Host.Data.RpcHandler (Handler)
+import Ribosome.Host.Handler (rpcFunction)
+import qualified Ribosome.Report as Report
+import Ribosome.Test.Screenshot (awaitScreenshot)
+import Ribosome.Test.SocketTmux (testHandlersSocketTmux)
+
+stopInfo :: Handler r ()
+stopInfo =
+  stop (Report "report an info Stop by echoing" [] Info)
+
+stopError :: Handler r ()
+stopError =
+  stop (Report "report an error Stop by responding" [] Error)
+
+test_report :: UnitTest
+test_report =
+  testHandlersSocketTmux [rpcFunction "StopInfo" Sync stopInfo, rpcFunction "StopError" Sync stopError] do
+    nvimCommand "mode"
+    Log.debug "don't report Log.debug message"
+    awaitScreenshot False "report-1" 0
+    Log.info "report Log.info message"
+    awaitScreenshot False "report-2" 0
+    Report.warn "report Report.warn message" []
+    awaitScreenshot False "report-3" 0
+    nvimInput ":call StopInfo()<cr>"
+    awaitScreenshot False "report-4" 0
+    nvimInput ":call StopError()<cr>"
+    awaitScreenshot False "report-5" 0
diff --git a/test/Ribosome/Test/RpcTest.hs b/test/Ribosome/Test/RpcTest.hs
deleted file mode 100644
--- a/test/Ribosome/Test/RpcTest.hs
+++ /dev/null
@@ -1,154 +0,0 @@
-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
deleted file mode 100644
--- a/test/Ribosome/Test/ScratchTest.hs
+++ /dev/null
@@ -1,95 +0,0 @@
-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
deleted file mode 100644
--- a/test/Ribosome/Test/SettingTest.hs
+++ /dev/null
@@ -1,51 +0,0 @@
-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/SocketTmuxTest.hs b/test/Ribosome/Test/SocketTmuxTest.hs
new file mode 100644
--- /dev/null
+++ b/test/Ribosome/Test/SocketTmuxTest.hs
@@ -0,0 +1,12 @@
+module Ribosome.Test.SocketTmuxTest where
+
+import Polysemy.Test (UnitTest, assert)
+
+import Ribosome.Host.Api.Effect (nvimGetVar, nvimSetVar)
+import Ribosome.Test.SocketTmux (testSocketTmux)
+
+test_socketTmux :: UnitTest
+test_socketTmux =
+  testSocketTmux do
+    nvimSetVar "test" True
+    assert =<< nvimGetVar "test"
diff --git a/test/Ribosome/Test/SyntaxTest.hs b/test/Ribosome/Test/SyntaxTest.hs
--- a/test/Ribosome/Test/SyntaxTest.hs
+++ b/test/Ribosome/Test/SyntaxTest.hs
@@ -1,48 +1,22 @@
 module Ribosome.Test.SyntaxTest where
 
-import TestError (TestError)
-import Hedgehog (TestT)
-import Chiasma.Data.TmuxError (TmuxError)
-import Chiasma.Test.Tmux (TmuxTestConf(..))
+import Polysemy.Test (UnitTest)
 
 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.Syntax (Syntax (Syntax), syntaxHighlight, syntaxMatch)
 import Ribosome.Test.Screenshot (awaitScreenshot)
-import Ribosome.Test.Tmux (tmuxSpec')
-
-data SyntaxSpecError =
-  Test TestError
-  |
-  Tmux TmuxError
-
-deepPrisms ''SyntaxSpecError
-
-instance ReportError SyntaxSpecError where
-  errorReport _ =
-    undefined
+import Ribosome.Test.SocketTmux (testSocketTmux)
 
 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
+  Syntax [syntaxMatch "TestColons" "::"] [
+    syntaxHighlight "TestColons" [("cterm", "reverse"), ("ctermfg", "1"), ("gui", "reverse"), ("guifg", "#dc322f")]
+  ] []
 
 test_syntax :: UnitTest
 test_syntax =
-  tmuxSpec' def { ttcWidth = 300, ttcHeight = 51, ttcGui = False } (defaultTestConfig "syntax") def syntaxSpec
+  testSocketTmux do
+    setCurrentBufferContent ["function :: String -> Int", "function _ = 5"]
+    _ <- executeSyntax syntax
+    awaitScreenshot False "syntax" 0
diff --git a/test/Ribosome/Test/THTest.hs b/test/Ribosome/Test/THTest.hs
deleted file mode 100644
--- a/test/Ribosome/Test/THTest.hs
+++ /dev/null
@@ -1,45 +0,0 @@
-{-# 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
deleted file mode 100644
--- a/test/Ribosome/Test/WatcherTest.hs
+++ /dev/null
@@ -1,46 +0,0 @@
-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
deleted file mode 100644
--- a/test/Ribosome/Test/WindowTest.hs
+++ /dev/null
@@ -1,43 +0,0 @@
-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/Skip.hs b/test/Skip.hs
new file mode 100644
--- /dev/null
+++ b/test/Skip.hs
@@ -0,0 +1,11 @@
+module Skip where
+
+import qualified Data.Text.IO as Text
+import Polysemy.Test (UnitTest)
+import System.Environment (lookupEnv)
+
+skipUnlessX :: UnitTest -> UnitTest
+skipUnlessX test = do
+  liftIO (lookupEnv "DISPLAY") >>= \case
+    Just _ -> test
+    Nothing -> liftIO (Text.putStrLn "Skipping test due to lack of display")
diff --git a/test/TestError.hs b/test/TestError.hs
deleted file mode 100644
--- a/test/TestError.hs
+++ /dev/null
@@ -1,28 +0,0 @@
-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
