okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
6e82811
main
1"""Admission and consumption failures leave recoverable, atomic identified decisions.""" 2 3import base64 4import json 5import random 6import struct 7import zlib 8import sqlite3 9import time 10import urllib .error 11 12from harness import exclusive ,operation_id 13from image_limits_test import png 14from operations_test import InputScenario 15 16SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000 17 18 19class OperationLifecycleTests (InputScenario ): 20def database (self ): 21return sqlite3 .connect (self .app .home / "albedo.sqlite" ,timeout = 10 ) 22 23def create (self ,session_id ,intent ): 24with self .app .api ( 25f"/sessions/ { session_id } " , 26intent , 27method = "PUT" , 28headers = {"If-None-Match" :"*" }, 29 )as response : 30return json .load (response ) 31 32@exclusive 33def test_creation_failure_rolls_back_session_and_receipt (self ): 34session_id = operation_id () 35intent = { 36"kind" :"new" , 37"workspace" :str (self .app .workspace ), 38"provider_profile" :self .app .profile , 39 } 40with self .database ()as database : 41before = database .execute ("SELECT count(*) FROM sessions" ).fetchone ()[0 ] 42database .execute ( 43"CREATE TRIGGER fail_creation BEFORE INSERT ON sessions BEGIN SELECT RAISE(ABORT,'fixture'); END" 44 ) 45with self .assertRaises (urllib .error .HTTPError ): 46self .create (session_id ,intent ) 47with self .database ()as database : 48self .assertEqual ( 49database .execute ("SELECT count(*) FROM sessions" ).fetchone ()[0 ],before 50 ) 51self .assertEqual ( 52database .execute ( 53"SELECT count(*) FROM operations WHERE id=?" , (session_id ,) 54 ).fetchone ()[0 ], 550 , 56 ) 57database .execute ("DROP TRIGGER fail_creation" ) 58created = self .create (session_id ,intent ) 59self .assertEqual (created ["id" ],session_id ) 60with self .app .api (f"/sessions/ { session_id } " )as response : 61self .assertEqual (json .load (response )["creation" ],created ["creation" ]) 62 63@exclusive 64def test_admission_failure_can_retry_original_id (self ): 65session = self .app .session () 66input_id = operation_id () 67intent = {"kind" :"message" ,"text" :"durable input" } 68with self .database ()as database : 69database .execute ( 70"CREATE TRIGGER fail_admission BEFORE INSERT ON pending_inputs BEGIN SELECT RAISE(ABORT,'fixture'); END" 71 ) 72with self .assertRaises (urllib .error .HTTPError ): 73self .put_input (session ,input_id ,intent ) 74with self .database ()as database : 75self .assertEqual ( 76database .execute ( 77"SELECT count(*) FROM operations WHERE id=?" , (input_id ,) 78 ).fetchone ()[0 ], 790 , 80 ) 81self .assertEqual ( 82database .execute ( 83"SELECT count(*) FROM transcript WHERE session=?" , (session ,) 84 ).fetchone ()[0 ], 850 , 86 ) 87database .execute ("DROP TRIGGER fail_admission" ) 88accepted = self .put_input (session ,input_id ,intent ) 89self .committed (session ,input_id ) 90self .app .idle (session ) 91self .assertEqual ( 92self .put_input (session ,input_id ,intent )["acceptance_order" ], 93accepted ["acceptance_order" ], 94 ) 95self .assertEqual ( 96 [entry ["input_id" ]for entry in self .users (session )], [input_id ] 97 ) 98 99@exclusive 100def test_consumption_failure_keeps_pending_input_until_storage_recovers (self ): 101session = self .app .session () 102input_id = operation_id () 103intent = { 104"kind" :"message" , 105"text" :"recover after storage repair" , 106"images" : [ 107 { 108"mime_type" :"image/png" , 109"data" :base64 .b64encode (png (2 ,2 )).decode (), 110 } 111 ], 112 } 113with self .database ()as database : 114database .execute ( 115"CREATE TRIGGER fail_consumption BEFORE INSERT ON submission_events BEGIN SELECT RAISE(ABORT,'fixture'); END" 116 ) 117accepted = self .put_input (session ,input_id ,intent ) 118self .assertEqual (accepted ["admission" ],"accepted" ) 119self .assertEqual (self .receipt (session ,input_id )["delivery" ],"pending" ) 120with self .database ()as database : 121self .assertEqual ( 122database .execute ( 123"SELECT count(*) FROM pending_inputs WHERE operation_id=?" , 124 (input_id ,), 125 ).fetchone ()[0 ], 1261 , 127 ) 128self .assertEqual ( 129database .execute ( 130"SELECT count(*) FROM transcript WHERE session=?" , (session ,) 131 ).fetchone ()[0 ], 1320 , 133 ) 134self .assertEqual ( 135database .execute ("SELECT count(*) FROM images" ).fetchone ()[0 ],0 136 ) 137self .assertEqual ( 138database .execute ("SELECT count(*) FROM submission_events" ).fetchone ()[ 1390 140 ], 1410 , 142 ) 143self .app .restart (crash = True ) 144self .assertEqual ( 145self .put_input (session ,input_id ,intent )["acceptance_order" ], 146accepted ["acceptance_order" ], 147 ) 148self .assertEqual (self .receipt (session ,input_id )["delivery" ],"pending" ) 149with self .database ()as database : 150database .execute ("DROP TRIGGER fail_consumption" ) 151self .committed (session ,input_id ) 152self .app .idle (session ) 153self .app .restart (crash = True ) 154self .assertEqual (self .receipt (session ,input_id )["delivery" ],"committed" ) 155self .conflict (session ,input_id , {** intent ,"text" :"changed after restart" }) 156self .assertEqual ( 157 [entry ["input_id" ]for entry in self .users (session )], [input_id ] 158 ) 159 160@exclusive 161def test_repeated_image_commits_grow_only_compact_metadata (self ): 162session = self .app .session () 163width ,height = 256 ,256 164 165def chunk (kind ,data ): 166body = kind + data 167return ( 168struct .pack (">I" ,len (data )) 169+ body 170+ struct .pack (">I" ,zlib .crc32 (body )) 171 ) 172 173pixels = random .Random (1 ).randbytes (width * height * 3 ) 174rows = b"" .join ( 175b"\x00" + pixels [start :start + width * 3 ] 176for start in range (0 ,len (pixels ),width * 3 ) 177 ) 178image = ( 179b"\x89PNG\r\n\x1a\n" 180+ chunk (b"IHDR" ,struct .pack (">IIBBBBB" ,width ,height ,8 ,2 ,0 ,0 ,0 )) 181+ chunk (b"IDAT" ,zlib .compress (rows )) 182+ chunk (b"IEND" ,b"" ) 183 ) 184attachment = { 185"mime_type" :"image/png" , 186"data" :base64 .b64encode (image ).decode (), 187 } 188 189def durable_bytes (): 190with self .database ()as database : 191return sum ( 192database .execute ( 193f"SELECT coalesce(sum(length( { column } )),0) FROM { table } " 194 ).fetchone ()[0 ] 195for table ,column in ( 196 ("images" ,"data" ), 197 ("transcript" ,"payload" ), 198 ("submission_events" ,"payload" ), 199 ("continuation_markers" ,"payload" ), 200 ("pending_inputs" ,"payload" ), 201 ("operations" ,"response" ), 202 ) 203 ) 204 205inputs ,sizes = [], [] 206for index in range (3 ): 207input_id = operation_id () 208inputs .append (input_id ) 209self .put_input ( 210session , 211input_id , 212 { 213"kind" :"message" , 214"text" :"same image" , 215"client_id" :f"image-echo- { index } " , 216"images" : [attachment ], 217 }, 218 ) 219self .committed (session ,input_id ) 220self .app .idle (session ) 221sizes .append (durable_bytes ()) 222self .assertLess (sizes [2 ]- sizes [0 ],len (image )) 223self .app .restart (crash = True ) 224users = self .users (session ) 225self .assertEqual ([entry ["input_id" ]for entry in users ],inputs ) 226for index ,input_id in enumerate (inputs ): 227self .assertEqual ( 228self .receipt (session ,input_id )["client_id" ],f"image-echo- { index } " 229 ) 230metadata = [ 231part ["image" ] 232for entry in users 233for part in entry ["content" ] 234if part ["kind" ]== "image" 235 ] 236self .assertEqual ( 237 [ 238 (item ["width" ],item ["height" ],item ["original_bytes" ]) 239for item in metadata 240 ], 241 [(width ,height ,len (image ))]* 3 , 242 ) 243 244def test_expired_and_future_ids_cannot_create_work (self ): 245now = int (time .time ()* 1000 ) 246for timestamp ,status ,code in ( 247 (now - SEVEN_DAYS_MS - 10000 ,410 ,"identity_expired" ), 248 (now + 6 * 60 * 1000 ,400 ,"identity_future" ), 249 ): 250session_id = operation_id (timestamp ) 251with self .assertRaises (urllib .error .HTTPError )as failure : 252self .create ( 253session_id , {"kind" :"new" ,"workspace" :str (self .app .workspace )} 254 ) 255self .assertEqual (failure .exception .code ,status ) 256self .assertEqual (json .load (failure .exception )["code" ],code ) 257with self .database ()as database : 258self .assertEqual ( 259database .execute ( 260"SELECT count(*) FROM operations WHERE id=?" , (session_id ,) 261 ).fetchone ()[0 ], 2620 , 263 ) 264 265@exclusive 266def test_rejected_creation_replays_after_workspace_is_repaired (self ): 267session_id = operation_id () 268workspace = self .app .workspace / "missing" 269intent = { 270"kind" :"new" , 271"workspace" :str (workspace ), 272"provider_profile" :self .app .profile , 273 } 274with self .assertRaises (urllib .error .HTTPError )as original : 275self .create (session_id ,intent ) 276problem = json .load (original .exception ) 277with self .assertRaises (urllib .error .HTTPError )as lookup : 278self .app .api (f"/sessions/ { session_id } " ) 279self .assertEqual ( 280json .load (lookup .exception )["decision" ]["admission" ],"rejected" 281 ) 282workspace .mkdir () 283self .app .restart (crash = True ) 284with self .assertRaises (urllib .error .HTTPError )as repeated : 285self .create (session_id ,intent ) 286self .assertEqual (repeated .exception .code ,original .exception .code ) 287self .assertEqual (json .load (repeated .exception ),problem ) 288with self .database ()as database : 289self .assertEqual ( 290database .execute ("SELECT count(*) FROM sessions" ).fetchone ()[0 ],0 291 ) 292 293@exclusive 294def test_failed_cancellation_does_not_discard_pending_input (self ): 295session = self .app .session () 296input_id = operation_id () 297with self .database ()as database : 298database .execute ( 299"CREATE TRIGGER hold_consumption BEFORE INSERT ON transcript WHEN NEW.row_class='user' BEGIN SELECT RAISE(ABORT,'fixture'); END" 300 ) 301self .put_input ( 302session , 303input_id , 304 {"kind" :"message" ,"text" :"keep until cancellation commits" }, 305 ) 306with self .database ()as database : 307database .execute ( 308"CREATE TRIGGER fail_cancellation BEFORE UPDATE ON operations WHEN NEW.delivery='cancelled' BEGIN SELECT RAISE(ABORT,'fixture'); END" 309 ) 310resource = f"/sessions/ { session } /inputs/ { input_id } /cancel" 311with self .assertRaises (urllib .error .HTTPError ): 312self .app .api (resource , {}) 313self .assertEqual (self .receipt (session ,input_id )["delivery" ],"pending" ) 314with self .database ()as database : 315database .execute ("DROP TRIGGER fail_cancellation" ) 316with self .app .api (resource , {})as response : 317self .assertEqual (json .load (response )["result" ],"cancelled" ) 318self .app .restart (crash = True ) 319self .assertEqual (self .receipt (session ,input_id )["delivery" ],"cancelled" ) 320with self .database ()as database : 321self .assertEqual ( 322database .execute ( 323"SELECT count(*) FROM pending_inputs WHERE operation_id=?" , 324 (input_id ,), 325 ).fetchone ()[0 ], 3260 , 327 )