fix: require idle workers for submit

This commit is contained in:
2026-09-23 00:36:54 +09:00
parent d8444e49bd
commit 9857bf04f7
16 changed files with 451 additions and 264 deletions
+37 -7
View File
@@ -759,6 +759,12 @@ impl App {
}
return None;
}
if self.worker_status != WorkerStatus::Idle {
self.push_error(
"Submit requires an idle Worker; use Alt-n Notify for work already in progress.",
);
return None;
}
self.record_input_history(segments.clone());
self.input.clear();
Some(self.method_for_run(segments))
@@ -773,7 +779,9 @@ impl App {
.iter()
.any(|segment| matches!(segment, Segment::UploadedFile { .. }))
{
self.push_error("Notify accepts text only; remove attachments or queue a Submit.");
self.push_error(
"Notify accepts text only; remove attachments or wait until the Worker is idle to Submit.",
);
return None;
}
let message = Segment::flatten_to_text(&segments);
@@ -3514,16 +3522,22 @@ mod completion_flow_tests {
}
#[test]
fn running_submit_is_sent_to_the_worker_and_not_queued_locally() {
fn running_submit_is_rejected_locally_and_preserves_the_draft() {
let mut app = App::new("test".into());
app.set_worker_status(WorkerStatus::Running);
insert_text(&mut app, "queued turn");
insert_text(&mut app, "advisory context");
let method = app.submit_input();
assert!(matches!(method, Some(Method::Submit { .. })));
assert!(method.is_none());
assert_eq!(app.queued_input_count(), 0);
assert_eq!(input_text(&app), "");
assert_eq!(input_text(&app), "advisory context");
assert!(app.blocks.iter().any(|block| matches!(
block,
Block::Alert { message, .. }
if message.contains("requires an idle Worker")
&& message.contains("Alt-n Notify")
)));
}
#[test]
@@ -3554,6 +3568,17 @@ mod completion_flow_tests {
assert_eq!(app.queued_input_count(), 1);
}
#[test]
fn paused_non_empty_submit_is_rejected_locally_and_preserves_the_draft() {
let mut app = App::new("test".into());
app.set_worker_status(WorkerStatus::Paused);
insert_text(&mut app, "different request");
assert!(app.submit_input().is_none());
assert_eq!(input_text(&app), "different request");
assert_eq!(app.queued_input_count(), 0);
}
#[test]
fn paused_empty_submit_still_resumes_immediately() {
let mut app = App::new("test".into());
@@ -4609,13 +4634,18 @@ mod completion_flow_tests {
}
#[test]
fn input_history_records_running_submits_and_suppresses_consecutive_duplicates() {
fn input_history_records_only_an_idle_submit_after_running_rejection() {
let mut app = App::new("test".into());
app.running = true;
app.set_worker_status(WorkerStatus::Running);
for c in "repeat".chars() {
app.insert_char(c);
}
assert!(app.submit_input().is_none());
assert_eq!(app.input_history_len(), 0);
assert_eq!(input_text(&app), "repeat");
app.set_worker_status(WorkerStatus::Idle);
assert!(app.submit_input().is_some());
assert_eq!(app.input_history_len(), 1);
assert_eq!(app.queued_input_count(), 0);
+10 -16
View File
@@ -1682,8 +1682,8 @@ mod tests {
assert!(connection.awaiting_attachment_acceptance.is_empty());
}
#[tokio::test]
async fn running_attachment_submit_failure_restores_draft_without_exiting_console() {
#[test]
fn running_attachment_submit_is_rejected_without_consuming_the_draft() {
let file = UploadedFileRef {
artifact_id: "artifact-queued".into(),
file_name: "queued.txt".into(),
@@ -1694,7 +1694,7 @@ mod tests {
sha256: "a".repeat(64),
source_entry_id: None,
};
let mut connection = ConsoleConnection {
let connection = ConsoleConnection {
client: Client::new(FailOnceSocket {
fail_next_send: true,
}),
@@ -1709,13 +1709,10 @@ mod tests {
let mut app = App::new("worker".into());
app.set_worker_status(WorkerStatus::Running);
app.input.insert_str("queued inspect");
let method = app
.submit_input()
.expect("running Submit is sent immediately");
send_console_method(&mut app, &mut connection, &method)
.await
.unwrap();
assert!(
app.submit_input().is_none(),
"running Submit must be rejected before transport"
);
assert_eq!(app.input.plain_text(), "queued inspect");
assert_eq!(connection.pending_attachments, vec![file]);
@@ -2035,7 +2032,7 @@ mod tests {
}
#[test]
fn running_enter_sends_submit_to_worker() {
fn running_enter_rejects_submit_and_preserves_the_draft() {
let mut app = App::new("agent".to_string());
app.set_worker_status(WorkerStatus::Running);
for c in "queued".chars() {
@@ -2048,13 +2045,10 @@ mod tests {
);
}
assert!(matches!(
handle_key(&mut app, KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)),
Some(Method::Submit { .. })
));
assert!(handle_key(&mut app, KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)).is_none());
assert_eq!(app.queued_input_count(), 0);
assert_eq!(input_text(&app), "");
assert_eq!(input_text(&app), "queued");
}
#[test]
+1 -1
View File
@@ -1868,7 +1868,7 @@ fn draw_status(frame: &mut Frame, app: &App, area: Rect) {
.add_modifier(Modifier::BOLD),
));
spans.push(Span::styled(
" — Enter to resume, Ctrl-X to cancel, type to start new turn",
" — Enter to resume, Ctrl-X to cancel, Alt-n to notify",
Style::default().fg(Color::DarkGray),
));
} else if !app.running {