diff --git a/lib/apps/fabro-cli/src/commands/run/petri_worker.rs b/lib/apps/fabro-cli/src/commands/run/petri_worker.rs index aab64bd01..9403a2d6d 100644 --- a/lib/apps/fabro-cli/src/commands/run/petri_worker.rs +++ b/lib/apps/fabro-cli/src/commands/run/petri_worker.rs @@ -367,9 +367,11 @@ impl PetriControls { } /// Stop the named stage's model turn, `text` as its next input when - /// given. A refusal is a `run.notice` whose code says why: `no_live_turn` + /// given. A refusal is a `run.notice` whose code says why (`no_live_turn` /// when the stage has no model turn in flight, `no_such_stage` when the - /// name is not running, `interrupt_refused` otherwise. + /// name is not running, `interrupt_refused` otherwise) and whose message + /// names the stage and the reason as Petri spells it, for the web and + /// the CLI to show. async fn interrupt(&self, stage: Option<&str>, text: Option<&str>, actor: &Principal) { match self.controls.interrupt(stage, text).await { Ok(stage) => { @@ -382,9 +384,12 @@ impl PetriControls { ); } Err(error) => { - warn!(run_id = %self.run_id, error = %error, "interrupt refused"); - self.notice(interrupt_refusal_code(&error), error.to_string()) - .await; + warn!(run_id = %self.run_id, stage, error = %error, "interrupt refused"); + self.notice( + interrupt_refusal_code(&error), + interrupt_refusal_message(stage, &error), + ) + .await; } } } @@ -416,6 +421,16 @@ fn interrupt_refusal_code(error: &SteerError) -> &'static str { } } +/// The notice message of a refused interrupt: the stage it named, and the +/// reason as Petri's `ControlError` (or the resolution's own refusal) +/// spells it. +fn interrupt_refusal_message(stage: Option<&str>, error: &SteerError) -> String { + match stage { + Some(stage) => format!("Interrupt of stage `{stage}` refused: {error}"), + None => format!("Interrupt refused: {error}"), + } +} + /// The wire name of a control, for a log line. fn control_name(message: &WorkerControlMessage) -> &'static str { match message { diff --git a/lib/apps/fabro-cli/tests/it/scenario/petri_controls.rs b/lib/apps/fabro-cli/tests/it/scenario/petri_controls.rs index de0075758..987aac247 100644 --- a/lib/apps/fabro-cli/tests/it/scenario/petri_controls.rs +++ b/lib/apps/fabro-cli/tests/it/scenario/petri_controls.rs @@ -794,8 +794,8 @@ async fn an_interrupt_ends_the_turn_and_its_text_is_the_next_input() { /// blocked on, named by its node, is refused by Petri with `no_live_turn`; /// unnamed, with no agent stage live, the worker refuses it with /// `interrupt_refused`. Both refusals are `run.notice` records on the -/// stream, nothing is delivered, and the gate's question is untouched: its -/// answer routes the run to its end. +/// stream naming the stage and Petri's reason, nothing is delivered, and +/// the gate's question is untouched: its answer routes the run to its end. #[tokio::test(flavor = "multi_thread")] async fn an_interrupt_of_a_gate_stage_is_refused_with_no_live_turn() { if host_plugin().is_none() { @@ -836,10 +836,18 @@ async fn an_interrupt_of_a_gate_stage_is_refused_with_no_live_turn() { assert_eq!(count_of(&names, "control.requested"), 0, "{names:?}"); let refused = notices(&run_stream(&server, &run_id).await); assert_eq!(refused.len(), 2, "{refused:?}"); + // The notice names the stage and carries Petri's reason as it spells + // it, so the web and the CLI can show both. assert_eq!(refused[0].0, "no_live_turn", "{refused:?}"); - assert_eq!(refused[0].1, "the stage has no model turn to interrupt"); + assert_eq!( + refused[0].1, + "Interrupt of stage `gate` refused: the stage has no model turn to interrupt" + ); assert_eq!(refused[1].0, "interrupt_refused", "{refused:?}"); - assert_eq!(refused[1].1, "Run has no active steerable agent session."); + assert_eq!( + refused[1].1, + "Interrupt refused: Run has no active steerable agent session." + ); assert_eq!(run_status(&server, &run_id).await, "blocked"); answer(&server, &run_id, &question_id, json!({ "kind": "yes" })).await;