@@ -17,6 +17,7 @@ use harmont_cloud::{
1717 HarmontClient , HarmontError ,
1818 logs:: { LogEvent , StreamKind } ,
1919 models:: { build_is_terminal, job_is_terminal} ,
20+ types:: JobState ,
2021} ;
2122use hm_plugin_protocol:: events:: { BuildEvent , PlanSummary , StdStream } ;
2223use uuid:: Uuid ;
@@ -56,6 +57,27 @@ fn duration_ms(start: Option<DateTime<Utc>>, end: Option<DateTime<Utc>>) -> u64
5657 }
5758}
5859
60+ /// Whether a job has reached a state where its logs exist (running or already
61+ /// terminal), and so a log stream should be started for it.
62+ ///
63+ /// Matching the typed [`JobState`] enum (rather than `to_string()`/`as_str()`
64+ /// against string literals) makes the set of states exhaustive: when the cloud
65+ /// adds a new `JobState` variant the compiler forces this decision to be
66+ /// revisited, and a misspelled state can no longer silently drop a job's logs.
67+ const fn job_logs_available ( state : JobState ) -> bool {
68+ match state {
69+ JobState :: Running
70+ | JobState :: Passed
71+ | JobState :: Failed
72+ | JobState :: TimedOut
73+ | JobState :: Canceling
74+ | JobState :: Canceled
75+ | JobState :: TimingOut => true ,
76+ // No logs yet (not started) or never produced (skipped).
77+ JobState :: Pending | JobState :: Scheduled | JobState :: Assigned | JobState :: Skipped => false ,
78+ }
79+ }
80+
5981/// Map a terminal build state to the process exit code the renderer and the
6082/// `hm run` driver use. `passed` → 0, `canceled` → 130 (SIGINT-cancel, mirrors
6183/// [`crate::BuildStatus::Canceled`]), everything else (`failed`, and any
@@ -131,18 +153,7 @@ pub async fn watch_build(
131153 // state where logs exist (running or already terminal).
132154 let jobs = client. list_jobs ( org, pipeline, number) . await ?;
133155 for job in & jobs {
134- let state = job. state . to_string ( ) ;
135- let logs_available = matches ! (
136- state. as_str( ) ,
137- "running"
138- | "passed"
139- | "failed"
140- | "timed_out"
141- | "canceling"
142- | "canceled"
143- | "timing_out"
144- ) ;
145- if logs_available && streaming. insert ( job. id ) {
156+ if job_logs_available ( job. state ) && streaming. insert ( job. id ) {
146157 let name = job. name . clone ( ) . unwrap_or_else ( || "job" . to_string ( ) ) ;
147158 let idx = * chain_idx. entry ( job. id ) . or_insert_with ( || {
148159 let i = next_idx;
@@ -400,7 +411,34 @@ async fn emit(
400411
401412#[ cfg( test) ]
402413mod tests {
403- use super :: exit_code_for_state;
414+ use super :: { JobState , exit_code_for_state, job_logs_available} ;
415+
416+ #[ test]
417+ fn logs_available_for_running_and_terminal_states ( ) {
418+ for state in [
419+ JobState :: Running ,
420+ JobState :: Passed ,
421+ JobState :: Failed ,
422+ JobState :: TimedOut ,
423+ JobState :: Canceling ,
424+ JobState :: Canceled ,
425+ JobState :: TimingOut ,
426+ ] {
427+ assert ! ( job_logs_available( state) , "expected logs for {state}" ) ;
428+ }
429+ }
430+
431+ #[ test]
432+ fn no_logs_before_start_or_when_skipped ( ) {
433+ for state in [
434+ JobState :: Pending ,
435+ JobState :: Scheduled ,
436+ JobState :: Assigned ,
437+ JobState :: Skipped ,
438+ ] {
439+ assert ! ( !job_logs_available( state) , "expected no logs for {state}" ) ;
440+ }
441+ }
404442
405443 #[ test]
406444 fn passed_is_zero_canceled_is_130_else_is_one ( ) {
0 commit comments