diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,34 @@
 # Revision history for pgmq-config
 
+## 0.7.0.0 -- 2026-10-26
+
+### Breaking Changes
+
+- Add `DetectedQueueNameCollision QueueName (NonEmpty Text)` and
+  `UnsupportedNotifyOnPartitionedQueue QueueName`, with matching JSON tags and
+  fields. Exhaustive `ReconcileAction` matches must handle both constructors.
+
+### New Features
+
+- Add hand-written `ToJSON` instances for `ReconcileAction`, `QueueType`,
+  `PartitionConfig` and `ObservedQueueType`, with an aeson `^>=2.2` library dependency.
+  Reports use snake_case tags and field names; optional throttle/premake values
+  encode as null, preserving declared defaults. Goldens cover every action and queue
+  shape. The encoding is a published compatibility surface under design note 020;
+  remove downstream orphan instances for these types.
+
+### Bug Fixes
+
+- Report foreign name collisions before creating missing aliases or sub-resources.
+- Report partition notifications without enabling or updating unsupported resources.
+  Drift still reconciles supported sub-resources against observed shape. Correct
+  the guide's reconciliation and idempotence contract.
+
+### Other Changes
+
+- Ephemeral PostgreSQL roots are created/validated as owner-only (`0700`), existing
+  owned roots are tightened, unsafe paths are rejected and registry contents survive.
+
 ## 0.6.1.1 -- 2026-09-25
 
 Coordinated family version bump; the library is unchanged from 0.6.1.0.
diff --git a/pgmq-config.cabal b/pgmq-config.cabal
--- a/pgmq-config.cabal
+++ b/pgmq-config.cabal
@@ -1,6 +1,6 @@
 cabal-version: 3.4
 name: pgmq-config
-version: 0.6.1.1
+version: 0.7.0.0
 synopsis:
   Declarative queue configuration for PGMQ (PostgreSQL Message Queue)
 
@@ -27,7 +27,9 @@
 category: Database
 build-type: Simple
 extra-doc-files: CHANGELOG.md
