packages feed

tasklite-core-0.5.0.0: test/LibSpec.hs

module LibSpec where

import Protolude (
  Bool (True),
  ExitCode (ExitFailure),
  Maybe (..),
  Text,
  isJust,
  pure,
  show,
  ($),
  (&),
  (/=),
  (<),
  (<>),
  (==),
 )
import Protolude qualified as P

import Control.Arrow ((>>>))
import Data.Aeson (decode)
import Data.List.Utils (subIndex)
import Data.Text (unpack)
import Data.Text qualified as T
import Data.Time.ISO8601.Duration qualified as Iso
import Database.SQLite.Simple (SQLData (SQLNull), query_)
import Test.Hspec (
  Spec,
  context,
  it,
  shouldBe,
  shouldContain,
  shouldEndWith,
  shouldNotBe,
  shouldNotContain,
  shouldNotSatisfy,
  shouldSatisfy,
  shouldStartWith,
  shouldThrow,
 )

import Config (defaultConfig)
import Data.Hourglass (TimeFormat (..), timePrint)
import FullTask (FullTask, emptyFullTask)
import FullTask qualified
import ImportExport (EditMode (ApplyPreEdit), editTaskByTask)
import Lib (
  addNote,
  addTag,
  addTask,
  countTasks,
  deleteNote,
  deleteTag,
  deleteTasks,
  doTasks,
  duplicateTasks,
  headTasks,
  infoTask,
  insertRecord,
  insertTags,
  listNotes,
  logTask,
  newTasks,
  nextTask,
  recurTasks,
  repeatTasks,
  runFilter,
  setDueUtc,
  setReadyUtc,
  unrepeatTasks,
  updateTask,
 )
import Note (Note)
import System.Hourglass (dateCurrent)
import Task (
  Task (
    body,
    closed_utc,
    due_utc,
    group_ulid,
    metadata,
    modified_utc,
    ready_utc,
    repetition_duration,
    state,
    ulid,
    user
  ),
  TaskState (Done),
  emptyTask,
 )
import TaskToNote (TaskToNote (TaskToNote, ulid))
import TaskToNote qualified
import TaskToTag (TaskToTag)
import TaskToTag qualified
import TestUtils (withMemoryDb)
import Utils (parseUtc, zeroTime, zeroUlidTxt)


exampleTask :: Task
exampleTask =
  emptyTask
    { Task.ulid = "01hq68smfe0r9entg3x4rb9441"
    , Task.body = "Buy milk"
    , Task.state = Nothing
    , Task.modified_utc = "2024-02-21 16:43:17"
    , Task.due_utc = Just "2025-07-08 10:22:56"
    , Task.user = "john_vdg1c2hz"
    , Task.metadata = "{\"source\":\"fridge\"}" & decode
    }


task1 :: Task
task1 =
  emptyTask
    { Task.ulid = "01hs68z7mdg4ktpxbv0yfafznq"
    , Task.body = "New task 1"
    , Task.modified_utc = "2024-03-17 13:17:44.461"
    }


taskMultiLine :: Task
taskMultiLine =
  emptyTask
    { Task.ulid = "01hx48cnjhp18mts3c44zk3gen"
    , Task.body =
        "New task\n\
        \with several lines\n\
        \and line breaks"
    }


normTask :: Task -> Task
normTask task =
  task
    { Task.ulid = ""
    , Task.modified_utc = ""
    , Task.due_utc = Nothing
    , Task.user = ""
    }


replaceBs :: P.ByteString -> P.ByteString -> P.ByteString -> P.ByteString
replaceBs needle replacement haystack = do
  haystack
    & P.decodeUtf8
    & T.replace (P.decodeUtf8 needle) (P.decodeUtf8 replacement)
    & P.encodeUtf8


