diff --git a/.clj-kondo/config.edn b/.clj-kondo/config.edn
index 526d273b2f6db00c904839ab7967963938bef1be..a5df1d9182d589d2b84aa5bc2e6309d983eb9e9f 100644
--- a/.clj-kondo/config.edn
+++ b/.clj-kondo/config.edn
@@ -567,6 +567,7 @@
    metabase.test.data/$ids                                                                                                   hooks.metabase.test.data/$ids
    metabase.test.data/dataset                                                                                                hooks.metabase.test.data/dataset
    metabase.test.data/mbql-query                                                                                             hooks.metabase.test.data/mbql-query
+   metabase.test.data/mbql-query-no-test                                                                                     hooks.metabase.test.data/mbql-query
    metabase.test.data/run-mbql-query                                                                                         hooks.metabase.test.data/mbql-query
    metabase.test.util.async/with-open-channels                                                                               hooks.common/let-with-optional-value-for-last-binding
    metabase.test.util.log/with-log-level                                                                                     hooks.common/with-ignored-first-arg
@@ -580,6 +581,7 @@
    metabase.test/dataset                                                                                                     hooks.metabase.test.data/dataset
    metabase.test/discard-setting-changes                                                                                     hooks.common/with-ignored-first-arg
    metabase.test/mbql-query                                                                                                  hooks.metabase.test.data/mbql-query
+   metabase.test/mbql-query-no-test                                                                                          hooks.metabase.test.data/mbql-query
    metabase.test/query                                                                                                       hooks.metabase.test.data/mbql-query
    metabase.test/test-drivers                                                                                                hooks.common/do*
    metabase.test/run-mbql-query                                                                                              hooks.metabase.test.data/mbql-query
diff --git a/src/metabase/lib/convert.cljc b/src/metabase/lib/convert.cljc
index 064f867450b15ae49380556a1823481d2db2f8b1..d92cfa8c7bb2d8e6cdc5679a2f26b6bf215f98b9 100644
--- a/src/metabase/lib/convert.cljc
+++ b/src/metabase/lib/convert.cljc
@@ -1,6 +1,7 @@
 (ns metabase.lib.convert
   (:require
    [clojure.set :as set]
+   [medley.core :as m]
    [metabase.lib.dispatch :as lib.dispatch]
    [metabase.lib.options :as lib.options]
    [metabase.lib.util :as lib.util]))
@@ -25,6 +26,9 @@
   [query]
   query)
 
+(def ^:private stage-keys
+  [:aggregation :breakout :expressions :fields :filter :order-by :joins])
+
 (defmethod ->pMBQL :mbql.stage/mbql
   [stage]
   (reduce
@@ -33,7 +37,7 @@
        stage
        (update stage k ->pMBQL)))
    stage
