66
77use std:: sync:: Arc ;
88
9+ use async_trait:: async_trait;
910use chain:: { AxonInfo , ChainError , Metagraph , WeightsTlockPayload } ;
1011use chain:: { ChainClient , FakeChain , FakeChainConfig } ;
1112use challenge_agentic:: SimAgent ;
@@ -15,7 +16,10 @@ use prism_challenge::{
1516 example_valid_request, submission_id, GatewayClient , GatewayClientConfig , MemoryPrismStore ,
1617 Orchestrator , OrchestratorConfig , PrismStore , ScoringMode , Stage , StatePatch , SubmissionState ,
1718} ;
18- use prism_lium:: { EvalJobBackend , SimLiumBackend } ;
19+ use prism_lium:: {
20+ CostGuardrailError , EvalJobBackend , Instance , InstanceSpec , LiumError , Offer , RemoteExecResult ,
21+ SimLiumBackend ,
22+ } ;
1923use prism_review:: SimReviewer ;
2024use std:: sync:: Mutex ;
2125
@@ -337,3 +341,162 @@ async fn emit_and_submit_covers_expected_set() {
337341 assert_eq ! ( tip. epoch, 7 ) ;
338342 assert_eq ! ( store. emit_cursor( 541 ) . await . unwrap( ) , Some ( 7 ) ) ;
339343}
344+
345+ /// Provision always fails with a fixed Lium error (no rent).
346+ struct ProvisionFail ( & ' static str ) ;
347+
348+ #[ async_trait]
349+ impl EvalJobBackend for ProvisionFail {
350+ async fn list_offers ( & self , _: Option < f64 > ) -> Result < Vec < Offer > , LiumError > {
351+ Ok ( Vec :: new ( ) )
352+ }
353+ async fn provision ( & self , _: & InstanceSpec ) -> Result < Instance , LiumError > {
354+ if self . 0 == "capacity" {
355+ return Err ( CostGuardrailError :: NoCapacity . into ( ) ) ;
356+ }
357+ Err ( LiumError :: Api (
358+ "POST /executors/x/rent -> 400 You don't have permission to rent this template." . into ( ) ,
359+ ) )
360+ }
361+ async fn terminate ( & self , _: & str ) -> Result < ( ) , LiumError > {
362+ Ok ( ( ) )
363+ }
364+ async fn verify_terminated ( & self , _: & str ) -> Result < bool , LiumError > {
365+ Ok ( true )
366+ }
367+ async fn exec_eval (
368+ & self ,
369+ _: & str ,
370+ _: & str ,
371+ _: & str ,
372+ _: Option < & [ u8 ] > ,
373+ ) -> Result < RemoteExecResult , LiumError > {
374+ Err ( LiumError :: Exec ( "unreachable" . into ( ) ) )
375+ }
376+ }
377+
378+ fn row_from_example ( id : & str , req : & prism_challenge:: SubmissionRequest ) -> SubmissionState {
379+ SubmissionState {
380+ id : id. to_owned ( ) ,
381+ miner_hotkey : req. miner_hotkey . clone ( ) ,
382+ miner_coldkey : None ,
383+ epoch : 7 ,
384+ netuid : 541 ,
385+ status : Stage :: Queued ,
386+ architecture_py : req. architecture_py . clone ( ) ,
387+ training_py : req. training_py . clone ( ) ,
388+ tree_blob : None ,
389+ label : req. label . clone ( ) ,
390+ pod_id : None ,
391+ pod_provider : None ,
392+ receipt : None ,
393+ metrics_json : None ,
394+ bpb : None ,
395+ arch_id : None ,
396+ review : None ,
397+ similarity : None ,
398+ final_score : None ,
399+ retry_count : 0 ,
400+ error_detail : None ,
401+ created_at_ms : 1 ,
402+ updated_at_ms : 1 ,
403+ }
404+ }
405+
406+ fn orch_with_backend (
407+ store : & Arc < MemoryPrismStore > ,
408+ chain : & Arc < LockedFake > ,
409+ backend : Arc < dyn EvalJobBackend > ,
410+ ) -> Orchestrator < LockedFake > {
411+ let gateway = Arc :: new (
412+ GatewayClient :: new ( GatewayClientConfig {
413+ base_url : "dry-run" . into ( ) ,
414+ max_attempts : 1 ,
415+ backoff : std:: time:: Duration :: from_millis ( 1 ) ,
416+ } )
417+ . unwrap ( ) ,
418+ ) ;
419+ Orchestrator :: new (
420+ OrchestratorConfig {
421+ netuid : 541 ,
422+ scoring_mode : ScoringMode :: Shadow ,
423+ auto_retry_max : 0 ,
424+ claim_poll : std:: time:: Duration :: from_millis ( 10 ) ,
425+ ..Default :: default ( )
426+ } ,
427+ Arc :: clone ( store) as Arc < dyn PrismStore > ,
428+ backend,
429+ Arc :: new ( SimReviewer :: new ( ) ) ,
430+ Arc :: new ( SimAgent :: new ( ) ) ,
431+ & gateway,
432+ Arc :: clone ( chain) ,
433+ sk ( ) ,
434+ )
435+ }
436+
437+ #[ tokio:: test]
438+ async fn no_capacity_requeues_with_b200_note ( ) {
439+ let store = Arc :: new ( MemoryPrismStore :: new ( ) ) ;
440+ let chain = Arc :: new ( LockedFake ( Mutex :: new ( fake_chain ( ) ) ) ) ;
441+ let orch = orch_with_backend (
442+ & store,
443+ & chain,
444+ Arc :: new ( ProvisionFail ( "capacity" ) ) as Arc < dyn EvalJobBackend > ,
445+ ) ;
446+ let req = example_valid_request ( ) ;
447+ let id = submission_id ( & req) ;
448+ store
449+ . insert_queued ( & row_from_example ( & id, & req) )
450+ . await
451+ . unwrap ( ) ;
452+
453+ assert ! ( orch. cycle_once( ) . await . unwrap( ) ) ;
454+ let row = store. get ( & id) . await . unwrap ( ) . expect ( "row" ) ;
455+ assert_eq ! ( row. status, Stage :: Queued , "{row:?}" ) ;
456+ assert_eq ! ( row. retry_count, 0 , "sold-out must not burn retry_count" ) ;
457+ assert ! ( row. final_score. is_none( ) ) ;
458+ let detail = row. error_detail . unwrap_or_default ( ) ;
459+ assert ! (
460+ detail. contains( "B200s are currently out of capacity on Lium" ) ,
461+ "{detail}"
462+ ) ;
463+ let events = store. events ( & id) . await . unwrap ( ) ;
464+ let queued_note = events. iter ( ) . any ( |e| {
465+ e. stage == Stage :: Queued
466+ && e. detail . as_ref ( ) . is_some_and ( |d| {
467+ d. get ( "no_capacity" ) == Some ( & serde_json:: json!( true ) )
468+ && d. get ( "note" )
469+ . and_then ( |n| n. as_str ( ) )
470+ . is_some_and ( |n| n. contains ( "B200s are currently out of capacity" ) )
471+ } )
472+ } ) ;
473+ assert ! ( queued_note, "events={events:?}" ) ;
474+
475+ assert ! ( orch. cycle_once( ) . await . unwrap( ) ) ;
476+ let row = store. get ( & id) . await . unwrap ( ) . expect ( "row" ) ;
477+ assert_eq ! ( row. status, Stage :: Queued , "next tick still queued" ) ;
478+ }
479+
480+ #[ tokio:: test]
481+ async fn template_permission_stays_failed ( ) {
482+ let store = Arc :: new ( MemoryPrismStore :: new ( ) ) ;
483+ let chain = Arc :: new ( LockedFake ( Mutex :: new ( fake_chain ( ) ) ) ) ;
484+ let orch = orch_with_backend (
485+ & store,
486+ & chain,
487+ Arc :: new ( ProvisionFail ( "permission" ) ) as Arc < dyn EvalJobBackend > ,
488+ ) ;
489+ let req = example_valid_request ( ) ;
490+ let id = submission_id ( & req) ;
491+ store
492+ . insert_queued ( & row_from_example ( & id, & req) )
493+ . await
494+ . unwrap ( ) ;
495+
496+ assert ! ( orch. cycle_once( ) . await . unwrap( ) ) ;
497+ let row = store. get ( & id) . await . unwrap ( ) . expect ( "row" ) ;
498+ assert_eq ! ( row. status, Stage :: Failed , "{row:?}" ) ;
499+ let detail = row. error_detail . unwrap_or_default ( ) ;
500+ assert ! ( detail. contains( "permission" ) , "{detail}" ) ;
501+ assert ! ( !detail. contains( "B200s are currently out of capacity" ) ) ;
502+ }
0 commit comments