spec :: Spec
spec = do
  let
    now = "2024-05-08 10:04:17" & parseUtc & P.fromMaybe zeroTime
    conf = defaultConfig

  it "initially contains no tasks" $ do
    withMemoryDb conf $ \memConn -> do
      tasks <- headTasks conf now memConn Nothing
      unpack (show tasks) `shouldStartWith` "No tasks available"

  it "inserts a task" $ do
    withMemoryDb conf $ \memConn -> do
      let task =
            emptyTask
              { Task.ulid = "01hrvhc0h1pncbczxym16642mm"
              , Task.body = "Directly inserted task"
              , Task.state = Just Done
              }

      insertRecord "tasks" memConn task
      tasks :: [Task] <- query_ memConn "SELECT * FROM tasks"
      tasks `shouldBe` [task]

  it "adds a new task" $ do
    withMemoryDb conf $ \memConn -> do
      result <- addTask conf memConn ["Just a test"]
      unpack (show result)
        `shouldStartWith` "πŸ†• Added task \"Just a test\" with id"

  context "When a task exists" $ do
    it "updates a task" $ do
      withMemoryDb conf $ \memConn -> do
        let initialTask =
              emptyTask
                { Task.ulid = "01hrvhdddfwsrnp6dd8h7tp8h4"
                , Task.body = "New task"
                , Task.state = Just Done
                }
            newTask = initialTask{body = "Updated task"}

        insertRecord "tasks" memConn initialTask
        updateTask memConn newTask
        tasks :: [Task] <- query_ memConn "SELECT * FROM tasks"

        case tasks of
          [updatedTask] -> do
            -- Task should have a different `modified_utc` value
            updatedTask `shouldSatisfy` (\task -> task.modified_utc /= "")
            updatedTask{modified_utc = ""} `shouldBe` newTask
          _ ->
            P.die "Found more than one task"

    it "lists next task" $ do
      withMemoryDb conf $ \memConn -> do
        insertRecord "tasks" memConn exampleTask
        result <- nextTask conf memConn
        unpack (show result) `shouldContain` "Buy milk"

    it "adds a tag" $ do
      withMemoryDb conf $ \memConn -> do
        insertRecord "tasks" memConn exampleTask
        tagResult <- addTag conf memConn "test" [exampleTask.ulid]
        unpack (show tagResult)
          `shouldStartWith` "🏷  Added tag \"test\" to task"
        taskToTags :: [TaskToTag] <-
          query_ memConn "SELECT * FROM task_to_tag"
        case taskToTags of
          [taskToTag] -> do
            taskToTag.ulid `shouldNotBe` ""
            taskToTag.task_ulid `shouldNotBe` ""
            taskToTag.tag `shouldBe` "test"
          _ -> P.die "More than one task_to_tag row found"

    it "strips leading + when adding a tag" $ do
      withMemoryDb conf $ \memConn -> do
        insertRecord "tasks" memConn exampleTask
        tagResult <- addTag conf memConn "+test" [exampleTask.ulid]
        unpack (show tagResult)
          `shouldStartWith` "🏷  Added tag \"test\" to task"
        taskToTags :: [TaskToTag] <-
          query_ memConn "SELECT * FROM task_to_tag"
        case taskToTags of
          [taskToTag] -> taskToTag.tag `shouldBe` "test"
          _ -> P.die "More than one task_to_tag row found"

    it "deletes a tag" $ do
      withMemoryDb conf $ \memConn -> do
        insertRecord "tasks" memConn exampleTask
        _ <- addTag conf memConn "test" [exampleTask.ulid]
        delResult <- deleteTag conf memConn "test" [exampleTask.ulid]
        unpack (show delResult)
          `shouldStartWith` "πŸ’₯ Removed tag \"test\" of task"
        taskToTags :: [TaskToTag] <-
          query_ memConn "SELECT * FROM task_to_tag"
        taskToTags `shouldBe` []

    it "doesn't delete a tag that does not exist" $ do
      withMemoryDb conf $ \memConn -> do
        insertRecord "tasks" memConn exampleTask
        delResult <- deleteTag conf memConn "test" [exampleTask.ulid]
        unpack (show delResult) `shouldContain` "not set"

    it "adds a note" $ do
      withMemoryDb conf $ \memConn -> do
        insertRecord "tasks" memConn exampleTask
        noteResult <- addNote conf memConn "A test note" [exampleTask.ulid]
        unpack (show noteResult)
          `shouldStartWith` "πŸ—’  Added a note to task"

    -- it "correctly wraps multiline notes" $ do
    --   See error:
    --   tl info x795vqf

    it "sets due UTC" $ do
      withMemoryDb conf $ \memConn -> do
        insertRecord "tasks" memConn exampleTask
        let utcTxt = "2087-03-21 17:43:00"
        case parseUtc utcTxt of
          Nothing -> P.die "Invalid UTC string"
          Just utcStamp -> do
            result <- setDueUtc conf memConn utcStamp [exampleTask.ulid]
            unpack (show result)
              `shouldStartWith` ( "πŸ“… Set due UTC of task \""
                                    <> T.unpack exampleTask.body
                                    <> "\" with id \""
                                    <> T.unpack exampleTask.ulid
                                    <> "\" to \""
                                    <> T.unpack utcTxt
                                    <> "\""
                                )

    it "sets ready UTC" $ do
      withMemoryDb conf $ \memConn -> do
        insertRecord "tasks" memConn exampleTask
        let utcTxt = "2059-07-11 04:55:16"
        case parseUtc utcTxt of
          Nothing -> P.die "Invalid UTC string"
          Just utcStamp -> do
            result <- setReadyUtc conf memConn utcStamp [exampleTask.ulid]
            unpack (show result)
              `shouldStartWith` ( "πŸ“… Set ready UTC of task \""
                                    <> T.unpack exampleTask.body
                                    <> "\" with id \""
                                    <> T.unpack exampleTask.ulid
                                    <> "\" to \""
                                    <> T.unpack utcTxt
                                    <> "\""
                                )
            tasks :: [Task] <- query_ memConn "SELECT * FROM tasks"
            case tasks of
              [updatedTask] -> do
                updatedTask `shouldSatisfy` (\task -> isJust task.ready_utc)
              _ -> P.die "Found more than one task"

    it "completes it" $ do
      withMemoryDb conf $ \memConn -> do
        insertRecord "tasks" memConn exampleTask
        doResult <- doTasks conf memConn Nothing [exampleTask.ulid]
        unpack (show doResult) `shouldStartWith` "βœ… Finished task"
        tasks :: [Task] <- query_ memConn "SELECT * FROM tasks"
        case tasks of
          [updatedTask] -> do
            updatedTask `shouldSatisfy` (\task -> task.state == Just Done)
            updatedTask `shouldSatisfy` (\task -> isJust task.closed_utc)
          _ -> P.die "Found more than one task"

    it "deletes it" $ do
      withMemoryDb conf $ \memConn -> do
        insertRecord "tasks" memConn exampleTask
        deleteResult <- deleteTasks conf memConn [exampleTask.ulid]
        unpack (show deleteResult) `shouldStartWith` "❌ Deleted task"
        tasks :: [Task] <- query_ memConn "SELECT * FROM tasks"
        tasks `shouldBe` []
        tags :: [TaskToTag] <- query_ memConn "SELECT * FROM task_to_tag"
        tags `shouldBe` []
        notes :: [Note] <- query_ memConn "SELECT * FROM task_to_note"
        notes `shouldBe` []

    it "duplicates a task" $ do
      withMemoryDb conf $ \memConn -> do
        insertRecord "tasks" memConn exampleTask
        duplicationResult <- duplicateTasks conf memConn [exampleTask.ulid]
        unpack (show duplicationResult)
          `shouldStartWith` "πŸ‘―  Created a duplicate of task \"Buy milk\""
        tasks :: [Task] <- query_ memConn "SELECT * FROM tasks"
        case tasks of
          [taskA, taskB] -> do
            normTask taskA `shouldBe` normTask exampleTask
            normTask taskB `shouldBe` normTask exampleTask
          _ -> P.die "Must have exactly two tasks"

    it "deletes obsolete fields on duplication" $ do
      withMemoryDb conf $ \memConn -> do
        insertRecord "tasks" memConn exampleTask
        let
          zeroDur = Iso.DurationWeek (Iso.DurWeek 0)
          oneMonth = "P1M" & Iso.parseDuration & P.fromRight zeroDur
        _ <- repeatTasks conf memConn oneMonth [exampleTask.ulid]
        _ <- duplicateTasks conf memConn [exampleTask.ulid]
        tasks :: [Task] <-
          query_ memConn "SELECT * FROM tasks ORDER BY ulid DESC"
        case tasks of
          [dupe, original] -> do
            original.group_ulid `shouldSatisfy` P.isJust
            original.repetition_duration `shouldSatisfy` P.isJust
            dupe.group_ulid `shouldSatisfy` P.isNothing
            dupe.repetition_duration `shouldSatisfy` P.isNothing
            dupe.user `shouldNotBe` original.user
            normTask dupe `shouldBe` normTask exampleTask
          _ -> P.die "Must have exactly two tasks"

  it "adds a task with tags and due date" $ do
    withMemoryDb conf $ \memConn -> do
      _ <- addTask conf memConn ["Just a test +tag due:2082-10-03 +what"]
      (tasks :: [FullTask]) <- query_ memConn "SELECT * FROM tasks_view"
      case tasks of
        [updatedTask] -> do
          updatedTask `shouldSatisfy` (\task -> task.ulid /= "")
          updatedTask `shouldSatisfy` (\task -> task.modified_utc /= "")
          updatedTask `shouldSatisfy` (\task -> task.user /= "")
          updatedTask
            { FullTask.ulid = ""
            , FullTask.modified_utc = ""
            , FullTask.user = ""
            }
            `shouldBe` emptyFullTask
              { FullTask.body = "Just a test"
              , FullTask.due_utc = Just "2082-10-03 00:00:00"
              , FullTask.priority = Just 2.0
              , FullTask.tags = Just ["tag", "what"]
              }
        _ -> P.die "Found more than one task"

  it "deduplicates tags when adding a task" $ do
    withMemoryDb conf $ \memConn -> do
      _ <- addTask conf memConn ["Buy milk +drink +drink"]
      (tasks :: [FullTask]) <- query_ memConn "SELECT * FROM tasks_view"
      case tasks of
        [updatedTask] -> do
          updatedTask `shouldSatisfy` (\task -> task.ulid /= "")
          updatedTask `shouldSatisfy` (\task -> task.modified_utc /= "")
          updatedTask `shouldSatisfy` (\task -> task.user /= "")
          updatedTask
            { FullTask.ulid = ""
            , FullTask.modified_utc = ""
            , FullTask.user = ""
            }
            `shouldBe` emptyFullTask
              { FullTask.body = "Buy milk"
              , FullTask.priority = Just 2.0
              , FullTask.tags = Just ["drink"]
              }
        _ -> P.die "Found more than one task"

  it "logs a task" $ do
    withMemoryDb conf $ \memConn -> do
      result <- logTask conf memConn ["Just a test"]
      unpack (show result)
        `shouldStartWith` "πŸ“ Logged task \"Just a test\" with id"

  it "dies on invalid filter expressions" $ do
    withMemoryDb conf $ \memConn -> do
      runFilter conf now memConn [" "] Nothing `shouldThrow` (== ExitFailure 1)

  it "counts tasks" $ do
    withMemoryDb defaultConfig $ \memConn -> do
      let
        task2 =
          emptyTask
            { Task.ulid = "01hs690f9hkzk9z7zews9j2k1d"
            , Task.body = "New task 2"
            }

      count0 <- countTasks defaultConfig memConn P.mempty
      show count0 `shouldBe` ("0" :: Text)

      insertRecord "tasks" memConn task1
      count1 <- countTasks defaultConfig memConn P.mempty
      show count1 `shouldBe` ("1" :: Text)

      insertRecord "tasks" memConn task2
      count2 <- countTasks defaultConfig memConn P.mempty
      show count2 `shouldBe` ("2" :: Text)

      warnings <- insertTags conf memConn Nothing task2 ["test"]
      P.show warnings `shouldBe` T.empty
      countWithTag <- countTasks defaultConfig memConn (Just ["+test"])
      show countWithTag `shouldBe` ("1" :: Text)

      pure ()

  it "gets new tasks" $ do
    withMemoryDb defaultConfig $ \memConn -> do
      let
        task2 =
          emptyTask
            { Task.ulid = "01hs6zsf3c0vqx6egfnmbqtmvy"
            , Task.body = "New task 2"
            , Task.closed_utc = Just "2024-04-10T18:54:10Z"
            , Task.state = Just Done
            }

      insertRecord "tasks" memConn task1
      insertRecord "tasks" memConn task2

      cliOutput <- newTasks defaultConfig now memConn (Just ["state:done"]) Nothing
      show cliOutput `shouldContain` "New task 2"
      show cliOutput `shouldNotContain` "New task 1"

  it "shows warning if a tag is duplicated" $ do
    withMemoryDb defaultConfig $ \memConn -> do
      let newTag = "test"
      insertRecord "tasks" memConn task1
      warnings <- insertTags conf memConn Nothing task1 [newTag]
      P.show warnings `shouldBe` T.empty

      cliOutput <- addTag defaultConfig memConn newTag [task1.ulid]
      show cliOutput `shouldEndWith` "Tag \"test\" is already assigned"

  it "repeats a task and prevents recurring same task" $ do
    withMemoryDb defaultConfig $ \memConn -> do
      insertRecord "tasks" memConn task1
      let oneWeek = Iso.DurationWeek (Iso.DurWeek 1)
      repeatRes <- repeatTasks defaultConfig memConn oneWeek [task1.ulid]
      P.show repeatRes `shouldContain` "Set repeat duration of task"

      recurRes <- recurTasks defaultConfig memConn oneWeek [task1.ulid]
      P.show recurRes `shouldContain` "is already in a repetition series"

      unrepeatRes <- unrepeatTasks defaultConfig memConn [task1.ulid]
      P.show unrepeatRes `shouldContain` "Removed repetition duration"

      recurRes2 <- recurTasks defaultConfig memConn oneWeek [task1.ulid]
      P.show recurRes2 `shouldContain` "Set recurrence duration of task"

      repeatRes2 <- repeatTasks defaultConfig memConn oneWeek [task1.ulid]
      P.show repeatRes2 `shouldContain` "is already in a recurrence series"

  context "Editing a task" $ do
    it "shows warning if a tag was duplicated" $ do
      withMemoryDb defaultConfig $ \memConn -> do
        let existTag = "existing-tag"
        insertRecord "tasks" memConn task1
        warnings <- insertTags conf memConn Nothing task1 [existTag]
        P.show warnings `shouldBe` T.empty
        cliOutput <-
          editTaskByTask
            conf
            ( ApplyPreEdit $
                replaceBs
                  "\n...\n"
                  ("\ntags: " <> P.show [existTag, "new-tag"] <> "\n...\n")
            )
            memConn
            task1
        let errMsg = "Tag \"" <> T.unpack existTag <> "\" is already assigned"
        show cliOutput `shouldContain` errMsg

    it "lets you change the closed_utc" $ do
      withMemoryDb defaultConfig $ \memConn -> do
        insertRecord "tasks" memConn task1

        cliOutput <-
          editTaskByTask
            conf
            ( ApplyPreEdit $
                replaceBs
                  "state: null"
                  "state: Done"
                  >>> replaceBs
                    "closed_utc: null"
                    "closed_utc: 2024-05-08 10:04"
            )
            memConn
            task1
        show cliOutput `shouldContain` "Edited task"
        tasks :: [Task] <- query_ memConn "SELECT * FROM tasks"
        case tasks of
          [task] -> do
            task.closed_utc `shouldBe` Just "2024-05-08 10:04:00.000"
            task.state `shouldBe` Just Done
            now_ <- dateCurrent
            let today = now_ & timePrint (toFormat ("YYYY-MM-DD" :: [P.Char]))
            T.unpack task.modified_utc `shouldStartWith` today
          _ -> P.die "Found more than one task"

    it "lets you add notes" $ do
      withMemoryDb defaultConfig $ \memConn -> do
        insertRecord "tasks" memConn task1
        cliOutput <-
          editTaskByTask
            conf
            ( ApplyPreEdit $
                replaceBs
                  "\n...\n"
                  ("\nnotes: " <> P.show ["A short note" :: Text] <> "\n...\n")
            )
            memConn
            task1
        show cliOutput `shouldStartWith` "✏️  Edited task \"New task 1\""

        taskToNotes :: [TaskToNote] <-
          query_ memConn "SELECT * FROM task_to_note"
        case taskToNotes of
          [taskToNote] -> do
            taskToNote.ulid
              `shouldNotSatisfy` (\ulid -> zeroUlidTxt `T.isPrefixOf` ulid)
            taskToNote.note `shouldBe` "A short note"
          _ -> P.die "Found more than one task_to_tag row"

    it "de-duplicates notes" $ do
      withMemoryDb defaultConfig $ \memConn -> do
        insertRecord "tasks" memConn task1
        let note = "A short note"
        insertRecord
          "task_to_note"
          memConn
          TaskToNote
            { TaskToNote.ulid = "01hxsjgzmdx48yzk39v852razr"
            , TaskToNote.task_ulid = task1.ulid
            , TaskToNote.note = note
            }
        taskInfo <- infoTask defaultConfig memConn task1.ulid
        show taskInfo `shouldContain` T.unpack note
        cliOutput <-
          editTaskByTask
            conf
            ( ApplyPreEdit $
                replaceBs "notes: []" $
                  "notes: " <> P.show [note, note]
            )
            memConn
            task1
        show cliOutput `shouldStartWith` "✏️  Edited task \"New task 1\""

        taskToNotes :: [TaskToNote] <-
          query_ memConn "SELECT * FROM task_to_note"
        case taskToNotes of
          [taskToNoteA, taskToNoteB] -> do
            taskToNoteA.ulid
              `shouldNotSatisfy` (\ulid -> zeroUlidTxt `T.isPrefixOf` ulid)
            taskToNoteB.ulid
              `shouldNotSatisfy` (\ulid -> zeroUlidTxt `T.isPrefixOf` ulid)
            taskToNoteA.note `shouldBe` "A short note"
            taskToNoteB.note `shouldBe` "A short note"
          _ -> P.die "Found more than one task_to_tag row"

    it "lets you set metadata to null" $ do
      withMemoryDb defaultConfig $ \memConn -> do
        insertRecord "tasks" memConn task1{metadata = Just "{\"a\":\"b\"}"}
        cliOutput <-
          editTaskByTask
            conf
            (ApplyPreEdit $ replaceBs "\n...\n" "\nmetadata: null\n...\n")
            memConn
            task1
        show cliOutput `shouldStartWith` "✏️  Edited task \"New task 1\""
        tasks :: [[SQLData]] <- query_ memConn "SELECT metadata FROM tasks"
        case tasks of
          [[metadata]] -> do
            metadata `shouldBe` SQLNull
          _ -> P.die "Found more than one task"

  it "only shows first line of multi-line tasks in task listing" $ do
    withMemoryDb defaultConfig $ \memConn -> do
      insertRecord "tasks" memConn taskMultiLine

      tasks <- headTasks conf now memConn Nothing
      show tasks `shouldContain` "New task β–Ό"
      show tasks `shouldNotContain` "with several lines"

  it "keeps line breaks of multi-line tasks in info view" $ do
    withMemoryDb defaultConfig $ \memConn -> do
      insertRecord "tasks" memConn taskMultiLine

      cliOutput <- infoTask defaultConfig memConn taskMultiLine.ulid
      show cliOutput
        `shouldContain` "New task\n\
                        \with several lines\n\
                        \and line breaks"

  it "lists all notes descending by creation UTC" $ do
    withMemoryDb defaultConfig $ \memConn -> do
      insertRecord "tasks" memConn task1
      let
        taskToNote1 =
          TaskToNote
            { TaskToNote.ulid = "01hx4eyxxvs5b75ynxrztcz87f"
            , TaskToNote.task_ulid = task1.ulid
            , TaskToNote.note = "The first note"
            }
        note1Id = taskToNote1.ulid & T.takeEnd 3 & T.unpack
        taskToNote2 =
          TaskToNote
            { TaskToNote.ulid = "01hx4f3f764sma7n8bahvwjeed"
            , TaskToNote.task_ulid = task1.ulid
            , TaskToNote.note = "The second note"
            }
        note2Id = taskToNote2.ulid & T.takeEnd 3 & T.unpack

      insertRecord "task_to_note" memConn taskToNote1
      insertRecord "task_to_note" memConn taskToNote2

      cliOutput <- listNotes defaultConfig memConn

      show cliOutput `shouldContain` note1Id
      show cliOutput `shouldContain` note2Id

      let
        posUlid1 = subIndex note1Id (show cliOutput)
        posUlid2 = subIndex note2Id (show cliOutput)

      --  Newer notes should be listed first
      (posUlid2 < posUlid1) `shouldBe` True

  it "lets you delete a note" $ do
    withMemoryDb defaultConfig $ \memConn -> do
      insertRecord "tasks" memConn task1
      let noteId = "01hwcqk9nnwjypzw9kr646nqce"
      insertRecord
        "task_to_note"
        memConn
        TaskToNote
          { TaskToNote.ulid = noteId
          , TaskToNote.task_ulid = task1.ulid
          , TaskToNote.note = "The note content"
          }

      cliOutput <- deleteNote defaultConfig memConn noteId

      (show cliOutput :: Text)
        `shouldBe` "\128165 Deleted note \"01hwcqk9nnwjypzw9kr646nqce\" \
                   \of task \"01hs68z7mdg4ktpxbv0yfafznq\""