review(p9-3): 회차 1 지적 반영

Esc 후 재질문 시 detached prior worker + 새 worker 동시 in-flight 가능
했음. Ollama endpoint 에 두 요청 동시 발사 → 응답 시간 두 배 + stream
혼동. spawn_ask_worker 진입 시 `s.thread.is_some()` 검사 추가, 이전
worker 가 still alive 면 Enter 무시. input bar 의 busy 텍스트 가 세
상태 (streaming / awaiting prior / idle) 분리 표시 — 사용자가 Enter
가 왜 안 먹히는지 즉시 확인.

회귀 테스트 `enter_with_detached_prior_thread_is_blocked` 추가 — never-
ending 더미 thread 를 hand-install 후 Enter no-op 검증, 종료 시 thread
take() 로 leak 명시 (test process 종료 시 OS 가 reap).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-02 15:27:39 +00:00
parent f08fefec1d
commit ad7bd7d309
2 changed files with 56 additions and 2 deletions

View File

@@ -328,6 +328,41 @@ fn explain_toggle_changes_panel_title() {
);
}
#[test]
fn enter_with_detached_prior_thread_is_blocked() {
// R1 fix: after Esc, the prior worker is detached (thread still
// running, rx cleared, streaming=false). A new Enter must NOT
// spawn a second worker against the same Ollama endpoint until
// the prior thread finishes.
let mut app = fresh_app();
{
let s = app.ask.as_mut().unwrap();
s.input = "another question".into();
s.streaming = false;
// Simulate a detached prior worker by hand-installing a
// never-ending JoinHandle. (We can't easily make a sleeping
// thread without timing flakiness; an empty-loop shim works.)
s.thread = Some(std::thread::spawn(|| {
// Loop until the test drops the JoinHandle's owner via
// App going out of scope. is_finished() will report
// false until then.
loop {
std::thread::sleep(std::time::Duration::from_millis(100));
}
}));
}
let outcome = handle_key_ask(
&mut app,
KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE),
);
// Enter is a no-op while a prior thread is attached.
assert_eq!(outcome, KeyOutcome::Continue);
let s = app.ask.as_ref().unwrap();
assert!(!s.streaming, "no second worker spawned");
// Detach so the never-ending thread can be reaped on test exit.
let _leaked = app.ask.as_mut().unwrap().thread.take();
}
#[test]
fn no_ask_state_returns_to_library() {
let mut config = Config::defaults();