Skip to content

Commit 590174d

Browse files
fix(parse): surface :repl/exception! as structured :repl-exception failure
Closes #17. parse-cljs-eval-response was passing shadow's :repl/exception! sentinel through as a value, producing {:ok? true :value :repl/exception! :rfp.wire/value-fits? true} — the same false-trust-signal class as #6, one layer down (#6 was :value nil; this is :value :<sentinel>). A caller that trusts :ok? true would treat the sentinel as a value and reason from it. The sentinel fires when shadow-cljs's printer throws while serialising the eval result. Most common trigger is forms touching re-frame.core/dispatch or subscribe — re-frame internal types / reagent reactions throw inside pr-str. The bug bites the eval-cljs escape-hatch op specifically, forcing users into the dispatch.sh-only workflow and losing programmatic re-frame interaction. Fix: in parse-cljs-eval-response, when the parsed result equals :repl/exception!, throw structured ex-info {:reason :repl-exception :raw-response <res> :hint "..."}. eval-op's existing try/catch surfaces it as {:ok? false :reason :repl-exception ...}. Hint names workarounds — but only ones that actually work, per the issue's own evidence (the (do ... :ok) wrap was tested in the issue report and STILL surfaced :repl/exception!, so we don't suggest it): - For dispatch flows: scripts/dispatch.sh (routes around the eval channel via the runtime's wire layer). - For subscribe reads: deref the underlying ratom rather than the subscribe ratom directly. - Notes the more robust wire/return! layer fix as a follow-up. Adds a :repl-exception row to docs/skill/troubleshooting.md so agents can translate the new reason. 2 new deftests cover the sentinel detection and the negative case (a legitimate :ok keyword result still passes through, no regression). Out of scope: the deeper wire/return! fix that would pre-stringify re-frame internal types so shadow's printer never sees them. The issue itself flags this as the more robust direction; a separate focused issue would track it. The parse-side detection is the trust-signal fix that unblocks the eval-cljs escape hatch from silently lying about success today. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent d7650a4 commit 590174d

3 files changed

Lines changed: 52 additions & 1 deletion

File tree

docs/skill/troubleshooting.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Every script returns structured edn like `{:ok? false :reason ...}`. Translate t
1616
| `:timed-out? true` | The dispatch cascade did not settle. The tab may be backgrounded, debugger paused, or async chain continuing. |
1717
| `:connection :lost` | Reconnect with `scripts/discover-app.sh`. |
1818
| `:bad-arg` | A positional or flag arg failed to parse. The response carries `:got` (the offending input) and `:hint` (correct usage). Common cause: passing a flag where a positional integer is expected — e.g. `trace-recent.sh --limit 25` (the script takes `<window-ms>` as its only positional). |
19+
| `:repl-exception` | shadow-cljs's printer threw while serialising the eval result (sentinel `:repl/exception!`). Most common cause: `eval-cljs.sh` of a form that touches `re-frame.core/dispatch` or `subscribe` — re-frame internal types / reagent reactions throw inside `pr-str`. Workaround: route dispatches through `scripts/dispatch.sh` instead; for subscribe reads, deref the underlying ratom rather than the subscribe ratom directly. |
1920

2021
## Multi-Build Setups
2122

scripts/ops.clj

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -548,7 +548,22 @@
548548
nil
549549

550550
:else
551-
(safe-edn (peek (:results outer)))))
551+
(let [parsed (safe-edn (peek (:results outer)))]
552+
;; #17 — :repl/exception! is shadow's marker for a
553+
;; printer throw on the eval result. Most commonly:
554+
;; forms that touch re-frame.core/dispatch or subscribe
555+
;; return reagent reactions / re-frame internal types
556+
;; whose pr-str throws while shadow is serialising for
557+
;; the wire. Old behaviour passed the sentinel through
558+
;; as `{:value :repl/exception! :ok? true}` — a false
559+
;; trust signal of the same class as #6. Surface as a
560+
;; structured runtime exception instead.
561+
(if (= :repl/exception! parsed)
562+
(throw (ex-info "shadow returned :repl/exception! — eval result couldn't be serialised"
563+
{:reason :repl-exception
564+
:raw-response res
565+
:hint "shadow-cljs's printer threw while serialising the eval result. Most common cause: forms that touch re-frame.core/dispatch or subscribe — re-frame's internal types / reagent reactions throw inside pr-str. Workarounds: for dispatch flows use scripts/dispatch.sh (routes around the eval channel via the runtime's wire layer); for subscribe reads, pull the deref'd value out yourself before it crosses the wire (e.g. `(deref @some-ratom)` rather than the subscribe ratom directly). A more robust fix at the wire/return! layer is tracked separately."}))
566+
parsed))))
552567

553568
;; Newer shadow may return {:err "..."} on cljs errors.
554569
(and (map? outer) (:err outer))

tests/ops_smoke.bb

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1260,6 +1260,41 @@
12601260
(is (str/includes? (:hint @emitted) "register-epoch-cb")
12611261
":hint mentions the native cb as an alternative to 10x")))))
12621262

1263+
;; ---------------------------------------------------------------------------
1264+
;; Tests for issue #17: parse-cljs-eval-response must surface
1265+
;; :repl/exception! as a structured runtime exception, not pass it
1266+
;; through as a value with :ok? true (the false-trust pattern).
1267+
;; ---------------------------------------------------------------------------
1268+
1269+
(deftest parse-surfaces-repl-exception-sentinel
1270+
(testing "shadow's :repl/exception! sentinel inside :results becomes :reason :repl-exception"
1271+
(let [res {:value (pr-str
1272+
{:results [(pr-str :repl/exception!)]
1273+
:out ""
1274+
:err ""
1275+
:ns 'cljs.user})}
1276+
thrown (try (#'ops/parse-cljs-eval-response res)
1277+
(catch clojure.lang.ExceptionInfo e
1278+
{:msg (.getMessage e) :data (ex-data e)}))]
1279+
(is (= :repl-exception (-> thrown :data :reason))
1280+
":repl-exception is the structured failure")
1281+
(is (contains? (:data thrown) :raw-response)
1282+
":raw-response carries shadow's response for debugging")
1283+
(is (str/includes? (-> thrown :data :hint) "dispatch.sh")
1284+
":hint points at the dispatch.sh workaround")
1285+
(is (str/includes? (-> thrown :data :hint) "wire/return!")
1286+
":hint mentions the more robust wire-layer fix as a follow-up"))))
1287+
1288+
(deftest parse-still-returns-real-keyword-results
1289+
(testing "no regression: a legitimate keyword result (NOT :repl/exception!) passes through"
1290+
(let [res {:value (pr-str
1291+
{:results [(pr-str :ok)]
1292+
:out ""
1293+
:err ""
1294+
:ns 'cljs.user})}]
1295+
(is (= :ok (#'ops/parse-cljs-eval-response res))
1296+
"ordinary keyword results are unaffected by the sentinel detection"))))
1297+
12631298
;; ---------------------------------------------------------------------------
12641299
;; Tests for issue #18: tail-build's --probe must detect every
12651300
;; error↔value transition as a flip, not just value→value.

0 commit comments

Comments
 (0)