review(회차1): refusal yellow + Ctrl-L race fix
회차 1 actionable 2건 반영. - (UX 회귀) push_turn_lines 가 answer_color_override: Option<Color> 추가 받음. render_answer 가 마지막 turn 에 한해 last_answer.grounded == false 면 Yellow override 전달 → P9-3 의 refusal 시각 구분 contract 가 transcript 안에서도 보존. test: render_refusal_turn_in_transcript_uses_yellow_when_last_answer_ungrounded 가 buffer 의 Yellow R 셀 검사로 검증. - (race) Ctrl-L 가 turns/conversation_id/last_answer/partial/ current_question/scroll 외에도 thread/rx/streaming 까지 detach. in-flight worker 가 다음 frame 에 finish 해도 새 conv 의 stale Turn 으로 graduate 안 됨 — JoinHandle Drop 으로 detach (P9-3 Esc cancel pattern 동일). worker 자체는 background 에서 SQLite answers 에 \"실패한 conv\" 흔적 commit. ctrl_l_clears_conversation_state test 가 streaming/thread/rx 도 함께 검증. 18 PASS. clippy clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -414,6 +414,9 @@ fn ctrl_l_clears_conversation_state() {
|
||||
s.partial = "leftover".into();
|
||||
s.current_question = Some("in flight".into());
|
||||
s.scroll = 5;
|
||||
s.streaming = true;
|
||||
// Note: thread / rx 는 JoinHandle 인 만큼 직접 mock 어려움 —
|
||||
// streaming flag 만으로 detach side-effect 검증.
|
||||
}
|
||||
let outcome = handle_key_ask(
|
||||
&mut app,
|
||||
@@ -427,6 +430,59 @@ fn ctrl_l_clears_conversation_state() {
|
||||
assert!(s.partial.is_empty(), "partial cleared");
|
||||
assert!(s.current_question.is_none(), "current_question cleared");
|
||||
assert_eq!(s.scroll, 0, "scroll reset");
|
||||
// 회차 1 fix: streaming flag + thread/rx 도 detach.
|
||||
assert!(!s.streaming, "streaming flag cleared");
|
||||
assert!(s.thread.is_none(), "thread detached");
|
||||
assert!(s.rx.is_none(), "rx detached");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_refusal_turn_in_transcript_uses_yellow_when_last_answer_ungrounded() {
|
||||
let mut app = fresh_app();
|
||||
{
|
||||
let s = app.ask.as_mut().unwrap();
|
||||
let mut ans = make_answer(false, Some(RefusalReason::ScoreGate), "REFUSED BODY");
|
||||
ans.citations.clear();
|
||||
s.turns.push(Turn {
|
||||
question: "Q".into(),
|
||||
answer: ans.answer.clone(),
|
||||
citations: Vec::new(),
|
||||
created_at: ans.created_at,
|
||||
});
|
||||
s.last_answer = Some(ans);
|
||||
}
|
||||
let backend = TestBackend::new(80, 24);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let area = Rect::new(0, 0, 80, 24);
|
||||
render_ask(f, area, &app);
|
||||
})
|
||||
.unwrap();
|
||||
// Find the cell containing the first character of REFUSED BODY
|
||||
// and assert its fg is Yellow (the refusal-style override).
|
||||
let buffer = terminal.backend().buffer().clone();
|
||||
let mut found = None;
|
||||
for y in 0..buffer.area.height {
|
||||
for x in 0..buffer.area.width {
|
||||
let cell = &buffer[(x, y)];
|
||||
if cell.symbol() == "R" {
|
||||
// First R after Q: line — likely the answer body.
|
||||
// Check fg color.
|
||||
if let ratatui::style::Color::Yellow = cell.fg {
|
||||
found = Some((x, y));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if found.is_some() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
found.is_some(),
|
||||
"expected at least one yellow R cell from REFUSED BODY in the transcript"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user