@@ -463,3 +463,82 @@ async fn metadata_and_history_paginate_filter_and_survive_restart() -> TestResul
463463 app. store . close ( ) . await ;
464464 Ok ( ( ) )
465465}
466+
467+ #[ tokio:: test]
468+ async fn resignation_and_draw_agreements_replay_after_restart ( ) -> TestResult {
469+ let directory = tempfile:: tempdir ( ) ?;
470+ let settings = config ( & directory) ;
471+ let app = fixture ( & settings) . await ?;
472+ let ( _, initial) = call ( & app. router , "POST" , "/v1/matches" , json ! ( { "game_id" : "tictactoe" } ) , None ) . await ?;
473+ let id = match_id ( & initial) ?;
474+ let ( status, resigned) = call ( & app. router , "POST" , & format ! ( "/v1/matches/{id}/resign" ) ,
475+ json ! ( { "seat" : 1 , "turn" : 0 } ) , None ) . await ?;
476+ assert_eq ! ( status, StatusCode :: OK ) ;
477+ assert_eq ! ( resigned[ "turn" ] , 0 ) ;
478+ assert_eq ! ( resigned[ "observation" ] , initial[ "observation" ] ) ;
479+ assert_eq ! ( resigned[ "returns" ] , json!( [ 1.0 , -1.0 ] ) ) ;
480+ assert_eq ! ( resigned[ "outcome" ] , json!( { "reason" : "resigned" , "seat" : 1 } ) ) ;
481+ assert_eq ! ( resigned[ "terminated" ] , true ) ;
482+ assert_eq ! ( resigned[ "legal_actions" ] , json!( [ ] ) ) ;
483+ let ( status, _) = call ( & app. router , "POST" , & format ! ( "/v1/matches/{id}/actions" ) ,
484+ json ! ( { "seat" : 0 , "turn" : 0 , "action" : "r1c1" } ) , None ) . await ?;
485+ assert_eq ! ( status, StatusCode :: CONFLICT ) ;
486+ let ( _, created) = call ( & app. router , "POST" , "/v1/matches" , json ! ( { "game_id" : "connect4" } ) , None ) . await ?;
487+ let drawn_id = match_id ( & created) ?;
488+ let offer_url = format ! ( "/v1/matches/{drawn_id}/offer-draw" ) ;
489+ let ( _, offered) = call ( & app. router , "POST" , & offer_url, json ! ( { "seat" : 0 , "turn" : 0 } ) , None ) . await ?;
490+ assert_eq ! ( offered[ "draw_offer" ] , 0 ) ;
491+ assert_eq ! ( offered[ "terminated" ] , false ) ;
492+ let ( _, duplicate) = call ( & app. router , "POST" , & offer_url, json ! ( { "seat" : 0 , "turn" : 0 } ) , None ) . await ?;
493+ assert_eq ! ( offered, duplicate) ;
494+ let ( _, drawn) = call ( & app. router , "POST" , & offer_url, json ! ( { "seat" : 1 , "turn" : 0 } ) , None ) . await ?;
495+ assert_eq ! ( drawn[ "outcome" ] , json!( { "reason" : "agreed_draw" } ) ) ;
496+ assert_eq ! ( drawn[ "returns" ] , json!( [ 0.0 , 0.0 ] ) ) ;
497+ assert ! ( drawn. get( "draw_offer" ) . is_none( ) ) ;
498+ let ( _, doc) = call ( & app. router , "GET" , "/v1/openapi.json" , Value :: Null , None ) . await ?;
499+ assert ! ( doc[ "components" ] [ "schemas" ] [ "MatchOutcome" ] . is_object( ) ) ;
500+ app. store . close ( ) . await ;
501+ let app = fixture ( & settings) . await ?;
502+ for ( id, expected) in [ ( id, resigned) , ( drawn_id, drawn) ] {
503+ let ( _, replay) = call ( & app. router , "GET" , & format ! ( "/v1/matches/{id}/replay?seat=1" ) , Value :: Null , None ) . await ?;
504+ assert_eq ! ( replay[ "states" ] , json!( [ expected] ) ) ;
505+ let ( _, metadata) = call ( & app. router , "GET" , & format ! ( "/v1/matches/{id}" ) , Value :: Null , None ) . await ?;
506+ assert_eq ! ( metadata[ "status" ] , "finished" ) ;
507+ assert_eq ! ( metadata[ "outcome" ] , expected[ "outcome" ] ) ;
508+ }
509+ app. store . close ( ) . await ;
510+ Ok ( ( ) )
511+ }
512+
513+ #[ tokio:: test]
514+ async fn controls_validate_turns_expire_offers_and_commit_once_under_races ( ) -> TestResult {
515+ use gfa_api_types:: { ControlRequest , MoveRequest } ;
516+ let directory = tempfile:: tempdir ( ) ?;
517+ let app = fixture ( & config ( & directory) ) . await ?;
518+ let ( _, initial) = call ( & app. router , "POST" , "/v1/matches" , json ! ( { "game_id" : "tictactoe" } ) , None ) . await ?;
519+ let id = match_id ( & initial) ?;
520+ for ( request, expected) in [
521+ ( json ! ( { "seat" : 0 , "turn" : 1 } ) , StatusCode :: CONFLICT ) ,
522+ ( json ! ( { "seat" : 9 , "turn" : 0 } ) , StatusCode :: UNPROCESSABLE_ENTITY ) ,
523+ ( json ! ( { "seat" : 0 } ) , StatusCode :: UNPROCESSABLE_ENTITY ) ,
524+ ] {
525+ let ( status, _) = call ( & app. router , "POST" , & format ! ( "/v1/matches/{id}/resign" ) , request, None ) . await ?;
526+ assert_eq ! ( status, expected) ;
527+ }
528+ app. service . offer_draw ( & id, ControlRequest { seat : 0 , turn : 0 } ) . await ?;
529+ app. service . make_move ( & id, MoveRequest {
530+ seat : 0 , turn : 0 , action : json ! ( "r2c2" ) , reasoning : None ,
531+ } , None ) . await ?;
532+ let offer = app. service . offer_draw ( & id, ControlRequest { seat : 1 , turn : 1 } ) . await ?;
533+ assert ! ( !offer. terminated) ;
534+ assert_eq ! ( offer. draw_offer, Some ( 1 ) ) ;
535+ let ( a, b) = tokio:: join!(
536+ app. service. resign( & id, ControlRequest { seat: 0 , turn: 1 } ) ,
537+ app. service. resign( & id, ControlRequest { seat: 1 , turn: 1 } ) ,
538+ ) ;
539+ assert_eq ! ( usize :: from( a. is_ok( ) ) + usize :: from( b. is_ok( ) ) , 1 ) ;
540+ assert ! ( a. err( ) . into_iter( ) . chain( b. err( ) ) . all( |error|
541+ error. code == "STALE_TURN" || error. code == "MATCH_FINISHED" ) ) ;
542+ app. store . close ( ) . await ;
543+ Ok ( ( ) )
544+ }
0 commit comments