@@ -10,7 +10,7 @@ use notify::{
1010use serde_json:: { Map , Value } ;
1111use tokio:: signal;
1212use tokio:: time:: { Instant , sleep} ;
13- use tracing:: { info , warn } ;
13+ use tracing:: { error , info } ;
1414use unicode_width:: UnicodeWidthStr ;
1515
1616use crate :: config:: { CompiledWatchGroup , Config , LogStyle } ;
@@ -85,7 +85,12 @@ impl Engine {
8585 let tx_watcher = tx. clone ( ) ;
8686 let mut watcher = RecommendedWatcher :: new (
8787 move |result| {
88- let _ = tx_watcher. send ( result) ;
88+ if let Err ( error) = tx_watcher. send ( result) {
89+ error ! (
90+ "failed to forward watcher event into runtime loop: {}" ,
91+ error
92+ ) ;
93+ }
8994 } ,
9095 NotifyConfig :: default ( ) ,
9196 ) ?;
@@ -125,9 +130,7 @@ impl Engine {
125130 }
126131 }
127132 batch = next_batch( & rx, self . config. debounce( ) ) => {
128- let Some ( events) = batch? else {
129- continue ;
130- } ;
133+ let events = batch?;
131134 let workflows = classify_events( & self . config. root, & watch_groups, & events) ;
132135 if !workflows. is_empty( ) {
133136 runtime. handle_event( RuntimeEvent :: WatchChanges { workflows } ) ;
@@ -136,12 +139,17 @@ impl Engine {
136139 }
137140 }
138141 }
139- Some ( event) = external_event_rx. recv( ) => {
140- runtime. handle_event( RuntimeEvent :: WorkflowTrigger {
141- workflow_name: event. workflow_name,
142- } ) ;
143- if execute_runtime_effects( & mut runtime, & mut adapter) . await ? {
144- return Ok ( ( ) ) ;
142+ event = external_event_rx. recv( ) => {
143+ match event {
144+ Some ( event) => {
145+ runtime. handle_event( RuntimeEvent :: WorkflowTrigger {
146+ workflow_name: event. workflow_name,
147+ } ) ;
148+ if execute_runtime_effects( & mut runtime, & mut adapter) . await ? {
149+ return Ok ( ( ) ) ;
150+ }
151+ }
152+ None => return Err ( anyhow!( "external event channel disconnected" ) ) ,
145153 }
146154 }
147155 }
@@ -225,10 +233,10 @@ impl RuntimeEffectAdapter for LiveRuntimeAdapter<'_, '_> {
225233
226234 async fn run_workflow ( & mut self , workflow_name : & str , changed_files : & [ String ] ) -> Result < ( ) > {
227235 info ! ( "running workflow {}" , workflow_name) ;
228- let Some ( _ ) = self . config . workflow . get ( workflow_name ) else {
229- warn ! ( "skipping missing workflow {}" , workflow_name ) ;
230- return Ok ( ( ) ) ;
231- } ;
236+ self . config
237+ . workflow
238+ . get ( workflow_name )
239+ . ok_or_else ( || anyhow ! ( "runtime requested missing workflow '{workflow_name}'" ) ) ? ;
232240 let mut adapter = LiveWorkflowAdapter {
233241 processes : self . processes ,
234242 state : self . state ,
@@ -306,10 +314,10 @@ async fn run_workflow(
306314 changed_files : & [ String ] ,
307315) -> Result < ( ) > {
308316 info ! ( "running workflow {}" , workflow_name) ;
309- let Some ( _ ) = config. workflow . get ( workflow_name ) else {
310- warn ! ( "skipping missing workflow {}" , workflow_name ) ;
311- return Ok ( ( ) ) ;
312- } ;
317+ config
318+ . workflow
319+ . get ( workflow_name )
320+ . ok_or_else ( || anyhow ! ( "runtime requested missing workflow '{workflow_name}'" ) ) ? ;
313321 let mut adapter = LiveWorkflowAdapter { processes, state } ;
314322 run_workflow_machine ( config, & mut adapter, workflow_name, changed_files) . await
315323}
@@ -381,10 +389,10 @@ fn boxed_banner_lines(message: &str) -> [String; 3] {
381389async fn next_batch (
382390 rx : & mpsc:: Receiver < notify:: Result < Event > > ,
383391 debounce : Duration ,
384- ) -> Result < Option < Vec < Event > > > {
392+ ) -> Result < Vec < Event > > {
385393 let first = match rx. recv ( ) {
386394 Ok ( result) => result?,
387- Err ( _) => return Ok ( None ) ,
395+ Err ( _) => return Err ( anyhow ! ( "watcher event channel disconnected" ) ) ,
388396 } ;
389397 let start = Instant :: now ( ) ;
390398 let mut events = vec ! [ first] ;
@@ -395,7 +403,7 @@ async fn next_batch(
395403 Err ( mpsc:: TryRecvError :: Disconnected ) => break ,
396404 }
397405 }
398- Ok ( Some ( events) )
406+ Ok ( events)
399407}
400408
401409fn classify_events (
@@ -495,6 +503,22 @@ mod tests {
495503 assert_eq ! ( grouped[ "content" ] , vec![ "content/posts/example.md" ] ) ;
496504 }
497505
506+ #[ tokio:: test]
507+ async fn next_batch_errors_when_watcher_channel_disconnects ( ) {
508+ let ( _tx, rx) = mpsc:: channel ( ) ;
509+ drop ( _tx) ;
510+
511+ let error = next_batch ( & rx, Duration :: from_millis ( 10 ) )
512+ . await
513+ . expect_err ( "channel disconnect should error" ) ;
514+
515+ assert ! (
516+ error
517+ . to_string( )
518+ . contains( "watcher event channel disconnected" )
519+ ) ;
520+ }
521+
498522 #[ test]
499523 fn classify_changes_by_workflow_accepts_private_var_event_paths ( ) {
500524 let root = PathBuf :: from ( "/var/folders/example/tmp" ) ;
@@ -1081,4 +1105,34 @@ mod tests {
10811105 ]
10821106 ) ;
10831107 }
1108+
1109+ #[ tokio:: test]
1110+ async fn missing_runtime_workflow_returns_error ( ) {
1111+ let config = Config {
1112+ root : PathBuf :: from ( "." ) ,
1113+ debounce_ms : 100 ,
1114+ state_file : Some ( PathBuf :: from ( "./state.json" ) ) ,
1115+ startup_workflows : vec ! [ ] ,
1116+ watch : BTreeMap :: new ( ) ,
1117+ process : BTreeMap :: new ( ) ,
1118+ hook : BTreeMap :: new ( ) ,
1119+ event_server : crate :: config:: EventServerConfig :: default ( ) ,
1120+ event : BTreeMap :: new ( ) ,
1121+ workflow : BTreeMap :: new ( ) ,
1122+ } ;
1123+ let state_path = unique_state_path ( ) ;
1124+ let state = SessionState :: load ( state_path. clone ( ) ) . expect ( "load state" ) ;
1125+ let mut processes = ProcessManager :: new ( & config) ;
1126+
1127+ let error = run_workflow ( & config, & mut processes, & state, "missing" , & [ ] )
1128+ . await
1129+ . expect_err ( "missing workflow should error" ) ;
1130+
1131+ assert ! (
1132+ error
1133+ . to_string( )
1134+ . contains( "runtime requested missing workflow 'missing'" )
1135+ ) ;
1136+ let _ = std:: fs:: remove_file ( state_path) ;
1137+ }
10841138}
0 commit comments