@@ -2399,29 +2399,36 @@ fn direct_engine_marks_vfs_dead_after_transport_errors() {
23992399 )
24002400 . expect ( "v2 vfs should register" ) ;
24012401 let db = open_database ( vfs, & harness. actor_id ) . expect ( "sqlite database should open" ) ;
2402+ let ctx = direct_vfs_ctx ( & db) ;
2403+
2404+ {
2405+ let mut state = ctx. state . write ( ) ;
2406+ state. write_buffer . dirty . insert ( 1 , empty_db_page ( ) ) ;
2407+ state. db_size_pages = 1 ;
2408+ }
24022409
24032410 hooks. fail_next_commit ( "InjectedTransportError: commit transport dropped" ) ;
2404- let err = sqlite_exec (
2405- db. as_ptr ( ) ,
2406- "CREATE TABLE broken (id INTEGER PRIMARY KEY, value TEXT NOT NULL);" ,
2407- )
2408- . expect_err ( "failing transport commit should surface as an IO error" ) ;
2411+ let err = ctx
2412+ . flush_dirty_pages ( )
2413+ . expect_err ( "failing transport commit should surface as a VFS error" ) ;
24092414 assert ! (
2410- err. contains ( "I/O" ) || err . contains( "disk I/O" ) ,
2411- "sqlite should surface transport failure as an IO error: {err}" ,
2415+ matches! ( err, CommitBufferError :: Other ( ref message ) if message . contains( "InjectedTransportError" ) ) ,
2416+ "VFS should surface transport failure as an IO error: {err:? }" ,
24122417 ) ;
24132418 assert ! (
2414- direct_vfs_ctx( & db) . is_dead( ) ,
2415- "transport error should kill the v2 VFS"
2419+ !ctx. is_dead( ) ,
2420+ "transport error should not poison the v2 VFS"
2421+ ) ;
2422+ assert ! (
2423+ ctx. clone_fatal_error( ) . is_none( ) ,
2424+ "transport error should not be stored as fatal"
24162425 ) ;
24172426 assert_eq ! (
24182427 db. take_last_kv_error( ) . as_deref( ) ,
24192428 Some ( "InjectedTransportError: commit transport dropped" ) ,
24202429 ) ;
2421- assert ! (
2422- sqlite_query_i64( db. as_ptr( ) , "PRAGMA page_count;" ) . is_err( ) ,
2423- "subsequent reads should fail once the VFS is dead" ,
2424- ) ;
2430+ ctx. flush_dirty_pages ( )
2431+ . expect ( "retry after unapplied transport failure should succeed" ) ;
24252432}
24262433
24272434#[ test]
@@ -2459,13 +2466,23 @@ fn flush_dirty_pages_marks_vfs_dead_after_transport_error() {
24592466 "flush failure should surface as a transport error: {err:?}" ,
24602467 ) ;
24612468 assert ! (
2462- ctx. is_dead( ) ,
2463- "flush transport failure should poison the VFS"
2469+ !ctx. is_dead( ) ,
2470+ "flush transport failure should not poison the VFS"
2471+ ) ;
2472+ assert ! (
2473+ ctx. clone_fatal_error( ) . is_none( ) ,
2474+ "flush transport failure should not be stored as fatal"
24642475 ) ;
24652476 assert_eq ! (
24662477 db. take_last_kv_error( ) . as_deref( ) ,
24672478 Some ( "InjectedTransportError: flush transport dropped" ) ,
24682479 ) ;
2480+ ctx. flush_dirty_pages ( )
2481+ . expect ( "retry after unapplied transport failure should succeed" ) ;
2482+ assert ! (
2483+ ctx. state. read( ) . write_buffer. dirty. is_empty( ) ,
2484+ "successful retry should clear dirty pages" ,
2485+ ) ;
24692486}
24702487
24712488#[ test]
@@ -2505,13 +2522,120 @@ fn commit_atomic_write_marks_vfs_dead_after_transport_error() {
25052522 "atomic-write failure should surface as a transport error: {err:?}" ,
25062523 ) ;
25072524 assert ! (
2508- ctx. is_dead( ) ,
2509- "commit_atomic_write transport failure should poison the VFS" ,
2525+ !ctx. is_dead( ) ,
2526+ "commit_atomic_write transport failure should not poison the VFS" ,
2527+ ) ;
2528+ assert ! (
2529+ ctx. clone_fatal_error( ) . is_none( ) ,
2530+ "commit_atomic_write transport failure should not be stored as fatal" ,
25102531 ) ;
25112532 assert_eq ! (
25122533 db. take_last_kv_error( ) . as_deref( ) ,
25132534 Some ( "InjectedTransportError: atomic transport dropped" ) ,
25142535 ) ;
2536+ ctx. commit_atomic_write ( )
2537+ . expect ( "retry after unapplied atomic transport failure should succeed" ) ;
2538+ assert ! (
2539+ !ctx. state. read( ) . write_buffer. in_atomic_write,
2540+ "successful retry should leave atomic-write mode" ,
2541+ ) ;
2542+ }
2543+
2544+ #[ test]
2545+ fn lost_commit_response_fails_later_on_head_fence_mismatch ( ) {
2546+ let runtime = direct_runtime ( ) ;
2547+ let harness = DirectEngineHarness :: new ( ) ;
2548+ let engine = runtime. block_on ( harness. open_engine ( ) ) ;
2549+ let transport = Arc :: new ( DirectDepotTransport :: new ( engine) ) ;
2550+ let hooks = transport. direct_hooks ( ) ;
2551+ let vfs = SqliteVfs :: register_with_transport (
2552+ & next_test_name ( "sqlite-direct-vfs" ) ,
2553+ transport,
2554+ harness. actor_id . clone ( ) ,
2555+ runtime. handle ( ) . clone ( ) ,
2556+ VfsConfig :: default ( ) ,
2557+ None ,
2558+ )
2559+ . expect ( "v2 vfs should register" ) ;
2560+ let db = open_database ( vfs, & harness. actor_id ) . expect ( "sqlite database should open" ) ;
2561+ let ctx = direct_vfs_ctx ( & db) ;
2562+
2563+ {
2564+ let mut state = ctx. state . write ( ) ;
2565+ state. write_buffer . dirty . insert ( 1 , empty_db_page ( ) ) ;
2566+ state. db_size_pages = 1 ;
2567+ }
2568+
2569+ hooks. fail_next_commit_after_apply ( "InjectedTransportError: commit response lost" ) ;
2570+ let err = ctx
2571+ . flush_dirty_pages ( )
2572+ . expect_err ( "lost commit response should surface as a transport error" ) ;
2573+ assert ! (
2574+ matches!( err, CommitBufferError :: Other ( ref message) if message. contains( "commit response lost" ) ) ,
2575+ "lost response should be ambiguous before the next fence check: {err:?}" ,
2576+ ) ;
2577+ assert ! (
2578+ !ctx. is_dead( ) ,
2579+ "ambiguous lost response should not immediately poison the VFS"
2580+ ) ;
2581+ assert ! ( ctx. clone_fatal_error( ) . is_none( ) ) ;
2582+
2583+ let err = ctx
2584+ . flush_dirty_pages ( )
2585+ . expect_err ( "retry after applied lost response should hit the stale head fence" ) ;
2586+ assert ! (
2587+ matches!( err, CommitBufferError :: FenceMismatch ( ref message) if message. contains( "head fence mismatch" ) ) ,
2588+ "retry should confirm the stale fence: {err:?}" ,
2589+ ) ;
2590+ assert ! ( ctx. is_dead( ) ) ;
2591+ assert ! (
2592+ ctx. clone_fatal_error( )
2593+ . is_some_and( |message| message. contains( "head fence mismatch" ) ) ,
2594+ "confirmed fence mismatch should be stored as fatal"
2595+ ) ;
2596+ }
2597+
2598+ #[ test]
2599+ fn unapplied_commit_transport_failure_can_retry_from_same_head ( ) {
2600+ let runtime = direct_runtime ( ) ;
2601+ let harness = DirectEngineHarness :: new ( ) ;
2602+ let engine = runtime. block_on ( harness. open_engine ( ) ) ;
2603+ let transport = Arc :: new ( DirectDepotTransport :: new ( engine) ) ;
2604+ let hooks = transport. direct_hooks ( ) ;
2605+ let vfs = SqliteVfs :: register_with_transport (
2606+ & next_test_name ( "sqlite-direct-vfs" ) ,
2607+ transport,
2608+ harness. actor_id . clone ( ) ,
2609+ runtime. handle ( ) . clone ( ) ,
2610+ VfsConfig :: default ( ) ,
2611+ None ,
2612+ )
2613+ . expect ( "v2 vfs should register" ) ;
2614+ let db = open_database ( vfs, & harness. actor_id ) . expect ( "sqlite database should open" ) ;
2615+ let ctx = direct_vfs_ctx ( & db) ;
2616+
2617+ {
2618+ let mut state = ctx. state . write ( ) ;
2619+ state. write_buffer . dirty . insert ( 1 , empty_db_page ( ) ) ;
2620+ state. db_size_pages = 1 ;
2621+ }
2622+
2623+ hooks. fail_next_commit ( "InjectedTransportError: commit dropped before apply" ) ;
2624+ let err = ctx
2625+ . flush_dirty_pages ( )
2626+ . expect_err ( "pre-apply transport error should surface" ) ;
2627+ assert ! (
2628+ matches!( err, CommitBufferError :: Other ( ref message) if message. contains( "before apply" ) ) ,
2629+ "pre-apply failure should be generic transport error: {err:?}" ,
2630+ ) ;
2631+ assert ! ( !ctx. is_dead( ) ) ;
2632+ assert ! ( ctx. clone_fatal_error( ) . is_none( ) ) ;
2633+
2634+ ctx. flush_dirty_pages ( )
2635+ . expect ( "retry from same head should succeed" ) ;
2636+ let state = ctx. state . read ( ) ;
2637+ assert ! ( state. write_buffer. dirty. is_empty( ) ) ;
2638+ assert ! ( state. head_txid. is_some( ) ) ;
25152639}
25162640
25172641#[ test]
@@ -3038,8 +3162,8 @@ fn direct_engine_fresh_reopen_recovers_after_poisoned_handle() {
30383162 "sqlite should surface transport failure as an IO error: {err}" ,
30393163 ) ;
30403164 assert ! (
3041- direct_vfs_ctx( & db) . is_dead( ) ,
3042- "transport error should kill the live VFS" ,
3165+ ! direct_vfs_ctx( & db) . is_dead( ) ,
3166+ "transport error should not poison the live VFS" ,
30433167 ) ;
30443168
30453169 drop ( db) ;
0 commit comments