From 6118d0a9c7cde23a9b408f5f3c422ee42eb6c1e3 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Sat, 11 Jul 2026 10:00:43 +0300 Subject: [PATCH] Fix the workspace shelf aliasing live state, and rec-save-as! message Two bugs found while documenting the recording features: - ws-save!/ws-save-as! shelved @workspace directly, so the stored copy shared the live workspace's :children atom. Mutating the workspace after saving (clearing the log, tracing more) corrupted the shelved copy, and a later load restored nothing. Shelve a fully-dereferenced snapshot instead - the same thing recordings already do - via a new optional `prep` hook on the shelf, and re-atomize the root on load so the restored workspace can record again. - rec-save-as! reported "id 'null' to slot 'null'": it ran (juxt :id :rec-slot) over the recording atom instead of its value. Deref first. Adds a regression test that mutates the workspace after saving and asserts the reload is unaffected (fails without the snapshot fix). --- CHANGELOG.md | 7 +++++++ src/sayid/core.clj | 1 + src/sayid/shelf.clj | 16 +++++++++------- src/sayid/workspace.clj | 14 +++++++++++--- test/sayid/workspace_test.clj | 9 +++++++++ 5 files changed, 37 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de98eae..5088fdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Change Log +## Unreleased + +### Bugs fixed + +* Fix the workspace shelf (`ws-save!`/`ws-save-as!`/`ws-load!`) storing a live reference instead of a snapshot: mutating the workspace after saving (e.g. clearing the log) corrupted the shelved copy, so a later load restored nothing. It now shelves a fully-dereferenced snapshot, like a recording does. +* Fix `rec-save-as!` reporting `id 'null' to slot 'null'` - it read the recording atom instead of its value. + ## [0.7.0] - 2026-07-10 * Editor support for the new data functionality: `sayid-tap-trace` (`C-c s d t`) taps the workspace to your data tool, and `sayid-capture-baseline` (`C-c s d b`) / `sayid-diff-traces` (`C-c s d d`) drive the snapshot-then-compare flow. Backed by new `sayid-tap-trace`, `sayid-capture-baseline` and `sayid-diff-traces` nREPL ops. diff --git a/src/sayid/core.clj b/src/sayid/core.clj index 6e8b472..bd7cda3 100644 --- a/src/sayid/core.clj +++ b/src/sayid/core.clj @@ -334,6 +334,7 @@ user> (-> #'f1 meta :source) (->> (#'rec/save-as! recording (:rec-ns @config) slot) + deref ((juxt :id :rec-slot)) (apply format "Saved recording with id '%s' to slot '%s'."))) (util/defalias r-sa! rec-save-as!) diff --git a/src/sayid/shelf.clj b/src/sayid/shelf.clj index 4f152a7..200e361 100644 --- a/src/sayid/shelf.clj +++ b/src/sayid/shelf.clj @@ -4,8 +4,11 @@ (defn save! - [item shelf slot-kw mk-ex-msg-fn] - (let [item' @item + "Shelve ITEM's value under its SLOT-KW slot in namespace SHELF. PREP, when + given, transforms the value before it's stored - the workspace shelf uses this + to store an independent snapshot rather than a live, still-mutating value." + [item shelf slot-kw mk-ex-msg-fn & [prep]] + (let [item' ((or prep identity) @item) slot (slot-kw item')] (if (symbol? slot) (sym/def-ns-var shelf slot item') @@ -14,11 +17,10 @@ item')) (defn save-as! - [item shelf slot-kw slot mk-ex-msg-fn] - (doto item - (swap! assoc slot-kw - (sym/qualify-sym shelf slot)) - (save! shelf slot-kw mk-ex-msg-fn))) + [item shelf slot-kw slot mk-ex-msg-fn & [prep]] + (swap! item assoc slot-kw (sym/qualify-sym shelf slot)) + (save! item shelf slot-kw mk-ex-msg-fn prep) + item) (defn safe-to-load? [item shelf slot-kw & [force]] diff --git a/src/sayid/workspace.clj b/src/sayid/workspace.clj index 6df8585..852128a 100644 --- a/src/sayid/workspace.clj +++ b/src/sayid/workspace.clj @@ -147,12 +147,15 @@ :children kids :arg-map forced-arg-map)))) +;; Shelve a fully-dereferenced snapshot (like a recording), so the stored copy is +;; independent of the live workspace's mutating child atoms. (defn save! [ws ws-shelf] (shelf/save! ws ws-shelf :ws-slot - #(format "Workspace must have a symbol value in :ws-slot. Value was `%s`. Try `save-as!` instead." %))) + #(format "Workspace must have a symbol value in :ws-slot. Value was `%s`. Try `save-as!` instead." %) + deep-deref!)) (defn save-as! [ws ws-shelf slot] @@ -160,7 +163,8 @@ ws-shelf :ws-slot slot - #(format "Workspace must have a symbol value in :ws-slot. Value was `%s`. Try `save-as!` instead." %))) + #(format "Workspace must have a symbol value in :ws-slot. Value was `%s`. Try `save-as!` instead." %) + deep-deref!)) ;; TODO remove traces before unloading a ws??? (defn load! @@ -170,4 +174,8 @@ :ws-slot slot "Current workspace is not saved. Use :f as last arg to force, or else `save!` first." - force)) + force) + ;; The shelved snapshot is fully realized (its children are plain vectors); + ;; re-atomize the root so the restored workspace can record new calls again. + (swap! ws update :children #(if (instance? clojure.lang.Atom %) % (atom (vec %)))) + @ws) diff --git a/test/sayid/workspace_test.clj b/test/sayid/workspace_test.clj index 311d3be..a079820 100644 --- a/test/sayid/workspace_test.clj +++ b/test/sayid/workspace_test.clj @@ -58,6 +58,15 @@ :ws-slot '$ws/test1 :arg-map nil})))) +(t/deftest save-is-an-independent-snapshot + (t/testing "mutating the workspace after saving doesn't corrupt the shelved copy" + (ws/save-as! *ws* *shelf* 'test1) + ;; append to the live workspace's call log after the save + (swap! (:children @*ws*) conj {:id :later :children (atom [])}) + (ws/load! *ws* *shelf* 'test1 :f) + (t/is (= [] (:children (ws/deep-deref! *ws*))) + "the reloaded snapshot still has the empty log it was saved with"))) + (t/deftest load-with-value (t/testing "load with value" (ws/save-as! *ws* *shelf* 'test1)