okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
79b55b3
main
1//// Kernel image wire decoding and old journal records must survive format 2//// changes; synthetic corrupted and legacy records cannot arise in E2E. 3 4import albedo/daemon/images 5import albedo/daemon/store 6import albedo/harness/extensions/python/cells 7import albedo/harness/extensions/python/kernel as python 8import albedo/harness/extensions/python/migrations/cell_images 9import albedo/openai_api/types 10import gleam/dynamic 11import gleam/dynamic/decode 12import gleam/erlang/atom 13import gleam/json 14import gleam/list 15import gleam/option .{ None , Some } 16import gleeunit/should 17import sqlight 18 19/// A PNG signature and IHDR header for a 2x3 image: enough for the header 20/// check a stored record gets on load. 21const png = "iVBORw0KGgoAAAANSUhEUgAAAAIAAAAD" 22 23pub fn the_kernel_decoder_reads_images_at_the_boundary_test () -> Result ( 24python . Outcome , 25json . DecodeError , 26) { 27let frame = fn ( images ) { 28json . object ([ 29#( "id" , json . string ( "cell" )), 30#( "status" , json . string ( "ok" )), 31#( "output" , json . string ( "" )), 32#( "value" , json . string ( "" )), 33#( "truncated" , json . bool ( False )), 34.. images 35]) 36|> json . to_string 37} 38// A kernel from before images sends no field at all. 39let assert Ok ( plain ) = json . parse ( frame ([]), python . outcome_decoder ()) 40#( plain . images , plain . image_errors ) |> should . equal (#([], [])) 41// Whole PNGs, IEND included: the daemon refuses an image cut short. 42let small = "iVBORw0KGgoAAAANSUhEUgAAAAIAAAADCAIAAAA2iEnWAAAAAElFTkSuQmCC" 43let wide = "iVBORw0KGgoAAAANSUhEUgAAAAUAAAADCAIAAADUVFKvAAAAAElFTkSuQmCC" 44let images = 45json . array ([ small , "bm90IGFuIGltYWdl" , wide , "not base64!" ], json . string ) 46let assert Ok ( mixed ) = 47json . parse ( frame ([#( "images" , images )]), python . outcome_decoder ()) 48// Both lists keep the order the cell showed its images in. 49mixed . images 50|> list . map ( fn ( image ) { 51let #( _ , width , height , _ ) = types . image_meta ( image ) 52#( width , height ) 53}) 54|> should . equal ([#( 2 , 3 ), #( 5 , 3 )]) 55mixed . image_errors 56|> should . equal ([ 57"image 2: not a PNG, JPEG, or WebP albedo can read" , 58"image 4: not a PNG, JPEG, or WebP albedo can read" , 59]) 60// A wrong shape is a broken kernel, not an unreadable image. 61let assert Error ( _ ) = 62json . parse ( 63frame ([#( "images" , json . array ([ 1 ], json . int ))]), 64python . outcome_decoder (), 65) 66} 67 68/// The record shapes albedo stored before tool results carried images. 69type LegacyInput { 70ToolOutput ( String , String ) 71} 72 73type LegacyOutcome { 74Outcome ( String , python . Status , String , String , Bool ) 75} 76 77pub fn records_saved_before_images_still_load_test () -> Nil { 78let assert Ok ( input ) = 79unpack_input ( term_to_binary (#( 1 , ToolOutput ( "call" , "text" )))) 80input |> should . equal ( types . ToolOutput ( "call" , "text" , [])) 81let saved = Outcome ( "cell" , python . Succeeded , "out" , "'v'" , False ) 82let assert Ok ( Ok ( outcome )) = unpack_cell ( term_to_binary (#( 1 , Ok ( saved )))) 83outcome 84|> should . equal ( python . Outcome ( 85"cell" , 86python . Succeeded , 87"out" , 88"'v'" , 89False , 90[], 91[], 92None , 93)) 94} 95 96pub fn a_journaled_image_is_checked_again_on_load_test () -> Nil { 97// Journaled before cells were timed, so its duration is unknown. 98let assert Ok ( Ok ( outcome )) = unpack_cell ( journaled_png ( 2 )) 99outcome . duration |> should . equal ( None ) 100// The payload is 2 pixels wide; a record claiming 9 was altered. 101unpack_cell ( journaled_png ( 9 )) |> should . equal ( Error ( Nil )) 102} 103 104/// A journaled cell outcome carrying one PNG, built term by term. 105fn journaled_png ( width : Int ) -> BitArray { 106let atom = fn ( name ) { atom . to_dynamic ( atom . create ( name )) } 107let image = 108dynamic . array ([ 109atom ( "image" ), 110dynamic . string ( "image/png" ), 111dynamic . string ( png ), 112dynamic . int ( width ), 113dynamic . int ( 3 ), 114dynamic . int ( 24 ), 115]) 116let outcome = 117dynamic . array ([ 118atom ( "outcome" ), 119dynamic . string ( "cell" ), 120atom ( "succeeded" ), 121dynamic . string ( "" ), 122dynamic . string ( "" ), 123dynamic . bool ( False ), 124dynamic . list ([ image ]), 125dynamic . list ([]), 126]) 127term_to_binary (#( 1 , Ok ( outcome ))) 128} 129 130/// Legacy cell records are not producible by the current daemon wire format. 131pub fn old_cell_images_migrate_without_losing_their_readers_test () -> Nil { 132let path = temporary_database () 133let assert Ok ( ledger ) = store . start ( path , images . schema ) 134cell_images . run ( ledger , path <> ".missing-backup" ) |> should . equal ( Ok ( 0 )) 135let assert Ok ( _ ) = cells . initialise ( ledger ) 136let assert Ok ( _ ) = 137store . write ( 138ledger , 139"INSERT INTO cells(id,session,source,status,payload) VALUES('cell','s','show_image(...)','finished',?)" , 140[ sqlight . blob ( journaled_png ( 2 ))], 141) 142let backup = path <> ".backup" 143cell_images . run ( ledger , backup ) |> should . equal ( Ok ( 1 )) 144cell_images . run ( ledger , backup ) |> should . equal ( Ok ( 0 )) 145let assert Ok ( cell ) = cells . get ( ledger , "cell" ) 146let assert Some ( Ok ( outcome )) = cell . outcome 147let assert [ stored ] = outcome . images 148let assert types . StoredData ( read : read , ..) = types . image_data ( stored ) 149read () |> should . equal ( Ok ( png )) 150let assert Ok ([ kind ]) = 151store . read ( 152ledger , 153"SELECT typeof(data) FROM images" , 154[], 155decode . field ( 0 , decode . string , decode . success ), 156) 157kind |> should . equal ( "blob" ) 158store . close ( ledger ) 159cleanup ( path ) 160cleanup ( backup ) 161} 162 163@external ( erlang , "albedo_runtime_test_support" , "temporary_database" ) 164fn temporary_database () -> String 165 166@external ( erlang , "albedo_runtime_test_support" , "cleanup" ) 167fn cleanup ( path : String ) -> Nil 168 169@external ( erlang , "erlang" , "term_to_binary" ) 170fn term_to_binary ( value : a) -> BitArray 171 172fn unpack_input ( bytes : BitArray ) -> Result ( types . Input , Nil ) { 173unpack_with ( bytes , fn ( _ ) { Error ( Nil ) }) 174} 175 176@external ( erlang , "albedo_conversation" , "unpack" ) 177fn unpack_with ( 178bytes : BitArray , 179read : fn ( String ) -> Result ( String , Nil ), 180) -> Result ( types . Input , Nil ) 181 182@external ( erlang , "albedo_native" , "unpack_cell" ) 183fn unpack_cell ( 184bytes : BitArray , 185) -> Result ( Result ( python . Outcome , python . Error ), Nil )