Files
kebab/crates/kebab-core/tests/mock_embedder.rs
altair823 2d68827cf5 refactor(core): 빈 re-export shim crate kebab-embed/kebab-llm → kebab-core 흡수
kebab-embed/kebab-llm 은 "새 type 없음"을 자처한 순수 re-export 셸이었다
(trait 은 이미 kebab-core 소유, mock + test helper 만 보유). "kebab-core
재구성 시 안정 surface" 라는 명분은 1인 RAG 엔 speculative YAGNI. 흡수:

- kebab-core 에 default-OFF `mock` feature + src/mock.rs (MockEmbedder,
  MockLanguageModel, assert_vector_shape/assert_unit_norm/assert_finish_chunk
  을 kebab_core:: → crate:: import 만 바꿔 verbatim 이동).
- production import 2곳(kebab-embed-local, kebab-llm-local) +
  test import 다수(search/rag/parse-image/embed-local) 를 kebab_core 로 repoint.
  mock 쓰는 crate 는 dev-dep 에 features=["mock"] (default 빌드 무영향).
- shim 자체 테스트: mock 동작 테스트는 kebab-core/tests/ 로 이동,
  reexports.rs(셸 재수출 테스트)는 폐기.
- crates/kebab-embed, crates/kebab-llm 삭제 + workspace member/deps 정리.
- ARCHITECTURE/HANDOFF/component README 의 crate 그래프·표·rationale 갱신
  (22 → 20 crates). llm-local 의 broken intra-doc link 2건도 정리.

trait surface·동작 불변 (test-only + import-rename). workspace build 는 mock
default-OFF 라 mock 코드 미컴파일. 적대적 검증 3렌즈(build-test-integrity +
behavior-identity[mock byte-identical] + dead-crate-completeness) 통과,
clippy --workspace -D warnings 클린.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Mc6W1fgsrbFKTsqA6P8La
2026-06-27 01:06:14 +00:00

