use crate::transform::utils::{get_comment_prefix, get_comment_suffix, score_node_kind};
use crate::{Language, Result};
use std::ops::Range;
#[derive(Debug, Clone)]
pub(crate) struct NodeSpan {
pub transformed_range: Range<usize>,
pub node_kind: &'static str,
}
impl NodeSpan {
pub fn new(transformed_range: Range<usize>, node_kind: &'static str) -> Self {
Self {
transformed_range,
node_kind,
}
}
fn line_count(&self) -> usize {
self.transformed_range
.end
.saturating_sub(self.transformed_range.start)
}
}
pub(crate) fn truncate_to_lines(
text: &str,
spans: &[NodeSpan],
language: Language,
max_lines: usize,
) -> Result<String> {
if spans.is_empty() {
return simple_line_truncate(text, language, max_lines);
}
let lines: Vec<&str> = text.lines().collect();
if lines.len() <= max_lines {
return Ok(text.to_string());
}
let valid_spans: Vec<&NodeSpan> = spans
.iter()
.filter(|s| s.line_count() > 0 && s.transformed_range.start < lines.len())
.collect();
if valid_spans.is_empty() {
return simple_line_truncate(text, language, max_lines);
}
let mut scored: Vec<(u8, &NodeSpan)> = valid_spans
.iter()
.map(|span| (score_node_kind(span.node_kind), *span))
.collect();
scored.sort_by(|a, b| {
b.0.cmp(&a.0).then_with(|| {
a.1.transformed_range
.start
.cmp(&b.1.transformed_range.start)
})
});
let mut selected: Vec<(u8, &NodeSpan)> = Vec::new();
let mut lines_used: usize = 0;
for &(priority, span) in &scored {
let clamped_end = span.transformed_range.end.min(lines.len());
let clamped_lines = clamped_end.saturating_sub(span.transformed_range.start);
if clamped_lines == 0 {
continue;
}
if lines_used + clamped_lines <= max_lines {
selected.push((priority, span));
lines_used += clamped_lines;
} else if selected.is_empty() {
selected.push((priority, span));
break;
}
}
selected.sort_by_key(|(_, s)| s.transformed_range.start);
let selected_spans: Vec<&NodeSpan> = selected.iter().map(|(_, s)| *s).collect();
let mut markers = count_markers(&selected_spans, lines.len());
while lines_used + markers > max_lines && selected.len() > 1 {
let Some(drop_idx) = selected
.iter()
.enumerate()
.min_by(|(_, a), (_, b)| {
a.0.cmp(&b.0).then_with(|| {
b.1.transformed_range
.start
.cmp(&a.1.transformed_range.start)
})
})
.map(|(idx, _)| idx)
else {
break; };
let (_, dropped_span) = selected.remove(drop_idx);
let dropped_lines = dropped_span
.transformed_range
.end
.min(lines.len())
.saturating_sub(dropped_span.transformed_range.start);
lines_used -= dropped_lines;
let selected_spans: Vec<&NodeSpan> = selected.iter().map(|(_, s)| *s).collect();
markers = count_markers(&selected_spans, lines.len());
}
let selected: Vec<&NodeSpan> = selected.into_iter().map(|(_, s)| s).collect();
if selected.is_empty() {
return simple_line_truncate(text, language, max_lines);
}
let prefix = get_comment_prefix(language);
let suffix = get_comment_suffix(language);
let omission_marker = format!("{} ... (truncated){}", prefix, suffix);
let mut result_lines: Vec<&str> = Vec::with_capacity(max_lines);
let mut last_end: usize = 0;
if selected[0].transformed_range.start > 0 {
result_lines.push(&omission_marker);
}
for span in &selected {
let start = span.transformed_range.start;
let end = span.transformed_range.end.min(lines.len());
if start > last_end && last_end > 0 {
result_lines.push(&omission_marker);
}
let remaining_budget = max_lines.saturating_sub(result_lines.len() + 1);
let span_end = end.min(start + remaining_budget);
for line_idx in start..span_end {
if line_idx < lines.len() {
result_lines.push(lines[line_idx]);
}
}
last_end = end;
}
if last_end < lines.len() && result_lines.len() < max_lines {
result_lines.push(&omission_marker);
}
result_lines.truncate(max_lines);
let mut output = result_lines.join("\n");
if text.ends_with('\n') {
output.push('\n');
}
Ok(output)
}
pub(crate) fn simple_line_truncate(
text: &str,
language: Language,
max_lines: usize,
) -> Result<String> {
let lines: Vec<&str> = text.lines().collect();
if lines.len() <= max_lines {
return Ok(text.to_string());
}
let prefix = get_comment_prefix(language);
let suffix = get_comment_suffix(language);
let marker = format!(
"{} ... ({} lines truncated){}",
prefix,
lines.len() - max_lines + 1,
suffix
);
let content_lines = max_lines.saturating_sub(1);
let mut result: Vec<&str> = lines[..content_lines].to_vec();
result.push(&marker);
let mut output = result.join("\n");
if text.ends_with('\n') {
output.push('\n');
}
Ok(output)
}
pub(crate) fn simple_last_line_truncate(
text: &str,
language: Language,
n: usize,
) -> Result<String> {
let total = text.lines().count();
if total <= n {
return Ok(text.to_string());
}
let prefix = get_comment_prefix(language);
let suffix = get_comment_suffix(language);
let content_lines = n.saturating_sub(1);
let omitted = total - n + 1;
let marker = format!("{} ... ({} lines above){}", prefix, omitted, suffix);
let skip = total - content_lines;
let mut result: Vec<&str> = Vec::with_capacity(n);
result.push(&marker);
result.extend(text.lines().skip(skip));
let mut output = result.join("\n");
if text.ends_with('\n') {
output.push('\n');
}
Ok(output)
}
fn count_markers(selected: &[&NodeSpan], total_lines: usize) -> usize {
if selected.is_empty() {
return 0;
}
let mut count = 0;
if selected[0].transformed_range.start > 0 {
count += 1;
}
for i in 1..selected.len() {
let prev_end = selected[i - 1].transformed_range.end.min(total_lines);
let curr_start = selected[i].transformed_range.start;
if curr_start > prev_end {
count += 1;
}
}
let last_end = selected[selected.len() - 1]
.transformed_range
.end
.min(total_lines);
if last_end < total_lines {
count += 1;
}
count
}
pub(crate) fn truncate_to_token_budget<F>(
text: &str,
language: Language,
token_budget: usize,
count_tokens: F,
known_token_count: Option<usize>,
) -> Result<String>
where
F: Fn(&str) -> usize,
{
let full_count = known_token_count.unwrap_or_else(|| count_tokens(text));
debug_assert!(
known_token_count.is_none() || known_token_count == Some(count_tokens(text)),
"known_token_count ({:?}) does not match actual count ({})",
known_token_count,
count_tokens(text),
);
if full_count <= token_budget {
return Ok(text.to_string());
}
let lines: Vec<&str> = text.lines().collect();
if lines.is_empty() {
return Ok(String::new());
}
let prefix = get_comment_prefix(language);
let suffix = get_comment_suffix(language);
let make_marker = |truncated_count: usize| {
format!(
"{} ... ({} lines truncated){}",
prefix, truncated_count, suffix
)
};
let joined = lines.join("\n");
let mut byte_end: Vec<usize> = Vec::with_capacity(lines.len());
let mut pos: usize = 0;
for (i, line) in lines.iter().enumerate() {
if i > 0 {
pos += 1; }
pos += line.len();
byte_end.push(pos);
}
let mut lo: usize = 1;
let mut hi: usize = lines.len();
let mut best: usize = 0;
while lo <= hi {
let mid = lo + (hi - lo) / 2;
let marker = make_marker(lines.len() - mid);
let content_slice = &joined[..byte_end[mid - 1]];
let mut candidate = String::with_capacity(content_slice.len() + 1 + marker.len());
candidate.push_str(content_slice);
candidate.push('\n');
candidate.push_str(&marker);
if count_tokens(&candidate) <= token_budget {
best = mid;
lo = mid + 1;
} else {
hi = mid - 1;
}
}
let marker = make_marker(lines.len() - best);
if best == 0 && count_tokens(&marker) > token_budget {
return Ok(String::new());
}
let mut output = if best > 0 {
let content_slice = &joined[..byte_end[best - 1]];
let mut s = String::with_capacity(content_slice.len() + 1 + marker.len() + 1);
s.push_str(content_slice);
s.push('\n');
s.push_str(&marker);
s
} else {
marker
};
if text.ends_with('\n') {
output.push('\n');
}
Ok(output)
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::panic)] mod tests {
use super::*;
#[test]
fn test_no_truncation_when_within_budget() {
let text = "line 1\nline 2\nline 3\n";
let spans = vec![NodeSpan::new(0..3, "source_file")];
let result = truncate_to_lines(text, &spans, Language::TypeScript, 10).unwrap();
assert_eq!(result, text);
}
#[test]
fn test_no_truncation_when_exact_budget() {
let text = "line 1\nline 2\nline 3\n";
let spans = vec![NodeSpan::new(0..3, "source_file")];
let result = truncate_to_lines(text, &spans, Language::TypeScript, 3).unwrap();
assert_eq!(result, text);
}
#[test]
fn test_truncation_respects_max_lines() {
let text = "import foo\ntype A = string\nfunction bar() {}\nfunction baz() {}\nlet x = 1\n";
let spans = vec![
NodeSpan::new(0..1, "import_statement"),
NodeSpan::new(1..2, "type_alias_declaration"),
NodeSpan::new(2..3, "function_declaration"),
NodeSpan::new(3..4, "function_declaration"),
NodeSpan::new(4..5, "expression_statement"),
];
let result = truncate_to_lines(text, &spans, Language::TypeScript, 3).unwrap();
let line_count = result.lines().count();
assert!(
line_count <= 3,
"Expected at most 3 lines, got {}: {:?}",
line_count,
result
);
}
#[test]
fn test_priority_ordering_types_over_functions() {
let text = "function foo() {}\ninterface Bar {}\nfunction baz() {}\n";
let spans = vec![
NodeSpan::new(0..1, "function_declaration"),
NodeSpan::new(1..2, "interface_declaration"),
NodeSpan::new(2..3, "function_declaration"),
];
let result = truncate_to_lines(text, &spans, Language::TypeScript, 3).unwrap();
assert!(
result.contains("interface Bar"),
"Should contain the interface: {:?}",
result
);
}
#[test]
fn test_priority_ordering_types_over_imports() {
let text = "import foo from 'foo'\ntype A = string\nimport bar from 'bar'\n";
let spans = vec![
NodeSpan::new(0..1, "import_statement"),
NodeSpan::new(1..2, "type_alias_declaration"),
NodeSpan::new(2..3, "import_statement"),
];
let result = truncate_to_lines(text, &spans, Language::TypeScript, 3).unwrap();
assert!(
result.contains("type A"),
"Should contain the type alias: {:?}",
result
);
}
#[test]
fn test_omission_markers_between_gaps() {
let text = "type A = string\nlet x = 1\nlet y = 2\nlet z = 3\ntype B = number\n";
let spans = vec![
NodeSpan::new(0..1, "type_alias_declaration"),
NodeSpan::new(1..2, "expression_statement"),
NodeSpan::new(2..3, "expression_statement"),
NodeSpan::new(3..4, "expression_statement"),
NodeSpan::new(4..5, "type_alias_declaration"),
];
let result = truncate_to_lines(text, &spans, Language::TypeScript, 4).unwrap();
assert!(
result.contains("// ... (truncated)"),
"Should contain omission marker: {:?}",
result
);
}
#[test]
fn test_python_omission_marker() {
let text = "import os\ndef foo(): pass\ndef bar(): pass\n";
let spans = vec![
NodeSpan::new(0..1, "import_statement"),
NodeSpan::new(1..2, "function_definition"),
NodeSpan::new(2..3, "function_definition"),
];
let result = truncate_to_lines(text, &spans, Language::Python, 2).unwrap();
assert!(
result.contains("# ... (truncated)"),
"Python should use # for omission marker: {:?}",
result
);
}
#[test]
fn test_markdown_omission_marker() {
let text = "# Heading 1\n## Heading 2\n## Heading 3\n## Heading 4\n";
let spans = vec![
NodeSpan::new(0..1, "atx_heading"),
NodeSpan::new(1..2, "atx_heading"),
NodeSpan::new(2..3, "atx_heading"),
NodeSpan::new(3..4, "atx_heading"),
];
let result = truncate_to_lines(text, &spans, Language::Markdown, 3).unwrap();
assert!(
result.contains("<!-- ... (truncated) -->"),
"Markdown should use HTML comment for omission marker: {:?}",
result
);
}
#[test]
fn test_empty_spans_falls_back_to_simple() {
let text = "line 1\nline 2\nline 3\nline 4\nline 5\n";
let spans: Vec<NodeSpan> = vec![];
let result = truncate_to_lines(text, &spans, Language::TypeScript, 3).unwrap();
let line_count = result.lines().count();
assert!(
line_count <= 3,
"Expected at most 3 lines, got {}",
line_count
);
}
#[test]
fn test_simple_line_truncate() {
let text = "line 1\nline 2\nline 3\nline 4\nline 5\n";
let result = simple_line_truncate(text, Language::TypeScript, 3).unwrap();
let line_count = result.lines().count();
assert!(
line_count <= 3,
"Expected at most 3 lines, got {}",
line_count
);
assert!(result.contains("line 1"));
assert!(result.contains("line 2"));
assert!(result.contains("// ... (3 lines truncated)"));
}
#[test]
fn test_simple_line_truncate_no_truncation() {
let text = "line 1\nline 2\n";
let result = simple_line_truncate(text, Language::TypeScript, 5).unwrap();
assert_eq!(result, text);
}
#[test]
fn test_max_lines_1_returns_one_line() {
let text = "type A = string\nfunction foo() {}\n";
let spans = vec![
NodeSpan::new(0..1, "type_alias_declaration"),
NodeSpan::new(1..2, "function_declaration"),
];
let result = truncate_to_lines(text, &spans, Language::TypeScript, 1).unwrap();
let line_count = result.lines().count();
assert!(
line_count <= 1,
"Expected at most 1 line, got {}: {:?}",
line_count,
result
);
}
#[test]
fn test_source_order_preservation() {
let text = "type A = string\ntype B = number\ntype C = boolean\nlet x = 1\nlet y = 2\n";
let spans = vec![
NodeSpan::new(0..1, "type_alias_declaration"),
NodeSpan::new(1..2, "type_alias_declaration"),
NodeSpan::new(2..3, "type_alias_declaration"),
NodeSpan::new(3..4, "expression_statement"),
NodeSpan::new(4..5, "expression_statement"),
];
let result = truncate_to_lines(text, &spans, Language::TypeScript, 5).unwrap();
let result_lines: Vec<&str> = result.lines().collect();
let type_a_pos = result_lines.iter().position(|l| l.contains("type A"));
let type_b_pos = result_lines.iter().position(|l| l.contains("type B"));
if let (Some(a), Some(b)) = (type_a_pos, type_b_pos) {
assert!(a < b, "type A should appear before type B in output");
}
}
#[test]
fn test_multi_line_span_respected() {
let text = "interface Foo {\n name: string\n age: number\n}\nlet x = 1\n";
let spans = vec![
NodeSpan::new(0..4, "interface_declaration"),
NodeSpan::new(4..5, "expression_statement"),
];
let result = truncate_to_lines(text, &spans, Language::TypeScript, 5).unwrap();
assert!(
result.contains("interface Foo"),
"Should contain the interface: {:?}",
result
);
assert!(
result.contains("name: string"),
"Should contain interface body: {:?}",
result
);
}
#[test]
fn test_trailing_newline_preserved() {
let text = "line 1\nline 2\nline 3\nline 4\n";
let spans = vec![
NodeSpan::new(0..1, "type_alias_declaration"),
NodeSpan::new(1..4, "expression_statement"),
];
let result = truncate_to_lines(text, &spans, Language::TypeScript, 3).unwrap();
assert!(
result.ends_with('\n'),
"Should preserve trailing newline: {:?}",
result
);
}
#[test]
fn test_no_trailing_newline_when_original_lacks_it() {
let text = "line 1\nline 2\nline 3\nline 4";
let spans = vec![
NodeSpan::new(0..1, "type_alias_declaration"),
NodeSpan::new(1..4, "expression_statement"),
];
let result = truncate_to_lines(text, &spans, Language::TypeScript, 3).unwrap();
assert!(
!result.ends_with('\n'),
"Should not add trailing newline: {:?}",
result
);
}
#[test]
fn test_max_lines_zero_with_spans_does_not_panic() {
let text = "type A = string\nfunction foo() {}\n";
let spans = vec![
NodeSpan::new(0..1, "type_alias_declaration"),
NodeSpan::new(1..2, "function_declaration"),
];
let result = truncate_to_lines(text, &spans, Language::TypeScript, 0).unwrap();
assert_eq!(
result, "\n",
"max_lines=0 with trailing newline should produce only the preserved newline"
);
}
#[test]
fn test_simple_line_truncate_max_lines_zero_does_not_panic() {
let text = "line 1\nline 2\nline 3\n";
let result = simple_line_truncate(text, Language::TypeScript, 0).unwrap();
let line_count = result.lines().count();
assert!(
line_count <= 1,
"simple_line_truncate with max_lines=0 should produce at most 1 line, got {}: {:?}",
line_count,
result
);
}
#[test]
fn test_overlapping_spans_output_within_budget() {
let text = "line 0\nline 1\nline 2\nline 3\nline 4\nline 5\n";
let spans = vec![
NodeSpan::new(0..3, "type_alias_declaration"), NodeSpan::new(1..4, "type_alias_declaration"), NodeSpan::new(3..6, "function_declaration"), ];
let result = truncate_to_lines(text, &spans, Language::TypeScript, 4).unwrap();
let line_count = result.lines().count();
assert!(
line_count <= 4,
"Overlapping spans should not cause output to exceed budget of 4 lines, got {}: {:?}",
line_count,
result
);
}
#[test]
fn test_adjacent_spans_output_within_budget() {
let text = "line 0\nline 1\nline 2\nline 3\nline 4\nline 5\n";
let spans = vec![
NodeSpan::new(0..2, "type_alias_declaration"), NodeSpan::new(2..4, "type_alias_declaration"), NodeSpan::new(4..6, "function_declaration"), ];
let result = truncate_to_lines(text, &spans, Language::TypeScript, 4).unwrap();
let line_count = result.lines().count();
assert!(
line_count <= 4,
"Adjacent spans should not cause output to exceed budget of 4 lines, got {}: {:?}",
line_count,
result
);
}
#[test]
fn test_count_markers_empty() {
let selected: Vec<&NodeSpan> = vec![];
assert_eq!(count_markers(&selected, 10), 0);
}
#[test]
fn test_count_markers_no_gaps() {
let s1 = NodeSpan::new(0..3, "type_alias_declaration");
let s2 = NodeSpan::new(3..6, "function_declaration");
let selected: Vec<&NodeSpan> = vec![&s1, &s2];
assert_eq!(count_markers(&selected, 6), 0);
}
#[test]
fn test_count_markers_with_gaps() {
let s1 = NodeSpan::new(0..1, "type_alias_declaration");
let s2 = NodeSpan::new(3..4, "type_alias_declaration");
let selected: Vec<&NodeSpan> = vec![&s1, &s2];
assert_eq!(count_markers(&selected, 10), 2);
}
#[test]
fn test_count_markers_leading_and_trailing() {
let s1 = NodeSpan::new(2..4, "function_declaration");
let selected: Vec<&NodeSpan> = vec![&s1];
assert_eq!(count_markers(&selected, 10), 2);
}
#[test]
fn test_noncontiguous_spans_marker_accounting() {
let text = "type A\nexpr1\nexpr2\ntype B\nexpr3\nexpr4\nfn foo()\nexpr5\nexpr6\nexpr7\n";
let spans = vec![
NodeSpan::new(0..1, "type_alias_declaration"), NodeSpan::new(1..2, "expression_statement"), NodeSpan::new(2..3, "expression_statement"), NodeSpan::new(3..4, "type_alias_declaration"), NodeSpan::new(4..5, "expression_statement"), NodeSpan::new(5..6, "expression_statement"), NodeSpan::new(6..7, "function_declaration"), NodeSpan::new(7..8, "expression_statement"), NodeSpan::new(8..9, "expression_statement"), NodeSpan::new(9..10, "expression_statement"), ];
let result = truncate_to_lines(text, &spans, Language::TypeScript, 5).unwrap();
let result_lines: Vec<&str> = result.lines().collect();
assert!(
result_lines.len() <= 5,
"Output should not exceed 5 lines, got {}: {:?}",
result_lines.len(),
result
);
assert!(
result.contains("type A"),
"Should contain type A (priority 5): {:?}",
result
);
assert!(
result.contains("type B"),
"Should contain type B (priority 5): {:?}",
result
);
assert!(
!result.contains("fn foo()"),
"Function should be trimmed to make room for markers: {:?}",
result
);
}
#[test]
fn test_trim_prefers_dropping_low_priority() {
let text = "type A\nimport B\nfn foo()\nexpr1\n";
let spans = vec![
NodeSpan::new(0..1, "type_alias_declaration"), NodeSpan::new(1..2, "import_statement"), NodeSpan::new(2..3, "function_declaration"), NodeSpan::new(3..4, "expression_statement"), ];
let result = truncate_to_lines(text, &spans, Language::TypeScript, 3).unwrap();
assert!(
result.contains("type A"),
"Should keep highest priority (type): {:?}",
result
);
assert!(
!result.contains("import B") || result.contains("fn foo()"),
"Import (prio 3) should be dropped before function (prio 4). Got: {:?}",
result
);
assert!(result.lines().count() <= 3);
}
#[test]
fn test_trim_tiebreak_drops_last_position() {
let text = "type A\nexpr\ntype B\nexpr2\n";
let spans = vec![
NodeSpan::new(0..1, "type_alias_declaration"), NodeSpan::new(1..2, "expression_statement"), NodeSpan::new(2..3, "type_alias_declaration"), NodeSpan::new(3..4, "expression_statement"), ];
let result = truncate_to_lines(text, &spans, Language::TypeScript, 2).unwrap();
if result.contains("type A") && !result.contains("type B") {
} else if result.contains("type A") && result.contains("type B") {
} else {
panic!(
"Unexpected tie-break result: expected type B (higher position) to be dropped \
before type A, or both to fit. Got: {:?}",
result
);
}
assert!(result.lines().count() <= 2);
}
fn word_count(s: &str) -> usize {
s.split_whitespace().count()
}
#[test]
fn test_token_budget_no_truncation_when_within_budget() {
let text = "line one\nline two\nline three\n";
let result =
truncate_to_token_budget(text, Language::TypeScript, 100, word_count, None).unwrap();
assert_eq!(result, text);
}
#[test]
fn test_token_budget_truncates_when_over_budget() {
let text = "word1 word2\nword3 word4\nword5 word6\nword7 word8\n";
let result =
truncate_to_token_budget(text, Language::TypeScript, 6, word_count, None).unwrap();
let token_count = word_count(&result);
assert!(
token_count <= 6,
"Output should have at most 6 word-tokens, got {}: {:?}",
token_count,
result
);
}
#[test]
fn test_token_budget_includes_omission_marker() {
let text = "line one\nline two\nline three\nline four\nline five\n";
let result =
truncate_to_token_budget(text, Language::TypeScript, 5, word_count, None).unwrap();
assert!(
result.contains("truncated"),
"Should contain omission marker: {:?}",
result
);
}
#[test]
fn test_token_budget_preserves_trailing_newline() {
let text = "line one\nline two\nline three\n";
let result =
truncate_to_token_budget(text, Language::TypeScript, 5, word_count, None).unwrap();
assert!(
result.ends_with('\n'),
"Should preserve trailing newline: {:?}",
result
);
}
#[test]
fn test_token_budget_no_trailing_newline_when_absent() {
let text = "line one\nline two\nline three";
let result =
truncate_to_token_budget(text, Language::TypeScript, 4, word_count, None).unwrap();
assert!(
!result.ends_with('\n'),
"Should not add trailing newline: {:?}",
result
);
}
#[test]
fn test_token_budget_empty_input() {
let text = "";
let result =
truncate_to_token_budget(text, Language::TypeScript, 10, word_count, None).unwrap();
assert_eq!(result, "");
}
#[test]
fn test_token_budget_very_small_budget() {
let text = "line one\nline two\nline three\n";
let result =
truncate_to_token_budget(text, Language::TypeScript, 1, word_count, None).unwrap();
assert_eq!(
result, "",
"When budget is smaller than the marker, return empty string: {:?}",
result
);
}
#[test]
fn test_token_budget_python_marker_syntax() {
let text = "def foo(): pass\ndef bar(): pass\ndef baz(): pass\n";
let result = truncate_to_token_budget(text, Language::Python, 5, word_count, None).unwrap();
if result.contains("truncated") {
assert!(
result.contains("# ..."),
"Python should use # for omission marker: {:?}",
result
);
}
}
#[test]
fn test_token_budget_marker_only_output() {
let text = "line one\nline two\nline three\n";
let result =
truncate_to_token_budget(text, Language::TypeScript, 5, word_count, None).unwrap();
assert!(
result.contains("truncated"),
"Should contain omission marker: {:?}",
result
);
assert!(
!result.contains("line one"),
"Should not contain any content lines: {:?}",
result
);
let token_count = word_count(&result);
assert!(
token_count <= 5,
"Marker-only output should be within budget, got {} tokens: {:?}",
token_count,
result
);
}
#[test]
fn test_token_budget_output_invariant() {
let text =
"word1 word2 word3\nword4 word5 word6\nword7 word8 word9\nword10 word11 word12\n";
for budget in 1..20 {
let result =
truncate_to_token_budget(text, Language::TypeScript, budget, word_count, None)
.unwrap();
let token_count = word_count(&result);
assert!(
token_count <= budget,
"Budget {}: output has {} word-tokens, expected <= {}: {:?}",
budget,
token_count,
budget,
result
);
}
}
#[test]
fn test_token_budget_known_count_skips_recount_when_over_budget() {
let text = "word1 word2\nword3 word4\nword5 word6\nword7 word8\n";
let known = word_count(text); let result =
truncate_to_token_budget(text, Language::TypeScript, 6, word_count, Some(known))
.unwrap();
let token_count = word_count(&result);
assert!(
token_count <= 6,
"With known count over budget, output should be truncated to <= 6 tokens, got {}: {:?}",
token_count,
result
);
assert!(
result.contains("truncated"),
"Should contain omission marker: {:?}",
result
);
}
#[test]
fn test_token_budget_known_count_returns_early_when_within_budget() {
let text = "line one\nline two\nline three\n";
let actual_count = word_count(text);
let call_count = std::cell::Cell::new(0u32);
let counting_fn = |s: &str| -> usize {
if s == text {
call_count.set(call_count.get() + 1);
}
s.split_whitespace().count()
};
let result = truncate_to_token_budget(
text,
Language::TypeScript,
100,
counting_fn,
Some(actual_count),
)
.unwrap();
assert_eq!(result, text, "Fast-path should return text unchanged");
let calls = call_count.get();
assert!(
calls <= 1,
"count_tokens should not be called via unwrap_or_else when known_token_count is Some \
(expected <= 1 full-text call from debug_assert, got {})",
calls
);
}
#[test]
fn test_token_budget_known_count_none_behaves_like_before() {
let text =
"word1 word2 word3\nword4 word5 word6\nword7 word8 word9\nword10 word11 word12\n";
for budget in 1..20 {
let result_none =
truncate_to_token_budget(text, Language::TypeScript, budget, word_count, None)
.unwrap();
let result_some = truncate_to_token_budget(
text,
Language::TypeScript,
budget,
word_count,
Some(word_count(text)),
)
.unwrap();
assert_eq!(
result_none, result_some,
"Budget {}: None and Some(actual_count) should produce identical output",
budget
);
}
}
#[test]
fn test_last_line_no_truncation_when_within_budget() {
let text = "line 1\nline 2\nline 3\n";
let result = simple_last_line_truncate(text, Language::TypeScript, 5).unwrap();
assert_eq!(result, text);
}
#[test]
fn test_last_line_no_truncation_when_exact() {
let text = "line 1\nline 2\nline 3\n";
let result = simple_last_line_truncate(text, Language::TypeScript, 3).unwrap();
assert_eq!(result, text);
}
#[test]
fn test_last_line_truncation_keeps_last_lines() {
let text = "line 1\nline 2\nline 3\nline 4\nline 5\n";
let result = simple_last_line_truncate(text, Language::TypeScript, 3).unwrap();
let result_lines: Vec<&str> = result.lines().collect();
assert_eq!(result_lines.len(), 3);
assert!(result_lines[0].contains("... (3 lines above)"));
assert_eq!(result_lines[1], "line 4");
assert_eq!(result_lines[2], "line 5");
}
#[test]
fn test_last_line_truncation_preserves_trailing_newline() {
let text = "line 1\nline 2\nline 3\nline 4\n";
let result = simple_last_line_truncate(text, Language::TypeScript, 2).unwrap();
assert!(
result.ends_with('\n'),
"Should preserve trailing newline: {:?}",
result
);
}
#[test]
fn test_last_line_truncation_no_trailing_newline() {
let text = "line 1\nline 2\nline 3\nline 4";
let result = simple_last_line_truncate(text, Language::TypeScript, 2).unwrap();
assert!(
!result.ends_with('\n'),
"Should not add trailing newline: {:?}",
result
);
}
#[test]
fn test_last_line_truncation_python_marker() {
let text = "def foo(): pass\ndef bar(): pass\ndef baz(): pass\n";
let result = simple_last_line_truncate(text, Language::Python, 2).unwrap();
assert!(
result.contains("# ... (2 lines above)"),
"Python should use # for marker: {:?}",
result
);
}
#[test]
fn test_last_line_truncation_markdown_marker() {
let text = "# H1\n## H2\n## H3\n## H4\n";
let result = simple_last_line_truncate(text, Language::Markdown, 2).unwrap();
assert!(
result.contains("<!-- ... (3 lines above) -->"),
"Markdown should use HTML comment for marker: {:?}",
result
);
}
#[test]
fn test_last_line_truncation_single_line_budget() {
let text = "line 1\nline 2\nline 3\n";
let result = simple_last_line_truncate(text, Language::TypeScript, 1).unwrap();
let result_lines: Vec<&str> = result.lines().collect();
assert_eq!(result_lines.len(), 1);
assert!(result_lines[0].contains("... (3 lines above)"));
}
}