okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
61c43ca
main
1/// Legacy schema migrations preserve titles and event timestamps; E2E uses current schemas. 2import albedo/daemon/conversation 3import albedo/daemon/store 4import albedo/daemon/transcript 5import albedo/harness/runtime 6import albedo/openai_api/types 7import gleam/list 8import gleam/option .{ None , Some } 9import gleeunit/should 10import sqlight 11 12@external ( erlang , "albedo_runtime_test_support" , "temporary_database" ) 13fn temporary_database () -> String 14 15@external ( erlang , "albedo_runtime_test_support" , "cleanup" ) 16fn cleanup ( path : String ) -> Nil 17 18fn session ( id : String ) -> conversation . Info { 19conversation . Info ( 20id , 21"new session" , 22"/tmp" , 23"provider" , 24"model" , 25types . Responses , 26conversation . Idle , 27None , 28None , 29) 30} 31 32pub fn latest_user_title_survives_restart_and_legacy_migration_test () -> Nil { 33let path = temporary_database () 34let assert Ok ( host ) = runtime . start ( path ) 35let ledger = runtime . ledger ( host ) 36let assert Ok ( _ ) = conversation . initialise ( ledger ) 37let assert Ok ( _ ) = conversation . create ( ledger , session ( "session" )) 38let assert Ok ( _ ) = 39conversation . commit ( 40ledger , 41"session" , 42[ types . User ( "earlier title" ), types . Assistant ( "not the title" )], 43conversation . Model , 44) 45let assert Ok ( _ ) = 46conversation . commit ( 47ledger , 48"session" , 49[ types . User ( " latest\naccepted 👩💻 " )], 50conversation . Model , 51) 52let assert Ok ( _ ) = 53conversation . commit ( 54ledger , 55"session" , 56[ 57types . Assistant ( "must not replace it" ), 58types . ToolOutput ( "call" , "output" , []), 59], 60conversation . Idle , 61) 62let assert Ok ([ before ]) = conversation . list ( ledger ) 63before . title |> should . equal ( "latest accepted 👩💻" ) 64 65// Recreate the old schema shape; startup performs this transcript scan once. 66let assert Ok ( _ ) = 67store . query ( ledger , fn ( db ) { 68sqlight . exec ( 69"ALTER TABLE sessions DROP COLUMN title; ALTER TABLE sessions DROP COLUMN last_assistant_at" , 70db , 71) 72}) 73runtime . stop ( host ) 74 75let assert Ok ( restarted ) = runtime . start ( path ) 76let ledger = runtime . ledger ( restarted ) 77let assert Ok ( _ ) = conversation . initialise ( ledger ) 78let assert Ok ([ after ]) = conversation . list ( ledger ) 79after . title |> should . equal ( "latest accepted 👩💻" ) 80after . last_assistant_at |> should . equal ( None ) 81runtime . stop ( restarted ) 82cleanup ( path ) 83} 84 85pub fn migration_recovers_placeholder_titles_and_activity_order_test () -> Nil { 86let path = temporary_database () 87let assert Ok ( host ) = runtime . start ( path ) 88let ledger = runtime . ledger ( host ) 89let assert Ok ( _ ) = conversation . initialise ( ledger ) 90let assert Ok ( _ ) = conversation . create ( ledger , session ( "first" )) 91let assert Ok ( _ ) = conversation . create ( ledger , session ( "second" )) 92let assert Ok ( _ ) = 93conversation . commit ( 94ledger , 95"second" , 96[ types . User ( "older prompt" )], 97conversation . Idle , 98) 99let assert Ok ( _ ) = 100conversation . commit ( 101ledger , 102"first" , 103[ types . User ( "newest prompt" )], 104conversation . Idle , 105) 106let assert Ok ( _ ) = 107store . query ( ledger , fn ( db ) { 108sqlight . exec ( 109"UPDATE sessions SET title='new session',activity_seq=NULL" , 110db , 111) 112}) 113 114// The null activity marker makes this a one-time legacy backfill. It also 115// distinguishes a placeholder title from a real current-session title. 116let assert Ok ( _ ) = conversation . initialise ( ledger ) 117let assert Ok ([ first , second ]) = conversation . list ( ledger ) 118first . id |> should . equal ( "first" ) 119first . title |> should . equal ( "newest prompt" ) 120second . id |> should . equal ( "second" ) 121second . title |> should . equal ( "older prompt" ) 122runtime . stop ( host ) 123cleanup ( path ) 124} 125 126pub fn transcript_timestamps_migrate_without_invention_and_roundtrip_test () -> Nil { 127let path = temporary_database () 128let assert Ok ( host ) = runtime . start ( path ) 129let ledger = runtime . ledger ( host ) 130let assert Ok ( _ ) = conversation . initialise ( ledger ) 131let assert Ok ( _ ) = conversation . create ( ledger , session ( "timestamped" )) 132let assert Ok ( _ ) = 133conversation . commit ( 134ledger , 135"timestamped" , 136[ types . User ( "legacy" )], 137conversation . Idle , 138) 139 140// Recreate the legacy transcript shape. Migration adds a nullable column and 141// must not claim to know when this row was accepted. 142let assert Ok ( _ ) = 143store . query ( ledger , fn ( db ) { 144sqlight . exec ( 145"ALTER TABLE transcript DROP COLUMN timestamp; ALTER TABLE transcript DROP COLUMN provider; ALTER TABLE transcript DROP COLUMN thought_ms" , 146db , 147) 148}) 149let assert Ok ( _ ) = conversation . initialise ( ledger ) 150conversation . load_entries ( ledger , "timestamped" ) 151|> should . equal ( 152Ok ([ 153transcript . Entry ( 154types . User ( "legacy" ), 155None , 156None , 157None , 158Some ( transcript . SourceRef ( "timestamped" , 1 )), 159), 160]), 161) 162 163let new_inputs = [ types . User ( "current" ), types . Assistant ( "answer" )] 164let assert Ok ( timestamp ) = 165conversation . commit ( ledger , "timestamped" , new_inputs , conversation . Idle ) 166let assert True = timestamp > 1_000_000_000_000 167let assert Ok ( entries ) = conversation . load_entries ( ledger , "timestamped" ) 168entries 169|> should . equal ([ 170transcript . Entry ( 171types . User ( "legacy" ), 172None , 173None , 174None , 175Some ( transcript . SourceRef ( "timestamped" , 1 )), 176), 177transcript . Entry ( 178types . User ( "current" ), 179Some ( timestamp ), 180None , 181None , 182Some ( transcript . SourceRef ( "timestamped" , 2 )), 183), 184transcript . Entry ( 185types . Assistant ( "answer" ), 186Some ( timestamp ), 187None , 188None , 189Some ( transcript . SourceRef ( "timestamped" , 3 )), 190), 191]) 192conversation . load ( ledger , "timestamped" ) 193|> should . equal ( Ok ([ types . User ( "legacy" ), .. new_inputs ])) 194 195let assert Ok ( snapshot ) = conversation . snapshot ( ledger , "timestamped" ) 196let range = conversation . Range ( snapshot , conversation . After ( 0 ), 100 , 0 ) 197let assert Ok ( page ) = conversation . read_range ( ledger , range ) 198list . map ( page . entries , fn ( sourced ) { sourced . entry }) 199|> should . equal ( entries ) 200page . has_more |> should . be_false 201 202runtime . stop ( host ) 203let assert Ok ( restarted ) = runtime . start ( path ) 204let ledger = runtime . ledger ( restarted ) 205let assert Ok ( _ ) = conversation . initialise ( ledger ) 206conversation . load_entries ( ledger , "timestamped" ) |> should . equal ( Ok ( entries )) 207conversation . read_range ( ledger , range ) |> should . equal ( Ok ( page )) 208runtime . stop ( restarted ) 209cleanup ( path ) 210}