-extra-source-files: test/fixtures/pgmq-1.12.0.sql
+extra-source-files:
+  test/fixtures/pgmq-1.12.0.sql
+  test/golden/*.json
 
 flag effectful
   description: Enable effectful integration
@@ -64,21 +66,22 @@
     OverloadedStrings
 
   build-depends:
+    aeson ^>=2.2,
     base >=4.18 && <5,
     containers >=0.6 && <0.8,
     generic-lens ^>=2.2 || ^>=2.3,
     hasql ^>=1.10,
     hasql-pool ^>=1.4,
     lens ^>=5.3,
-    pgmq-core >=0.6 && <0.7,
-    pgmq-hasql >=0.6 && <0.7,
+    pgmq-core >=0.7 && <0.8,
+    pgmq-hasql >=0.7 && <0.8,
     text ^>=2.1,
 
   if flag(effectful)
     exposed-modules: Pgmq.Config.Effectful
     build-depends:
       effectful-core ^>=2.6 || ^>=2.7,
-      pgmq-effectful >=0.6 && <0.7,
+      pgmq-effectful >=0.7 && <0.8,
   hs-source-dirs: src
   default-language: GHC2024
 
@@ -94,11 +97,14 @@
     -with-rtsopts=-N
 
   other-modules:
+    CollisionSpec
     ConfigSpec
     EphemeralDb
     ForeignQueueSpec
+    JsonGoldenSpec
     NotifyCrashSpec
     PartitionSpec
+    RootPermissionsSpec
 
   default-extensions:
     DeriveGeneric
@@ -114,19 +120,20 @@
     base >=4.18 && <5,
     bytestring >=0.11 && <0.13,
     directory,
-    ephemeral-pg >=0.3.1.0,
+    ephemeral-pg >=0.3.1 && <0.4,
     generic-lens ^>=2.2 || ^>=2.3,
     hasql,
     hasql-pool ^>=1.4,
     lens ^>=5.3,
     pg-migrate,
-    pgmq-config >=0.6 && <0.7,
-    pgmq-core >=0.6 && <0.7,
-    pgmq-hasql >=0.6 && <0.7,
-    pgmq-migration >=0.6 && <0.7,
+    pgmq-config >=0.7 && <0.8,
+    pgmq-core >=0.7 && <0.8,
+    pgmq-hasql >=0.7 && <0.8,
+    pgmq-migration >=0.7 && <0.8,
     postgresql-libpq >=0.10.1 && <0.12,
     random ^>=1.2,
     tasty ^>=1.5,
+    tasty-golden ^>=2.3.6,
     tasty-hunit ^>=0.10,
     text,
     time ^>=1.14,
@@ -139,4 +146,4 @@
     cpp-options: -DPGMQ_EFFECTFUL
     build-depends:
       effectful-core ^>=2.6 || ^>=2.7,
-      pgmq-effectful >=0.6 && <0.7,
+      pgmq-effectful >=0.7 && <0.8,
diff --git a/src/Pgmq/Config.hs b/src/Pgmq/Config.hs
--- a/src/Pgmq/Config.hs
+++ b/src/Pgmq/Config.hs
@@ -91,7 +91,9 @@
 --
 -- * A queue whose observed shape (standard, unlogged, or partitioned)
 --   contradicts the declared one is reported as
---   'Pgmq.Config.Types.DetectedQueueTypeDrift' and nothing is mutated.
+--   'Pgmq.Config.Types.DetectedQueueTypeDrift'. The queue's shape is not mutated;
+--   its notify, FIFO-index and binding declarations are still reconciled against
+--   its observed shape.
 --   Changing a queue's type means dropping and recreating it, destroying every
 --   message it holds; that is an operator's decision, not a startup task.
 --
@@ -99,8 +101,19 @@
 --   compared, because @pgmq.list_queues()@ does not report them. Only the
 --   three-way shape is checked.
 --
+-- * Foreign mixed-case names colliding with the declared physical table are
+--   reported as 'DetectedQueueNameCollision'. Without an exact registry row,
+--   no queue or sub-resources are created. With an exact row, sub-resources proceed.
+--   Detection uses the initial snapshot; it does not serialize foreign alias creation.
+--
+-- * Notifications on an observed or newly created partitioned queue are reported
+--   as 'UnsupportedNotifyOnPartitionedQueue', with no enable, update or re-enable.
+--   Existing throttle rows are left unchanged. See
+--   @docs/design/015-notification-delivery-contract.md@.
+--
 -- Calling this on every application startup is the intended usage, and a second
--- run against an unchanged config issues no mutations at all. One caveat about
+-- run against unchanged config and unchanged, already-satisfied database state
+-- issues no mutations. A later run restores crash-lost ordinary throttle rows. One caveat about
 -- /concurrent/ startups: queue creation and topic binding are serialized
 -- server-side and converge, but enabling insert notifications on a
 -- brand-new queue from two replicas at once can fail one of them with SQLSTATE
@@ -133,7 +146,10 @@
 -- order: exactly one queue-existence action per config
 -- ('Pgmq.Config.Types.CreatedQueue', 'Pgmq.Config.Types.SkippedQueue', or
 -- 'Pgmq.Config.Types.DetectedQueueTypeDrift'), then the notify, FIFO, and
--- topic-binding actions for that config. Every @Skipped@ action means no
+-- topic-binding actions for that config. A missing exact row with colliding names
+-- instead reports only 'DetectedQueueNameCollision'; when an exact row exists,
+-- that action follows the ordinary existence action and sub-resources proceed.
+-- Every @Skipped@ action means no
 -- statement was issued.
 ensureQueuesReport :: [QueueConfig] -> Session [ReconcileAction]
 ensureQueuesReport = ensureQueuesReportWith sessionOps
diff --git a/src/Pgmq/Config/Reconcile.hs b/src/Pgmq/Config/Reconcile.hs
--- a/src/Pgmq/Config/Reconcile.hs
+++ b/src/Pgmq/Config/Reconcile.hs
@@ -14,6 +14,8 @@
 import Control.Lens ((^.))
 import Data.Generics.Labels ()
 import Data.Int (Int32)
+import Data.List (sort)
+import Data.List.NonEmpty qualified as NE
 import Data.Map.Strict qualified as Map
 import Data.Maybe (fromMaybe)
 import Data.Set qualified as Set
@@ -75,6 +77,7 @@
 
   let existingQueuesByName =
         Map.fromList [(q ^. #unvalidatedName, q) | q <- existingQueues]
+      collisionsByLowerName = Map.fromListWith (++) [(T.toLower n, [n]) | n <- Map.keys existingQueuesByName]
       existingBindingSet =
         Set.fromList
           [ (b ^. #bindingQueueName, topicPatternToText (b ^. #bindingPattern))
@@ -92,7 +95,7 @@
 
   concat
     <$> traverse
-      (reconcileQueue ops existingQueuesByName existingBindingSet existingNotifyByName existingFifoSet)
+      (reconcileQueue ops existingQueuesByName collisionsByLowerName existingBindingSet existingNotifyByName existingFifoSet)
       configs
 
 -- | Reconcile a single queue config against existing state, returning actions taken.
@@ -100,88 +103,97 @@
   (Monad m) =>
   ReconcileOps m ->
   Map.Map T.Text UnvalidatedQueue ->
+  Map.Map T.Text [T.Text] ->
   Set.Set (T.Text, T.Text) ->
   Map.Map T.Text Int32 ->
   Set.Set T.Text ->
   QueueConfig ->
   m [ReconcileAction]
-reconcileQueue ops existingQueues existingBindings existingNotify existingFifo cfg = do
+reconcileQueue ops existingQueues collisionsByLowerName existingBindings existingNotify existingFifo cfg = do
   let qn = cfg ^. #queueName
       qnText = queueNameToText qn
       declaredType = cfg ^. #queueType
 
-  -- Queue creation, or — when the queue is already there — a shape comparison.
-  -- Drift is reported and never repaired: the only way to change a queue's type
-  -- is to drop and recreate it, which would destroy its messages.
-  queueAction <-
-    case Map.lookup qnText existingQueues of
-      Just observed ->
-        let observedType = observedQueueType observed
-         in pure
-              [ if declaredShape declaredType == observedType
-                  then SkippedQueue qn
-                  else DetectedQueueTypeDrift qn declaredType observedType
-              ]
-      Nothing -> do
-        case declaredType of
-          StandardQueue ->
-            (ops ^. #createQueue) qn
-          UnloggedQueue ->
-            (ops ^. #createUnloggedQueue) qn
-          PartitionedQueue pc ->
-            let query =
-                  StmtTypes.CreatePartitionedQueue
-                    { queueName = qn,
-                      partitionInterval = pc ^. #partitionInterval,
-                      retentionInterval = pc ^. #retentionInterval
-                    }
-             in case pc ^. #premake of
-                  Nothing -> (ops ^. #createPartitionedQueue) query
-                  Just n -> (ops ^. #createPartitionedQueueWithPremake) query n
-        pure [CreatedQueue qn declaredType]
+      colliding = NE.nonEmpty (sort (filter (/= qnText) (Map.findWithDefault [] qnText collisionsByLowerName)))
+      collisionAction = maybe [] (\names -> [DetectedQueueNameCollision qn names]) colliding
+      effectiveShape = maybe (declaredShape declaredType) observedQueueType (Map.lookup qnText existingQueues)
 
-  -- Notification. A missing row is enabled; a row whose interval already
-  -- matches is left strictly alone (re-enabling would reset last_notified_at);
-  -- a row whose interval differs is updated in place.
-  notifyAction <- case cfg ^. #notifyInsert of
-    Nothing -> pure []
-    Just nc ->
-      let declaredMs = fromMaybe defaultThrottleMs (nc ^. #throttleMs)
-       in case Map.lookup qnText existingNotify of
-            Nothing -> do
-              (ops ^. #enableNotifyInsert)
-                StmtTypes.EnableNotifyInsert
-                  { queueName = qn,
-                    throttleIntervalMs = nc ^. #throttleMs
-                  }
-              pure [EnabledNotify qn (nc ^. #throttleMs)]
-            Just observedMs
-              | observedMs == declaredMs -> pure [SkippedNotify qn]
-              | otherwise -> do
-                  (ops ^. #updateNotifyInsert)
-                    StmtTypes.UpdateNotifyInsert
+  if Map.notMember qnText existingQueues && not (null collisionAction)
+    then pure collisionAction
+    else do
+      -- Queue creation, or — when the queue is already there — a shape comparison.
+      -- Drift is reported and never repaired: the only way to change a queue's type
+      -- is to drop and recreate it, which would destroy its messages.
+      queueAction <-
+        case Map.lookup qnText existingQueues of
+          Just observed ->
+            let observedType = observedQueueType observed
+             in pure
+                  [ if declaredShape declaredType == observedType
+                      then SkippedQueue qn
+                      else DetectedQueueTypeDrift qn declaredType observedType
+                  ]
+          Nothing -> do
+            case declaredType of
+              StandardQueue ->
+                (ops ^. #createQueue) qn
+              UnloggedQueue ->
+                (ops ^. #createUnloggedQueue) qn
+              PartitionedQueue pc ->
+                let query =
+                      StmtTypes.CreatePartitionedQueue
+                        { queueName = qn,
+                          partitionInterval = pc ^. #partitionInterval,
+                          retentionInterval = pc ^. #retentionInterval
+                        }
+                 in case pc ^. #premake of
+                      Nothing -> (ops ^. #createPartitionedQueue) query
+                      Just n -> (ops ^. #createPartitionedQueueWithPremake) query n
+            pure [CreatedQueue qn declaredType]
+
+      -- Notification. A missing row is enabled; a row whose interval already
+      -- matches is left strictly alone (re-enabling would reset last_notified_at);
+      -- a row whose interval differs is updated in place.
+      notifyAction <- case cfg ^. #notifyInsert of
+        Nothing -> pure []
+        Just _ | effectiveShape == ObservedPartitioned -> pure [UnsupportedNotifyOnPartitionedQueue qn]
+        Just nc ->
+          let declaredMs = fromMaybe defaultThrottleMs (nc ^. #throttleMs)
+           in case Map.lookup qnText existingNotify of
+                Nothing -> do
+                  (ops ^. #enableNotifyInsert)
+                    StmtTypes.EnableNotifyInsert
                       { queueName = qn,
-                        throttleIntervalMs = declaredMs
+                        throttleIntervalMs = nc ^. #throttleMs
                       }
-                  pure [UpdatedNotifyThrottle qn observedMs declaredMs]
+                  pure [EnabledNotify qn (nc ^. #throttleMs)]
+                Just observedMs
+                  | observedMs == declaredMs -> pure [SkippedNotify qn]
+                  | otherwise -> do
+                      (ops ^. #updateNotifyInsert)
+                        StmtTypes.UpdateNotifyInsert
+                          { queueName = qn,
+                            throttleIntervalMs = declaredMs
+                          }
+                      pure [UpdatedNotifyThrottle qn observedMs declaredMs]
 
-  -- FIFO index. The catalog snapshot makes the report truthful; the underlying
-  -- pgmq.create_fifo_index is CREATE INDEX IF NOT EXISTS, so losing a race with
-  -- a concurrent replica degrades to a no-op rather than an error.
-  fifoAction <-
-    if cfg ^. #fifoIndex
-      then
-        if Set.member qnText existingFifo
-          then pure [SkippedFifoIndex qn]
-          else do
-            (ops ^. #createFifoIndex) qn
-            pure [CreatedFifoIndex qn]
-      else pure []
+      -- FIFO index. The catalog snapshot makes the report truthful; the underlying
+      -- pgmq.create_fifo_index is CREATE INDEX IF NOT EXISTS, so losing a race with
+      -- a concurrent replica degrades to a no-op rather than an error.
+      fifoAction <-
+        if cfg ^. #fifoIndex
+          then
+            if Set.member qnText existingFifo
+              then pure [SkippedFifoIndex qn]
+              else do
+                (ops ^. #createFifoIndex) qn
+                pure [CreatedFifoIndex qn]
+          else pure []
 
-  -- Topic bindings
-  bindingActions <- concat <$> traverse (reconcileBinding ops qn qnText existingBindings) (cfg ^. #topicBindings)
+      -- Topic bindings
+      bindingActions <- concat <$> traverse (reconcileBinding ops qn qnText existingBindings) (cfg ^. #topicBindings)
 
-  pure (queueAction ++ notifyAction ++ fifoAction ++ bindingActions)
+      pure (queueAction ++ collisionAction ++ notifyAction ++ fifoAction ++ bindingActions)
 
 -- | The shape a declared config asks for, reduced to what @pgmq.list_queues()@
 -- can actually report. Partition interval, retention and premake are deliberately
diff --git a/src/Pgmq/Config/Types.hs b/src/Pgmq/Config/Types.hs
--- a/src/Pgmq/Config/Types.hs
+++ b/src/Pgmq/Config/Types.hs
@@ -29,9 +29,11 @@
   )
 where
 
-import Control.Lens ((%~), (&))
+import Control.Lens ((%~), (&), (^.))
+import Data.Aeson (Encoding, KeyValue, Series, ToJSON (..), object, pairs, (.=))
 import Data.Generics.Labels ()
 import Data.Int (Int32)
+import Data.List.NonEmpty (NonEmpty)
 import Data.Text (Text)
 import GHC.Generics (Generic)
 import Pgmq.Types (QueueName, TopicPattern)
@@ -123,10 +125,11 @@
   | -- | The queue exists but its observed shape contradicts the declared one.
     -- Fields: queue, declared type, observed type.
     --
-    -- Nothing was mutated and nothing will be: converting a queue between
-    -- standard, unlogged, and partitioned means dropping and recreating it,
-    -- destroying every message it holds, which a startup reconciler must never
-    -- do. Resolving the drift is an operator decision. This action replaces
+    -- The queue itself is not mutated: conversion between standard, unlogged,
+    -- and partitioned requires destroying its messages. Notification, FIFO-index
+    -- and topic-binding declarations are still reconciled against the observed
+    -- queue and each reports its own action. Resolving drift is an operator
+    -- decision. This action replaces
     -- 'SkippedQueue' for the queue it concerns, so the report still carries
     -- exactly one queue-existence action per declared config.
     --
@@ -135,6 +138,21 @@
     -- and retention settings, because @pgmq.list_queues()@ does not report
     -- them.
     DetectedQueueTypeDrift !QueueName !QueueType !ObservedQueueType
+  | -- | Foreign registry names share the declared name's lowercased physical
+    -- table. Fields: declared queue, every colliding registry name in ascending
+    -- lexical order. Without an exact row this replaces 'CreatedQueue' and no
+    -- notification, FIFO-index or binding declarations run. With an exact row,
+    -- this follows the existence action and sub-resources proceed on that row.
+    -- This action mutates nothing. Detection uses the initial snapshot; a foreign
+    -- client can still create an alias afterward. Resolving aliases follows
+    -- @docs/design/016-queue-name-validation.md@.
+    DetectedQueueNameCollision !QueueName !(NonEmpty Text)
+  | -- | Insert notifications were declared on an observed or newly created
+    -- partitioned queue. Its cloned leaf triggers cannot deliver on
+    -- 'Pgmq.Types.notifyChannelName'; see
+    -- @docs/design/015-notification-delivery-contract.md@. Nothing was enabled,
+    -- updated or re-enabled; an existing foreign throttle row is left unchanged.
+    UnsupportedNotifyOnPartitionedQueue !QueueName
   deriving stock (Show)
 
 -- | The throttle interval pgmq applies when none is given: 250 milliseconds.
@@ -181,3 +199,99 @@
 withTopicBinding :: TopicPattern -> QueueConfig -> QueueConfig
 withTopicBinding pat cfg =
   cfg & #topicBindings %~ (++ [pat])
+
+-- JSON wire encodings ---------------------------------------------------------
+
+encodeFields :: [Series] -> Encoding
+encodeFields = pairs . mconcat
+
+-- | Encodes as @{"type":"standard"}@, @{"type":"unlogged"}@, or
+-- @{"type":"partitioned","partition_interval":…,"retention_interval":…,"premake":…}@.
+-- See @docs/design/020-json-wire-encodings.md@.
+queueTypeFields :: (KeyValue e kv) => QueueType -> [kv]
+queueTypeFields qt = case qt of
+  StandardQueue -> ["type" .= ("standard" :: Text)]
+  UnloggedQueue -> ["type" .= ("unlogged" :: Text)]
+  PartitionedQueue pc -> ("type" .= ("partitioned" :: Text)) : partitionConfigFields pc
+
+-- | JSON wire contract: @docs/design/020-json-wire-encodings.md@.
+instance ToJSON QueueType where
+  toJSON = object . queueTypeFields
+  toEncoding = encodeFields . queueTypeFields
+
+-- | @premake@ is @null@ when creation uses the server default. See
+-- @docs/design/020-json-wire-encodings.md@.
+partitionConfigFields :: (KeyValue e kv) => PartitionConfig -> [kv]
+partitionConfigFields pc =
+  [ "partition_interval" .= (pc ^. #partitionInterval),
+    "retention_interval" .= (pc ^. #retentionInterval),
+    "premake" .= (pc ^. #premake)
+  ]
+
+-- | JSON wire contract: @docs/design/020-json-wire-encodings.md@.
+instance ToJSON PartitionConfig where
+  toJSON = object . partitionConfigFields
+  toEncoding = encodeFields . partitionConfigFields
+
+-- | Encodes as the bare strings @"standard"@, @"unlogged"@, @"partitioned"@.
+observedQueueTypeText :: ObservedQueueType -> Text
+observedQueueTypeText ot = case ot of
+  ObservedStandard -> "standard"
+  ObservedUnlogged -> "unlogged"
+  ObservedPartitioned -> "partitioned"
+
+-- | JSON wire contract: @docs/design/020-json-wire-encodings.md@.
+instance ToJSON ObservedQueueType where
+  toJSON = toJSON . observedQueueTypeText
+  toEncoding = toEncoding . observedQueueTypeText
+
+-- | A tagged object: @"action"@ holds the snake_case constructor name and the
+-- remaining keys name the fields. @enabled_notify@'s @throttle_interval_ms@
+-- is @null@ when the config declared the pgmq default ('defaultThrottleMs')
+-- rather than a number; that is a different fact from declaring @250@.
+-- Encode-only. The encoding is a published compatibility surface; see
+-- @docs/design/020-json-wire-encodings.md@. Every constructor added later
+-- must be encoded by the same rule and pinned in
+-- @pgmq-config/test/JsonGoldenSpec.hs@.
+reconcileActionFields :: (KeyValue e kv) => ReconcileAction -> [kv]
+reconcileActionFields action = case action of
+  CreatedQueue q t ->
+    ["action" .= tag "created_queue", "queue" .= q, "queue_type" .= t]
+  EnabledNotify q ms ->
+    ["action" .= tag "enabled_notify", "queue" .= q, "throttle_interval_ms" .= ms]
+  CreatedFifoIndex q ->
+    ["action" .= tag "created_fifo_index", "queue" .= q]
+  BoundTopic q p ->
+    ["action" .= tag "bound_topic", "queue" .= q, "pattern" .= p]
+  SkippedQueue q ->
+    ["action" .= tag "skipped_queue", "queue" .= q]
+  SkippedNotify q ->
+    ["action" .= tag "skipped_notify", "queue" .= q]
+  SkippedFifoIndex q ->
+    ["action" .= tag "skipped_fifo_index", "queue" .= q]
+  SkippedTopicBinding q p ->
+    ["action" .= tag "skipped_topic_binding", "queue" .= q, "pattern" .= p]
+  UpdatedNotifyThrottle q observed declared ->
+    [ "action" .= tag "updated_notify_throttle",
+      "queue" .= q,
+      "observed_throttle_interval_ms" .= observed,
+      "declared_throttle_interval_ms" .= declared
+    ]
+  DetectedQueueTypeDrift q declared observed ->
+    [ "action" .= tag "detected_queue_type_drift",
+      "queue" .= q,
+      "declared" .= declared,
+      "observed" .= observed
+    ]
+  DetectedQueueNameCollision q names ->
+    ["action" .= tag "detected_queue_name_collision", "queue" .= q, "colliding_names" .= names]
+  UnsupportedNotifyOnPartitionedQueue q ->
+    ["action" .= tag "unsupported_notify_on_partitioned_queue", "queue" .= q]
+  where
+    tag :: Text -> Text
+    tag = id
+
+-- | JSON wire contract: @docs/design/020-json-wire-encodings.md@.
+instance ToJSON ReconcileAction where
+  toJSON = object . reconcileActionFields
+  toEncoding = encodeFields . reconcileActionFields
diff --git a/test/CollisionSpec.hs b/test/CollisionSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/CollisionSpec.hs
@@ -0,0 +1,80 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+-- | Schema-selectable isolated foreign-name fixtures for each backend.
+module CollisionSpec (tests) where
+
+import Control.Monad (forM_)
+import Data.Foldable (toList)
+import Data.List (sort)
+import Data.Text (Text)
+import Hasql.Pool qualified as Pool
+import Hasql.Session qualified as Session
+import PartitionSpec (backends, isolated, reconcile, right, session)
+import Pgmq.Config
+import Pgmq.Types (parseQueueName, parseTopicPattern)
+import Test.Tasty (TestTree, testGroup)
+import Test.Tasty.HUnit (assertFailure, testCase, (@?=))
+
+tests :: TestTree
+tests =
+  testGroup
+    "CollisionSpec"
+    [ testGroup
+        (if backend then "effectful" else "direct")
+        [ testCase "single alias blocks every declared sub-resource, including repeat" (collision backend ["Billing_Events"]),
+          testCase "multiple aliases block every declared sub-resource, including repeat" (collision backend ["Billing_Events", "BILLING_EVENTS"]),
+          testCase "exact row reports collision while reconciling sub-resources" (exactRow backend),
+          testCase "noncolliding foreign name leaves declared queues unaffected" (noncolliding backend)
+        ]
+    | backend <- backends
+    ]
+
+collision :: Bool -> [Text] -> IO ()
+collision backend names = isolated $ \pool -> do
+  forM_ names $ \name -> sql pool ("SELECT pgmq.create('" <> name <> "')")
+  q <- right (parseQueueName "billing_events")
+  other <- right (parseQueueName "other")
+  pat <- right (parseTopicPattern "billing.*")
+  let config = withTopicBinding pat (withFifoIndex (withNotifyInsert (Just 0) (standardQueue q)))
+  forM_ [1 :: Int, 2] $ \_ -> do
+    actions <- right =<< reconcile backend pool [config, standardQueue other]
+    case actions of
+      [DetectedQueueNameCollision actual colliding, next] -> do
+        actual @?= q
+        sort (toList colliding) @?= sort names
+        case next of
+          CreatedQueue n _ -> n @?= other
+          SkippedQueue n -> n @?= other
+          unexpected -> assertFailure (show unexpected)
+      unexpected -> assertFailure (show unexpected)
+    sql pool ("DO $$ BEGIN IF (SELECT count(*) FROM pgmq.meta WHERE lower(queue_name)='billing_events') <> " <> (if length names == 1 then "1" else "2") <> " THEN RAISE EXCEPTION 'collision inserted metadata alias'; END IF; END $$")
+    sql pool "DO $$ BEGIN IF EXISTS (SELECT FROM pgmq.notify_insert_throttle WHERE queue_name='billing_events') OR to_regclass('pgmq.q_billing_events_fifo_idx') IS NOT NULL OR EXISTS (SELECT FROM pgmq.topic_bindings WHERE queue_name='billing_events') THEN RAISE EXCEPTION 'collision mutated sub-resources'; END IF; IF NOT EXISTS (SELECT FROM pgmq.meta WHERE queue_name='other') THEN RAISE EXCEPTION 'unrelated queue blocked'; END IF; END $$"
+
+sql :: Pool.Pool -> Text -> IO ()
+sql pool command = session pool (Session.script command)
+
+exactRow :: Bool -> IO ()
+exactRow backend = isolated $ \pool -> do
+  sql pool "SELECT pgmq.create('Billing_Events'); SELECT pgmq.create('BILLING_EVENTS'); SELECT pgmq.create('billing_events')"
+  q <- right (parseQueueName "billing_events")
+  other <- right (parseQueueName "other")
+  pat <- right (parseTopicPattern "billing.*")
+  let config = withTopicBinding pat (withFifoIndex (withNotifyInsert (Just 0) (standardQueue q)))
+  actions <- right =<< reconcile backend pool [config, standardQueue other]
+  case actions of
+    [SkippedQueue n, DetectedQueueNameCollision c names, EnabledNotify e _, CreatedFifoIndex f, BoundTopic b _, CreatedQueue o _] -> do
+      [n, c, e, f, b] @?= replicate 5 q
+      o @?= other
+      sort (toList names) @?= ["BILLING_EVENTS", "Billing_Events"]
+    unexpected -> assertFailure (show unexpected)
+  sql pool "DO $$ BEGIN IF (SELECT count(*) FROM pgmq.meta WHERE lower(queue_name)='billing_events') <> 3 OR NOT EXISTS (SELECT FROM pgmq.notify_insert_throttle WHERE queue_name='billing_events') OR to_regclass('pgmq.q_billing_events_fifo_idx') IS NULL OR NOT EXISTS (SELECT FROM pgmq.topic_bindings WHERE queue_name='billing_events') THEN RAISE EXCEPTION 'exact row sub-resources missing'; END IF; END $$"
+
+noncolliding :: Bool -> IO ()
+noncolliding backend = isolated $ \pool -> do
+  sql pool "SELECT pgmq.create('ForeignQueue')"
+  q <- right (parseQueueName "billing_events")
+  other <- right (parseQueueName "other")
+  actions <- right =<< reconcile backend pool [standardQueue q, standardQueue other]
+  case actions of
+    [CreatedQueue n _, CreatedQueue o _] -> [n, o] @?= [q, other]
+    unexpected -> assertFailure (show unexpected)
diff --git a/test/ConfigSpec.hs b/test/ConfigSpec.hs
--- a/test/ConfigSpec.hs
+++ b/test/ConfigSpec.hs
@@ -342,6 +342,8 @@
 actionForQueue qn (SkippedTopicBinding q _) = q == qn
 actionForQueue qn (UpdatedNotifyThrottle q _ _) = q == qn
 actionForQueue qn (DetectedQueueTypeDrift q _ _) = q == qn
+actionForQueue qn (DetectedQueueNameCollision q _) = q == qn
+actionForQueue qn (UnsupportedNotifyOnPartitionedQueue q) = q == qn
 
 -- | Silent-variant version of 'testEnsureQueuesIdempotent': call 'ensureQueues'
 -- twice with the same standard-queue config and confirm the queue exists exactly
diff --git a/test/EphemeralDb.hs b/test/EphemeralDb.hs
--- a/test/EphemeralDb.hs
+++ b/test/EphemeralDb.hs
@@ -5,13 +5,15 @@
   ( -- * Database setup
     withPgmqDb,
     ephemeralConfig,
+    ensureOwnerOnlyRoot,
 
     -- * Re-exports
     StartError,
   )
 where
 
-import Control.Monad (filterM, when)
+import Control.Monad (filterM, unless, when)
+import Data.Bits ((.&.))
 import Data.List.NonEmpty (NonEmpty (..))
 import Data.Monoid (Last (..))
 import Data.Text.IO qualified as TextIO
@@ -32,8 +34,11 @@
 import Hasql.Pool.Config qualified as PoolConfig
 import Hasql.Session qualified as Session
 import Pgmq.Migration qualified as Migration
-import System.Directory (createDirectoryIfMissing, doesFileExist)
+import System.Directory (doesFileExist)
 import System.Environment (lookupEnv)
+import System.IO.Error (catchIOError, isAlreadyExistsError)
+import System.Posix.Directory qualified as Posix
+import System.Posix.Files (deviceID, fileID, fileMode, fileOwner, getSymbolicLinkStatus, isDirectory, setFileMode)
 import System.Posix.User (getEffectiveUserID)
 
 -- | Root directory for ephemeral PostgreSQL clusters.
@@ -51,11 +56,31 @@
   uid <- getEffectiveUserID
   pure ("/tmp/ephpg-pgmq-hs-" <> show uid)
 
+-- | Establish a real effective-uid-owned root without deleting its contents.
+ensureOwnerOnlyRoot :: FilePath -> IO ()
+ensureOwnerOnlyRoot root = do
+  catchIOError (Posix.createDirectory root 0o700) $ \err ->
+    unless (isAlreadyExistsError err) (ioError err)
+  uid <- getEffectiveUserID
+  before <- getSymbolicLinkStatus root
+  unless (isDirectory before && fileOwner before == uid) $
+    ioError (userError ("Unsafe ephemeral PostgreSQL root: " <> root))
+  setFileMode root 0o700
+  after <- getSymbolicLinkStatus root
+  unless
+    ( isDirectory after
+        && fileOwner after == uid
+        && deviceID before == deviceID after
+        && fileID before == fileID after
+        && fileMode after .&. 0o7777 == 0o700
+    )
+    (ioError (userError ("Ephemeral PostgreSQL root changed or is not owner-only: " <> root)))
+
 -- | Cached-startup configuration pinned to 'ephemeralRoot'.
 ephemeralConfig :: IO Config
 ephemeralConfig = do
   root <- ephemeralRoot
-  createDirectoryIfMissing True root
+  ensureOwnerOnlyRoot root
   pure defaultConfig {temporaryRoot = Last (Just root)}
 
 -- | Run an action with a temporary PostgreSQL database that has pgmq schema installed
diff --git a/test/JsonGoldenSpec.hs b/test/JsonGoldenSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/JsonGoldenSpec.hs
@@ -0,0 +1,62 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+-- | One golden per 'ReconcileAction' constructor. A constructor without a
+-- golden is a defect: the report is a published wire shape (see
+-- @docs/design/020-json-wire-encodings.md@) once a consumer logs or serves it.
+module JsonGoldenSpec (tests) where
+
+import Data.Aeson (ToJSON (..), Value, decode, encode)
+import Data.List.NonEmpty (NonEmpty (..))
+import Pgmq.Config.Types
+import Pgmq.Types (QueueName, TopicPattern, parseQueueName, parseTopicPattern)
+import Test.Tasty (TestTree, localOption, testGroup)
+import Test.Tasty.Golden (NoCreateFile (..), goldenVsString)
+import Test.Tasty.HUnit (assertEqual, testCase)
+
+tests :: TestTree
+tests =
+  localOption (NoCreateFile True) $
+    testGroup
+      "ReconcileAction wire encodings"
+      [ golden "created_queue_standard" (CreatedQueue q StandardQueue),
+        golden "created_queue_unlogged" (CreatedQueue q UnloggedQueue),
+        golden "created_queue_partitioned" (CreatedQueue q (PartitionedQueue partition)),
+        golden "created_queue_partitioned_default_premake" (CreatedQueue q (PartitionedQueue partition {premake = Nothing})),
+        golden "enabled_notify" (EnabledNotify q (Just 1000)),
+        golden "enabled_notify_default" (EnabledNotify q Nothing),
+        golden "created_fifo_index" (CreatedFifoIndex q),
+        golden "bound_topic" (BoundTopic q pat),
+        golden "skipped_queue" (SkippedQueue q),
+        golden "skipped_notify" (SkippedNotify q),
+        golden "skipped_fifo_index" (SkippedFifoIndex q),
+        golden "skipped_topic_binding" (SkippedTopicBinding q pat),
+        golden "updated_notify_throttle" (UpdatedNotifyThrottle q 250 1000),
+        golden "detected_queue_type_drift" (DetectedQueueTypeDrift q UnloggedQueue ObservedPartitioned),
+        golden "detected_queue_name_collision" (DetectedQueueNameCollision q ("ORDERS" :| ["Orders"])),
+        golden "unsupported_notify_on_partitioned_queue" (UnsupportedNotifyOnPartitionedQueue q),
+        golden "queue_type_standard" StandardQueue,
+        golden "queue_type_unlogged" UnloggedQueue,
+        golden "queue_type_partitioned" (PartitionedQueue partition),
+        golden "partition_config" partition,
+        golden "partition_config_default" (partition {premake = Nothing}),
+        golden "observed_standard" ObservedStandard,
+        golden "observed_unlogged" ObservedUnlogged,
+        golden "observed_partitioned" ObservedPartitioned
+      ]
+  where
+    golden name value =
+      testGroup
+        name
+        [ goldenVsString "bytes" ("test/golden/" <> name <> ".json") (pure (encode value <> "\n")),
+          testCase "toJSON and toEncoding agree" $
+            assertEqual "encoder parity" (Just (toJSON value)) (decode (encode value) :: Maybe Value)
+        ]
+
+q :: QueueName
+q = either (error . show) id (parseQueueName "orders")
+
+pat :: TopicPattern
+pat = either (error . show) id (parseTopicPattern "orders.*")
+
+partition :: PartitionConfig
+partition = PartitionConfig {partitionInterval = "daily", retentionInterval = "7 days", premake = Just 2}
diff --git a/test/Main.hs b/test/Main.hs
--- a/test/Main.hs
+++ b/test/Main.hs
@@ -2,11 +2,14 @@
 
 module Main (main) where
 
+import CollisionSpec qualified
 import ConfigSpec qualified
-import EphemeralDb (withPgmqDb)
+import EphemeralDb (ensureOwnerOnlyRoot, withPgmqDb)
 import ForeignQueueSpec qualified
+import JsonGoldenSpec qualified
 import NotifyCrashSpec qualified
 import PartitionSpec qualified
+import RootPermissionsSpec qualified
 import Test.Tasty (defaultMain, testGroup)
 
 main :: IO ()
@@ -15,8 +18,12 @@
     let tree =
           testGroup
             "pgmq-config"
-            [ ConfigSpec.tests pool,
+            [ RootPermissionsSpec.tests ensureOwnerOnlyRoot,
+              JsonGoldenSpec.tests,
+              ConfigSpec.tests pool,
               PartitionSpec.tests,
+              -- Foreign names require an isolated, schema-selectable server.
+              CollisionSpec.tests,
               -- NotifyCrashSpec manages its own PostgreSQL instance: it crashes
               -- the server, which the shared pool above could not survive.
               NotifyCrashSpec.tests,
diff --git a/test/NotifyCrashSpec.hs b/test/NotifyCrashSpec.hs
--- a/test/NotifyCrashSpec.hs
+++ b/test/NotifyCrashSpec.hs
@@ -35,6 +35,7 @@
   )
 import EphemeralDb (ephemeralConfig)
 import EphemeralPg qualified as Pg
+
 -- 'shutdownMode' names a field of both Pg.Config and Pg.Database, so the record
 -- update below needs the selector from the module that defines only Database.
 import EphemeralPg.Database qualified as PgDb
@@ -70,7 +71,9 @@
     -- | Channel of the notification delivered by the post-crash send, if any.
     obsNotifyAfterCrash :: !(Maybe ByteString),
     -- | Queue names in the throttle table after a reconcile ran.
-    obsThrottlesAfterReconcile :: ![Text]
+    obsThrottlesAfterReconcile :: ![Text],
+    -- | Restored nonzero interval, without restarting the application.
+    obsRestoredInterval :: !Int64
   }
 
 tests :: TestTree
@@ -102,6 +105,7 @@
         testCase "post-crash: a reconcile restores the throttle row" $ do
           obs <- getObs
           obsThrottlesAfterReconcile obs @?= [queueNameToText (obsQueueName obs)]
+          obsRestoredInterval obs @?= 3600000
       ]
 
 -- | Start a dedicated PostgreSQL instance, enable notify, crash it, recover it,
@@ -118,7 +122,7 @@
   db0 <- readIORef ref
   installPgmq db0
 
-  -- Pre-crash: create the queue, enable unthrottled notify, send one message.
+  -- Pre-crash: create the queue, enable a nonzero throttle, send one message.
   -- No listener is opened yet: an immediate shutdown kills every pre-crash
   -- connection, so a listener created here could never see the assertion's
   -- notification.
@@ -128,7 +132,7 @@
       Sessions.enableNotifyInsert
         StmtTypes.EnableNotifyInsert
           { StmtTypes.queueName = qn,
-            StmtTypes.throttleIntervalMs = Just 0
+            StmtTypes.throttleIntervalMs = Just 3600000
           }
     runSession pool (sendProbe qn "before-crash")
     -- ephemeral-pg runs PostgreSQL with fsync, synchronous_commit and
@@ -153,8 +157,9 @@
       _ <- runSession pool (sendProbe qn "after-crash")
       awaitNotify conn 20
 
-    runSession pool (ensureQueues [withNotifyInsert (Just 0) (standardQueue qn)])
+    runSession pool (ensureQueues [withNotifyInsert (Just 3600000) (standardQueue qn)])
     throttlesAfterReconcile <- listThrottleNames pool
+    restored <- runSession pool (Session.statement (queueNameToText qn) (preparable "SELECT throttle_interval_ms::bigint FROM pgmq.notify_insert_throttle WHERE queue_name=$1" (E.param (E.nonNullable E.text)) (D.singleRow (D.column (D.nonNullable D.int8)))))
 
     pure
       CrashObservations
@@ -164,7 +169,8 @@
           obsTriggersAfterCrash = triggers,
           obsQueueLengthAfterCrash = queueLength,
           obsNotifyAfterCrash = LibPQ.notifyRelname <$> notified,
-          obsThrottlesAfterReconcile = throttlesAfterReconcile
+          obsThrottlesAfterReconcile = throttlesAfterReconcile,
+          obsRestoredInterval = restored
         }
 
 -- | Stop PostgreSQL with SIGQUIT and start it again on the same data directory.
@@ -297,7 +303,8 @@
 awaitNotify conn attempts
   | attempts <= 0 = pure Nothing
   | otherwise = do
-      _ <- LibPQ.consumeInput conn
+      ok <- LibPQ.consumeInput conn
+      unless ok (assertFailure "libpq consumeInput failed")
       pending <- LibPQ.notifies conn
       case pending of
         Just n -> pure (Just n)
diff --git a/test/PartitionSpec.hs b/test/PartitionSpec.hs
--- a/test/PartitionSpec.hs
+++ b/test/PartitionSpec.hs
@@ -1,6 +1,6 @@
 {-# LANGUAGE CPP #-}
 
-module PartitionSpec (tests) where
+module PartitionSpec (tests, backends, isolated, reconcile, right, session) where
 
 import Control.Exception (finally)
 import Control.Monad (forM_)
@@ -71,6 +71,43 @@
               counts pool qn >>= (@?= [expected, expected])
       | requested <- [Nothing, Just 2]
       ]
+      ++ [ testCase "unsupported partition notify installs no trigger or throttle" $ isolated $ \pool -> withPartman pool $ do
+             qn <- right (parseQueueName "partition_notify")
+             let config = withNotifyInsert (Just 0) (partitionedQueue qn (PartitionConfig "10" "100" Nothing))
+             first <- right =<< reconcile effectful pool [config]
+             case first of
+               [CreatedQueue q _, UnsupportedNotifyOnPartitionedQueue n] -> [q, n] @?= [qn, qn]
+               other -> assertFailure (show other)
+             second <- right =<< reconcile effectful pool [config]
+             case second of
+               [SkippedQueue q, UnsupportedNotifyOnPartitionedQueue n] -> [q, n] @?= [qn, qn]
+               other -> assertFailure (show other)
+             session pool (Session.script "DO $$ BEGIN IF EXISTS (SELECT FROM pgmq.notify_insert_throttle WHERE queue_name='partition_notify') OR EXISTS (SELECT FROM pg_trigger WHERE tgrelid='pgmq.q_partition_notify'::regclass AND tgname='trigger_notify_queue_insert_listeners') THEN RAISE EXCEPTION 'unsupported notification installed'; END IF; END $$"),
+           testCase "unsupported existing partition throttle remains unchanged" $ isolated $ \pool -> withPartman pool $ do
+             qn <- right (parseQueueName "partition_existing")
+             session pool (Session.script "SELECT pgmq.create_partitioned('partition_existing','10','100'); SELECT pgmq.enable_notify_insert('partition_existing',250); CREATE TABLE public.before_throttle AS SELECT * FROM pgmq.notify_insert_throttle")
+             actions <- right =<< reconcile effectful pool [withNotifyInsert (Just 500) (partitionedQueue qn (PartitionConfig "10" "100" Nothing))]
+             case actions of
+               [SkippedQueue q, UnsupportedNotifyOnPartitionedQueue n] -> [q, n] @?= [qn, qn]
+               other -> assertFailure (show other)
+             session pool (Session.script "DO $$ BEGIN IF EXISTS ((TABLE public.before_throttle EXCEPT TABLE pgmq.notify_insert_throttle) UNION ALL (TABLE pgmq.notify_insert_throttle EXCEPT TABLE public.before_throttle)) THEN RAISE EXCEPTION 'partition throttle changed'; END IF; END $$"),
+           testCase "observed partition declared standard reports drift and unsupported" $ isolated $ \pool -> withPartman pool $ do
+             qn <- right (parseQueueName "partition_drift")
+             session pool (Session.script "SELECT pgmq.create_partitioned('partition_drift','10','100')")
+             actions <- right =<< reconcile effectful pool [withNotifyInsert (Just 0) (standardQueue qn)]
+             case actions of
+               [DetectedQueueTypeDrift q StandardQueue ObservedPartitioned, UnsupportedNotifyOnPartitionedQueue n] -> [q, n] @?= [qn, qn]
+               other -> assertFailure (show other)
+             session pool (Session.script "DO $$ BEGIN IF EXISTS (SELECT FROM pgmq.notify_insert_throttle WHERE queue_name='partition_drift') THEN RAISE EXCEPTION 'observed partition notification installed'; END IF; END $$"),
+           testCase "observed standard declared partition reconciles notifications" $ isolated $ \pool -> do
+             qn <- right (parseQueueName "standard_drift")
+             session pool (Session.script "SELECT pgmq.create('standard_drift')")
+             actions <- right =<< reconcile effectful pool [withNotifyInsert (Just 0) (partitionedQueue qn (PartitionConfig "10" "100" Nothing))]
+             case actions of
+               [DetectedQueueTypeDrift q (PartitionedQueue _) ObservedStandard, EnabledNotify n (Just 0)] -> [q, n] @?= [qn, qn]
+               other -> assertFailure (show other)
+             session pool (Session.script "DO $$ BEGIN IF NOT EXISTS (SELECT FROM pgmq.notify_insert_throttle WHERE queue_name='standard_drift' AND throttle_interval_ms=0) THEN RAISE EXCEPTION 'standard notification missing'; END IF; END $$")
+         ]
       ++ [ testCase "zero and negative counts fail on new queues" $ forM_ [0, -1] $ \n -> isolated $ \pool -> withPartman pool $ do
              stock <- (== Just "1.12.0") <$> lookupEnv "PGMQ_TEST_SCHEMA_VERSION"
              qn <- right (parseQueueName "invalid_partition_config")
diff --git a/test/RootPermissionsSpec.hs b/test/RootPermissionsSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/RootPermissionsSpec.hs
@@ -0,0 +1,63 @@
+-- | Check each suite's local root helper using private disposable fixtures.
+module RootPermissionsSpec (tests) where
+
+import Control.Exception (IOException, bracket, try)
+import Data.Bits ((.&.))
+import System.Directory (getTemporaryDirectory, removePathForcibly)
+import System.Posix.Directory qualified as Posix
+import System.Posix.Files (createSymbolicLink, fileMode, getSymbolicLinkStatus, isDirectory, setFileMode)
+import System.Posix.Temp (mkdtemp)
+import Test.Tasty (TestTree, testGroup)
+import Test.Tasty.HUnit (assertBool, assertFailure, testCase, (@?=))
+
+tests :: (FilePath -> IO ()) -> TestTree
+tests ensure =
+  testGroup
+    "RootPermissionsSpec"
+    [ testCase "missing root is a real 0700 directory" $ private $ \parent -> do
+        let root = parent <> "/root"
+        ensure root
+        status <- getSymbolicLinkStatus root
+        assertBool "real directory" (isDirectory status)
+        fileMode status .&. 0o7777 @?= 0o700,
+      testCase "owned 0755 directory is tightened without losing contents" $ private $ \parent -> do
+        let root = parent <> "/root"
+        Posix.createDirectory root 0o755
+        setFileMode root 0o755
+        writeFile (root <> "/sentinel") "preserve"
+        ensure root
+        mode root >>= (@?= 0o700)
+        readFile (root <> "/sentinel") >>= (@?= "preserve"),
+      testCase "repeated call preserves contents and permissions" $ private $ \parent -> do
+        let root = parent <> "/root"
+        ensure root
+        writeFile (root <> "/sentinel") "preserve"
+        ensure root
+        mode root >>= (@?= 0o700)
+        readFile (root <> "/sentinel") >>= (@?= "preserve"),
+      testCase "regular file is rejected unchanged" $ private $ \parent -> do
+        let root = parent <> "/root"
+        writeFile root "preserve"
+        setFileMode root 0o644
+        rejected (ensure root)
+        mode root >>= (@?= 0o644)
+        readFile root >>= (@?= "preserve"),
+      testCase "symlink is rejected without chmod of its target" $ private $ \parent -> do
+        let root = parent <> "/root"
+            target = parent <> "/target"
+        Posix.createDirectory target 0o755
+        setFileMode target 0o755
+        writeFile (target <> "/sentinel") "preserve"
+        createSymbolicLink target root
+        rejected (ensure root)
+        mode target >>= (@?= 0o755)
+        readFile (target <> "/sentinel") >>= (@?= "preserve")
+    ]
+  where
+    mode path = (.&. 0o7777) . fileMode <$> getSymbolicLinkStatus path
+    rejected action = do
+      result <- try action :: IO (Either IOException ())
+      case result of
+        Left _ -> pure ()
+        Right () -> assertFailure "unsafe root accepted"
+    private action = bracket (getTemporaryDirectory >>= \tmp -> mkdtemp (tmp <> "/pgmq-root-test-")) removePathForcibly action
diff --git a/test/golden/bound_topic.json b/test/golden/bound_topic.json
new file mode 100644
--- /dev/null
+++ b/test/golden/bound_topic.json
@@ -0,0 +1,1 @@
+{"action":"bound_topic","queue":"orders","pattern":"orders.*"}
diff --git a/test/golden/created_fifo_index.json b/test/golden/created_fifo_index.json
new file mode 100644
--- /dev/null
+++ b/test/golden/created_fifo_index.json
@@ -0,0 +1,1 @@
+{"action":"created_fifo_index","queue":"orders"}
diff --git a/test/golden/created_queue_partitioned.json b/test/golden/created_queue_partitioned.json
new file mode 100644
--- /dev/null
+++ b/test/golden/created_queue_partitioned.json
@@ -0,0 +1,1 @@
+{"action":"created_queue","queue":"orders","queue_type":{"type":"partitioned","partition_interval":"daily","retention_interval":"7 days","premake":2}}
diff --git a/test/golden/created_queue_partitioned_default_premake.json b/test/golden/created_queue_partitioned_default_premake.json
new file mode 100644
--- /dev/null
+++ b/test/golden/created_queue_partitioned_default_premake.json
@@ -0,0 +1,1 @@
+{"action":"created_queue","queue":"orders","queue_type":{"type":"partitioned","partition_interval":"daily","retention_interval":"7 days","premake":null}}
diff --git a/test/golden/created_queue_standard.json b/test/golden/created_queue_standard.json
new file mode 100644
--- /dev/null
+++ b/test/golden/created_queue_standard.json
@@ -0,0 +1,1 @@
+{"action":"created_queue","queue":"orders","queue_type":{"type":"standard"}}
diff --git a/test/golden/created_queue_unlogged.json b/test/golden/created_queue_unlogged.json
new file mode 100644
--- /dev/null
+++ b/test/golden/created_queue_unlogged.json
@@ -0,0 +1,1 @@
+{"action":"created_queue","queue":"orders","queue_type":{"type":"unlogged"}}
diff --git a/test/golden/detected_queue_name_collision.json b/test/golden/detected_queue_name_collision.json
new file mode 100644
--- /dev/null
+++ b/test/golden/detected_queue_name_collision.json
@@ -0,0 +1,1 @@
+{"action":"detected_queue_name_collision","queue":"orders","colliding_names":["ORDERS","Orders"]}
diff --git a/test/golden/detected_queue_type_drift.json b/test/golden/detected_queue_type_drift.json
new file mode 100644
--- /dev/null
+++ b/test/golden/detected_queue_type_drift.json
@@ -0,0 +1,1 @@
+{"action":"detected_queue_type_drift","queue":"orders","declared":{"type":"unlogged"},"observed":"partitioned"}
diff --git a/test/golden/enabled_notify.json b/test/golden/enabled_notify.json
new file mode 100644
--- /dev/null
+++ b/test/golden/enabled_notify.json
@@ -0,0 +1,1 @@
+{"action":"enabled_notify","queue":"orders","throttle_interval_ms":1000}
diff --git a/test/golden/enabled_notify_default.json b/test/golden/enabled_notify_default.json
new file mode 100644
--- /dev/null
+++ b/test/golden/enabled_notify_default.json
@@ -0,0 +1,1 @@
+{"action":"enabled_notify","queue":"orders","throttle_interval_ms":null}
diff --git a/test/golden/observed_partitioned.json b/test/golden/observed_partitioned.json
new file mode 100644
--- /dev/null
+++ b/test/golden/observed_partitioned.json
@@ -0,0 +1,1 @@
+"partitioned"
diff --git a/test/golden/observed_standard.json b/test/golden/observed_standard.json
new file mode 100644
--- /dev/null
+++ b/test/golden/observed_standard.json
@@ -0,0 +1,1 @@
+"standard"
diff --git a/test/golden/observed_unlogged.json b/test/golden/observed_unlogged.json
new file mode 100644
--- /dev/null
+++ b/test/golden/observed_unlogged.json
@@ -0,0 +1,1 @@
+"unlogged"
diff --git a/test/golden/partition_config.json b/test/golden/partition_config.json
new file mode 100644
--- /dev/null
+++ b/test/golden/partition_config.json
@@ -0,0 +1,1 @@
+{"partition_interval":"daily","retention_interval":"7 days","premake":2}
diff --git a/test/golden/partition_config_default.json b/test/golden/partition_config_default.json
new file mode 100644
--- /dev/null
+++ b/test/golden/partition_config_default.json
@@ -0,0 +1,1 @@
+{"partition_interval":"daily","retention_interval":"7 days","premake":null}
diff --git a/test/golden/queue_type_partitioned.json b/test/golden/queue_type_partitioned.json
new file mode 100644
--- /dev/null
+++ b/test/golden/queue_type_partitioned.json
@@ -0,0 +1,1 @@
+{"type":"partitioned","partition_interval":"daily","retention_interval":"7 days","premake":2}
diff --git a/test/golden/queue_type_standard.json b/test/golden/queue_type_standard.json
new file mode 100644
--- /dev/null
+++ b/test/golden/queue_type_standard.json
@@ -0,0 +1,1 @@
+{"type":"standard"}
diff --git a/test/golden/queue_type_unlogged.json b/test/golden/queue_type_unlogged.json
new file mode 100644
--- /dev/null
+++ b/test/golden/queue_type_unlogged.json
@@ -0,0 +1,1 @@
+{"type":"unlogged"}
diff --git a/test/golden/skipped_fifo_index.json b/test/golden/skipped_fifo_index.json
new file mode 100644
--- /dev/null
+++ b/test/golden/skipped_fifo_index.json
@@ -0,0 +1,1 @@
+{"action":"skipped_fifo_index","queue":"orders"}
diff --git a/test/golden/skipped_notify.json b/test/golden/skipped_notify.json
new file mode 100644
--- /dev/null
+++ b/test/golden/skipped_notify.json
@@ -0,0 +1,1 @@
+{"action":"skipped_notify","queue":"orders"}
diff --git a/test/golden/skipped_queue.json b/test/golden/skipped_queue.json
new file mode 100644
--- /dev/null
+++ b/test/golden/skipped_queue.json
@@ -0,0 +1,1 @@
+{"action":"skipped_queue","queue":"orders"}
diff --git a/test/golden/skipped_topic_binding.json b/test/golden/skipped_topic_binding.json
new file mode 100644
--- /dev/null
+++ b/test/golden/skipped_topic_binding.json
@@ -0,0 +1,1 @@
+{"action":"skipped_topic_binding","queue":"orders","pattern":"orders.*"}
diff --git a/test/golden/unsupported_notify_on_partitioned_queue.json b/test/golden/unsupported_notify_on_partitioned_queue.json
new file mode 100644
--- /dev/null
+++ b/test/golden/unsupported_notify_on_partitioned_queue.json
@@ -0,0 +1,1 @@
+{"action":"unsupported_notify_on_partitioned_queue","queue":"orders"}
diff --git a/test/golden/updated_notify_throttle.json b/test/golden/updated_notify_throttle.json
new file mode 100644
--- /dev/null
+++ b/test/golden/updated_notify_throttle.json
@@ -0,0 +1,1 @@
+{"action":"updated_notify_throttle","queue":"orders","observed_throttle_interval_ms":250,"declared_throttle_interval_ms":1000}
