혼합 출처 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
191 lines
5.6 KiB
Rust
191 lines
5.6 KiB
Rust
//! p9-fb-24: integration tests for the always-visible status bar.
|
|
|
|
use kebab_config::Config;
|
|
use kebab_tui::{App, Pane};
|
|
use ratatui::Terminal;
|
|
use ratatui::backend::TestBackend;
|
|
use ratatui::layout::Rect;
|
|
|
|
fn fresh_app(focus: Pane) -> App {
|
|
let mut config = Config::defaults();
|
|
config.storage.data_dir = "/tmp/kebab-tui-status-bar-tests-noop".to_string();
|
|
config.workspace.root = Some("/tmp/kebab-tui-status-bar-tests-noop/workspace".to_string());
|
|
let mut app = App::new(config).expect("App::new");
|
|
app.focus = focus;
|
|
app
|
|
}
|
|
|
|
fn render_to_string(app: &App, width: u16) -> String {
|
|
let backend = TestBackend::new(width, 1);
|
|
let mut terminal = Terminal::new(backend).unwrap();
|
|
terminal
|
|
.draw(|f| kebab_tui::render_status_bar(f, Rect::new(0, 0, width, 1), app))
|
|
.unwrap();
|
|
let buffer = terminal.backend().buffer().clone();
|
|
(0..buffer.area.height)
|
|
.map(|y| {
|
|
(0..buffer.area.width)
|
|
.map(|x| buffer[(x, y)].symbol())
|
|
.collect::<String>()
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.join("\n")
|
|
}
|
|
|
|
#[test]
|
|
fn status_bar_shows_kebab_version_first() {
|
|
let app = fresh_app(Pane::Library);
|
|
let rendered = render_to_string(&app, 100);
|
|
let expected = format!("kebab v{}", env!("CARGO_PKG_VERSION"));
|
|
assert!(
|
|
rendered.contains(&expected),
|
|
"version not in status bar: rendered=\n{rendered}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn status_bar_shows_pane_label() {
|
|
for (focus, expected) in [
|
|
(Pane::Library, "Library"),
|
|
(Pane::Search, "Search"),
|
|
(Pane::Ask, "Ask"),
|
|
(Pane::Inspect, "Inspect"),
|
|
(Pane::Jobs, "Jobs"),
|
|
] {
|
|
let app = fresh_app(focus);
|
|
let rendered = render_to_string(&app, 100);
|
|
assert!(
|
|
rendered.contains(expected),
|
|
"pane label '{expected}' not visible for focus={focus:?}: rendered=\n{rendered}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn status_bar_shows_doc_count() {
|
|
let app = fresh_app(Pane::Library);
|
|
let rendered = render_to_string(&app, 100);
|
|
assert!(
|
|
rendered.contains("0 docs"),
|
|
"doc count missing: rendered=\n{rendered}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn status_bar_idle_when_no_dynamic_state() {
|
|
let app = fresh_app(Pane::Library);
|
|
let rendered = render_to_string(&app, 100);
|
|
assert!(
|
|
rendered.contains("idle"),
|
|
"idle marker missing: rendered=\n{rendered}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn status_bar_shows_streaming_when_ask_streaming() {
|
|
let mut app = fresh_app(Pane::Ask);
|
|
app.ask = Some(kebab_tui::AskState {
|
|
streaming: true,
|
|
..Default::default()
|
|
});
|
|
let rendered = render_to_string(&app, 100);
|
|
assert!(
|
|
rendered.contains("streaming…"),
|
|
"streaming marker missing: rendered=\n{rendered}"
|
|
);
|
|
assert!(
|
|
!rendered.contains("idle"),
|
|
"idle should not appear when streaming: rendered=\n{rendered}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn status_bar_shows_searching_when_search_worker_active() {
|
|
let mut app = fresh_app(Pane::Search);
|
|
app.search = Some(kebab_tui::SearchState {
|
|
searching: true,
|
|
..Default::default()
|
|
});
|
|
let rendered = render_to_string(&app, 100);
|
|
assert!(
|
|
rendered.contains("searching…"),
|
|
"searching marker missing: rendered=\n{rendered}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn status_bar_shows_ask_conv_id_when_in_ask_with_context() {
|
|
let mut app = fresh_app(Pane::Ask);
|
|
app.ask = Some(kebab_tui::AskState {
|
|
conversation_id: Some("conv_a3f9b2c1d4e5f6a7b8c9d0e1f2a3b4c5".to_string()),
|
|
current_question: Some("test?".to_string()),
|
|
..Default::default()
|
|
});
|
|
let rendered = render_to_string(&app, 100);
|
|
assert!(
|
|
rendered.contains("conv_a3f9b2c1…"),
|
|
"8-hex prefix conv id missing: rendered=\n{rendered}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn status_bar_omits_conv_id_when_ask_has_no_context() {
|
|
let mut app = fresh_app(Pane::Ask);
|
|
app.ask = Some(kebab_tui::AskState::default());
|
|
let rendered = render_to_string(&app, 100);
|
|
assert!(
|
|
!rendered.contains("conv_"),
|
|
"conv id should not appear without context: rendered=\n{rendered}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn status_bar_omits_conv_id_outside_ask() {
|
|
let mut app = fresh_app(Pane::Library);
|
|
app.ask = Some(kebab_tui::AskState {
|
|
conversation_id: Some("conv_a3f9b2c1d4e5f6a7b8c9d0e1f2a3b4c5".to_string()),
|
|
current_question: Some("test?".to_string()),
|
|
..Default::default()
|
|
});
|
|
let rendered = render_to_string(&app, 100);
|
|
assert!(
|
|
!rendered.contains("conv_"),
|
|
"conv id leaked outside Ask pane: rendered=\n{rendered}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn status_bar_shows_ingest_progress_in_dynamic_slot() {
|
|
use std::sync::Arc;
|
|
use std::sync::atomic::AtomicBool;
|
|
let mut app = fresh_app(Pane::Library);
|
|
let (_tx, rx) = std::sync::mpsc::channel();
|
|
app.ingest_state = Some(kebab_tui::IngestState {
|
|
rx,
|
|
counts: kebab_app::AggregateCounts {
|
|
scanned: 40,
|
|
..Default::default()
|
|
},
|
|
current_path: Some("notes/foo.md".to_string()),
|
|
current_idx: 12,
|
|
started_at: std::time::Instant::now(),
|
|
terminal_at: None,
|
|
aborted: false,
|
|
thread: None,
|
|
cancel: Arc::new(AtomicBool::new(false)),
|
|
});
|
|
let rendered = render_to_string(&app, 200);
|
|
assert!(
|
|
rendered.contains("12/40"),
|
|
"ingest progress fragment missing: rendered=\n{rendered}"
|
|
);
|
|
assert!(
|
|
rendered.contains("30%"),
|
|
"ingest percentage missing: rendered=\n{rendered}"
|
|
);
|
|
assert!(
|
|
!rendered.contains("idle"),
|
|
"idle should not appear during ingest: rendered=\n{rendered}"
|
|
);
|
|
}
|