use super::*;
struct Chars<'t>(&'t str);
impl MeasureText for Chars<'_> {
fn measure(&self, text: &str) -> usize {
text.chars().count()
}
}
impl RangeMeasure for Chars<'_> {
type Error = Error;
fn measure_range(&self, start: usize, end: usize) -> Result<usize, Error> {
Ok(self.0[start..end].chars().count())
}
}
fn cut(text: &str, opts: &WindowOptions, ceiling: usize) -> Result<Vec<(usize, usize)>, Error> {
chunk_long(&Chars(text), text, opts, ceiling)
.map(|chunks| chunks.iter().map(|c| (c.start(), c.end())).collect())
}
const CEILING: usize = 512;
type Spans = &'static [(usize, usize)];
#[test]
fn the_shared_chunker_cuts_granites_geometry() {
let w = WindowOptions::new;
let cases: &[(&str, WindowOptions, Spans)] = &[
("aa\n\nbbbbb", w(5), &[(0, 4), (4, 9)]),
("aa\n\nbb", w(5), &[(0, 4), (4, 6)]),
("aaaaa\n\nbbb", w(5), &[(0, 5), (5, 10)]),
("aaaaa\n\nbbbbb", w(5), &[(0, 5), (5, 7), (7, 12)]),
("\n\naa\n\nbbb", w(5), &[(0, 4), (4, 9)]),
("\n\naa\n\nbbbbb", w(5), &[(0, 4), (4, 6), (6, 11)]),
("\n\naaaaa", w(5), &[(0, 2), (2, 7)]),
("aaa\n\nbb\n\n", w(5), &[(0, 5), (5, 9)]),
("aaaaa\n\n", w(5), &[(0, 5), (5, 7)]),
("\n\naaa", w(5), &[(0, 5)]),
("aaa\n\n", w(5), &[(0, 5)]),
("aaa, bbb, ccc", w(5), &[(0, 5), (5, 10), (10, 13)]),
(" ", w(5), &[(0, 3)]),
(" ", w(5), &[(0, 5)]),
("aaaaa bbbbb", w(5), &[(0, 5), (5, 10), (10, 15)]),
(" aaaaa", w(5), &[(0, 5), (5, 10)]),
("aaaaa ", w(5), &[(0, 5), (5, 10)]),
("aaa\n\nbb", w(5), &[(0, 5), (5, 7)]),
(
"aaa\n\nbb",
w(5).with_tail(TailPolicy::PadFull),
&[(0, 5), (5, 7)],
),
(
"aaa\n\nbb",
w(5).with_tail(TailPolicy::DropBelowMin(2)),
&[(0, 5), (5, 7)],
),
(
"aaa\n\nbb",
w(5).with_tail(TailPolicy::DropBelowMin(3)),
&[(0, 3), (3, 7)],
),
(
"aaa",
w(5).with_tail(TailPolicy::DropBelowMin(4)),
&[(0, 3)],
),
(
"aa bb cc dd ee",
w(5).with_overlap(2),
&[(0, 5), (3, 8), (6, 11), (9, 14)],
),
("", w(5), &[]),
("", w(5).with_max_windows(0), &[]),
(
"aaaaa\n\nbbbbb",
w(5).with_max_windows(3),
&[(0, 5), (5, 7), (7, 12)],
),
(" ", w(5).with_max_windows(1), &[(0, 3)]),
];
let mut moved = Vec::new();
for &(text, opts, expected) in cases {
match cut(text, &opts, CEILING) {
Ok(got) if got == expected => {}
other => moved.push(format!(
"{text:?} at {opts}: expected {expected:?}, got {other:?}"
)),
}
}
assert!(
moved.is_empty(),
"the shared chunker moved granite's geometry:\n{}",
moved.join("\n")
);
}
#[test]
fn the_shared_chunker_refuses_with_granites_payloads() {
let w = WindowOptions::new;
let windowing: &[(&str, WindowOptions, WinditError)] = &[
(
"aaaaa\n\nbbbbb",
w(5).with_max_windows(2),
WinditError::TooManyWindows { got: 3, max: 2 },
),
(
"\n\naaaaa\n\nbbbbb\n\n",
w(5).with_max_windows(3),
WinditError::TooManyWindows { got: 5, max: 3 },
),
(
" ",
w(5).with_max_windows(0),
WinditError::TooManyWindows { got: 1, max: 0 },
),
(
"aaaaa\n\nbbbbb\n\nccccc",
w(5).with_max_windows(1),
WinditError::TooManyWindows { got: 2, max: 1 },
),
("aaa", w(0), WinditError::ZeroWindow),
(
"aaa",
w(5).with_overlap(5),
WinditError::OverlapGeWindow {
overlap: 5,
window: 5,
},
),
];
let mut wrong = Vec::new();
for &(text, opts, expected) in windowing {
match cut(text, &opts, CEILING) {
Err(Error::Windowing(got)) if got == expected => {}
other => wrong.push(format!(
"{text:?} at {opts}: expected {expected:?}, got {other:?}"
)),
}
}
let spaces = " ".repeat(513);
let contentless = [
(spaces.clone(), w(512), (0, 513, 513, 512)),
(
spaces.clone(),
w(512).with_max_windows(0),
(0, 513, 513, 512),
),
(format!("a{spaces}b"), w(1), (1, 514, 513, 512)),
(format!("{spaces}a"), w(1), (0, 513, 513, 512)),
(format!("a{spaces}"), w(1), (1, 514, 513, 512)),
];
for (text, opts, expected) in &contentless {
match cut(text, opts, CEILING) {
Err(Error::ContentlessInputOverBudget(o))
if (o.start(), o.end(), o.tokens(), o.max()) == *expected => {}
other => wrong.push(format!(
"{} bytes at {opts}: expected ContentlessInputOverBudget {expected:?}, got {other:?}",
text.len()
)),
}
}
assert_eq!(
cut(&" ".repeat(512), &w(512), CEILING).ok(),
Some(vec![(0, 512)])
);
assert!(
wrong.is_empty(),
"the shared chunker refused differently:\n{}",
wrong.join("\n")
);
}
#[test]
fn the_ceiling_is_the_doors_parameter() {
let w = WindowOptions::new;
let cases: &[(&str, WindowOptions, Spans, (usize, usize))] = &[
(" ", w(5), &[(0, 5)], (0, 5)),
(" aaaaa", w(5), &[(0, 5), (5, 10)], (0, 5)),
(
"aaaaa bbbbb",
w(5),
&[(0, 5), (5, 10), (10, 15)],
(5, 10),
),
("aaaaa ", w(5), &[(0, 5), (5, 10)], (5, 10)),
];
let mut wrong = Vec::new();
for &(text, opts, fits, (start, end)) in cases {
match cut(text, &opts, 4) {
Err(Error::ContentlessInputOverBudget(o))
if (o.start(), o.end(), o.tokens(), o.max()) == (start, end, 5, 4) => {}
other => wrong.push(format!(
"{text:?} at {opts}, ceiling 4: expected a refusal of {start}..{end} carrying 4, got \
{other:?}"
)),
}
match cut(text, &opts, 5) {
Ok(got) if got == fits => {}
other => wrong.push(format!(
"{text:?} at {opts}, ceiling 5: expected {fits:?}, got {other:?}"
)),
}
}
match cut(" ", &w(5).with_max_windows(0), 4) {
Err(Error::ContentlessInputOverBudget(o)) if (o.tokens(), o.max()) == (5, 4) => {}
other => wrong.push(format!(
"cap 0, ceiling 4: expected ContentlessInputOverBudget, got {other:?}"
)),
}
match cut(" ", &w(5).with_max_windows(0), 5) {
Err(Error::Windowing(WinditError::TooManyWindows { got: 1, max: 0 })) => {}
other => wrong.push(format!(
"cap 0, ceiling 5: expected TooManyWindows, got {other:?}"
)),
}
assert!(
wrong.is_empty(),
"the ceiling is not the caller's:\n{}",
wrong.join("\n")
);
let opts = LongTextOptions::from(w(5));
match validate_long_input("any", &opts, 4) {
Err(Error::WindowOverBudget(over)) => assert_eq!((over.window(), over.max()), (5, 4)),
other => panic!("expected WindowOverBudget carrying 4, got {other:?}"),
}
assert!(validate_long_input("any", &opts, 5).is_ok());
}
#[cfg(feature = "granite")]
#[test]
fn each_shared_refusal_prints_granites_message() {
use crate::embeddings::granite;
let cases: [(Error, &str); 4] = [
(
Error::Windowing(WinditError::TooManyWindows { got: 3, max: 2 }),
"windowed-sequence processing failed: produced 3 windows, exceeding the maximum of 2",
),
(
Error::WindowOverBudget(WindowOverBudget::new(513, 512)),
"embed_long window budget 513 exceeds the model's fixed 512-token input window",
),
(
Error::InputTooLarge(InputTooLarge::new(8_388_608, 1_048_576)),
"text input is 8388608 bytes, exceeding the configured 1048576-byte limit",
),
(
Error::ContentlessInputOverBudget(ContentlessInputOverBudget::new(5, 10, 5, 4)),
"contentless text at bytes 5..10 measures 5 tokens, exceeding the model's fixed 4-token \
window",
),
];
for (shared, printed) in cases {
assert_eq!(shared.to_string(), printed, "the shared message moved");
assert_eq!(
granite::Error::from(shared).to_string(),
printed,
"granite's message moved"
);
}
}
#[cfg(all(feature = "granite", feature = "serde"))]
fn golden_lines(prefixes: &[&str]) -> Vec<&'static str> {
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/lifted_observables_812da2d.txt"
))
.lines()
.filter(|line| !line.starts_with("# "))
.filter(|line| prefixes.iter().any(|prefix| line.starts_with(prefix)))
.collect()
}
#[cfg(all(feature = "granite", feature = "serde"))]
fn mismatches(rendered: &[String], golden: &[&str]) -> Vec<String> {
let mut out = Vec::new();
if rendered.len() != golden.len() {
out.push(format!(
"line count: rendered {}, golden {}",
rendered.len(),
golden.len()
));
}
for (index, (got, want)) in rendered.iter().zip(golden).enumerate() {
if got != want {
out.push(format!(
"line {index}\n rendered: {got}\n golden: {want}"
));
}
}
out
}
#[cfg(all(feature = "granite", feature = "serde"))]
#[test]
fn the_long_text_observables_are_the_pre_lift_observables() {
use crate::embeddings::granite;
let mut rendered = Vec::new();
rendered.push(format!(
"granite::Error::WindowOverBudget: {}",
granite::Error::WindowOverBudget(granite::WindowOverBudget::new(513, 512))
));
rendered.push(format!(
"granite::Error::InputTooLarge: {}",
granite::Error::InputTooLarge(granite::error::InputTooLarge::new(8_388_608, 1_048_576))
));
rendered.push(format!(
"granite::Error::ContentlessInputOverBudget: {}",
granite::Error::ContentlessInputOverBudget(granite::ContentlessInputOverBudget::new(
5, 10, 5, 512
))
));
let windowing =
granite::Error::Windowing(granite::error::WinditError::TooManyWindows { got: 3, max: 2 });
rendered.push(format!(
"granite::Error::Windowing: {windowing} | source={:?}",
std::error::Error::source(&windowing).map(|s| s.to_string())
));
rendered.push(format!(
"payload debug: {:?} {:?} {:?}",
granite::WindowOverBudget::new(1, 2),
granite::InputTooLarge::new(3, 4),
granite::error::ContentlessInputOverBudget::new(5, 6, 7, 8)
));
for options in [
granite::LongTextOptions::new(),
granite::LongTextOptions::default(),
granite::LongTextOptions::from(granite::WindowOptions::new(64).with_overlap(8)),
granite::LongTextOptions::new()
.with_max_input_bytes(4096)
.with_tail_policy(granite::TailPolicy::DropBelowMin(3)),
] {
rendered.push(format!(
"LongTextOptions: display={options} | debug={options:?} | json={} | postcard={:?} | tail={:?} | window={}",
serde_json::to_string(&options).unwrap(),
postcard::to_allocvec(&options).unwrap(),
options.tail_policy(),
options.window_options().window(),
));
}
rendered.push(format!(
"LongTextOptions from {{}}: {:?}",
serde_json::from_str::<granite::LongTextOptions>("{}").unwrap()
));
rendered.push(format!(
"LongTextOptions refusal: {}",
serde_json::from_str::<granite::LongTextOptions>(r#"{"max_input_byte":4096}"#).unwrap_err()
));
let golden = golden_lines(&[
"granite::Error::",
"payload debug: WindowOverBudget",
"LongTextOptions",
]);
assert_eq!(golden.len(), 11, "the long-text law covers 11 golden lines");
let moved = mismatches(&rendered, &golden);
assert!(
moved.is_empty(),
"{} observable(s) differ from the pre-lift golden:\n{}",
moved.len(),
moved.join("\n")
);
}
#[cfg(feature = "serde")]
#[test]
fn a_cap_misplaced_in_the_tail_policy_is_refused() {
let document = r#"{"window_options":{"window":512,"hop":512,"tail":{"kind":"keep_with_coverage","max_windows":1}}}"#;
let refused = serde_json::from_str::<LongTextOptions>(document).unwrap_err();
assert!(refused.to_string().contains("max_windows"), "{refused}");
}