🐛 Store the fragments a library absorption changes

Absorbing a library into a file that uses it (unpublishing or deleting
the library) saved the file without its features. Saving stores the
changed pointer-map fragments only when the features name
fdata/pointer-map, so with that feature the file pointed at a fragment
of components that was never written, and loading it failed with
:fragment-not-found. The file's features now go with the save.

rpc-file-test/unpublish-library-absorbs-it-into-the-files-using-it
unpublishes a library with a copy in another file, then loads that
file. The existing absorption test turns fdata/pointer-map off, so it
missed this.

AI-assisted-by: claude-opus-5-5
This commit is contained in:
alonso.torres 2026-10-16 10:47:55 +02:00
parent 5fdf0c036f
commit 9c701da24a
2 changed files with 89 additions and 0 deletions

View File

@ -1008,6 +1008,8 @@
:library-id (str (:id ldata))
:file-id (str file-id))
;; the features say how to store the data: with fdata/pointer-map, the
;; fragments the absorption changed
(bfc/update-file! cfg {:id file-id
:features (:features file)
:migrations (:migrations file)

View File

@ -2132,6 +2132,93 @@
(t/is (= (:id file-2) (:file-id (get rows 0))))
(t/is (nil? (:deleted-at (get rows 0)))))))
(defn- create-library-copy!
"Create a shared library with a component and a file using it with a copy
of it. Returns the profile, both files and the ids involved."
[]
(let [profile (th/create-profile* 1)
params {:profile-id (:id profile)
:project-id (:default-project-id profile)}
library (th/create-file* 1 (assoc params :is-shared true))
file (th/create-file* 2 params)
c-id (uuid/random)
main-id (uuid/random)
inst-id (uuid/random)
l-page (first (get-in library [:data :pages]))
f-page (first (get-in file [:data :pages]))]
(th/link-file-to-library* {:file-id (:id file)
:library-id (:id library)})
(update-file!
:file-id (:id library)
:profile-id (:id profile)
:changes
[{:type :add-obj
:page-id l-page
:id main-id
:parent-id uuid/zero
:frame-id uuid/zero
:obj (cts/setup-shape
{:id main-id
:name "Board"
:frame-id uuid/zero
:parent-id uuid/zero
:type :frame
:main-instance true
:component-root true
:component-file (:id library)
:component-id c-id})}
{:type :add-component
:path ""
:name "Board"
:main-instance-id main-id
:main-instance-page l-page
:id c-id
:anotation nil}])
(update-file!
:file-id (:id file)
:profile-id (:id profile)
:changes
[{:type :add-obj
:page-id f-page
:id inst-id
:parent-id uuid/zero
:frame-id uuid/zero
:obj (cts/setup-shape
{:id inst-id
:name "Board"
:frame-id uuid/zero
:parent-id uuid/zero
:type :frame
:main-instance false
:component-root true
:component-file (:id library)
:component-id c-id})}])
{:profile profile :library library :file file
:c-id c-id :inst-id inst-id :f-page f-page}))
(defn- check-absorbed!
"Check that the file of `setup` holds the library component, with its copy
pointing at it."
[{:keys [profile file c-id inst-id f-page]}]
(let [out (th/command! {::th/type :get-file
::rpc/profile-id (:id profile)
:id (:id file)})]
(t/is (th/success? out))
(t/is (some? (get-in out [:result :data :components c-id])))
(t/is (= (:id file)
(get-in out [:result :data :pages-index f-page :objects inst-id :component-file])))))
(t/deftest unpublish-library-absorbs-it-into-the-files-using-it
(let [{:keys [profile library] :as setup} (create-library-copy!)]
(t/is (th/success? (th/command! {::th/type :set-file-shared
::rpc/profile-id (:id profile)
:id (:id library)
:is-shared false})))
(check-absorbed! setup)))
(t/deftest delete-shared-library-with-storage-file-data
(binding [cf/config (assoc cf/config :file-data-backend "storage")]
;; Keep this test focused on the storage-backed file-data fix: create