-   [:aggregation :breakout :expressions :fields :filter :order-by :joins]))
+   stage-keys))
 
 (defmethod ->pMBQL :mbql/join
   [join]
@@ -83,9 +87,21 @@
   {:arglists '([x])}
   lib.dispatch/dispatch-value)
 
+(defn- lib-key? [x]
+  (and (qualified-keyword? x)
+       (= (namespace x) "lib")))
+
 (defn- disqualify [x]
-  #?(:cljs (when (number? x) (js-debugger)))
-  (select-keys x (remove qualified-keyword? (keys x))))
+  (->> x
+       keys
+       (remove lib-key?)
+       (select-keys x)))
+
+(defn- aggregation->legacy-MBQL [[tag options & args]]
+  (let [inner (into [tag] (map ->legacy-MBQL args))]
+    (if-let [aggregation-opts (not-empty (disqualify options))]
+      [:aggregation-options inner aggregation-opts]
+      inner)))
 
 (defn- clause-with-options->legacy-MBQL [[k options & args]]
   (if (map? options)
@@ -96,22 +112,47 @@
 
 (defmethod ->legacy-MBQL :default
   [x]
-  (if (and (vector? x)
-           (keyword? (first x)))
-    (clause-with-options->legacy-MBQL x)
-    (do
-      #?(:cljs (when-not (or (nil? x) (string? x) (number? x) (boolean? x) (keyword? x))
-                 (js/console.log "undefined ->legacy-MBQL for" (lib.dispatch/dispatch-value x) x)
-                 (throw (ex-info "undefined ->legacy-MBQL" {:dispatch-value (lib.dispatch/dispatch-value x)
-                                                            :value x}))))
-      x)))
-
-(defn- chain-stages [x]
-  (let [stages (map ->legacy-MBQL (:stages x))]
-    (reduce (fn [inner stage]
-              (assoc stage :source-query inner))
-            (first stages)
-            (rest stages))))
+  (cond
+    (and (vector? x)
+         (keyword? (first x))) (clause-with-options->legacy-MBQL x)
+    (map? x)                   (-> x
+                                   disqualify
+                                   (update-vals ->legacy-MBQL))
+    :else x))
+
+(doseq [clause [;; Aggregations
+                :count :avg :count-where :distinct
+                :max :median :min :percentile
+                :share :stddev :sum :sum-where
+
+                ;; Expressions
+                :+ :- :* :/
+                :case :coalesce
+                :abs :log :exp :sqrt :ceil :floor :round :power :interval
+                :relative-datetime :time :absolute-datetime :now :convert-timezone
+                :get-week :get-year :get-month :get-day :get-hour
+                :get-minute :get-second :get-quarter
+                :datetime-add :datetime-subtract
+                :concat :substring :replace :regexextract :length
+                :trim :ltrim :rtrim :upper :lower]]
+  (defmethod ->legacy-MBQL clause [input]
+    (aggregation->legacy-MBQL input)))
+
+(defn- chain-stages [{:keys [stages]}]
+  ;; :source-metadata aka :lib/stage-metadata is handled differently in the two formats.
+  ;; In legacy, an inner query might have both :source-query, and :source-metadata giving the metadata for that nested
+  ;; :source-query.
+  ;; In pMBQL, the :lib/stage-metadata is attached to the same stage it applies to.
+  ;; So when chaining pMBQL stages back into legacy form, if stage n has :lib/stage-metadata, stage n+1 needs
+  ;; :source-metadata attached.
+  (first (reduce (fn [[inner stage-metadata] stage]
+                   [(cond-> (->legacy-MBQL stage)
+                      inner          (assoc :source-query inner)
+                      stage-metadata (assoc :source-metadata (mapv ->legacy-MBQL (:columns stage-metadata))))
+                    ;; Get the :lib/stage-metadata off the original pMBQL stage, not the converted one.
+                    (:lib/stage-metadata stage)])
+                 nil
+                 stages)))
 
 (defmethod ->legacy-MBQL :dispatch-type/map [m]
   (-> m
@@ -139,37 +180,25 @@
            (chain-stages base))))
 
 (defmethod ->legacy-MBQL :mbql.stage/mbql [stage]
-  (-> stage
-      disqualify
-      (update-vals ->legacy-MBQL)))
+  (reduce #(m/update-existing %1 %2 ->legacy-MBQL)
+          (-> stage
+              disqualify
+              (m/update-existing :aggregation #(mapv aggregation->legacy-MBQL %)))
+          (remove #{:aggregation} stage-keys)))
 
 (defmethod ->legacy-MBQL :mbql.stage/native [stage]
   (-> stage
       disqualify
-      (set/rename-keys {:native :query})
       (update-vals ->legacy-MBQL)))
 
 (defmethod ->legacy-MBQL :mbql/query [query]
   (let [base        (disqualify query)
         inner-query (chain-stages base)
-        query-type  (if (-> query :stages first :lib/type (= :mbql.stage/native))
+        query-type  (if (-> query :stages last :lib/type (= :mbql.stage/native))
                       :native
-                      :query)
-        result
-        (merge (-> base
-                   (dissoc :stages)
-                   (update-vals ->legacy-MBQL))
-               {:type      query-type
-                query-type inner-query})]
-    #?(:cljs (js/console.log "->legacy-MBQL on query" query result))
-    result))
-
-;;; placeholder, feel free to delete @braden.
-(defmethod ->legacy-MBQL :count
-  [[_tag opts field]]
-  (let [clause (if field
-                 [:count (->legacy-MBQL field)]
-                 [:count])]
-    (if-let [aggregation-options-opts (not-empty (select-keys opts [:name :display-name]))]
-      [:aggregation-options clause aggregation-options-opts]
-      clause)))
+                      :query)]
+    (merge (-> base
+               (dissoc :stages)
+               (update-vals ->legacy-MBQL))
+           {:type      query-type
+            query-type inner-query})))
diff --git a/src/metabase/lib/js.cljs b/src/metabase/lib/js.cljs
index 6eea37e4fc2d92401c2e2e69de0e2adae658569f..3598fd6689f47d4e6ccd6f5c915ed224664e4626 100644
--- a/src/metabase/lib/js.cljs
+++ b/src/metabase/lib/js.cljs
@@ -85,12 +85,10 @@
   As an example of such a value, `(get-in card [:template-tags \"some-tag\" :widget-type])` can be `:date/all-options`."
   [x]
   (cond
-    (keyword? x)    (if-let [ns-part (namespace x)]
-                      (str ns-part "/" (name x))
-                      (name x))
-    (map? x)        (update-vals x fix-namespaced-values)
-    (sequential? x) (map fix-namespaced-values x)
-    :else           x))
+    (qualified-keyword? x)    (str (namespace x) "/" (name x))
+    (map? x)                  (update-vals x fix-namespaced-values)
+    (sequential? x)           (map fix-namespaced-values x)
+    :else                     x))
 
 (defn ^:export legacy-query
   "Coerce a CLJS pMBQL query back to (1) a legacy query (2) in vanilla JS."
diff --git a/test/metabase/api/dashboard_test.clj b/test/metabase/api/dashboard_test.clj
index 248818f1db9c67e8586355729c186ceed34a4cbf..a75e36252c953e0c1e40d0c83076c022977f78e9 100644
--- a/test/metabase/api/dashboard_test.clj
+++ b/test/metabase/api/dashboard_test.clj
@@ -2347,9 +2347,10 @@
       ;; card with agggregation and binning columns
       [Card [{mbql-card-id :id}
              (merge (mt/card-with-source-metadata-for-query
-                      (mt/mbql-query venues {:limit 5
-                                             :aggregation [:count]
-                                             :breakout [[:field %latitude {:binning {:strategy :num-bins :num-bins 10}}]]}))
+                      (mt/mbql-query-no-test venues
+                        {:limit 5
+                         :aggregation [:count]
+                         :breakout [[:field %latitude {:binning {:strategy :num-bins :num-bins 10}}]]}))
                     {:name        "MBQL question"
                      :database_id (mt/id)
                      :table_id    (mt/id :venues)})]
diff --git a/test/metabase/lib/convert_test.cljc b/test/metabase/lib/convert_test.cljc
index 281e9d4e1b211697cadbe20329c662d1cec3fcac..1671e91b7ed3b64ba15a9b671e4ae50280017ac4 100644
--- a/test/metabase/lib/convert_test.cljc
+++ b/test/metabase/lib/convert_test.cljc
@@ -107,3 +107,38 @@
                                         :aggregation  [[:aggregation-options
                                                         [:sum [:field 1 nil]]
                                                         {:display-name "Revenue"}]]}}))))