180 lines
5.8 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Integration tests for `MockEmbedder`. Gated behind the `mock` feature.
//!
//! Canonical invocation: `cargo test -p kebab-core --features mock`.
//! (Without `--features mock` this file compiles to nothing — the `cfg` gate
//! below short-circuits, since the mock lives in `kebab-core`'s own optional
//! `mock` module and cannot be enabled via a self dev-dependency.)
#![cfg(feature = "mock")]
use kebab_core::{
Embedder, EmbeddingInput, EmbeddingKind, EmbeddingModelId, EmbeddingVersion, MockEmbedder,
assert_unit_norm, assert_vector_shape,
};
use proptest::prelude::*;
fn mk(dims: usize) -> MockEmbedder {
MockEmbedder::new(
EmbeddingModelId("mock-test".into()),
EmbeddingVersion("0".into()),
dims,
)
}
#[test]
fn dyn_dispatch_through_box() {
let e: Box<dyn Embedder> = Box::new(mk(8));
assert_eq!(e.dimensions(), 8);
assert_eq!(e.model_id(), EmbeddingModelId("mock-test".into()));
assert_eq!(e.model_version(), EmbeddingVersion("0".into()));
let inputs = [EmbeddingInput {
text: "a fox",
kind: EmbeddingKind::Document,
}];
let v = e.embed(&inputs).expect("embed via box");
assert_eq!(v.len(), 1);
assert_vector_shape(&v, 8);
}
#[test]
fn identical_input_yields_byte_identical_vector() {
let e = mk(16);
let a = e
.embed(&[EmbeddingInput {
text: "the quick brown fox",
kind: EmbeddingKind::Document,
}])
.unwrap();
let b = e
.embed(&[EmbeddingInput {
text: "the quick brown fox",
kind: EmbeddingKind::Document,
}])
.unwrap();
// Vec<Vec<f32>> equality is byte-equal because we did not mutate
// either side and the hash + normalization path is pure.
assert_eq!(a, b);
}
#[test]
fn document_and_query_kinds_differ_for_same_text() {
let e = mk(32);
let inputs = [
EmbeddingInput {
text: "needle in haystack",
kind: EmbeddingKind::Document,
},
EmbeddingInput {
text: "needle in haystack",
kind: EmbeddingKind::Query,
},
];
let v = e.embed(&inputs).unwrap();
assert_eq!(v.len(), 2);
assert_vector_shape(&v, 32);
assert_ne!(
v[0], v[1],
"Document and Query kinds must produce different vectors for identical text"
);
}
#[test]
fn dimensions_match_construction() {
for dims in [1usize, 4, 64, 384, 768, 1024] {
let e = mk(dims);
assert_eq!(e.dimensions(), dims);
let v = e
.embed(&[EmbeddingInput {
text: "x",
kind: EmbeddingKind::Document,
}])
.unwrap();
assert_vector_shape(&v, dims);
}
}
#[test]
fn different_seeds_produce_different_vectors() {
let a = MockEmbedder::with_seed(
EmbeddingModelId("m".into()),
EmbeddingVersion("0".into()),
16,
0,
);
let b = MockEmbedder::with_seed(
EmbeddingModelId("m".into()),
EmbeddingVersion("0".into()),
16,
1,
);
let inputs = [EmbeddingInput {
text: "same input",
kind: EmbeddingKind::Document,
}];
assert_ne!(a.embed(&inputs).unwrap(), b.embed(&inputs).unwrap());
}
proptest! {
#![proptest_config(ProptestConfig {
cases: 100,
..ProptestConfig::default()
})]
/// 100 random `(text, kind)` pairs: every output vector must have
/// `len == dimensions`, contain only finite floats, contain no NaNs,
/// be L2 unit-norm within tolerance, be re-deterministic across calls,
/// differ between Document/Query kinds, and differ between distinct texts.
#[test]
fn random_inputs_yield_well_formed_vectors(
text in ".{0,256}",
text2 in ".{0,256}",
is_query in any::<bool>(),
// dims ≥ 2: a 1-dim unit-norm vector has only two possible values
// (`[1.0]` or `[-1.0]`), which makes the kind/text differential
// assertions degenerate. Pick a floor of 2 so the differentials
// exercise non-degenerate vector space.
dims in 2usize..=128,
) {
// Skip degenerate case where the two random texts collide; the
// "distinct text → distinct vector" assertion below requires them to
// differ.
prop_assume!(text != text2);
let e = mk(dims);
let kind = if is_query { EmbeddingKind::Query } else { EmbeddingKind::Document };
let v = e.embed(&[EmbeddingInput { text: &text, kind }]).unwrap();
prop_assert_eq!(v.len(), 1);
prop_assert_eq!(v[0].len(), dims);
for x in &v[0] {
prop_assert!(x.is_finite(), "component {x} not finite");
prop_assert!(!x.is_nan(), "component {x} is NaN");
}
// L2 unit-norm within tolerance. `5e-4` is a safe upper bound up to
// dims = 128 here (would-be floor: f32::EPSILON × √dims).
assert_unit_norm(&v, 5e-4);
// Re-determinism: embedding `text` as Document twice → byte-equal.
let doc_a = e
.embed(&[EmbeddingInput { text: &text, kind: EmbeddingKind::Document }])
.unwrap();
let doc_b = e
.embed(&[EmbeddingInput { text: &text, kind: EmbeddingKind::Document }])
.unwrap();
prop_assert_eq!(&doc_a, &doc_b, "Doc(text) must be byte-equal across calls");
// Kind differential: Doc(text) != Query(text).
let q = e
.embed(&[EmbeddingInput { text: &text, kind: EmbeddingKind::Query }])
.unwrap();
prop_assert_ne!(&doc_a, &q, "Doc(text) must differ from Query(text)");
// Text differential: Doc(text) != Doc(text2) when text != text2.
let doc_other = e
.embed(&[EmbeddingInput { text: &text2, kind: EmbeddingKind::Document }])
.unwrap();
prop_assert_ne!(&doc_a, &doc_other, "distinct texts must yield distinct Doc vectors");
}
}