style: cargo fmt --all (round 4 ingest log feature follow-up)
Phase C4 executor 의 마지막 `fix(test): clippy + fmt fixes` commit 이 test file 부분만 fmt 적용. workspace 전체 fmt 누락 발견 → cargo fmt --all 적용. 모든 import alphabetical reorder + line wrapping 정합. 추가 untracked artifact 동시 commit: - docs/superpowers/specs/2026-05-28-v0.20-ingest-log-spec.md (491 line, ACCEPT) - docs/superpowers/plans/2026-05-28-v0.20-ingest-log-plan.md (616 line, ACCEPT) workspace test: 1370 passed / 0 failed / 50 ignored, ingest_log_smoke green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,9 +7,8 @@
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
use kebab_config::Config;
|
||||
use kebab_core::{
|
||||
Answer, AnswerCitation, AnswerRetrievalSummary, Citation, ModelRef,
|
||||
PromptTemplateVersion, RefusalReason, SearchMode, TokenUsage, TraceId, Turn,
|
||||
WorkspacePath,
|
||||
Answer, AnswerCitation, AnswerRetrievalSummary, Citation, ModelRef, PromptTemplateVersion,
|
||||
RefusalReason, SearchMode, TokenUsage, TraceId, Turn, WorkspacePath,
|
||||
};
|
||||
use kebab_tui::{App, AskState, KeyOutcome, Pane, handle_key_ask, render_ask};
|
||||
use ratatui::Terminal;
|
||||
@@ -90,10 +89,7 @@ fn esc_returns_to_library_and_clears_streaming() {
|
||||
s.streaming = true;
|
||||
s.partial = "partial answer…".into();
|
||||
}
|
||||
let outcome = handle_key_ask(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE),
|
||||
);
|
||||
let outcome = handle_key_ask(&mut app, KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
|
||||
assert_eq!(outcome, KeyOutcome::SwitchPane(Pane::Library));
|
||||
let s = app.ask.as_ref().unwrap();
|
||||
assert!(!s.streaming);
|
||||
@@ -155,12 +151,20 @@ fn jk_scroll_in_normal_mode_type_in_insert() {
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::Char('j'), KeyModifiers::NONE),
|
||||
);
|
||||
assert_eq!(app.ask.as_ref().unwrap().scroll, 1, "j scrolls down in Normal");
|
||||
assert_eq!(
|
||||
app.ask.as_ref().unwrap().scroll,
|
||||
1,
|
||||
"j scrolls down in Normal"
|
||||
);
|
||||
handle_key_ask(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::Char('k'), KeyModifiers::NONE),
|
||||
);
|
||||
assert_eq!(app.ask.as_ref().unwrap().scroll, 0, "k scrolls up in Normal");
|
||||
assert_eq!(
|
||||
app.ask.as_ref().unwrap().scroll,
|
||||
0,
|
||||
"k scrolls up in Normal"
|
||||
);
|
||||
// Now Insert — j/k type.
|
||||
app.mode = kebab_tui::Mode::Insert;
|
||||
handle_key_ask(
|
||||
@@ -213,10 +217,7 @@ fn e_typed_into_input_when_input_nonempty() {
|
||||
#[test]
|
||||
fn enter_with_empty_input_is_continue() {
|
||||
let mut app = fresh_app();
|
||||
let outcome = handle_key_ask(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE),
|
||||
);
|
||||
let outcome = handle_key_ask(&mut app, KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
assert_eq!(outcome, KeyOutcome::Continue);
|
||||
assert!(!app.ask.as_ref().unwrap().streaming);
|
||||
}
|
||||
@@ -229,10 +230,7 @@ fn enter_while_streaming_is_noop() {
|
||||
s.input.push_str("anything");
|
||||
s.streaming = true;
|
||||
}
|
||||
handle_key_ask(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE),
|
||||
);
|
||||
handle_key_ask(&mut app, KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
// streaming flag remains true (no new worker spawned)
|
||||
assert!(app.ask.as_ref().unwrap().streaming);
|
||||
// No thread spawned because enter was a no-op.
|
||||
@@ -335,7 +333,10 @@ fn render_grounded_answer_with_citation() {
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
assert!(rendered.contains("test answer body"), "answer body rendered");
|
||||
assert!(
|
||||
rendered.contains("test answer body"),
|
||||
"answer body rendered"
|
||||
);
|
||||
assert!(rendered.contains("grounded ✓"), "grounded status visible");
|
||||
assert!(rendered.contains("notes/foo.md"), "citation path rendered");
|
||||
assert!(rendered.contains("[1]"), "citation marker rendered");
|
||||
@@ -346,7 +347,11 @@ fn render_refusal_score_gate_shows_status_without_citation_index_panic() {
|
||||
let mut app = fresh_app();
|
||||
{
|
||||
let s = app.ask.as_mut().unwrap();
|
||||
let mut ans = make_answer(false, Some(RefusalReason::ScoreGate), "insufficient grounding to answer.");
|
||||
let mut ans = make_answer(
|
||||
false,
|
||||
Some(RefusalReason::ScoreGate),
|
||||
"insufficient grounding to answer.",
|
||||
);
|
||||
ans.citations.clear(); // refusal often has no citations
|
||||
s.turns.push(Turn {
|
||||
question: "test refusal question".into(),
|
||||
@@ -374,7 +379,10 @@ fn render_refusal_score_gate_shows_status_without_citation_index_panic() {
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
assert!(rendered.contains("insufficient grounding"), "refusal body rendered");
|
||||
assert!(
|
||||
rendered.contains("insufficient grounding"),
|
||||
"refusal body rendered"
|
||||
);
|
||||
assert!(rendered.contains("grounded ✗"), "ungrounded status visible");
|
||||
assert!(rendered.contains("score_gate"), "refusal reason surfaced");
|
||||
}
|
||||
@@ -535,10 +543,7 @@ fn enter_with_detached_prior_thread_is_blocked() {
|
||||
}
|
||||
}));
|
||||
}
|
||||
let outcome = handle_key_ask(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE),
|
||||
);
|
||||
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();
|
||||
@@ -693,7 +698,10 @@ fn render_transcript_shows_completed_turns_in_order() {
|
||||
assert!(q1_pos < q2_pos, "chronological order: Q1 before Q2");
|
||||
assert!(rendered.contains("first question"), "first question text");
|
||||
assert!(rendered.contains("second answer"), "second answer text");
|
||||
assert!(rendered.contains("transcript (2 turns)"), "title shows count");
|
||||
assert!(
|
||||
rendered.contains("transcript (2 turns)"),
|
||||
"title shows count"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -772,10 +780,16 @@ fn left_arrow_then_typing_inserts_at_cursor_in_ask() {
|
||||
let mut app = fresh_app();
|
||||
app.mode = kebab_tui::Mode::Insert;
|
||||
for ch in "abc".chars() {
|
||||
handle_key_ask(&mut app, KeyEvent::new(KeyCode::Char(ch), KeyModifiers::NONE));
|
||||
handle_key_ask(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::Char(ch), KeyModifiers::NONE),
|
||||
);
|
||||
}
|
||||
handle_key_ask(&mut app, KeyEvent::new(KeyCode::Left, KeyModifiers::NONE));
|
||||
handle_key_ask(&mut app, KeyEvent::new(KeyCode::Char('X'), KeyModifiers::NONE));
|
||||
handle_key_ask(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::Char('X'), KeyModifiers::NONE),
|
||||
);
|
||||
let s = app.ask.as_ref().unwrap();
|
||||
assert_eq!(s.input.as_str(), "abXc", "X inserts before c, not at end");
|
||||
assert_eq!(s.input.cursor_col(), 3, "cursor sits between X and c");
|
||||
@@ -787,7 +801,10 @@ fn left_arrow_then_typing_inserts_at_cursor_in_ask() {
|
||||
fn right_arrow_at_end_is_noop_in_ask() {
|
||||
let mut app = fresh_app();
|
||||
app.mode = kebab_tui::Mode::Insert;
|
||||
handle_key_ask(&mut app, KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE));
|
||||
handle_key_ask(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE),
|
||||
);
|
||||
handle_key_ask(&mut app, KeyEvent::new(KeyCode::Right, KeyModifiers::NONE));
|
||||
let s = app.ask.as_ref().unwrap();
|
||||
assert_eq!(s.input.cursor_col(), 1);
|
||||
@@ -800,7 +817,10 @@ fn home_end_jump_cursor_in_ask() {
|
||||
let mut app = fresh_app();
|
||||
app.mode = kebab_tui::Mode::Insert;
|
||||
for ch in "hello".chars() {
|
||||
handle_key_ask(&mut app, KeyEvent::new(KeyCode::Char(ch), KeyModifiers::NONE));
|
||||
handle_key_ask(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::Char(ch), KeyModifiers::NONE),
|
||||
);
|
||||
}
|
||||
handle_key_ask(&mut app, KeyEvent::new(KeyCode::Home, KeyModifiers::NONE));
|
||||
assert_eq!(app.ask.as_ref().unwrap().input.cursor_col(), 0);
|
||||
@@ -815,7 +835,10 @@ fn delete_key_removes_char_at_cursor_in_ask() {
|
||||
let mut app = fresh_app();
|
||||
app.mode = kebab_tui::Mode::Insert;
|
||||
for ch in "abc".chars() {
|
||||
handle_key_ask(&mut app, KeyEvent::new(KeyCode::Char(ch), KeyModifiers::NONE));
|
||||
handle_key_ask(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::Char(ch), KeyModifiers::NONE),
|
||||
);
|
||||
}
|
||||
handle_key_ask(&mut app, KeyEvent::new(KeyCode::Home, KeyModifiers::NONE));
|
||||
handle_key_ask(&mut app, KeyEvent::new(KeyCode::Delete, KeyModifiers::NONE));
|
||||
@@ -831,14 +854,20 @@ fn hangul_left_arrow_rewinds_by_two_cols_in_ask() {
|
||||
let mut app = fresh_app();
|
||||
app.mode = kebab_tui::Mode::Insert;
|
||||
for ch in "한글".chars() {
|
||||
handle_key_ask(&mut app, KeyEvent::new(KeyCode::Char(ch), KeyModifiers::NONE));
|
||||
handle_key_ask(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::Char(ch), KeyModifiers::NONE),
|
||||
);
|
||||
}
|
||||
assert_eq!(app.ask.as_ref().unwrap().input.cursor_col(), 4);
|
||||
handle_key_ask(&mut app, KeyEvent::new(KeyCode::Left, KeyModifiers::NONE));
|
||||
assert_eq!(app.ask.as_ref().unwrap().input.cursor_col(), 2);
|
||||
// Inserting at the new cursor position lands between the two
|
||||
// syllables, proving cursor_col is not just a display annotation.
|
||||
handle_key_ask(&mut app, KeyEvent::new(KeyCode::Char('X'), KeyModifiers::NONE));
|
||||
handle_key_ask(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::Char('X'), KeyModifiers::NONE),
|
||||
);
|
||||
assert_eq!(app.ask.as_ref().unwrap().input.as_str(), "한X글");
|
||||
}
|
||||
|
||||
@@ -859,7 +888,10 @@ fn ask_state_default_follow_tail_is_true() {
|
||||
fn k_disengages_follow_tail_in_ask() {
|
||||
let mut app = fresh_app();
|
||||
app.mode = kebab_tui::Mode::Normal;
|
||||
handle_key_ask(&mut app, KeyEvent::new(KeyCode::Char('k'), KeyModifiers::NONE));
|
||||
handle_key_ask(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::Char('k'), KeyModifiers::NONE),
|
||||
);
|
||||
assert!(!app.ask.as_ref().unwrap().follow_tail);
|
||||
}
|
||||
|
||||
@@ -875,7 +907,10 @@ fn shift_g_re_engages_follow_tail_in_ask() {
|
||||
s.follow_tail = false;
|
||||
s.scroll = 7;
|
||||
}
|
||||
handle_key_ask(&mut app, KeyEvent::new(KeyCode::Char('G'), KeyModifiers::SHIFT));
|
||||
handle_key_ask(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::Char('G'), KeyModifiers::SHIFT),
|
||||
);
|
||||
let s = app.ask.as_ref().unwrap();
|
||||
assert!(s.follow_tail, "Shift-G re-engages follow-tail");
|
||||
assert_eq!(s.scroll, 0, "scroll cleared (renderer recomputes)");
|
||||
@@ -888,7 +923,10 @@ fn ctrl_l_resets_follow_tail_in_ask() {
|
||||
let mut app = fresh_app();
|
||||
app.mode = kebab_tui::Mode::Normal;
|
||||
app.ask.as_mut().unwrap().follow_tail = false;
|
||||
handle_key_ask(&mut app, KeyEvent::new(KeyCode::Char('l'), KeyModifiers::CONTROL));
|
||||
handle_key_ask(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::Char('l'), KeyModifiers::CONTROL),
|
||||
);
|
||||
assert!(app.ask.as_ref().unwrap().follow_tail);
|
||||
}
|
||||
|
||||
@@ -917,18 +955,12 @@ fn page_up_rewinds_scroll_saturating_and_freezes_follow_tail_in_ask() {
|
||||
app.mode = kebab_tui::Mode::Normal;
|
||||
app.ask.as_mut().unwrap().scroll = 25;
|
||||
app.ask.as_mut().unwrap().follow_tail = true;
|
||||
handle_key_ask(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::PageUp, KeyModifiers::NONE),
|
||||
);
|
||||
handle_key_ask(&mut app, KeyEvent::new(KeyCode::PageUp, KeyModifiers::NONE));
|
||||
let s = app.ask.as_ref().unwrap();
|
||||
assert_eq!(s.scroll, 15);
|
||||
assert!(!s.follow_tail);
|
||||
app.ask.as_mut().unwrap().scroll = 3;
|
||||
handle_key_ask(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::PageUp, KeyModifiers::NONE),
|
||||
);
|
||||
handle_key_ask(&mut app, KeyEvent::new(KeyCode::PageUp, KeyModifiers::NONE));
|
||||
assert_eq!(app.ask.as_ref().unwrap().scroll, 0);
|
||||
}
|
||||
|
||||
@@ -1173,7 +1205,6 @@ fn ask_state_multi_hop_field_default_false_and_round_trips() {
|
||||
assert!(!s.multi_hop, "settable back to false");
|
||||
}
|
||||
|
||||
|
||||
/// Small render helper shared with the rest of the test module's
|
||||
/// buffer-snapshot pattern. We define it locally here to avoid
|
||||
/// reaching into private internals.
|
||||
|
||||
@@ -23,16 +23,10 @@ fn fresh_app(focus: Pane) -> App {
|
||||
fn f1_toggles_cheatsheet_visibility() {
|
||||
let mut app = fresh_app(Pane::Library);
|
||||
assert!(!app.cheatsheet_visible(), "starts hidden");
|
||||
let consumed = cheatsheet_intercept(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::F(1), KeyModifiers::NONE),
|
||||
);
|
||||
let consumed = cheatsheet_intercept(&mut app, KeyEvent::new(KeyCode::F(1), KeyModifiers::NONE));
|
||||
assert!(consumed, "F1 must be consumed");
|
||||
assert!(app.cheatsheet_visible(), "F1 opens");
|
||||
let consumed = cheatsheet_intercept(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::F(1), KeyModifiers::NONE),
|
||||
);
|
||||
let consumed = cheatsheet_intercept(&mut app, KeyEvent::new(KeyCode::F(1), KeyModifiers::NONE));
|
||||
assert!(consumed, "second F1 also consumed");
|
||||
assert!(!app.cheatsheet_visible(), "F1 closes when open");
|
||||
}
|
||||
@@ -44,22 +38,13 @@ fn f1_toggles_cheatsheet_visibility() {
|
||||
fn esc_closes_cheatsheet_when_visible_otherwise_falls_through() {
|
||||
let mut app = fresh_app(Pane::Library);
|
||||
// Hidden → Esc falls through.
|
||||
let consumed = cheatsheet_intercept(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE),
|
||||
);
|
||||
let consumed = cheatsheet_intercept(&mut app, KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
|
||||
assert!(!consumed, "Esc with cheatsheet hidden must fall through");
|
||||
|
||||
// Visible → Esc closes + consumed.
|
||||
let _ = cheatsheet_intercept(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::F(1), KeyModifiers::NONE),
|
||||
);
|
||||
let _ = cheatsheet_intercept(&mut app, KeyEvent::new(KeyCode::F(1), KeyModifiers::NONE));
|
||||
assert!(app.cheatsheet_visible());
|
||||
let consumed = cheatsheet_intercept(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE),
|
||||
);
|
||||
let consumed = cheatsheet_intercept(&mut app, KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
|
||||
assert!(consumed, "Esc with cheatsheet visible must consume");
|
||||
assert!(!app.cheatsheet_visible());
|
||||
}
|
||||
@@ -76,10 +61,7 @@ fn modifier_keys_do_not_toggle_cheatsheet() {
|
||||
assert!(!consumed);
|
||||
assert!(!app.cheatsheet_visible());
|
||||
|
||||
let consumed = cheatsheet_intercept(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::F(1), KeyModifiers::ALT),
|
||||
);
|
||||
let consumed = cheatsheet_intercept(&mut app, KeyEvent::new(KeyCode::F(1), KeyModifiers::ALT));
|
||||
assert!(!consumed);
|
||||
assert!(!app.cheatsheet_visible());
|
||||
}
|
||||
@@ -90,10 +72,7 @@ fn modifier_keys_do_not_toggle_cheatsheet() {
|
||||
#[test]
|
||||
fn arbitrary_key_falls_through_when_cheatsheet_visible() {
|
||||
let mut app = fresh_app(Pane::Library);
|
||||
let _ = cheatsheet_intercept(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::F(1), KeyModifiers::NONE),
|
||||
);
|
||||
let _ = cheatsheet_intercept(&mut app, KeyEvent::new(KeyCode::F(1), KeyModifiers::NONE));
|
||||
assert!(app.cheatsheet_visible());
|
||||
for key in [
|
||||
KeyEvent::new(KeyCode::Char('j'), KeyModifiers::NONE),
|
||||
@@ -116,10 +95,7 @@ fn cheatsheet_popup_contains_global_and_pane_sections() {
|
||||
let mut app = fresh_app(Pane::Search);
|
||||
app.focus = Pane::Search;
|
||||
// Force visible — we're testing the renderer, not the toggle.
|
||||
let _ = cheatsheet_intercept(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::F(1), KeyModifiers::NONE),
|
||||
);
|
||||
let _ = cheatsheet_intercept(&mut app, KeyEvent::new(KeyCode::F(1), KeyModifiers::NONE));
|
||||
let backend = TestBackend::new(120, 40);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
terminal
|
||||
@@ -138,7 +114,10 @@ fn cheatsheet_popup_contains_global_and_pane_sections() {
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
assert!(rendered.contains("Global"), "Global section header present");
|
||||
assert!(rendered.contains("Library"), "Library section header present");
|
||||
assert!(
|
||||
rendered.contains("Library"),
|
||||
"Library section header present"
|
||||
);
|
||||
assert!(rendered.contains("Search"), "Search section header present");
|
||||
assert!(rendered.contains("Ask"), "Ask section header present");
|
||||
assert!(rendered.contains("F1"), "F1 binding listed");
|
||||
@@ -149,7 +128,9 @@ fn cheatsheet_popup_contains_global_and_pane_sections() {
|
||||
// the Inspect assertion when the body overflows; the rest of
|
||||
// the section-header asserts still cover the primary contract.
|
||||
if !rendered.contains("Inspect") {
|
||||
eprintln!("[note] Inspect section overflowed popup body — known limitation per p9-fb-21 HOTFIXES");
|
||||
eprintln!(
|
||||
"[note] Inspect section overflowed popup body — known limitation per p9-fb-21 HOTFIXES"
|
||||
);
|
||||
}
|
||||
// The "currently focused: <pane>" line lives at the bottom of
|
||||
// the popup; it might get clipped if the popup's content
|
||||
@@ -158,6 +139,8 @@ fn cheatsheet_popup_contains_global_and_pane_sections() {
|
||||
// the primary contract.
|
||||
let has_focused = rendered.contains("focused");
|
||||
if !has_focused {
|
||||
eprintln!("[note] 'focused' line absent — likely body overflowed popup height; sections still pinned");
|
||||
eprintln!(
|
||||
"[note] 'focused' line absent — likely body overflowed popup height; sections still pinned"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,8 @@ use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
use kebab_config::Config;
|
||||
use kebab_core::{
|
||||
AssetId, Block, BlockId, CanonicalDocument, Chunk, ChunkId, ChunkerVersion, CommonBlock,
|
||||
DocumentId, HeadingBlock, Inline, Lang, Metadata, ParserVersion, Provenance,
|
||||
ProvenanceEvent, ProvenanceKind, SourceSpan, SourceType, TextBlock, TrustLevel,
|
||||
WorkspacePath,
|
||||
DocumentId, HeadingBlock, Inline, Lang, Metadata, ParserVersion, Provenance, ProvenanceEvent,
|
||||
ProvenanceKind, SourceSpan, SourceType, TextBlock, TrustLevel, WorkspacePath,
|
||||
};
|
||||
use kebab_tui::{
|
||||
App, InspectState, InspectTarget, KeyOutcome, Pane, handle_key_inspect, render_inspect,
|
||||
@@ -61,7 +60,10 @@ fn make_doc() -> CanonicalDocument {
|
||||
}),
|
||||
];
|
||||
let mut user = serde_json::Map::new();
|
||||
user.insert("custom_key".into(), serde_json::Value::String("custom_val".into()));
|
||||
user.insert(
|
||||
"custom_key".into(),
|
||||
serde_json::Value::String("custom_val".into()),
|
||||
);
|
||||
|
||||
CanonicalDocument {
|
||||
doc_id,
|
||||
@@ -141,10 +143,7 @@ fn esc_returns_to_recorded_pane() {
|
||||
let s = app.inspect.as_mut().unwrap();
|
||||
s.return_to = Pane::Search;
|
||||
}
|
||||
let outcome = handle_key_inspect(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE),
|
||||
);
|
||||
let outcome = handle_key_inspect(&mut app, KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
|
||||
assert_eq!(outcome, KeyOutcome::SwitchPane(Pane::Search));
|
||||
}
|
||||
|
||||
@@ -200,16 +199,10 @@ fn page_down_scrolls_by_ten_in_inspect() {
|
||||
fn page_up_rewinds_by_ten_saturating_in_inspect() {
|
||||
let mut app = fresh_app();
|
||||
app.inspect.as_mut().unwrap().scroll = 25;
|
||||
handle_key_inspect(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::PageUp, KeyModifiers::NONE),
|
||||
);
|
||||
handle_key_inspect(&mut app, KeyEvent::new(KeyCode::PageUp, KeyModifiers::NONE));
|
||||
assert_eq!(app.inspect.as_ref().unwrap().scroll, 15);
|
||||
app.inspect.as_mut().unwrap().scroll = 3;
|
||||
handle_key_inspect(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::PageUp, KeyModifiers::NONE),
|
||||
);
|
||||
handle_key_inspect(&mut app, KeyEvent::new(KeyCode::PageUp, KeyModifiers::NONE));
|
||||
assert_eq!(app.inspect.as_ref().unwrap().scroll, 0);
|
||||
}
|
||||
|
||||
@@ -273,7 +266,10 @@ fn doc_view_renders_header_and_metadata() {
|
||||
rendered.contains("custom_key") || rendered.contains("custom_val"),
|
||||
"user metadata pretty-printed"
|
||||
);
|
||||
assert!(rendered.contains("provenance"), "provenance section visible");
|
||||
assert!(
|
||||
rendered.contains("provenance"),
|
||||
"provenance section visible"
|
||||
);
|
||||
assert!(rendered.contains("kb-source-fs"), "agent rendered");
|
||||
assert!(rendered.contains("blocks"), "blocks section visible");
|
||||
assert!(rendered.contains("Heading L1"), "block describe rendered");
|
||||
@@ -319,10 +315,16 @@ fn chunk_view_renders_text_and_block_ids() {
|
||||
s.chunk = Some(make_chunk());
|
||||
}
|
||||
let rendered = render_to_string(&app, 100, 40);
|
||||
assert!(rendered.contains("md-heading-v1"), "chunker_version rendered");
|
||||
assert!(
|
||||
rendered.contains("md-heading-v1"),
|
||||
"chunker_version rendered"
|
||||
);
|
||||
assert!(rendered.contains("Top / Sub"), "heading_path joined");
|
||||
assert!(rendered.contains("Line 1-5"), "source span described");
|
||||
assert!(rendered.contains("chunk body line one"), "text body rendered");
|
||||
assert!(
|
||||
rendered.contains("chunk body line one"),
|
||||
"text body rendered"
|
||||
);
|
||||
assert!(
|
||||
rendered.contains("embeddings (2)"),
|
||||
"block_id count rendered inline on embeddings header"
|
||||
@@ -343,8 +345,7 @@ fn inspect_doc_header_shows_stale_badge_when_threshold_exceeded() {
|
||||
s.target = Some(InspectTarget::Doc(DocumentId("d".repeat(32))));
|
||||
let mut doc = make_doc();
|
||||
// Backdate updated_at by 60 days so 60d > 30d threshold.
|
||||
doc.metadata.updated_at =
|
||||
OffsetDateTime::now_utc() - time::Duration::days(60);
|
||||
doc.metadata.updated_at = OffsetDateTime::now_utc() - time::Duration::days(60);
|
||||
s.doc = Some(doc);
|
||||
}
|
||||
let rendered = render_to_string(&app, 100, 40);
|
||||
@@ -372,8 +373,7 @@ fn inspect_doc_header_omits_stale_badge_when_fresh() {
|
||||
s.target = Some(InspectTarget::Doc(DocumentId("d".repeat(32))));
|
||||
let mut doc = make_doc();
|
||||
// 1 day old — under the 30d threshold.
|
||||
doc.metadata.updated_at =
|
||||
OffsetDateTime::now_utc() - time::Duration::days(1);
|
||||
doc.metadata.updated_at = OffsetDateTime::now_utc() - time::Duration::days(1);
|
||||
s.doc = Some(doc);
|
||||
}
|
||||
let rendered = render_to_string(&app, 100, 40);
|
||||
@@ -393,8 +393,7 @@ fn inspect_doc_header_omits_stale_badge_when_threshold_zero() {
|
||||
s.target = Some(InspectTarget::Doc(DocumentId("d".repeat(32))));
|
||||
let mut doc = make_doc();
|
||||
// Even a year-old doc must not get [STALE] when threshold = 0.
|
||||
doc.metadata.updated_at =
|
||||
OffsetDateTime::now_utc() - time::Duration::days(365);
|
||||
doc.metadata.updated_at = OffsetDateTime::now_utc() - time::Duration::days(365);
|
||||
s.doc = Some(doc);
|
||||
}
|
||||
let rendered = render_to_string(&app, 100, 40);
|
||||
@@ -410,10 +409,7 @@ fn no_inspect_state_returns_to_library() {
|
||||
config.storage.data_dir = "/tmp/kebab-tui-inspect-tests-noop".into();
|
||||
let mut app = App::new(config).unwrap();
|
||||
app.focus = Pane::Inspect;
|
||||
let outcome = handle_key_inspect(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE),
|
||||
);
|
||||
let outcome = handle_key_inspect(&mut app, KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
|
||||
assert_eq!(outcome, KeyOutcome::SwitchPane(Pane::Library));
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,12 @@ use time::OffsetDateTime;
|
||||
|
||||
fn make_doc(path: &str, title: &str, tags: Vec<&str>) -> DocSummary {
|
||||
DocSummary {
|
||||
doc_id: DocumentId(format!("{:0<32}", path.chars().filter(|c| c.is_alphanumeric()).collect::<String>())),
|
||||
doc_id: DocumentId(format!(
|
||||
"{:0<32}",
|
||||
path.chars()
|
||||
.filter(|c| c.is_alphanumeric())
|
||||
.collect::<String>()
|
||||
)),
|
||||
doc_path: WorkspacePath::new(path.into()).unwrap(),
|
||||
title: title.into(),
|
||||
lang: Lang("en".into()),
|
||||
@@ -88,10 +93,8 @@ fn handle_key_library_q_quits() {
|
||||
#[test]
|
||||
fn handle_key_library_esc_quits_when_no_overlay() {
|
||||
let mut app = app_with_docs(vec![]);
|
||||
let outcome = kebab_tui::handle_key_library(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE),
|
||||
);
|
||||
let outcome =
|
||||
kebab_tui::handle_key_library(&mut app, KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
|
||||
assert_eq!(outcome, KeyOutcome::Quit);
|
||||
}
|
||||
|
||||
@@ -118,10 +121,8 @@ fn handle_key_library_question_switches_to_ask() {
|
||||
#[test]
|
||||
fn handle_key_library_enter_does_not_switch_when_empty() {
|
||||
let mut app = app_with_docs(vec![]);
|
||||
let outcome = kebab_tui::handle_key_library(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE),
|
||||
);
|
||||
let outcome =
|
||||
kebab_tui::handle_key_library(&mut app, KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
assert_eq!(outcome, KeyOutcome::Continue);
|
||||
}
|
||||
|
||||
@@ -185,10 +186,8 @@ fn handle_key_library_arrow_down_moves_selection() {
|
||||
#[test]
|
||||
fn handle_key_library_enter_inspects_when_docs_present() {
|
||||
let mut app = app_with_docs(vec![make_doc("a.md", "A", vec![])]);
|
||||
let outcome = kebab_tui::handle_key_library(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE),
|
||||
);
|
||||
let outcome =
|
||||
kebab_tui::handle_key_library(&mut app, KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
assert_eq!(outcome, KeyOutcome::SwitchPane(Pane::Inspect));
|
||||
}
|
||||
|
||||
@@ -209,10 +208,8 @@ fn handle_key_library_f_opens_filter_overlay_then_enter_refreshes() {
|
||||
);
|
||||
}
|
||||
// Enter commits + refreshes.
|
||||
let o2 = kebab_tui::handle_key_library(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE),
|
||||
);
|
||||
let o2 =
|
||||
kebab_tui::handle_key_library(&mut app, KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
assert_eq!(o2, KeyOutcome::Refresh);
|
||||
}
|
||||
|
||||
@@ -235,10 +232,8 @@ fn filter_overlay_accepts_hangul_tags() {
|
||||
);
|
||||
}
|
||||
// Enter commits.
|
||||
let o2 = kebab_tui::handle_key_library(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE),
|
||||
);
|
||||
let o2 =
|
||||
kebab_tui::handle_key_library(&mut app, KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
assert_eq!(o2, KeyOutcome::Refresh);
|
||||
// The library filter should now contain "한글" as a tag.
|
||||
let filter = app.library_filter_for_testing();
|
||||
@@ -272,9 +267,9 @@ fn filter_overlay_render_places_cursor_on_focused_field() {
|
||||
// After draw, ratatui calls backend.set_cursor_position when the
|
||||
// frame's cursor_position is Some. The terminal's
|
||||
// get_cursor_position proxies to the backend.
|
||||
let pos = terminal.get_cursor_position().expect(
|
||||
"filter overlay must call set_cursor_position, so cursor pos must be readable",
|
||||
);
|
||||
let pos = terminal
|
||||
.get_cursor_position()
|
||||
.expect("filter overlay must call set_cursor_position, so cursor pos must be readable");
|
||||
// The Tags label ("tags_any (csv): ") has display_width 16; inner.x
|
||||
// is 1 (inside border). With empty input cursor_col=0, expected x=17.
|
||||
// We assert x>0 to avoid hardcoding the exact layout geometry while
|
||||
@@ -325,7 +320,10 @@ fn library_renders_column_header_row() {
|
||||
.lines()
|
||||
.position(|line| line.contains("TITLE"))
|
||||
.expect("TITLE header should be present");
|
||||
let lines_after = rendered.lines().skip(title_line_idx + 1).collect::<Vec<_>>();
|
||||
let lines_after = rendered
|
||||
.lines()
|
||||
.skip(title_line_idx + 1)
|
||||
.collect::<Vec<_>>();
|
||||
assert!(
|
||||
lines_after.iter().any(|line| line.contains("doc-")),
|
||||
"no data rows after header:\n{rendered}"
|
||||
@@ -338,7 +336,11 @@ fn library_renders_column_header_row() {
|
||||
#[test]
|
||||
fn library_renders_korean_titles_without_overflow() {
|
||||
let docs = vec![
|
||||
make_doc("ko/한글-노트.md", "러스트로 만드는 지식 베이스", vec!["rust", "한글"]),
|
||||
make_doc(
|
||||
"ko/한글-노트.md",
|
||||
"러스트로 만드는 지식 베이스",
|
||||
vec!["rust", "한글"],
|
||||
),
|
||||
make_doc("jp/漢字メモ.md", "日本語のテストドキュメント", vec!["jp"]),
|
||||
make_doc("mix/hello-세계.md", "Hello, 세계 mixed title", vec!["mix"]),
|
||||
];
|
||||
|
||||
@@ -24,12 +24,13 @@ fn esc_in_insert_flips_to_normal_and_consumes() {
|
||||
for &pane in &[Pane::Library, Pane::Search, Pane::Ask, Pane::Inspect] {
|
||||
let mut app = fresh_app(pane);
|
||||
app.mode = Mode::Insert;
|
||||
let consumed = mode_intercept(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE),
|
||||
);
|
||||
let consumed = mode_intercept(&mut app, KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
|
||||
assert!(consumed, "Esc in Insert must be consumed (pane: {pane:?})");
|
||||
assert_eq!(app.mode, Mode::Normal, "mode flipped to Normal (pane: {pane:?})");
|
||||
assert_eq!(
|
||||
app.mode,
|
||||
Mode::Normal,
|
||||
"mode flipped to Normal (pane: {pane:?})"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,10 +41,7 @@ fn esc_in_insert_flips_to_normal_and_consumes() {
|
||||
fn esc_in_normal_mode_falls_through() {
|
||||
let mut app = fresh_app(Pane::Library);
|
||||
assert_eq!(app.mode, Mode::Normal);
|
||||
let consumed = mode_intercept(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE),
|
||||
);
|
||||
let consumed = mode_intercept(&mut app, KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
|
||||
assert!(!consumed, "Esc in Normal must fall through to pane");
|
||||
assert_eq!(app.mode, Mode::Normal, "mode unchanged");
|
||||
}
|
||||
@@ -55,13 +53,21 @@ fn esc_in_normal_mode_falls_through() {
|
||||
fn i_in_normal_on_library_inspect_jobs_flips_to_insert() {
|
||||
for &pane in &[Pane::Library, Pane::Inspect, Pane::Jobs] {
|
||||
let mut app = fresh_app(pane);
|
||||
assert_eq!(app.mode, Mode::Normal, "auto_for({pane:?}) should be Normal");
|
||||
assert_eq!(
|
||||
app.mode,
|
||||
Mode::Normal,
|
||||
"auto_for({pane:?}) should be Normal"
|
||||
);
|
||||
let consumed = mode_intercept(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE),
|
||||
);
|
||||
assert!(consumed, "i in Normal on {pane:?} must be consumed");
|
||||
assert_eq!(app.mode, Mode::Insert, "mode flipped to Insert (pane: {pane:?})");
|
||||
assert_eq!(
|
||||
app.mode,
|
||||
Mode::Insert,
|
||||
"mode flipped to Insert (pane: {pane:?})"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +78,11 @@ fn i_in_normal_on_library_inspect_jobs_flips_to_insert() {
|
||||
fn i_on_search_or_ask_in_insert_falls_through_to_pane() {
|
||||
for &pane in &[Pane::Search, Pane::Ask] {
|
||||
let mut app = fresh_app(pane);
|
||||
assert_eq!(app.mode, Mode::Insert, "auto_for({pane:?}) should be Insert");
|
||||
assert_eq!(
|
||||
app.mode,
|
||||
Mode::Insert,
|
||||
"auto_for({pane:?}) should be Insert"
|
||||
);
|
||||
let consumed = mode_intercept(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE),
|
||||
@@ -96,7 +106,11 @@ fn i_on_search_or_ask_in_normal_flips_to_insert() {
|
||||
KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE),
|
||||
);
|
||||
assert!(consumed, "i on {pane:?}/Normal must intercept (p9-fb-21)");
|
||||
assert_eq!(app.mode, Mode::Insert, "mode flipped to Insert (pane: {pane:?})");
|
||||
assert_eq!(
|
||||
app.mode,
|
||||
Mode::Insert,
|
||||
"mode flipped to Insert (pane: {pane:?})"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,10 +121,7 @@ fn i_on_search_or_ask_in_normal_flips_to_insert() {
|
||||
fn modifier_keys_do_not_trigger_intercept() {
|
||||
let mut app = fresh_app(Pane::Library);
|
||||
app.mode = Mode::Insert;
|
||||
let consumed = mode_intercept(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::Esc, KeyModifiers::CONTROL),
|
||||
);
|
||||
let consumed = mode_intercept(&mut app, KeyEvent::new(KeyCode::Esc, KeyModifiers::CONTROL));
|
||||
assert!(!consumed, "Ctrl+Esc must fall through");
|
||||
assert_eq!(app.mode, Mode::Insert, "mode unchanged");
|
||||
|
||||
@@ -136,16 +147,19 @@ fn shift_modifier_passes_modifier_filter() {
|
||||
// 'i', so it falls through. Both are intentional.)
|
||||
let mut app = fresh_app(Pane::Library);
|
||||
app.mode = Mode::Insert;
|
||||
let consumed = mode_intercept(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::Esc, KeyModifiers::SHIFT),
|
||||
let consumed = mode_intercept(&mut app, KeyEvent::new(KeyCode::Esc, KeyModifiers::SHIFT));
|
||||
assert!(
|
||||
consumed,
|
||||
"Shift+Esc still toggles (modifier filter allows SHIFT)"
|
||||
);
|
||||
assert!(consumed, "Shift+Esc still toggles (modifier filter allows SHIFT)");
|
||||
|
||||
let mut app = fresh_app(Pane::Library);
|
||||
let consumed = mode_intercept(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::Char('I'), KeyModifiers::SHIFT),
|
||||
);
|
||||
assert!(!consumed, "Shift+I (capital) falls through — only lowercase 'i' toggles");
|
||||
assert!(
|
||||
!consumed,
|
||||
"Shift+I (capital) falls through — only lowercase 'i' toggles"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
use kebab_config::Config;
|
||||
use kebab_core::{
|
||||
Citation, ChunkId, ChunkerVersion, DocumentId, EmbeddingModelId, IndexVersion,
|
||||
RetrievalDetail, SearchHit, SearchMode, WorkspacePath,
|
||||
ChunkId, ChunkerVersion, Citation, DocumentId, EmbeddingModelId, IndexVersion, RetrievalDetail,
|
||||
SearchHit, SearchMode, WorkspacePath,
|
||||
};
|
||||
use kebab_tui::{
|
||||
App, KeyOutcome, Mode, Pane, SearchState, SearchWorkerMessage, build_jump_command,
|
||||
@@ -73,10 +73,7 @@ fn line_citation(path: &str, line: u32) -> Citation {
|
||||
#[test]
|
||||
fn esc_returns_to_library() {
|
||||
let mut app = fresh_app();
|
||||
let outcome = handle_key_search(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE),
|
||||
);
|
||||
let outcome = handle_key_search(&mut app, KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
|
||||
assert_eq!(outcome, KeyOutcome::SwitchPane(Pane::Library));
|
||||
}
|
||||
|
||||
@@ -134,20 +131,14 @@ fn enter_with_query_emits_refresh() {
|
||||
let s = app.search.as_mut().unwrap();
|
||||
s.input.push_str("rust");
|
||||
}
|
||||
let outcome = handle_key_search(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE),
|
||||
);
|
||||
let outcome = handle_key_search(&mut app, KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
assert_eq!(outcome, KeyOutcome::Refresh);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enter_with_empty_query_is_continue() {
|
||||
let mut app = fresh_app();
|
||||
let outcome = handle_key_search(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE),
|
||||
);
|
||||
let outcome = handle_key_search(&mut app, KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
assert_eq!(outcome, KeyOutcome::Continue);
|
||||
}
|
||||
|
||||
@@ -193,19 +184,23 @@ fn j_k_move_selection_within_bounds() {
|
||||
#[test]
|
||||
fn build_jump_command_line_uses_plus_n_for_vim() {
|
||||
let citation = line_citation("notes/foo.md", 42);
|
||||
let (program, args) =
|
||||
build_jump_command(&citation, "vim", Path::new("/tmp/workspace"));
|
||||
let (program, args) = build_jump_command(&citation, "vim", Path::new("/tmp/workspace"));
|
||||
assert_eq!(program, "vim");
|
||||
assert_eq!(args, vec!["+42".to_string(), "/tmp/workspace/notes/foo.md".into()]);
|
||||
assert_eq!(
|
||||
args,
|
||||
vec!["+42".to_string(), "/tmp/workspace/notes/foo.md".into()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_jump_command_line_uses_g_flag_for_code() {
|
||||
let citation = line_citation("notes/foo.md", 42);
|
||||
let (program, args) =
|
||||
build_jump_command(&citation, "code", Path::new("/tmp/workspace"));
|
||||
let (program, args) = build_jump_command(&citation, "code", Path::new("/tmp/workspace"));
|
||||
assert_eq!(program, "code");
|
||||
assert_eq!(args, vec!["-g".to_string(), "/tmp/workspace/notes/foo.md:42".into()]);
|
||||
assert_eq!(
|
||||
args,
|
||||
vec!["-g".to_string(), "/tmp/workspace/notes/foo.md:42".into()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -227,8 +222,18 @@ fn render_search_with_hits_shows_input_and_path() {
|
||||
s.input.push_str("rust traits");
|
||||
s.mode = SearchMode::Hybrid;
|
||||
s.hits = vec![
|
||||
make_hit(1, "notes/rust.md", "trait dispatch\nis dynamic", line_citation("notes/rust.md", 12)),
|
||||
make_hit(2, "notes/dyn.md", "dynamic dispatch\nvtable", line_citation("notes/dyn.md", 3)),
|
||||
make_hit(
|
||||
1,
|
||||
"notes/rust.md",
|
||||
"trait dispatch\nis dynamic",
|
||||
line_citation("notes/rust.md", 12),
|
||||
),
|
||||
make_hit(
|
||||
2,
|
||||
"notes/dyn.md",
|
||||
"dynamic dispatch\nvtable",
|
||||
line_citation("notes/dyn.md", 3),
|
||||
),
|
||||
];
|
||||
s.selected_hit = 0;
|
||||
}
|
||||
@@ -249,10 +254,19 @@ fn render_search_with_hits_shows_input_and_path() {
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
assert!(rendered.contains("hybrid"), "mode badge rendered: {rendered}");
|
||||
assert!(
|
||||
rendered.contains("hybrid"),
|
||||
"mode badge rendered: {rendered}"
|
||||
);
|
||||
assert!(rendered.contains("rust traits"), "input text rendered");
|
||||
assert!(rendered.contains("notes/rust.md"), "first hit path rendered");
|
||||
assert!(rendered.contains("notes/dyn.md"), "second hit path rendered");
|
||||
assert!(
|
||||
rendered.contains("notes/rust.md"),
|
||||
"first hit path rendered"
|
||||
);
|
||||
assert!(
|
||||
rendered.contains("notes/dyn.md"),
|
||||
"second hit path rendered"
|
||||
);
|
||||
}
|
||||
|
||||
/// p9-fb-32: Search pane prefixes the rank/score header line with a
|
||||
@@ -454,7 +468,12 @@ fn g_key_enqueues_pending_editor_request() {
|
||||
app.mode = kebab_tui::Mode::Normal;
|
||||
{
|
||||
let s = app.search.as_mut().unwrap();
|
||||
s.hits = vec![make_hit(1, "notes/x.md", "snippet", line_citation("notes/x.md", 42))];
|
||||
s.hits = vec![make_hit(
|
||||
1,
|
||||
"notes/x.md",
|
||||
"snippet",
|
||||
line_citation("notes/x.md", 42),
|
||||
)];
|
||||
s.selected_hit = 0;
|
||||
}
|
||||
assert!(app.pending_editor().is_none(), "queue starts empty");
|
||||
@@ -570,15 +589,18 @@ fn poll_worker_noop_when_no_rx() {
|
||||
/// 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 {
|
||||
fn search_state_with(
|
||||
input: &str,
|
||||
mode: SearchMode,
|
||||
searching: bool,
|
||||
last_query: Option<(String, SearchMode)>,
|
||||
) -> SearchState {
|
||||
let mut s = SearchState::default();
|
||||
s.input.push_str(input);
|
||||
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.input_dirty_at = Some(time::OffsetDateTime::now_utc() - time::Duration::seconds(1));
|
||||
s
|
||||
}
|
||||
|
||||
@@ -683,12 +705,7 @@ fn o_in_normal_with_hits_enters_inspect() {
|
||||
app.focus = Pane::Search;
|
||||
app.mode = Mode::Normal;
|
||||
let s = app.search.as_mut().unwrap();
|
||||
s.hits = vec![make_hit(
|
||||
1,
|
||||
"a.md",
|
||||
"snippet",
|
||||
line_citation("a.md", 1),
|
||||
)];
|
||||
s.hits = vec![make_hit(1, "a.md", "snippet", line_citation("a.md", 1))];
|
||||
s.selected_hit = 0;
|
||||
let outcome = kebab_tui::handle_key_search(
|
||||
&mut app,
|
||||
|
||||
Reference in New Issue
Block a user