+
+(deftest ^:parallel round-trip-test
+  ;; Miscellaneous queries that have caused test failures in the past, captured here for quick feedback.
+  (are [query] (= query (-> query lib.convert/->pMBQL lib.convert/->legacy-MBQL))
+       ;; :aggregation-options on a non-aggregate expression with an inner aggregate.
+       {:database 194
+        :query {:aggregation [[:aggregation-options
+                               [:- [:sum [:field 1677 nil]] 41]
+                               {:name "Sum-41"}]]
+                :breakout [[:field 1677 nil]]
+                :source-table 517}
+        :type :query}
+
+       ;; :aggregation-options nested, not at the top level under :aggregation
+       {:database 194
+        :query {:aggregation [[:- [:aggregation-options
+                                   [:sum [:field 1677 nil]]
+                                   {:name "Sum-41"}] 41]]
+                :breakout [[:field 1677 nil]]
+                :source-table 517}
+        :type :query}
+
+       {:database 67
+        :query {:aggregation [[:aggregation-options
+                               [:avg
+                                [:field
+                                 809
+                                 {:metabase.query-processor.util.add-alias-info/source-alias "RATING"
+                                  :metabase.query-processor.util.add-alias-info/source-table 224}]]
+                               {:name "avg"
+                                :metabase.query-processor.util.add-alias-info/desired-alias "avg"
+                                :metabase.query-processor.util.add-alias-info/position 1
+                                :metabase.query-processor.util.add-alias-info/source-alias "avg"}]]
+                :source-table 224}
+        :type :query}))
diff --git a/test/metabase/query_processor_test/expressions_test.clj b/test/metabase/query_processor_test/expressions_test.clj
index 0071a90db61ee14769a1c15f2c0dede1c5e8f65a..5ff4f4c2901e05382be91fd260cb7fd4b8f9635b 100644
--- a/test/metabase/query_processor_test/expressions_test.clj
+++ b/test/metabase/query_processor_test/expressions_test.clj
@@ -408,7 +408,7 @@
   (mt/test-drivers (mt/normal-drivers-with-feature :expressions)
     (testing "Can we use expression with same column name as table (#14267)"
       (mt/dataset sample-dataset
-        (let [query (mt/mbql-query products
+        (let [query (mt/mbql-query-no-test products
                       {:expressions {:CATEGORY [:concat $category "2"]}
                        :breakout    [:expression :CATEGORY]
                        :aggregation [:count]
diff --git a/test/metabase/test.clj b/test/metabase/test.clj
index 778778679f3ba68b3f386c7293162aeadbf4cf68..395fa102fcd39c6c390b839687adba31809bac54 100644
--- a/test/metabase/test.clj
+++ b/test/metabase/test.clj
@@ -107,6 +107,7 @@
   format-name
   id
   mbql-query
+  mbql-query-no-test
   native-query
   query
   run-mbql-query
diff --git a/test/metabase/test/data.clj b/test/metabase/test/data.clj
index b78e6f306ba366bb10e756d5d6601bece6d20f1c..294f54381d0361202a020acd909f0d2e1972ab42 100644
--- a/test/metabase/test/data.clj
+++ b/test/metabase/test/data.clj
@@ -40,6 +40,8 @@
    [metabase.db :as mdb]
    [metabase.db.schema-migrations-test.impl :as schema-migrations-test.impl]
    [metabase.driver.ddl.interface :as ddl.i]
+   [metabase.lib.convert :as lib.convert]
+   [metabase.mbql.normalize :refer [normalize]]
    [metabase.models.permissions-group :as perms-group]
    [metabase.query-processor :as qp]
    [metabase.test.data.impl :as data.impl]
@@ -118,6 +120,22 @@
   ([table-name & body]
    (mbql-query-impl/parse-tokens table-name `(do ~@body))))
 
+(defmacro mbql-query-no-test
+  "Macro for easily building MBQL queries for test purposes.
+
+  See [[mbql-query]] for the interface."
+  {:style/indent 1}
+  ([table-name]
+   `(mbql-query-no-test ~table-name {}))
+
+  ([table-name inner-query]
+   {:pre [(map? inner-query)]}
+   (as-> inner-query <>
+     (mbql-query-impl/parse-tokens table-name <>)
+     (mbql-query-impl/maybe-add-source-table <> table-name)
+     (mbql-query-impl/wrap-inner-query <>)
+     (vary-meta <> assoc :type :mbql))))
+
 (defmacro mbql-query
   "Macro for easily building MBQL queries for test purposes.
 
@@ -141,11 +159,10 @@
 
   ([table-name inner-query]
    {:pre [(map? inner-query)]}
-   (as-> inner-query <>
-     (mbql-query-impl/parse-tokens table-name <>)
-     (mbql-query-impl/maybe-add-source-table <> table-name)
-     (mbql-query-impl/wrap-inner-query <>)
-     (vary-meta <> assoc :type :mbql))))
+   `(let [query# (mbql-query-no-test ~table-name ~inner-query)]
+      (t/is (= (normalize query#) (-> query# normalize lib.convert/->pMBQL lib.convert/->legacy-MBQL normalize))
+            "Legacy MBQL queries should round trip to pMBQL and back")
+      query#)))
 
 (defmacro query
   "Like `mbql-query`, but operates on an entire 'outer' query rather than the 'inner' MBQL query. Like `mbql-query`,
@@ -184,7 +201,7 @@
   "Like `mbql-query`, but runs the query as well."
   {:style/indent 1}
   [table-name & [query]]
-  `(run-mbql-query* (mbql-query ~table-name ~(or query {}))))
+  `(run-mbql-query* (mbql-query-no-test ~table-name ~(or query {}))))
 
 (defn format-name
   "Format a SQL schema, table, or field identifier in the correct way for the current database by calling the current