cut PR v0.18.0 전 마지막 정리. 사용자 요청: "전체 코드베이스를 깔끔하고 알아보기 쉽게".
## Workspace lints
- `Cargo.toml` 의 `[workspace.lints.clippy]` 에 `pedantic = "warn"` (priority -1) + 의도적 allow-list 추가:
- cast_possible_truncation / cast_possible_wrap / cast_sign_loss / cast_precision_loss — ONNX i64 / hash modular reduction 등 의도적 truncation.
- doc_markdown / missing_errors_doc / missing_panics_doc — cosmetic doc style.
- too_many_lines / module_name_repetitions / must_use_candidate / needless_pass_by_value / manual_let_else / items_after_statements / similar_names — informational only.
- format_collect / match_wildcard_for_single_variants / trivially_copy_pass_by_ref / unnecessary_wraps — intentional patterns (exhaustive match, future Result variants 등).
- default_trait_access — `Foo::default()` 가 idiomatic.
- float_cmp — NLI / RRF score 의 explicit threshold 비교 의도.
- struct_excessive_bools / case_sensitive_file_extension_comparisons / naive_bytecount / ignore_without_reason — domain-specific 의도.
- format_push_string / return_self_not_must_use / match_same_arms — builder / wire-label / hot-path 패턴 보존.
- needless_continue / used_underscore_binding / nonminimal_bool / unreadable_literal / many_single_char_names / doc_link_with_quotes / assigning_clones / collapsible_str_replace / trivial_regex / elidable_lifetime_names / range_plus_one / explicit_iter_loop / implicit_hasher / ref_option — remaining low-value style.
- 각 24 crate `Cargo.toml` 에 `[lints] workspace = true` 추가.
## Auto-fix
`cargo clippy --workspace --all-targets --fix` 적용 — 128 files changed, 552 insertions / 472 deletions. 주로:
- uninlined_format_args (~18): `format!("{}", x)` → `format!("{x}")`.
- redundant_closure_for_method_calls (~33): `.map(|x| x.foo())` → `.map(T::foo)`.
- 그 외 mechanical refactor.
## 검증
- `cargo clippy --workspace --all-targets -j 1 -- -D warnings` clean (pedantic + 모든 lint group).
- `cargo test --workspace --no-fail-fast -j 1` — **1293 tests pass + 1 pre-existing flaky fail** (`kebab-mcp::tools_call_ask_multi_hop::ask_tool_routes_multi_hop_true_to_decompose_first`, HOTFIX candidate, cleanup 무관). 회귀 0.
Wire 영향: 없음.
Behavior 영향: 없음 (mechanical refactor only).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
184 lines
6.7 KiB
Rust
184 lines
6.7 KiB
Rust
//! Shared scaffolding for kb-store-vector integration tests.
|
|
//!
|
|
//! # Test policy
|
|
//!
|
|
//! Integration tests in this crate are marked `#[ignore]` and require
|
|
//! AVX-capable hardware. They are excluded from the default `cargo
|
|
//! test -p kb-store-vector` lane and only run when explicitly opted
|
|
//! in:
|
|
//!
|
|
//! ```text
|
|
//! cargo test -p kb-store-vector -- --ignored
|
|
//! ```
|
|
//!
|
|
//! The reason: LanceDB's f32 SIMD path uses unconditional AVX
|
|
//! intrinsics (`__m256` in `lance-linalg::simd::f32`). On x86_64
|
|
//! CPUs without AVX support — notably QEMU's default `qemu64` model
|
|
//! in CI sandboxes and some bare-metal dev boxes — those instructions
|
|
//! trigger `SIGILL: illegal instruction` at the first `vector_search`
|
|
//! call. Rather than silently turn that into a "passing" test (which
|
|
//! it isn't), we gate the integration suite behind `#[ignore]` and
|
|
//! call [`require_avx_or_panic`] inside each test body so that an
|
|
//! `--ignored` invocation on a non-AVX host fails loudly rather than
|
|
//! crashing later inside a Lance kernel.
|
|
//!
|
|
//! This mirrors P3-2's `#[ignore]` policy on tests that require a
|
|
//! model download — both are CI-lane decisions, not silent skips.
|
|
//!
|
|
//! Each test owns a `TempDir` (vector_dir + sqlite db live underneath
|
|
//! it), a fully-migrated `SqliteStore`, and a `LanceVectorStore`
|
|
//! pointed at both. We seed `documents` / `chunks` rows directly via
|
|
//! SQL (rather than going through `DocumentStore::put_document`) so
|
|
//! the tests stay independent of kb-parse-md / kb-normalize / kb-chunk
|
|
//! and so we can construct adversarial fixtures (filtered tags,
|
|
//! mismatched langs) without reproducing a Markdown round-trip.
|
|
|
|
#![allow(dead_code)]
|
|
|
|
use std::path::PathBuf;
|
|
use std::sync::Arc;
|
|
|
|
/// Panic if the host CPU lacks AVX. Called from every `#[ignore]`-d
|
|
/// integration test body so that `cargo test -- --ignored` on a
|
|
/// non-AVX host fails loudly with a clear message instead of crashing
|
|
/// later inside a Lance SIMD kernel with `SIGILL`.
|
|
///
|
|
/// On non-x86_64 hosts this is a no-op (Lance's AVX requirement is
|
|
/// x86-only — ARM/Apple Silicon paths use different intrinsics that
|
|
/// the workspace doesn't currently target).
|
|
pub fn require_avx_or_panic() {
|
|
#[cfg(target_arch = "x86_64")]
|
|
{
|
|
assert!(std::is_x86_feature_detected!("avx"),
|
|
"kb-store-vector integration test requires AVX-capable hardware; \
|
|
host CPU lacks AVX. Run on an AVX-capable machine. \
|
|
See crates/kb-store-vector/tests/common/mod.rs."
|
|
);
|
|
}
|
|
}
|
|
|
|
use kebab_config::Config;
|
|
use kebab_core::{
|
|
ChunkId, DocumentId, EmbeddingId, EmbeddingModelId, EmbeddingVersion, VectorRecord,
|
|
};
|
|
use kebab_store_sqlite::SqliteStore;
|
|
use kebab_store_vector::LanceVectorStore;
|
|
use rusqlite::params;
|
|
use tempfile::TempDir;
|
|
|
|
pub struct TestEnv {
|
|
pub temp: TempDir,
|
|
pub config: Config,
|
|
pub sqlite: Arc<SqliteStore>,
|
|
pub vector: LanceVectorStore,
|
|
}
|
|
|
|
impl TestEnv {
|
|
pub fn new() -> Self {
|
|
let temp = tempfile::tempdir().expect("tempdir");
|
|
let mut config = Config::defaults();
|
|
config.storage.data_dir = temp.path().to_string_lossy().into_owned();
|
|
let sqlite = SqliteStore::open(&config).unwrap();
|
|
sqlite.run_migrations().unwrap();
|
|
let sqlite = Arc::new(sqlite);
|
|
let vector = LanceVectorStore::new(&config, sqlite.clone()).unwrap();
|
|
Self {
|
|
temp,
|
|
config,
|
|
sqlite,
|
|
vector,
|
|
}
|
|
}
|
|
|
|
pub fn data_dir(&self) -> PathBuf {
|
|
self.temp.path().to_path_buf()
|
|
}
|
|
|
|
/// Insert minimum (asset, document, chunk) rows so phase-1
|
|
/// embedding_records inserts don't trip the FK to chunks /
|
|
/// documents.
|
|
pub fn seed_chunk(
|
|
&self,
|
|
chunk_id: &str,
|
|
doc_id: &str,
|
|
workspace_path: &str,
|
|
lang: &str,
|
|
tags: &[&str],
|
|
trust_level: &str,
|
|
) {
|
|
// Asset id derived from doc_id deterministically — every
|
|
// chunk gets its own asset to keep things simple.
|
|
let asset_id = format!("a{}", &doc_id[..31]);
|
|
let conn = self.sqlite.read_conn();
|
|
conn.execute(
|
|
"INSERT OR IGNORE INTO assets (
|
|
asset_id, source_uri, workspace_path, media_type, byte_len,
|
|
checksum, storage_kind, storage_path, discovered_at
|
|
) VALUES (?, ?, ?, ?, 0, ?, 'reference', ?, '1970-01-01T00:00:00Z')",
|
|
params![
|
|
asset_id,
|
|
format!("file://{workspace_path}"),
|
|
workspace_path,
|
|
"{}",
|
|
"deadbeefdeadbeefdeadbeefdeadbeef",
|
|
workspace_path,
|
|
],
|
|
)
|
|
.unwrap();
|
|
conn.execute(
|
|
"INSERT OR IGNORE INTO documents (
|
|
doc_id, asset_id, workspace_path, title, lang, source_type,
|
|
trust_level, parser_version, doc_version, schema_version,
|
|
metadata_json, provenance_json, created_at, updated_at
|
|
) VALUES (?, ?, ?, NULL, ?, 'markdown', ?, 'v1', 1, 1, '{}', '{}',
|
|
'1970-01-01T00:00:00Z', '1970-01-01T00:00:00Z')",
|
|
params![doc_id, asset_id, workspace_path, lang, trust_level],
|
|
)
|
|
.unwrap();
|
|
for t in tags {
|
|
conn.execute(
|
|
"INSERT OR IGNORE INTO document_tags (doc_id, tag) VALUES (?, ?)",
|
|
params![doc_id, t],
|
|
)
|
|
.unwrap();
|
|
}
|
|
conn.execute(
|
|
"INSERT OR IGNORE INTO chunks (
|
|
chunk_id, doc_id, text, heading_path_json, section_label,
|
|
source_spans_json, token_estimate, chunker_version,
|
|
policy_hash, block_ids_json, created_at
|
|
) VALUES (?, ?, 'hi', '[]', NULL, '[]', 1, 'v1', 'h', '[]', '1970-01-01T00:00:00Z')",
|
|
params![chunk_id, doc_id],
|
|
)
|
|
.unwrap();
|
|
}
|
|
}
|
|
|
|
/// Build a deterministic test VectorRecord from a few simple inputs.
|
|
/// `vector` is taken verbatim, `dimensions` is set from `vector.len()`.
|
|
pub fn make_record(
|
|
chunk_idx: u8,
|
|
doc_idx: u8,
|
|
vector: Vec<f32>,
|
|
text: &str,
|
|
heading: &[&str],
|
|
model: &str,
|
|
) -> VectorRecord {
|
|
let dim = vector.len();
|
|
let chunk_id = ChunkId(format!("{:032x}", 0x1100u32 + u32::from(chunk_idx)));
|
|
let doc_id = DocumentId(format!("{:032x}", 0xd0c0u32 + u32::from(doc_idx)));
|
|
let embedding_id =
|
|
EmbeddingId(format!("{:032x}", 0xeeee0000u32 + u32::from(chunk_idx)));
|
|
VectorRecord {
|
|
chunk_id,
|
|
embedding_id,
|
|
vector,
|
|
doc_id,
|
|
text: text.to_string(),
|
|
heading_path: heading.iter().map(std::string::ToString::to_string).collect(),
|
|
model_id: EmbeddingModelId(model.to_string()),
|
|
model_version: EmbeddingVersion("v1".to_string()),
|
|
dimensions: dim,
|
|
}
|
|
}
|