okami.mom/albedo

git clone https://git.nkp.pet/okami.mom/albedo

dawnimages: refuse a PNG or JPEG cut short where it is attached, so it never reaches history79b55b3

main
8.3 KiB258 linesraw
1import albedo/clock
2/// Image deduplication and legacy migration need database fixtures absent from E2E.
3import albedo/daemon/conversation
4import albedo/daemon/family
5import albedo/daemon/image
6import albedo/daemon/images
7import albedo/daemon/migrations/image_store
8import albedo/daemon/store
9import albedo/harness/runtime
10import albedo/openai_api/types
11import gleam/dynamic/decode
12import gleam/list
13import gleam/option.{None}
14import gleeunit/should
15import sqlight
16
17const png = "iVBORw0KGgoAAAANSUhEUgAAAAIAAAADCAIAAAA2iEnWAAAAAElFTkSuQmCC"
18
19@external(erlang, "albedo_runtime_test_support", "temporary_database")
20fn temporary_database() -> String
21
22@external(erlang, "albedo_runtime_test_support", "cleanup")
23fn cleanup(path: String) -> Nil
24
25@external(erlang, "albedo_image_store_test_support", "legacy_payload")
26fn legacy_payload(text: String, data: String) -> BitArray
27
28@external(erlang, "albedo_image_store_test_support", "exists")
29fn exists(path: String) -> Bool
30
31@external(erlang, "albedo_image_store_test_support", "legacy_fingerprint")
32fn old_fingerprint(source: String, text: String, data: String) -> String
33
34@external(erlang, "albedo_compaction", "fingerprint")
35fn fingerprint(value: a) -> String
36
37@external(erlang, "albedo_rolling", "legacy_fingerprint")
38fn legacy_fingerprint(value: a) -> Result(String, Nil)
39
40fn test_image() -> types.Image {
41  let assert Ok(value) = image.validate("image/png", png, 2, 3, 45)
42  value
43}
44
45fn session(id: String) -> conversation.Info {
46  conversation.Info(
47    id,
48    "new session",
49    "/tmp",
50    "provider",
51    "model",
52    types.Responses,
53    conversation.Idle,
54    None,
55    None,
56  )
57}
58
59fn ledger() -> #(String, store.Store) {
60  let path = temporary_database()
61  let assert Ok(host) = runtime.start(path)
62  let ledger = runtime.ledger(host)
63  let assert Ok(_) = conversation.initialise(ledger)
64  #(path, ledger)
65}
66
67fn count(ledger: store.Store, sql: String) -> Int {
68  let assert Ok([n]) =
69    store.query(ledger, fn(db) {
70      sqlight.query(sql, db, [], decode.field(0, decode.int, decode.success))
71    })
72  n
73}
74
75fn inputs() -> List(types.Input) {
76  [
77    types.UserImage("look", [test_image()]),
78    types.ToolOutput("call", "rendered", [test_image(), test_image()]),
79  ]
80}
81
82fn loaded(ledger: store.Store, id: String) -> List(types.Input) {
83  let assert Ok(entries) = conversation.load_entries(ledger, id)
84  list.map(entries, fn(entry) { entry.input })
85}
86
87pub fn commit_keeps_one_payload_and_rows_load_references_test() -> Nil {
88  let #(path, ledger) = ledger()
89  let assert Ok(_) = conversation.create(ledger, session("s"))
90  let assert Ok(_) =
91    conversation.commit(ledger, "s", inputs(), conversation.Idle)
92
93  count(ledger, "SELECT count(*) FROM images") |> should.equal(1)
94  count(ledger, "SELECT count(*) FROM images WHERE typeof(data)='blob'")
95  |> should.equal(1)
96  count(ledger, "SELECT length(data) FROM images") |> should.equal(45)
97  count(
98    ledger,
99    "SELECT count(*) FROM transcript WHERE instr(payload,CAST('"
100      <> png
101      <> "' AS BLOB))>0",
102  )
103  |> should.equal(0)
104
105  let assert [types.UserImage("look", [first]), types.ToolOutput(_, _, [_, _])] =
106    loaded(ledger, "s")
107  let assert types.StoredData(_, size, read) = types.image_data(first)
108  size |> should.equal(60)
109  read() |> should.equal(Ok(png))
110  types.image_meta(first) |> should.equal(#("image/png", 2, 3, 45))
111  cleanup(path)
112}
113
114pub fn deleting_a_session_releases_only_unshared_payloads_test() -> Nil {
115  let #(path, ledger) = ledger()
116  let assert Ok(_) = family.initialise(ledger)
117  let assert Ok(_) = conversation.create(ledger, session("a"))
118  let assert Ok(_) = conversation.create(ledger, session("b"))
119  let assert Ok(_) =
120    conversation.commit(ledger, "a", inputs(), conversation.Idle)
121  let assert Ok(_) =
122    conversation.commit(ledger, "b", inputs(), conversation.Idle)
123
124  let assert Ok(first) = conversation.capture(ledger, "a")
125  let assert Ok(claim) =
126    family.claim_deletion(
127      ledger,
128      family.DeletionRequest(
129        "a",
130        first.configuration.revision,
131        first.family.revision,
132        False,
133      ),
134      "delete-a",
135      clock.monotonic_ms() + 5000,
136    )
137  let assert Ok(_) = conversation.delete_claimed(ledger, "a", claim, [])
138  count(ledger, "SELECT count(*) FROM images") |> should.equal(1)
139  let assert [types.UserImage(_, [kept]), ..] = loaded(ledger, "b")
140  let assert types.StoredData(read: read, ..) = types.image_data(kept)
141  read() |> should.equal(Ok(png))
142
143  let assert Ok(second) = conversation.capture(ledger, "b")
144  let assert Ok(claim) =
145    family.claim_deletion(
146      ledger,
147      family.DeletionRequest(
148        "b",
149        second.configuration.revision,
150        second.family.revision,
151        False,
152      ),
153      "delete-b",
154      clock.monotonic_ms() + 5000,
155    )
156  let assert Ok(_) = conversation.delete_claimed(ledger, "b", claim, [])
157  count(ledger, "SELECT count(*) FROM images") |> should.equal(0)
158  cleanup(path)
159}
160
161pub fn migration_moves_legacy_payloads_once_after_a_backup_test() -> Nil {
162  let #(path, ledger) = ledger()
163  let assert Ok(_) = conversation.create(ledger, session("s"))
164  let assert Ok(_) =
165    store.query(ledger, fn(db) {
166      sqlight.query(
167        "INSERT INTO transcript(session,payload) VALUES('s',?)",
168        db,
169        [sqlight.blob(legacy_payload("old", png))],
170        decode.dynamic,
171      )
172    })
173  let assert [types.UserImage("old", [before])] = loaded(ledger, "s")
174  types.image_data(before) |> should.equal(types.InlineData(png))
175
176  let backup = path <> ".backup/before.sqlite"
177  image_store.run(ledger, backup) |> should.equal(Ok(1))
178  exists(backup) |> should.be_true
179  let assert [types.UserImage("old", [after])] = loaded(ledger, "s")
180  let assert types.StoredData(read: read, ..) = types.image_data(after)
181  read() |> should.equal(Ok(png))
182  image_store.run(ledger, backup) |> should.equal(Ok(0))
183  cleanup(path)
184}
185
186pub fn migration_converts_text_images_without_changing_references_test() -> Nil {
187  let #(path, ledger) = ledger()
188  let assert Ok(_) = conversation.create(ledger, session("s"))
189  let assert Ok(_) =
190    conversation.commit(
191      ledger,
192      "s",
193      [types.UserImage("look", [test_image()])],
194      conversation.Idle,
195    )
196  let assert [types.UserImage(_, [image])] = loaded(ledger, "s")
197  let assert types.StoredData(hash, _, _) = types.image_data(image)
198  let assert Ok(_) =
199    store.write(ledger, "UPDATE images SET data=? WHERE hash=?", [
200      sqlight.text(png),
201      sqlight.text(hash),
202    ])
203  count(ledger, "SELECT count(*) FROM images WHERE typeof(data)='text'")
204  |> should.equal(1)
205  images.reader(ledger)(hash) |> should.equal(Ok(png))
206
207  let backup = path <> ".backup/before-blob.sqlite"
208  image_store.run(ledger, backup) |> should.equal(Ok(0))
209  exists(backup) |> should.be_true
210  count(ledger, "SELECT count(*) FROM images WHERE typeof(data)='blob'")
211  |> should.equal(1)
212  count(ledger, "SELECT length(data) FROM images") |> should.equal(45)
213  let assert [types.UserImage(_, [migrated])] = loaded(ledger, "s")
214  let assert types.StoredData(migrated_hash, _, read) =
215    types.image_data(migrated)
216  migrated_hash |> should.equal(hash)
217  read() |> should.equal(Ok(png))
218  image_store.run(ledger, backup) |> should.equal(Ok(0))
219  cleanup(path)
220}
221
222pub fn failed_backup_keeps_text_images_untouched_test() -> Nil {
223  let #(path, ledger) = ledger()
224  let assert Ok(_) = conversation.create(ledger, session("s"))
225  let assert Ok(_) =
226    conversation.commit(
227      ledger,
228      "s",
229      [types.UserImage("look", [test_image()])],
230      conversation.Idle,
231    )
232  let assert Ok(_) =
233    store.write(ledger, "UPDATE images SET data=?", [sqlight.text(png)])
234  let assert Error(_) = image_store.run(ledger, path <> "/backup.sqlite")
235  count(ledger, "SELECT count(*) FROM images WHERE typeof(data)='text'")
236  |> should.equal(1)
237  cleanup(path)
238}
239
240pub fn fingerprints_ignore_where_a_payload_lives_test() -> Nil {
241  let #(path, ledger) = ledger()
242  let assert Ok(_) = conversation.create(ledger, session("s"))
243  let assert Ok(_) =
244    conversation.commit(
245      ledger,
246      "s",
247      [types.UserImage("look", [test_image()])],
248      conversation.Idle,
249    )
250  let stored = loaded(ledger, "s")
251  let inline = [types.UserImage("look", [test_image()])]
252
253  fingerprint(#("source", stored))
254  |> should.equal(fingerprint(#("source", inline)))
255  legacy_fingerprint(#("source", stored))
256  |> should.equal(Ok(old_fingerprint("source", "look", png)))
257  cleanup(path)
258}