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