use std::sync::Arc;
use super::tests::{create_test_change, AdapterHarness};
use super::*;
use crate::events::{ExecutionEvent, OperatorCommandEffect};
use crate::orchestration::operator_command::{OperatorMode, ParallelEligibility};
use crate::orchestration::run_control::testing::SchedulerCall;
use crate::tui::state::AppState;
use crate::tui::types::{AppExecutionMode, StopMode};
use crate::web::remote_control_api::dto::{CommandSpec, ErrorCode};
use crate::web::remote_control_api::executor::{RemoteControlExecutor, SharedServiceExecutor};
use crate::web::state::{WebEventSink, WebState};
const CHANGES: [&str; 2] = ["alpha", "beta"];
const ALPHA: &str = "alpha";
const BETA: &str = "beta";
#[derive(Debug, Clone, PartialEq, Eq)]
struct DispatchedRun {
change_ids: Vec<String>,
explicit_retry: bool,
scheduler_started: bool,
}
#[derive(Default)]
struct RunRecorder {
runs: std::sync::Mutex<Vec<DispatchedRun>>,
}
impl RunRecorder {
fn runs(&self) -> Vec<DispatchedRun> {
self.runs
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.clone()
}
}
#[async_trait::async_trait]
impl crate::events::EventSink for RunRecorder {
async fn on_event(&self, event: &ExecutionEvent) {
if let ExecutionEvent::OperatorCommandApplied {
effect:
OperatorCommandEffect::RunDispatched {
change_ids,
explicit_retry,
scheduler_started,
},
} = event
{
self.runs
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.push(DispatchedRun {
change_ids: change_ids.clone(),
explicit_retry: *explicit_retry,
scheduler_started: *scheduler_started,
});
}
}
async fn on_state_changed(&self, _state: &crate::orchestration::state::OrchestratorState) {}
}
#[derive(Debug, PartialEq, Eq)]
struct Settled {
core_mode: OperatorMode,
tui_mode: AppExecutionMode,
tui_idle: bool,
web_mode: String,
web_idle: bool,
runs: Vec<DispatchedRun>,
scheduler: Vec<SchedulerCall>,
statuses: Vec<(String, String)>,
explicit_retries: Vec<String>,
dispatches: usize,
revisions: u64,
}
struct Wired {
harness: AdapterHarness,
app: AppState,
web: Arc<WebState>,
runs: Arc<RunRecorder>,
baseline_revision: u64,
baseline_dispatches: usize,
}
impl Wired {
async fn settled(&self) -> Settled {
let statuses = {
let guard = self.harness.state.read().await;
CHANGES
.iter()
.map(|id| ((*id).to_string(), guard.display_status(id).to_string()))
.collect()
};
let web = self.web.get_state().await;
Settled {
core_mode: self.harness.core_mode.get(),
tui_mode: self.app.execution_mode,
tui_idle: self.app.persistent_scheduler_idle,
web_mode: web.app_mode,
web_idle: web.persistent_scheduler_idle,
runs: self.runs.runs(),
scheduler: self.harness.scheduler.calls(),
statuses,
explicit_retries: self
.harness
.queue
.drain_explicit_retries()
.await
.into_iter()
.map(|edge| edge.change_id)
.collect(),
dispatches: self.harness.dispatch_count() - self.baseline_dispatches,
revisions: self.web.remote_control().projection().revision() - self.baseline_revision,
}
}
async fn dispatch(&mut self, event: ExecutionEvent) {
self.harness.dispatcher.dispatch(event).await;
self.harness.deliver(&mut self.app).await;
}
fn baseline(&mut self) {
self.baseline_revision = self.web.remote_control().projection().revision();
self.baseline_dispatches = self.harness.dispatch_count();
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Setup {
MarkedOrdinary,
MarkedRetryableError,
Unmarked,
MarkedIneligible,
}
async fn arrange(harness: &AdapterHarness, setup: Setup) {
let mark_all = || {
harness
.marks
.replace(CHANGES.iter().map(|id| (*id).to_string()))
};
match setup {
Setup::MarkedOrdinary => mark_all(),
Setup::MarkedRetryableError => {
harness
.state
.write()
.await
.apply_execution_event(&ExecutionEvent::ProcessingError {
id: ALPHA.to_string(),
error: "apply reached its absolute runtime limit".to_string(),
});
harness.marks.replace([ALPHA.to_string()]);
}
Setup::Unmarked => {}
Setup::MarkedIneligible => {
mark_all();
harness
.parallel
.set_parallel_ineligible(CHANGES.iter().map(|id| {
(
(*id).to_string(),
ParallelEligibility::UncommittedProposalFiles,
)
}));
}
}
}
async fn running(setup: Setup, scheduler_live: bool) -> Wired {
let harness = AdapterHarness::new(&CHANGES);
arrange(&harness, setup).await;
harness.scheduler.set_running(scheduler_live);
let ineligible = harness.parallel.ineligible_ids();
let mut app = harness.app(&CHANGES);
for change in &mut app.changes {
change.parallel_eligibility = if ineligible.contains(&change.id) {
ParallelEligibility::UncommittedProposalFiles
} else {
ParallelEligibility::Eligible
};
}
app.publish_parallel_runtime();
app.apply_display_statuses_from_reducer(&harness.state.read().await.all_display_statuses());
app.sync_execution_marks_from_store();
app.execution_mode = AppExecutionMode::Running;
app.warning_message = None;
let web = Arc::new(WebState::new(&[]));
web.set_shared_state(harness.state.clone()).await;
web.set_execution_marks(harness.marks.clone()).await;
web.set_parallel_runtime(harness.parallel.clone()).await;
web.set_repo_root(std::path::PathBuf::from("/repo")).await;
let changes: Vec<_> = CHANGES.iter().map(|id| create_test_change(id)).collect();
web.seed_workspace_observation_for_tests(&changes, AppExecutionMode::Running.app_mode_token())
.await;
web.sync_remote_control_projection().await;
let runs = Arc::new(RunRecorder::default());
harness.attach(Arc::new(WebEventSink::new(web.clone())));
harness.attach(runs.clone());
harness.attach_revisions(web.clone());
harness.core_mode.set(OperatorMode::Running);
Wired {
harness,
app,
web,
runs,
baseline_revision: 0,
baseline_dispatches: 0,
}
}
async fn arranged(setup: Setup, scheduler_live: bool) -> Wired {
let mut wired = running(setup, scheduler_live).await;
wired
.dispatch(ExecutionEvent::PersistentSchedulerIdle)
.await;
let parked = wired.settled().await;
assert_eq!(
(
parked.core_mode,
parked.tui_mode,
parked.tui_idle,
parked.web_mode.as_str(),
parked.web_idle
),
(
OperatorMode::Select,
AppExecutionMode::Select,
true,
"select",
true
),
"the arrangement is persistent-idle Ready in Core and both frontends"
);
wired.baseline();
wired
}
async fn arranged_after_all_completed(setup: Setup, scheduler_live: bool) -> Wired {
let mut wired = running(setup, scheduler_live).await;
wired.dispatch(ExecutionEvent::AllCompleted).await;
let settled = wired.settled().await;
assert_eq!(
(
settled.core_mode,
settled.tui_mode,
settled.tui_idle,
settled.web_mode.as_str(),
settled.web_idle
),
(
OperatorMode::Select,
AppExecutionMode::Select,
false,
"select",
false
),
"an `AllCompleted` settlement leaves Ready with no idle episode of its own"
);
wired.baseline();
wired
}
async fn start_through_tui(setup: Setup, scheduler_live: bool) -> (Settled, String) {
let mut wired = arranged(setup, scheduler_live).await;
wired
.harness
.run(&mut wired.app, TuiCommand::StartProcessing(Vec::new()))
.await;
let report = wired
.app
.warning_message
.clone()
.or_else(|| wired.app.logs.last().map(|entry| entry.message.clone()))
.unwrap_or_default();
(wired.settled().await, report)
}
async fn start_through_v2(
setup: Setup,
scheduler_live: bool,
) -> (Settled, std::result::Result<Option<u64>, ErrorCode>) {
let mut wired = arranged(setup, scheduler_live).await;
let executor = SharedServiceExecutor::new(wired.harness.application.clone(), wired.web.clone());
let outcome = match executor.execute(&CommandSpec::Start).await {
Ok(summary) => Ok(summary.result_revision),
Err(failure) => Err(failure.error_code),
};
wired.harness.deliver(&mut wired.app).await;
(wired.settled().await, outcome)
}
#[tokio::test]
async fn idle_start_running_accepted_start_projects_running_on_every_adapter() {
let (tui, _) = start_through_tui(Setup::MarkedOrdinary, true).await;
let (v2, outcome) = start_through_v2(Setup::MarkedOrdinary, true).await;
assert_eq!(
tui, v2,
"a keypress and a remote Start must settle the same idle episode identically"
);
assert!(
matches!(outcome, Ok(Some(_))),
"the accepted Start names the revision its projection landed in, got {outcome:?}"
);
assert_eq!(
(tui.core_mode, tui.tui_mode, tui.web_mode.as_str()),
(OperatorMode::Running, AppExecutionMode::Running, "running"),
"the accepted Start opens the run episode everywhere at once"
);
assert!(
!tui.tui_idle && !tui.web_idle,
"the accepted Start closes the idle presentation episode"
);
assert_eq!(
tui.statuses,
vec![
(ALPHA.to_string(), "queued".to_string()),
(BETA.to_string(), "queued".to_string()),
],
"row status comes from reducer queue intent, not from the mode"
);
assert_eq!(
tui.runs,
vec![DispatchedRun {
change_ids: vec![ALPHA.to_string(), BETA.to_string()],
explicit_retry: false,
scheduler_started: false,
}],
"exactly one accepted dispatch, and it woke rather than spawned"
);
assert_eq!(
tui.scheduler,
vec![SchedulerCall::Notified],
"the live scheduler is notified and no second scheduler task is spawned"
);
assert_eq!(
(tui.dispatches, tui.revisions),
(1, 1),
"one command publishes one authoritative dispatch and one revision"
);
}
#[tokio::test]
async fn idle_start_running_accepted_retry_projects_running_without_changing_routing() {
let (tui, _) = start_through_tui(Setup::MarkedRetryableError, true).await;
let (v2, outcome) = start_through_v2(Setup::MarkedRetryableError, true).await;
assert_eq!(tui, v2, "retry routing is one shared decision");
assert!(matches!(outcome, Ok(Some(_))));
assert_eq!(
(tui.core_mode, tui.tui_mode, tui.web_mode.as_str()),
(OperatorMode::Running, AppExecutionMode::Running, "running"),
"an accepted retry opens the run episode like any other accepted Start"
);
assert!(!tui.tui_idle && !tui.web_idle);
assert_eq!(
tui.runs,
vec![DispatchedRun {
change_ids: vec![ALPHA.to_string()],
explicit_retry: true,
scheduler_started: false,
}],
"the retry keeps its explicit-retry semantics and its woken scheduler"
);
assert_eq!(
tui.explicit_retries,
vec![ALPHA.to_string()],
"the one-shot retry edge is armed exactly once, as before"
);
assert_eq!(tui.scheduler, vec![SchedulerCall::Notified]);
}
#[tokio::test]
async fn idle_start_running_refused_and_stale_start_stay_ready() {
let (targetless, report) = start_through_tui(Setup::Unmarked, true).await;
assert_eq!(
(
targetless.core_mode,
targetless.tui_mode,
targetless.web_mode.as_str()
),
(OperatorMode::Select, AppExecutionMode::Select, "select"),
"a Start with nothing to start leaves Ready alone"
);
assert!(
targetless.tui_idle && targetless.web_idle,
"a refused Start does not close the idle episode"
);
assert!(
targetless.runs.is_empty() && targetless.scheduler.is_empty(),
"nothing was dispatched, so nothing may be projected"
);
assert_eq!(
(targetless.dispatches, targetless.revisions),
(0, 0),
"a refusal publishes no authoritative outcome at all"
);
assert!(
!report.is_empty(),
"the operator is told the Start did nothing"
);
let (ineligible, _) = start_through_tui(Setup::MarkedIneligible, true).await;
assert_eq!(
(
ineligible.core_mode,
ineligible.tui_mode,
ineligible.web_mode.as_str()
),
(OperatorMode::Select, AppExecutionMode::Select, "select"),
"an ineligible target set is not an accepted Start either"
);
assert!(ineligible.tui_idle && ineligible.web_idle);
assert!(ineligible.runs.is_empty());
let (stale, _) = start_through_tui(Setup::MarkedOrdinary, false).await;
assert_eq!(
stale.runs,
vec![DispatchedRun {
change_ids: vec![ALPHA.to_string(), BETA.to_string()],
explicit_retry: false,
scheduler_started: true,
}],
"a stale presentation fact cannot make a dead scheduler a woken one"
);
assert_eq!(
stale.scheduler,
vec![SchedulerCall::Started {
targets: vec![ALPHA.to_string(), BETA.to_string()],
explicit_retry: false,
}],
"the ordinary route spawns the scheduler the presentation claimed existed"
);
assert_eq!(
(stale.core_mode, stale.tui_mode, stale.web_mode.as_str()),
(OperatorMode::Running, AppExecutionMode::Running, "running"),
"a really-started scheduler is Running by the pre-existing rule"
);
assert!(!stale.tui_idle && !stale.web_idle);
}
#[tokio::test]
async fn idle_start_running_raw_key_input_projects_nothing() {
let mut wired = arranged(Setup::MarkedOrdinary, true).await;
let mut ctx = wired.harness.context(&mut wired.app);
handle_tui_command(
TuiCommand::StartProcessing(Vec::new()),
&mut ctx,
&wired.harness.state,
)
.await
.expect("the key handler accepts the intent");
let pressed = wired.settled().await;
assert_eq!(
(
pressed.core_mode,
pressed.tui_mode,
pressed.web_mode.as_str()
),
(OperatorMode::Select, AppExecutionMode::Select, "select"),
"the key handler records intent; only an accepted outcome projects Running"
);
assert!(pressed.tui_idle && pressed.web_idle);
assert!(pressed.runs.is_empty());
assert_eq!((pressed.dispatches, pressed.revisions), (0, 0));
wired.harness.settle(&mut wired.app).await;
let settled = wired.settled().await;
assert_eq!(
(
settled.core_mode,
settled.tui_mode,
settled.web_mode.as_str()
),
(OperatorMode::Running, AppExecutionMode::Running, "running"),
"acceptance, not the keypress, is what opens the run episode"
);
}
#[tokio::test]
async fn idle_start_running_non_start_enqueue_does_not_project_running() {
let mut wired = arranged(Setup::Unmarked, true).await;
let executor = SharedServiceExecutor::new(wired.harness.application.clone(), wired.web.clone());
executor
.execute(&CommandSpec::SetQueueIntent {
change_id: ALPHA.to_string(),
queued: true,
})
.await
.expect("a client may add queue intent to a live idle scheduler");
wired.harness.deliver(&mut wired.app).await;
let enqueued = wired.settled().await;
assert_eq!(
(
enqueued.core_mode,
enqueued.tui_mode,
enqueued.web_mode.as_str()
),
(OperatorMode::Select, AppExecutionMode::Select, "select"),
"queue admission without an accepted Start outcome is not a run episode"
);
assert!(
enqueued.tui_idle && enqueued.web_idle,
"the idle episode survives a non-Start queue delta"
);
assert!(
enqueued.runs.is_empty(),
"no run dispatch exists to be read as an accepted Start"
);
assert_eq!(
enqueued.statuses,
vec![
(ALPHA.to_string(), "queued".to_string()),
(BETA.to_string(), "not queued".to_string()),
],
"the queue intent really was committed"
);
let before = wired.settled().await;
wired
.dispatch(ExecutionEvent::PersistentSchedulerIdle)
.await;
let after = wired.settled().await;
assert_eq!(
(
after.core_mode,
after.tui_mode,
after.tui_idle,
after.web_mode.clone(),
after.web_idle
),
(
before.core_mode,
before.tui_mode,
before.tui_idle,
before.web_mode.clone(),
before.web_idle
),
"a no-work idle edge observed while Select changes nothing"
);
wired
.dispatch(ExecutionEvent::WorkspacePreparationStarted {
change_id: ALPHA.to_string(),
})
.await;
let working = wired.settled().await;
assert_eq!(
(
working.core_mode,
working.tui_mode,
working.web_mode.as_str()
),
(OperatorMode::Running, AppExecutionMode::Running, "running"),
"typed admitted work remains the Running trigger for non-Start admission"
);
assert!(!working.tui_idle && !working.web_idle);
}
#[tokio::test]
async fn idle_start_running_no_work_park_returns_every_adapter_to_ready() {
let mut wired = arranged(Setup::MarkedOrdinary, true).await;
wired
.harness
.run(&mut wired.app, TuiCommand::StartProcessing(Vec::new()))
.await;
assert_eq!(wired.settled().await.tui_mode, AppExecutionMode::Running);
wired
.dispatch(ExecutionEvent::AnalysisStarted {
remaining_changes: 2,
attempt_id: "attempt-1".to_string(),
})
.await;
let analysing = wired.settled().await;
assert_eq!(
(
analysing.core_mode,
analysing.tui_mode,
analysing.web_mode.as_str()
),
(OperatorMode::Running, AppExecutionMode::Running, "running"),
"dependency analysis neither opens nor closes the episode"
);
wired
.dispatch(ExecutionEvent::PersistentSchedulerIdle)
.await;
let parked = wired.settled().await;
assert_eq!(
(
parked.core_mode,
parked.tui_mode,
parked.tui_idle,
parked.web_mode.as_str(),
parked.web_idle
),
(
OperatorMode::Select,
AppExecutionMode::Select,
true,
"select",
true
),
"one fresh idle transition returns Core and both frontends to Ready"
);
}
#[tokio::test]
async fn idle_start_running_cancel_stop_restores_the_interrupted_episode() {
let mut wired = arranged(Setup::MarkedOrdinary, true).await;
wired.harness.run(&mut wired.app, TuiCommand::Stop).await;
let stopping = wired.settled().await;
assert_eq!(
(
stopping.core_mode,
stopping.tui_mode,
stopping.web_mode.as_str()
),
(
OperatorMode::Stopping,
AppExecutionMode::Stopping,
"stopping"
)
);
assert!(
stopping.tui_idle && stopping.web_idle,
"an idle-origin stop keeps its episode identity"
);
assert_eq!(wired.app.stop_mode, StopMode::GracefulPending);
wired
.harness
.run(&mut wired.app, TuiCommand::CancelStop)
.await;
let cancelled = wired.settled().await;
assert_eq!(
(
cancelled.core_mode,
cancelled.tui_mode,
cancelled.tui_idle,
cancelled.web_mode.as_str(),
cancelled.web_idle
),
(
OperatorMode::Select,
AppExecutionMode::Select,
true,
"select",
true
),
"withdrawing an idle-origin stop restores Ready, not Running"
);
let mut wired = arranged(Setup::MarkedOrdinary, true).await;
wired
.harness
.run(&mut wired.app, TuiCommand::StartProcessing(Vec::new()))
.await;
wired.harness.run(&mut wired.app, TuiCommand::Stop).await;
assert_eq!(wired.settled().await.tui_mode, AppExecutionMode::Stopping);
wired
.harness
.run(&mut wired.app, TuiCommand::CancelStop)
.await;
let after_start = wired.settled().await;
assert_eq!(
(
after_start.core_mode,
after_start.tui_mode,
after_start.tui_idle,
after_start.web_mode.as_str(),
after_start.web_idle
),
(
OperatorMode::Running,
AppExecutionMode::Running,
false,
"running",
false
),
"the accepted Start already closed the episode, so cancel-stop returns to it"
);
let mut wired = arranged(Setup::MarkedOrdinary, true).await;
wired.harness.run(&mut wired.app, TuiCommand::Stop).await;
wired
.dispatch(ExecutionEvent::WorkspacePreparationStarted {
change_id: ALPHA.to_string(),
})
.await;
let raced = wired.settled().await;
assert_eq!(
(raced.core_mode, raced.tui_mode, raced.web_mode.as_str()),
(
OperatorMode::Stopping,
AppExecutionMode::Stopping,
"stopping"
),
"a pending graceful stop outranks work that started after it was requested"
);
assert!(
!raced.tui_idle && !raced.web_idle,
"work really started, so the idle episode is over even under a stop"
);
wired
.harness
.run(&mut wired.app, TuiCommand::CancelStop)
.await;
let resumed = wired.settled().await;
assert_eq!(
(
resumed.core_mode,
resumed.tui_mode,
resumed.tui_idle,
resumed.web_mode.as_str(),
resumed.web_idle
),
(
OperatorMode::Running,
AppExecutionMode::Running,
false,
"running",
false
),
"cancel-stop returns to Running once work has closed the episode"
);
}
fn scheduler_config() -> crate::config::OrchestratorConfig {
crate::config::OrchestratorConfig {
apply_command: Some("echo apply {change_id}".to_string()),
archive_command: Some("echo archive {change_id}".to_string()),
analyze_command: Some("echo analyze".to_string()),
acceptance_command: Some("echo acceptance".to_string()),
resolve_command: Some("echo resolve".to_string()),
..Default::default()
}
}
fn scheduler_over(
wired: &Wired,
graceful_stop: Arc<std::sync::atomic::AtomicBool>,
) -> (
tempfile::TempDir,
crate::parallel::ParallelExecutor,
tokio::sync::mpsc::Receiver<ExecutionEvent>,
) {
let repo = tempfile::TempDir::new().expect("a workspace root for the scheduler");
let (events_tx, events_rx) = tokio::sync::mpsc::channel(8);
let mut executor = crate::parallel::ParallelExecutor::new(
repo.path().to_path_buf(),
scheduler_config(),
Some(events_tx),
);
executor.set_persistent_lifetime();
executor.set_shared_orchestrator_state(wired.harness.state.clone());
executor.set_graceful_stop_flag(graceful_stop);
(repo, executor, events_rx)
}
#[tokio::test]
async fn idle_stopping_no_work_graceful_stop_settles_on_every_adapter() {
let mut wired = arranged_after_all_completed(Setup::MarkedOrdinary, true).await;
let graceful_stop = wired.harness.scheduler.graceful_stop_flag();
wired.harness.run(&mut wired.app, TuiCommand::Stop).await;
let stopping = wired.settled().await;
assert_eq!(
(
stopping.core_mode,
stopping.tui_mode,
stopping.web_mode.as_str()
),
(
OperatorMode::Stopping,
AppExecutionMode::Stopping,
"stopping"
),
"the accepted stop is projected as the pending request it is"
);
assert!(
graceful_stop.load(std::sync::atomic::Ordering::SeqCst),
"the accepted stop really recorded its request on the scheduler"
);
let (_repo, executor, mut scheduler_events) = scheduler_over(&wired, graceful_stop);
assert!(
!executor
.settle_graceful_stop_at_idle_boundary(&[create_test_change(ALPHA)])
.await,
"a graceful stop still owes the work boundary it asked to stop at"
);
assert!(
executor.settle_graceful_stop_at_idle_boundary(&[]).await,
"a graceful stop with nothing left owes no boundary, so this park is one"
);
let published = scheduler_events
.try_recv()
.expect("the scheduler published its own terminal stop at that boundary");
assert!(
matches!(published, ExecutionEvent::Stopped),
"the settlement is the run's one terminal stop, got {published:?}"
);
wired.dispatch(published).await;
let stopped = wired.settled().await;
assert_eq!(
(
stopped.core_mode,
stopped.tui_mode,
stopped.web_mode.as_str()
),
(OperatorMode::Stopped, AppExecutionMode::Stopped, "stopped"),
"Core, TUI, and Web all leave Stopping for the inactive state"
);
assert!(
!stopped.tui_idle && !stopped.web_idle,
"a terminal stop ends the idle episode on both frontends"
);
assert!(
stopped.runs.is_empty() && stopped.explicit_retries.is_empty(),
"no run, retry, or queue intent was synthesized to reach the stop"
);
assert_eq!(
stopped.statuses,
vec![
(ALPHA.to_string(), "not queued".to_string()),
(BETA.to_string(), "not queued".to_string()),
],
"the rows are exactly where the settled run left them"
);
}
#[tokio::test]
async fn idle_stopping_empty_boundary_settles_nothing_without_a_request() {
let wired = arranged(Setup::MarkedOrdinary, true).await;
let graceful_stop = wired.harness.scheduler.graceful_stop_flag();
let (_repo, executor, mut scheduler_events) = scheduler_over(&wired, graceful_stop);
assert!(
!executor.settle_graceful_stop_at_idle_boundary(&[]).await,
"an idle park with no pending stop stays parked"
);
assert!(
scheduler_events.try_recv().is_err(),
"a scheduler that did not settle publishes no terminal event"
);
}
#[tokio::test]
async fn idle_stopping_cancel_stop_after_all_completed_returns_to_ready() {
let mut wired = arranged_after_all_completed(Setup::MarkedOrdinary, true).await;
wired.harness.run(&mut wired.app, TuiCommand::Stop).await;
let stopping = wired.settled().await;
assert_eq!(
(
stopping.core_mode,
stopping.tui_mode,
stopping.tui_idle,
stopping.web_mode.as_str(),
stopping.web_idle
),
(
OperatorMode::Stopping,
AppExecutionMode::Stopping,
true,
"stopping",
true
),
"a stop admitted from parked Ready carries its idle-origin identity"
);
assert_eq!(wired.app.stop_mode, StopMode::GracefulPending);
wired
.harness
.run(&mut wired.app, TuiCommand::CancelStop)
.await;
let cancelled = wired.settled().await;
assert_eq!(
(
cancelled.core_mode,
cancelled.tui_mode,
cancelled.tui_idle,
cancelled.web_mode.as_str(),
cancelled.web_idle
),
(
OperatorMode::Select,
AppExecutionMode::Select,
true,
"select",
true
),
"withdrawing the stop restores the Ready it interrupted"
);
assert!(
cancelled.runs.is_empty(),
"nothing was dispatched, so no run episode may be claimed"
);
assert_eq!(
cancelled.statuses,
vec![
(ALPHA.to_string(), "not queued".to_string()),
(BETA.to_string(), "not queued".to_string()),
],
"no queue intent was synthesized by the stop or its withdrawal"
);
wired
.dispatch(ExecutionEvent::WorkspacePreparationStarted {
change_id: ALPHA.to_string(),
})
.await;
let working = wired.settled().await;
assert_eq!(
(
working.core_mode,
working.tui_mode,
working.web_mode.as_str()
),
(OperatorMode::Running, AppExecutionMode::Running, "running"),
"real work still opens the run episode from this Ready"
);
assert!(!working.tui_idle && !working.web_idle);
}
#[tokio::test]
async fn idle_start_running_force_stop_and_terminal_modes_are_preserved() {
let mut wired = arranged(Setup::MarkedOrdinary, true).await;
wired
.harness
.run(&mut wired.app, TuiCommand::StartProcessing(Vec::new()))
.await;
wired
.harness
.run(&mut wired.app, TuiCommand::ForceStop)
.await;
assert!(
wired
.harness
.scheduler
.calls()
.contains(&SchedulerCall::Cancelled),
"force stop cancels the same scheduler the Start woke"
);
wired.dispatch(ExecutionEvent::Stopped).await;
let stopped = wired.settled().await;
assert_eq!(
(
stopped.core_mode,
stopped.tui_mode,
stopped.web_mode.as_str()
),
(OperatorMode::Stopped, AppExecutionMode::Stopped, "stopped")
);
assert!(
!stopped.tui_idle && !stopped.web_idle,
"a terminal stop ends the idle episode"
);
wired
.dispatch(ExecutionEvent::PersistentSchedulerIdle)
.await;
let late = wired.settled().await;
assert_eq!(
(late.core_mode, late.tui_mode, late.web_mode.as_str()),
(OperatorMode::Stopped, AppExecutionMode::Stopped, "stopped"),
"the retained terminal mode is unchanged by a late idle event"
);
assert!(!late.tui_idle && !late.web_idle);
}