mod common;
use coremlit::embeddings::granite::{
Error, LongTextOptions, MAX_TOKENS, TailPolicy, TextEmbedder, WindowOptions,
};
fn embedder() -> TextEmbedder {
TextEmbedder::from_file(common::model_path()).unwrap_or_else(|e| panic!("load granite: {e}"))
}
fn long_document() -> String {
(0..32)
.map(|p| {
(0..40)
.map(|w| format!("paragraph{p}word{w}"))
.collect::<Vec<_>>()
.join(" ")
})
.collect::<Vec<_>>()
.join("\n\n")
}
#[test]
#[ignore = "requires local granite model (EMBEDKIT_TEST_MODELS)"]
fn long_document_aggregates_to_one_unit_norm_vector() {
let emb = embedder();
let doc = long_document();
let out = emb
.embed_long(&doc)
.expect("embed_long a multi-window document");
let norm_sq: f32 = out.as_slice().iter().map(|x| x * x).sum();
assert!(
(norm_sq - 1.0).abs() < 1e-5,
"aggregate is not unit-norm: norm² = {norm_sq}"
);
assert!(
out.as_slice().iter().all(|v| v.is_finite()),
"aggregate has a non-finite component"
);
}
#[test]
#[ignore = "requires local granite model (EMBEDKIT_TEST_MODELS)"]
fn single_window_text_matches_embed() {
let emb = embedder();
let text = "a compact sentence that fits comfortably inside one window";
let via_long = emb.embed_long(text).expect("embed_long a short text");
let via_embed = emb.embed(text).expect("embed the same text");
assert!(
via_long.is_close(&via_embed, 1e-5),
"single-window embed_long must match embed"
);
}
#[test]
#[ignore = "requires local granite model (EMBEDKIT_TEST_MODELS)"]
fn empty_text_errors_like_embed() {
let emb = embedder();
assert!(matches!(emb.embed_long(""), Err(Error::EmptyText)));
}
#[test]
#[ignore = "requires local granite model (EMBEDKIT_TEST_MODELS)"]
fn over_budget_window_rejected_before_any_prediction() {
let emb = embedder();
let err = emb
.embed_long_with(
"any text",
&LongTextOptions::from(WindowOptions::new(MAX_TOKENS + 1)),
)
.unwrap_err();
assert!(
matches!(err, Error::WindowOverBudget(ref b) if b.window() == MAX_TOKENS + 1 && b.max() == MAX_TOKENS),
"expected WindowOverBudget, got {err:?}"
);
}
#[test]
#[ignore = "requires local granite model (EMBEDKIT_TEST_MODELS)"]
fn whitespace_at_cap_zero_rejected_before_any_prediction() {
use coremlit::embeddings::granite::error::WinditError;
let emb = embedder();
let err = emb
.embed_long_with(
" ",
&LongTextOptions::from(WindowOptions::new(MAX_TOKENS).with_max_windows(0)),
)
.unwrap_err();
assert!(
matches!(
err,
Error::Windowing(WinditError::TooManyWindows { got: 1, max: 0 })
),
"expected Windowing(TooManyWindows {{ got: 1, max: 0 }}), got {err:?}"
);
}
#[test]
#[ignore = "requires local granite model (EMBEDKIT_TEST_MODELS)"]
fn whitespace_at_cap_one_matches_embed() {
let emb = embedder();
let via_long = emb
.embed_long_with(
" ",
&LongTextOptions::from(WindowOptions::new(MAX_TOKENS).with_max_windows(1)),
)
.expect("cap 1 admits the one whole-input prediction");
let via_embed = emb.embed(" ").expect("embed whitespace");
assert!(
via_long.is_close(&via_embed, 1e-5),
"whole-input fallback must match embed"
);
}
#[test]
#[ignore = "requires local granite model (EMBEDKIT_TEST_MODELS)"]
fn contentless_over_budget_input_is_refused() {
let emb = embedder();
let err = emb.embed_long(&" ".repeat(100_000)).unwrap_err();
assert!(
matches!(err, Error::ContentlessInputOverBudget(_)),
"expected ContentlessInputOverBudget, got {err:?}"
);
}
#[test]
#[ignore = "requires local granite model (EMBEDKIT_TEST_MODELS)"]
fn oversized_input_rejected_with_input_too_large() {
let emb = embedder();
let big = "x".repeat(8 * 1024 * 1024);
let err = emb
.embed_long_with(&big, &LongTextOptions::new().with_max_input_bytes(1 << 20))
.unwrap_err();
assert!(
matches!(err, Error::InputTooLarge(ref l) if l.got() == big.len() && l.max() == (1 << 20)),
"expected InputTooLarge, got {err:?}"
);
}
#[test]
#[ignore = "requires local granite model (EMBEDKIT_TEST_MODELS)"]
fn a_drop_below_min_geometry_embeds_and_never_drops_the_text() {
let emb = embedder();
let doc = long_document();
let geometry = WindowOptions::new(64);
for opts in [
LongTextOptions::from(geometry),
LongTextOptions::from(geometry).with_tail_policy(TailPolicy::DropBelowMin(48)),
] {
let out = emb
.embed_long_with(&doc, &opts)
.unwrap_or_else(|e| panic!("embed_long under {opts:?}: {e}"));
let norm_sq: f32 = out.as_slice().iter().map(|x| x * x).sum();
assert!(
(norm_sq - 1.0).abs() < 1e-5,
"aggregate under {opts:?} is not unit-norm: norm² = {norm_sq}"
);
assert!(
out.as_slice().iter().all(|v| v.is_finite()),
"aggregate under {opts:?} has a non-finite component"
);
}
let short = "a compact sentence that fits comfortably inside one window";
let via_long = emb
.embed_long_with(
short,
&LongTextOptions::from(WindowOptions::new(MAX_TOKENS))
.with_tail_policy(TailPolicy::DropBelowMin(MAX_TOKENS)),
)
.expect("a lone below-minimum chunk must still embed");
let via_embed = emb.embed(short).expect("embed the same text");
assert!(
via_long.is_close(&via_embed, 1e-5),
"the whole-input fallback must match embed"
);
}
#[test]
#[ignore = "requires local granite model (EMBEDKIT_TEST_MODELS); timing for PR notes"]
fn separatorless_cjk_embed_long_timing() {
let emb = embedder();
const RUN: &str = "你好世界模型推理文本嵌入检索";
let mut doc = String::new();
while doc.len() < 31_374 {
doc.push_str(RUN);
}
emb.embed_long(&doc).expect("warm-up");
let runs = 3;
let t0 = std::time::Instant::now();
for _ in 0..runs {
emb.embed_long(&doc).expect("embed_long");
}
let total_ms = t0.elapsed().as_secs_f64() * 1e3 / runs as f64;
let window: String = doc.chars().take(560).collect();
let t1 = std::time::Instant::now();
for _ in 0..runs {
emb.embed(&window).expect("embed");
}
let pred_ms = t1.elapsed().as_secs_f64() * 1e3 / runs as f64;
println!(
"[embed_long:cjk] bytes={} total={total_ms:.1}ms one_window_prediction={pred_ms:.1}ms",
doc.len()
);
}