feat(p10-1a-2): wire code ingest dispatch (ingest_one_code_asset)

Add `MediaType::Code("rust")` dispatch arm in `ingest_one_asset`,
`ingest_one_code_asset` fn (faithful mirror of `ingest_one_pdf_asset`),
and `backfill_code_lang` post-processing in `App::search_uncached`.
Integration test `code_ingest_smoke.rs` verifies full pipeline:
ingest `.rs` → Citation::Code hit with lang/symbol/line_start.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-19 20:14:59 +00:00
parent 808b92a6c5
commit 580576c2c6
5 changed files with 353 additions and 2 deletions

View File

@@ -0,0 +1,142 @@
//! p10-1A-2 Task 8: smoke test for Rust code ingest dispatch.
//!
//! Writes a single `.rs` file into a TempDir workspace, ingests it via
//! `kebab_app::ingest_with_config`, then searches for the symbol name and
//! asserts that the resulting `SearchHit` carries a `Citation::Code`
//! with the expected `lang`, `symbol`, and `line_start`.
//!
//! Mirrors the `pdf_pipeline.rs` harness: lexical-only (no AVX/fastembed),
//! no OCR / caption adapters needed.
mod common;
use common::{TestEnv, lexical_query};
use kebab_core::{Citation, IngestItemKind};
/// A `.rs` file with a single `pub fn add` symbol is ingested, and a
/// lexical search for "add" must return at least one `Citation::Code`
/// hit whose `lang == "rust"`, `symbol == Some("add")`, and
/// `line_start >= 1`.
#[test]
fn rust_file_ingests_and_searches_as_code_citation() {
let env = TestEnv::lexical_only();
// Write a minimal Rust file into the workspace root.
std::fs::write(
env.workspace_root.join("demo.rs"),
"/// adds two integers\npub fn add(a: i32, b: i32) -> i32 {\n a + b\n}\n",
)
.unwrap();
let report =
kebab_app::ingest_with_config(env.config.clone(), env.scope(), false)
.expect("ingest must succeed");
assert_eq!(report.errors, 0, "no errors expected: {report:?}");
let items = report.items.as_ref().expect("items present");
let code_item = items
.iter()
.find(|i| i.doc_path.0.ends_with("demo.rs"))
.expect("demo.rs item present");
assert_eq!(
code_item.kind,
IngestItemKind::New,
"first ingest must be New: {code_item:?}"
);
assert!(
code_item.block_count.unwrap_or(0) >= 1,
"at least one block expected: {code_item:?}"
);
assert!(
code_item.chunk_count.unwrap_or(0) >= 1,
"at least one chunk expected: {code_item:?}"
);
assert_eq!(
code_item.parser_version.as_ref().map(|p| p.0.as_str()),
Some("code-rust-v1"),
"parser_version must be code-rust-v1"
);
assert_eq!(
code_item.chunker_version.as_ref().map(|c| c.0.as_str()),
Some("code-rust-ast-v1"),
"chunker_version must be code-rust-ast-v1"
);
// Lexical search for the symbol name "add".
let hits = kebab_app::search_with_config(env.config.clone(), lexical_query("add"))
.expect("search must succeed");
let h = hits
.iter()
.find(|h| matches!(&h.citation, Citation::Code { .. }))
.expect("at least one Citation::Code hit for 'add'");
match &h.citation {
Citation::Code {
lang,
symbol,
line_start,
..
} => {
assert_eq!(
lang.as_deref(),
Some("rust"),
"citation.lang must be 'rust'"
);
assert_eq!(
symbol.as_deref(),
Some("add"),
"citation.symbol must be 'add'"
);
assert!(*line_start >= 1, "line_start must be ≥1");
}
_ => unreachable!(),
}
assert_eq!(
h.code_lang.as_deref(),
Some("rust"),
"SearchHit.code_lang must be 'rust'"
);
}
/// Re-ingesting the same `.rs` file without changes must report
/// `Unchanged` (incremental-skip path exercised).
#[test]
fn rust_file_re_ingest_is_unchanged() {
let env = TestEnv::lexical_only();
std::fs::write(
env.workspace_root.join("stable.rs"),
"pub fn noop() {}\n",
)
.unwrap();
let r1 =
kebab_app::ingest_with_config(env.config.clone(), env.scope(), false).unwrap();
let item1 = r1
.items
.as_ref()
.unwrap()
.iter()
.find(|i| i.doc_path.0.ends_with("stable.rs"))
.cloned()
.unwrap();
assert_eq!(item1.kind, IngestItemKind::New);
let r2 =
kebab_app::ingest_with_config(env.config.clone(), env.scope(), false).unwrap();
let item2 = r2
.items
.unwrap()
.into_iter()
.find(|i| i.doc_path.0.ends_with("stable.rs"))
.unwrap();
assert_eq!(
item2.kind,
IngestItemKind::Unchanged,
"identical bytes → Unchanged"
);
assert_eq!(item2.doc_id, item1.doc_id);
}