혼합 출처 KB(위키+jira 등)에서 색인은 전부 하되 질의 시 출처로 좁히는 provenance 레버. 전역 trust 곱셈가중(weighted-RRF)은 A/B 에서 반증(θ=0.85 만으로 incident MRR 0.918→0.340 절벽, 점수 압축) — 필터가 see-saw 없는 올바른 레버. - config [[workspace.sources]] (각 id/root/exclude/trust_level/source_type); 단일 root 는 implicit `default` source 로 정규화. validate: id 유일·비어있지 않음. - config schema v3→v4 (step_3_to_4, root→[[workspace.sources]] id=default 미러, 멱등) - V014 documents.source_id 컬럼+인덱스 (additive, DEFAULT 'default', 재색인 0) - Metadata.source_id + BodyHints trust precedence(frontmatter > source 기본값 > Primary) - ingest: --root 미지정 시 resolved_sources() 순회 + doc 마다 source_id/trust stamp - 검색 SearchFilters.source_type/source_id → lexical + vector 두 site (IN, OR) - CLI kebab search --source <id> / --source-type <type> (repeatable/comma-sep) 도그푸딩(620 doc, jira400+wiki220): --source wiki 로 개념 질의 MRR 0.780→0.810, --source jira 로 incident 0.918→0.975. trust precedence 실측(jira=secondary 기본값). version bump 0.28.0 → 0.29.0 (신규 CLI flag + config 키 + V014 migration → minor). follow-up: MCP search 필터 미노출 · kebab list source_id 미표시 · RAG provenance 라벨. 자세한 내용: tasks/HOTFIXES.md (2026-06-21), docs/release-notes/v0.29.0-draft.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Mc6W1fgsrbFKTsqA6P8La
166 lines
6.0 KiB
Rust
166 lines
6.0 KiB
Rust
//! p9-fb-12: integration tests for `mode_intercept`. Drives the
|
|
//! global i/Esc dispatch by constructing KeyEvents directly without
|
|
//! standing up the full run loop (terminal-side).
|
|
|
|
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
|
use kebab_config::Config;
|
|
use kebab_tui::{App, Mode, Pane, mode_intercept};
|
|
|
|
fn fresh_app(focus: Pane) -> App {
|
|
let mut config = Config::defaults();
|
|
config.storage.data_dir = "/tmp/kebab-tui-mode-tests-noop".to_string();
|
|
config.workspace.root = Some("/tmp/kebab-tui-mode-tests-noop/workspace".to_string());
|
|
let mut app = App::new(config).expect("App::new");
|
|
app.focus = focus;
|
|
app.mode = Mode::auto_for(focus);
|
|
app
|
|
}
|
|
|
|
/// p9-fb-12: `Esc` from Insert mode flips to Normal on any pane.
|
|
/// Returns `true` (consumed) so the pane handler doesn't ALSO see
|
|
/// the Esc as a "back to Library" signal.
|
|
#[test]
|
|
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));
|
|
assert!(consumed, "Esc in Insert must be consumed (pane: {pane:?})");
|
|
assert_eq!(
|
|
app.mode,
|
|
Mode::Normal,
|
|
"mode flipped to Normal (pane: {pane:?})"
|
|
);
|
|
}
|
|
}
|
|
|
|
/// p9-fb-12: `Esc` from Normal mode is a no-op (not consumed) so the
|
|
/// pane's existing Esc handler (e.g. Library `Esc` → quit) keeps
|
|
/// working.
|
|
#[test]
|
|
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));
|
|
assert!(!consumed, "Esc in Normal must fall through to pane");
|
|
assert_eq!(app.mode, Mode::Normal, "mode unchanged");
|
|
}
|
|
|
|
/// p9-fb-12: `i` in Normal mode on Library / Inspect / Jobs flips
|
|
/// to Insert. (`i` has no pre-fb-12 meaning on those panes, so the
|
|
/// global interception is safe.)
|
|
#[test]
|
|
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"
|
|
);
|
|
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:?})"
|
|
);
|
|
}
|
|
}
|
|
|
|
/// p9-fb-21 (was p9-fb-12): on Search/Ask the auto mode is Insert,
|
|
/// so `i` typed in that state must fall through (would otherwise
|
|
/// swallow a real letter the user is typing).
|
|
#[test]
|
|
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"
|
|
);
|
|
let consumed = mode_intercept(
|
|
&mut app,
|
|
KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE),
|
|
);
|
|
assert!(!consumed, "i on {pane:?}/Insert must fall through to pane");
|
|
assert_eq!(app.mode, Mode::Insert, "mode unchanged");
|
|
}
|
|
}
|
|
|
|
/// p9-fb-21: `i` in Normal on Search/Ask DOES intercept — the
|
|
/// dogfooding feedback was that once the user pressed Esc to leave
|
|
/// Insert, no key brought them back. `i` is the universal toggle
|
|
/// now (Search's pre-fb-21 `i`=chunk inspect was rebound to `o`).
|
|
#[test]
|
|
fn i_on_search_or_ask_in_normal_flips_to_insert() {
|
|
for &pane in &[Pane::Search, Pane::Ask] {
|
|
let mut app = fresh_app(pane);
|
|
app.mode = Mode::Normal;
|
|
let consumed = mode_intercept(
|
|
&mut app,
|
|
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:?})"
|
|
);
|
|
}
|
|
}
|
|
|
|
/// p9-fb-12: modifier-bearing keys (Ctrl+Esc, Alt+i) are NOT the
|
|
/// mode toggle. Falls through so chord handlers downstream get a
|
|
/// shot.
|
|
#[test]
|
|
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));
|
|
assert!(!consumed, "Ctrl+Esc must fall through");
|
|
assert_eq!(app.mode, Mode::Insert, "mode unchanged");
|
|
|
|
app.mode = Mode::Normal;
|
|
let consumed = mode_intercept(
|
|
&mut app,
|
|
KeyEvent::new(KeyCode::Char('i'), KeyModifiers::ALT),
|
|
);
|
|
assert!(!consumed, "Alt+i must fall through");
|
|
assert_eq!(app.mode, Mode::Normal, "mode unchanged");
|
|
}
|
|
|
|
/// p9-fb-12: SHIFT alone is allowed (the toggle keys are unshifted
|
|
/// `i` / `Esc`, but a future `Shift+Esc` chord is unlikely; pre-
|
|
/// allow SHIFT so capital-letter typing in Search/Ask doesn't
|
|
/// accidentally fall into the modifier-block branch).
|
|
#[test]
|
|
fn shift_modifier_passes_modifier_filter() {
|
|
// SHIFT+Esc is a strange combo but the filter passes it. (The
|
|
// actual outcome — does mode flip? — depends on the case
|
|
// matching i/Esc. SHIFT+Esc still matches KeyCode::Esc, so it
|
|
// toggles. SHIFT+I would be KeyCode::Char('I') (capital), NOT
|
|
// '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));
|
|
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"
|
|
);
|
|
}
|