review(p9-fb-08): 회차 1 nit 반영

- `SearchState.worker_thread` 필드 제거 — `JoinHandle` 을 저장만 하고
  어디서도 join 안 했음. fire_search 가 spawn 후 handle 을 즉시 drop
  하면 OS 가 thread 를 detach (search 는 pure read 라 cleanup 의무
  없음). YAGNI — ask.rs 의 thread 와 달리 cancel/observe 수요가 없는
  fire-and-forget. doc 으로 의도 명시.
- `debounce_due` 가 `pub` 으로 노출 — 새 skip 분기 (`searching && 같은
  query`) 회귀 테스트 추가:
  - `debounce_due_skips_when_in_flight_for_same_query`: 같은 input/mode
    재입력 시 spawn 안 함 (worker 누적 방지)
  - `debounce_due_fires_when_in_flight_for_different_query`: 사용자가
    in-flight 보다 빠르게 새 query 입력하면 정상 spawn (poll_worker 의
    stale guard 가 이전 결과 처리)
- `search_state_with` 헬퍼: `SearchState::default()` + field 재할당
  패턴이 clippy `field_reassign_with_default` 위반 → `#[allow(...)]`
  로 lint 무시 (테스트 helper 의 가독성 우선).

23 tests/search.rs + 35 lib + 18 ask + 12 inspect + 10 library = 98
통과. clippy clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-03 03:55:27 +00:00
parent fd8597c696
commit a99f81398c
4 changed files with 76 additions and 21 deletions

View File

@@ -8,7 +8,7 @@ use kebab_core::{
};
use kebab_tui::{
App, KeyOutcome, Pane, SearchState, SearchWorkerMessage, build_jump_command,
handle_key_search, poll_search_worker, render_search,
handle_key_search, poll_search_worker, render_search, search_debounce_due,
};
use ratatui::Terminal;
use ratatui::backend::TestBackend;
@@ -410,6 +410,56 @@ fn poll_worker_noop_when_no_rx() {
assert!(s.worker_rx.is_none());
}
/// Helper for the debounce_due tests — build a state with the four
/// fields the test cares about set, others default.
#[allow(clippy::field_reassign_with_default)]
fn search_state_with(input: &str, mode: SearchMode, searching: bool, last_query: Option<(String, SearchMode)>) -> SearchState {
let mut s = SearchState::default();
s.input = input.into();
s.mode = mode;
s.searching = searching;
s.last_query = last_query;
s.input_dirty_at = Some(
time::OffsetDateTime::now_utc() - time::Duration::seconds(1),
);
s
}
/// p9-fb-08 — `debounce_due` skips when an in-flight worker is
/// already running for the same `(input, mode)` pair. Without this
/// guard, a "phantom keystroke" (re-typing the same chars) would
/// pile up workers and burn CPU.
#[test]
fn debounce_due_skips_when_in_flight_for_same_query() {
let s = search_state_with(
"hello",
SearchMode::Hybrid,
true,
Some(("hello".into(), SearchMode::Hybrid)),
);
assert!(
!search_debounce_due(&s),
"in-flight worker for same query → debounce must skip"
);
}
/// p9-fb-08 — `debounce_due` still fires when a different query is
/// in flight (user typed past the in-flight one). The new spawn
/// makes the prior result stale (handled by `poll_worker`).
#[test]
fn debounce_due_fires_when_in_flight_for_different_query() {
let s = search_state_with(
"hello world",
SearchMode::Hybrid,
true,
Some(("hello".into(), SearchMode::Hybrid)),
);
assert!(
search_debounce_due(&s),
"in-flight worker for old query → new query still spawns"
);
}
/// p9-fb-08 — disconnected channel (worker panicked) clears the rx
/// + searching flag so the next debounce tick can re-fire cleanly.
#[test]