1use crate::core::config::CompressionLevel;
2use crate::core::rules_canonical::{self as rc, Wrapper};
3use crate::tools::CrpMode;
4
5const INSTRUCTION_CAP_TOKENS: usize = 800;
7
8#[cfg(test)]
20const STATIC_INSTRUCTION_BUDGET_TOKENS: usize = 545;
21#[cfg(test)]
22const STATIC_INSTRUCTION_BUDGET_TDD_TOKENS: usize = 675;
23#[cfg(all(test, windows))]
25const STATIC_INSTRUCTION_SHELL_HINT_TOKENS: usize = 25;
26#[cfg(all(test, not(windows)))]
27const STATIC_INSTRUCTION_SHELL_HINT_TOKENS: usize = 0;
28
29#[must_use]
30pub fn build_instructions(crp_mode: CrpMode) -> String {
31 build_instructions_with_client(crp_mode, "")
32}
33
34#[must_use]
35pub fn build_instructions_with_client(crp_mode: CrpMode, client_name: &str) -> String {
36 build_instructions_with_client_and_optional_session(crp_mode, client_name, None)
37}
38
39#[must_use]
45pub fn build_instructions_with_client_and_session(
46 crp_mode: CrpMode,
47 client_name: &str,
48 session: &crate::core::session::SessionState,
49) -> String {
50 build_instructions_with_client_and_optional_session(crp_mode, client_name, Some(session))
51}
52
53fn build_instructions_with_client_and_optional_session(
54 crp_mode: CrpMode,
55 client_name: &str,
56 session: Option<&crate::core::session::SessionState>,
57) -> String {
58 let cfg = crate::core::config::Config::load();
59 let minimal = cfg.minimal_overhead_effective_for_client(client_name);
60 let shadow = cfg.shadow_mode;
61 let level = if client_loads_compression_from_file(client_name) {
64 CompressionLevel::Off
65 } else {
66 CompressionLevel::effective(&cfg)
67 };
68 let persona_block = crate::core::persona::Persona::resolve(&cfg).prompt_block();
72 build_full_instructions(
73 crp_mode,
74 client_name,
75 minimal,
76 level,
77 shadow,
78 &persona_block,
79 session,
80 )
81}
82
83#[must_use]
92pub fn claude_code_static_instructions_for_test() -> String {
93 build_full_instructions(
94 CrpMode::Off,
95 "",
96 true,
97 CompressionLevel::Off,
98 false,
99 "",
100 None,
101 )
102}
103
104#[must_use]
106pub fn build_instructions_for_test(crp_mode: CrpMode) -> String {
107 let shadow = false;
108 let level = CompressionLevel::effective(&crate::core::config::Config::load());
111 let tp =
112 crate::core::tool_profiles::ToolProfile::from_config(&crate::core::config::Config::load());
113 let skeleton = rc::render(shadow, Wrapper::Bare, level, &tp);
114 let shell_hint = build_shell_hint();
115
116 let base = format!(
117 "{skeleton}\n\
118 {shell_hint}\n\
119 {decoder_block}\n\
120 {origin}",
121 decoder_block =
122 crate::core::protocol::instruction_decoder_block(matches!(crp_mode, CrpMode::Tdd)),
123 origin = crate::core::integrity::origin_line(),
124 );
125
126 match crp_mode_suffix(crp_mode) {
127 "" => base,
128 crp => format!("{base}\n\n{crp}"),
129 }
130}
131
132#[must_use]
136pub fn build_instructions_with_client_for_compiler(
137 crp_mode: CrpMode,
138 client_name: &str,
139 _unified_tool_mode: bool,
140) -> String {
141 let tp =
142 crate::core::tool_profiles::ToolProfile::from_config(&crate::core::config::Config::load());
143 let skeleton = rc::render(true, Wrapper::Bare, CompressionLevel::Off, &tp);
144 let shell_hint = build_shell_hint();
145
146 let base = format!(
147 "{skeleton}\n\
148 {shell_hint}\n\
149 {decoder_block}\n\
150 {origin}",
151 decoder_block =
152 crate::core::protocol::instruction_decoder_block(matches!(crp_mode, CrpMode::Tdd)),
153 origin = crate::core::integrity::origin_line(),
154 );
155
156 let _ = client_name;
157
158 match crp_mode_suffix(crp_mode) {
159 "" => base,
160 crp => format!("{base}\n\n{crp}"),
161 }
162}
163
164fn rotate_wakeup_manifest(session: &crate::core::session::SessionState, profile_name: &str) {
166 use crate::core::litm_calibration::{Position, record_outcome};
167 use crate::core::session::ManifestEntry;
168
169 let mut updated = session.clone();
170
171 for entry in &updated.wakeup_manifest {
172 if !entry.missed
173 && let Some(pos) = Position::parse(&entry.position)
174 {
175 record_outcome(&entry.profile, pos, true);
176 }
177 }
178
179 let mut manifest: Vec<ManifestEntry> = Vec::new();
180 let mut push = |key: &str, position: &str| {
181 let key = key.trim();
182 if !key.is_empty() {
183 manifest.push(ManifestEntry {
184 key: key.chars().take(80).collect(),
185 position: position.to_string(),
186 profile: profile_name.to_string(),
187 missed: false,
188 });
189 }
190 };
191
192 if let Some(ref task) = updated.task {
193 push(&task.description, "begin");
194 }
195 for d in updated.decisions.iter().rev().take(5) {
196 push(&d.summary, "begin");
197 }
198 for f in updated.findings.iter().rev().take(8) {
199 push(&f.summary, "end");
200 }
201 for n in updated.next_steps.iter().take(3) {
202 push(n, "end");
203 }
204
205 updated.wakeup_manifest = manifest;
206 let _ = updated.save();
207}
208
209#[must_use]
211pub fn claude_config_dir_display() -> String {
212 match std::env::var("CLAUDE_CONFIG_DIR") {
213 Ok(dir) if !dir.trim().is_empty() => {
214 let dir = dir.trim().to_string();
215 if dir.starts_with('~') {
216 dir
217 } else if let Some(home) = dirs::home_dir() {
218 let home_str = home.to_string_lossy();
219 if let Some(rest) = dir.strip_prefix(home_str.as_ref()) {
220 format!("~{rest}")
221 } else {
222 dir
223 }
224 } else {
225 dir
226 }
227 }
228 _ => "~/.claude".to_string(),
229 }
230}
231
232fn build_full_instructions(
235 crp_mode: CrpMode,
236 client_name: &str,
237 minimal: bool,
238 level: CompressionLevel,
239 shadow: bool,
240 persona_block: &str,
241 session: Option<&crate::core::session::SessionState>,
242) -> String {
243 let profile = crate::core::litm::LitmProfile::from_client_name(client_name);
244 let loaded_session = (!minimal).then_some(session).flatten();
245
246 let (session_block, litm_end_block) = match loaded_session {
247 Some(ref session) => {
248 rotate_wakeup_manifest(session, profile.name);
249 let share = crate::core::litm_calibration::begin_share(profile.name);
250 let mut positioned = crate::core::litm::position_optimize_with_share(session, share);
251 positioned.enforce_token_budget(crate::core::litm::active_session_budget());
254 let begin = format!(
255 "\n\n--- ACTIVE SESSION (LITM P1: begin position, profile: {}) ---\n{}\n---\n",
256 profile.name, positioned.begin_block
257 );
258 let end = if positioned.end_block.is_empty() {
259 String::new()
260 } else {
261 format!(
262 "\n--- SESSION RESUME (post-compaction) ---\n{}\n---\n",
263 positioned.end_block
264 )
265 };
266 (begin, end)
267 }
268 None => (String::new(), String::new()),
269 };
270
271 let project_root_for_blocks = if minimal {
272 None
273 } else {
274 loaded_session
275 .as_ref()
276 .and_then(|s| s.project_root.clone())
277 .or_else(|| {
278 std::env::current_dir()
279 .ok()
280 .map(|p| p.to_string_lossy().to_string())
281 })
282 };
283
284 let knowledge_block = match &project_root_for_blocks {
285 Some(root) => {
286 let knowledge = crate::core::knowledge::ProjectKnowledge::load(root);
287 match knowledge {
288 Some(k) if !k.facts.is_empty() || !k.patterns.is_empty() => {
289 let last_hash = loaded_session
290 .as_ref()
291 .and_then(|s| s.last_aaak_hash.as_deref());
292 let delta = k.format_aaak_delta(last_hash);
293 if delta.content.is_empty() {
294 String::new()
295 } else {
296 format!(
297 "\n--- PROJECT MEMORY (AAAK) ---\n{}\n---\n",
298 delta.content.trim()
299 )
300 }
301 }
302 _ => String::new(),
303 }
304 }
305 None => String::new(),
306 };
307
308 let gotcha_block = match &project_root_for_blocks {
309 Some(root) => {
310 let store = crate::core::gotcha_tracker::GotchaStore::load(root);
311 let files: Vec<String> = loaded_session
312 .as_ref()
313 .map(|s| s.files_touched.iter().map(|ft| ft.path.clone()).collect())
314 .unwrap_or_default();
315 let block = store.format_injection_block(&files);
316 if block.is_empty() {
317 String::new()
318 } else {
319 format!("\n{block}\n")
320 }
321 }
322 None => String::new(),
323 };
324
325 let health_block = match &project_root_for_blocks {
326 Some(root) => {
327 let block = crate::core::code_health::persist::format_session_block(root);
328 if block.is_empty() {
329 String::new()
330 } else {
331 format!("\n{block}\n")
332 }
333 }
334 None => String::new(),
335 };
336
337 let shell_hint = build_shell_hint();
338
339 let cfg = crate::core::config::Config::load();
353 let tool_profile = crate::core::tool_profiles::ToolProfile::from_config(&cfg);
354 let is_shadow_only = is_shadow_surface_active();
355 let skeleton = if client_loads_rules_from_file(client_name) {
356 let anchor = if client_is_hook_covered(client_name) {
357 if is_shadow_only {
358 shadow_only_anchor(&tool_profile)
359 } else {
360 hook_covered_anchor(&tool_profile)
361 }
362 } else {
363 skeleton_anchor(&tool_profile)
364 };
365 let compression = rc::compression_text(level);
366 if compression.is_empty() {
367 anchor
368 } else {
369 format!("{anchor}\n{compression}")
370 }
371 } else {
372 let effective_shadow = shadow && client_is_hook_covered(client_name);
380 rc::render(effective_shadow, Wrapper::Bare, level, &tool_profile)
381 };
382
383 let config_dir = claude_config_dir_display();
386
387 let persona_section = if persona_block.is_empty() {
390 String::new()
391 } else {
392 format!("\n{persona_block}")
393 };
394
395 let base = format!(
396 "{skeleton}\n\
397 {persona_section}\
398 {shell_hint}\n\
399 {decoder_block}\n\
400 Full instructions at {config_dir}/CLAUDE.md\n\
401 {session_block}\n\
402 {knowledge_block}\n\
403 {gotcha_block}\n\
404 {health_block}\n\
405 {origin}\n\
406 {litm_end_block}",
407 decoder_block =
408 crate::core::protocol::instruction_decoder_block(matches!(crp_mode, CrpMode::Tdd)),
409 origin = crate::core::integrity::origin_line(),
410 litm_end_block = litm_end_block
411 );
412
413 let guidance_suffix = crp_mode_suffix(crp_mode).to_string();
416
417 assemble_within_cap(&base, &guidance_suffix, INSTRUCTION_CAP_TOKENS)
418}
419
420fn crp_mode_suffix(crp_mode: CrpMode) -> &'static str {
421 match crp_mode {
422 CrpMode::Off => "",
423 CrpMode::Compact => {
424 "CRP MODE: compact — omit filler; abbreviate fn,cfg,impl,deps,req,res; \
425 diff lines (+/-) only; <=200 tok; trust tool outputs."
426 }
427 CrpMode::Tdd => {
428 "CRP MODE: tdd — max density; Fn refs + diff lines only \
429 (+F1:42 | -F1:10-15 | ~F1:42 old->new); <=150 tok; zero narration."
430 }
431 }
432}
433
434fn assemble_within_cap(base: &str, suffix: &str, cap_tokens: usize) -> String {
435 use crate::core::tokens::count_tokens;
436 let suffix = suffix.trim_end_matches('\n');
437 if suffix.is_empty() {
438 let full = base.to_string();
439 return if count_tokens(&full) > cap_tokens {
440 truncate_to_token_cap(&full, cap_tokens)
441 } else {
442 full
443 };
444 }
445
446 let full = format!("{base}\n\n{suffix}");
447 if count_tokens(&full) <= cap_tokens {
448 return full;
449 }
450
451 let suffix_tokens = count_tokens(suffix);
452 let Some(base_budget) = cap_tokens.checked_sub(suffix_tokens + 1) else {
453 return truncate_to_token_cap(&full, cap_tokens);
454 };
455 let trimmed_base = truncate_to_token_cap(base, base_budget);
456 format!("{trimmed_base}\n\n{suffix}")
457}
458
459fn truncate_to_token_cap(s: &str, cap_tokens: usize) -> String {
460 use crate::core::tokens::count_tokens;
461 if count_tokens(s) <= cap_tokens {
462 return s.to_string();
463 }
464 let cuts: Vec<usize> = s.match_indices('\n').map(|(i, _)| i).collect();
465 let (mut lo, mut hi) = (0usize, cuts.len());
466 let mut best: Option<usize> = None;
467 while lo < hi {
468 let mid = lo + (hi - lo) / 2;
469 let end = cuts[mid];
470 if end > 0 && count_tokens(&s[..end]) <= cap_tokens {
471 best = Some(end);
472 lo = mid + 1;
473 } else {
474 hi = mid;
475 }
476 }
477 if let Some(end) = best {
478 return s[..end].to_string();
479 }
480 let byte_approx = cap_tokens * 4;
481 let safe = s.floor_char_boundary(byte_approx.min(s.len()));
482 s[..safe].to_string()
483}
484
485#[must_use]
487pub fn claude_code_instructions() -> String {
488 build_instructions(CrpMode::Off)
489}
490
491fn skeleton_anchor(tp: &crate::core::tool_profiles::ToolProfile) -> String {
494 if tp.is_tool_enabled("ctx_compose") {
495 "lean-ctx active — your auto-loaded lean-ctx rules apply: \
496 ctx_* tools replace native Read/Grep/Shell/Glob (ctx_compose first)."
497 .into()
498 } else {
499 "lean-ctx active — your auto-loaded lean-ctx rules apply: \
500 ctx_* tools replace native Read/Grep/Shell/Glob."
501 .into()
502 }
503}
504
505fn hook_covered_anchor(tp: &crate::core::tool_profiles::ToolProfile) -> String {
507 let mut s =
508 String::from("lean-ctx active — hooks compress native Shell/Read/Grep transparently");
509 if tp.is_tool_enabled("ctx_compose") {
510 s.push_str("; call ctx_compose to orient");
511 }
512 if tp.is_tool_enabled("ctx_session") || tp.is_tool_enabled("ctx_knowledge") {
514 s.push_str(", ctx_search(action=semantic) / ctx_knowledge for meaning & memory");
515 }
516 s.push('.');
517 s
518}
519
520fn shadow_only_anchor(tp: &crate::core::tool_profiles::ToolProfile) -> String {
524 let mut s =
525 String::from("lean-ctx active — hooks compress native Shell/Read/Grep transparently");
526 let has_exclusive = tp.is_tool_enabled("ctx_compose")
527 || tp.is_tool_enabled("ctx_knowledge")
528 || tp.is_tool_enabled("ctx_session");
529 if has_exclusive {
530 s.push_str("; use ctx_call for compose/knowledge/session");
531 }
532 s.push('.');
533 s
534}
535
536fn is_shadow_surface_active() -> bool {
538 let cfg = crate::core::config::Config::load();
539 match cfg.tool_surface.as_deref() {
540 Some("shadow") => true,
541 Some("mcp") => false,
542 _ => cfg.shadow_mode, }
544}
545
546#[cfg(test)]
548const SKELETON_ANCHOR: &str = "lean-ctx active — your auto-loaded lean-ctx rules apply: \
549 ctx_* tools replace native Read/Grep/Shell/Glob (ctx_compose first).";
550
551fn client_loads_compression_from_file(client_name: &str) -> bool {
552 crate::core::home::resolve_home_dir().is_some_and(|home| {
553 crate::core::rules_channel::client_autoloads_compression(client_name, &home)
554 })
555}
556
557fn client_loads_rules_from_file(client_name: &str) -> bool {
558 crate::core::home::resolve_home_dir()
559 .is_some_and(|home| crate::core::rules_channel::client_autoloads_rules(client_name, &home))
560}
561
562fn client_is_hook_covered(client_name: &str) -> bool {
563 crate::core::home::resolve_home_dir()
564 .is_some_and(|home| crate::core::rules_channel::client_hook_covered(client_name, &home))
565}
566
567fn build_shell_hint() -> String {
568 if !cfg!(windows) {
569 return String::new();
570 }
571 let name = crate::shell::shell_name();
572 let is_posix = matches!(name.as_str(), "bash" | "sh" | "zsh" | "fish");
573 if is_posix {
574 format!("\nSHELL: {name} (POSIX) — no PowerShell cmdlets.\n")
575 } else if name.contains("powershell") || name.contains("pwsh") {
576 format!("\nSHELL: {name}. Use PowerShell cmdlets.\n")
577 } else {
578 format!("\nSHELL: {name}.\n")
579 }
580}
581
582#[cfg(test)]
583mod tests {
584 use super::*;
585 use crate::core::tokens::count_tokens;
586
587 #[test]
588 fn guidance_suffix_survives_oversized_base() {
589 let base = "SESSION LINE\n".repeat(4000);
590 let suffix = "OUTPUT STYLE: expert-terse\nFn refs only, diff lines only.";
591 let out = assemble_within_cap(&base, suffix, INSTRUCTION_CAP_TOKENS);
592 assert!(out.contains("OUTPUT STYLE: expert-terse"));
593 assert!(count_tokens(&out) <= INSTRUCTION_CAP_TOKENS);
594 assert!(out.len() < base.len());
595 }
596
597 #[test]
598 fn empty_client_never_dedups_compression() {
599 assert!(!client_loads_compression_from_file(""));
600 assert!(!client_loads_compression_from_file("totally-unknown-agent"));
601 }
602
603 #[test]
604 fn live_session_instructions_include_only_the_owned_session() {
605 let mut current = crate::core::session::SessionState::new();
606 current.add_finding(None, None, "current-instance-marker");
607 let mut other = crate::core::session::SessionState::new();
608 other.add_finding(None, None, "other-instance-marker");
609
610 let out = build_full_instructions(
611 CrpMode::Off,
612 "",
613 false,
614 CompressionLevel::Off,
615 false,
616 "",
617 Some(¤t),
618 );
619
620 assert!(out.contains("current-instance-marker"));
621 assert!(!out.contains("other-instance-marker"));
622 assert!(!out.contains("never echo tool output"));
623 }
624
625 #[test]
626 fn covered_client_gets_anchor_instead_of_skeleton() {
627 let _guard = crate::core::data_dir::test_env_lock();
630 let tmp = tempfile::tempdir().unwrap();
631 let home = tmp.path();
632 std::fs::create_dir_all(home.join(".cursor/rules")).unwrap();
633 let cfg = crate::core::config::Config::load();
634 let shadow = cfg.shadow_mode;
635 let tp = crate::core::tool_profiles::ToolProfile::Power;
636 std::fs::write(
637 home.join(".cursor/rules/lean-ctx.mdc"),
638 rc::render(
639 shadow,
640 Wrapper::Dedicated,
641 crate::core::config::CompressionLevel::Standard,
642 &tp,
643 ),
644 )
645 .unwrap();
646 let old_home = std::env::var("HOME").ok();
647 crate::test_env::set_var("HOME", home);
648 crate::test_env::set_var("LEAN_CTX_MINIMAL", "1");
649
650 let covered = build_instructions_with_client(CrpMode::Off, "cursor");
651 let uncovered = build_instructions_with_client(CrpMode::Off, "some-other-agent");
652
653 if let Some(h) = old_home {
654 crate::test_env::set_var("HOME", h);
655 } else {
656 crate::test_env::remove_var("HOME");
657 }
658 crate::test_env::remove_var("LEAN_CTX_MINIMAL");
659
660 assert!(
661 covered.contains(SKELETON_ANCHOR),
662 "covered client must get the anchor:\n{covered}"
663 );
664 if shadow {
665 assert!(
666 !covered.contains("auto-route"),
667 "covered shadow client must not re-pay the shadow nudge:\n{covered}"
668 );
669 } else {
670 assert!(
671 !covered.contains("MANDATORY MAPPING"),
672 "covered client must not re-pay the skeleton:\n{covered}"
673 );
674 }
675 assert!(
677 !covered.contains("OUTPUT STYLE:"),
678 "covered client must not re-pay the compression prompt:\n{covered}"
679 );
680 if shadow {
684 assert!(
685 !uncovered.contains("auto-route"),
686 "#987: uncovered client WITHOUT hooks must NOT get shadow nudge:\n{uncovered}"
687 );
688 assert!(
689 uncovered.contains("MANDATORY MAPPING") || uncovered.contains("ctx_"),
690 "uncovered client without hooks gets explicit tool mapping:\n{uncovered}"
691 );
692 } else {
693 assert!(
694 uncovered.contains("MANDATORY MAPPING"),
695 "uncovered client keeps the full skeleton:\n{uncovered}"
696 );
697 }
698 eprintln!(
699 "instructions footprint: covered={} tok, uncovered={} tok",
700 count_tokens(&covered),
701 count_tokens(&uncovered)
702 );
703 assert!(
704 count_tokens(&covered) < count_tokens(&uncovered),
705 "anchor path must be strictly cheaper"
706 );
707 }
708
709 #[test]
710 fn hook_covered_client_gets_hook_aware_anchor() {
711 let _guard = crate::core::data_dir::test_env_lock();
715 let tmp = tempfile::tempdir().unwrap();
716 let home = tmp.path();
717 std::fs::create_dir_all(home.join(".cursor/rules")).unwrap();
718 let tp = crate::core::tool_profiles::ToolProfile::Power;
719 std::fs::write(
720 home.join(".cursor/rules/lean-ctx.mdc"),
721 rc::render(
722 false,
723 Wrapper::HookCovered,
724 crate::core::config::CompressionLevel::Off,
725 &tp,
726 ),
727 )
728 .unwrap();
729 std::fs::write(
730 home.join(".cursor/hooks.json"),
731 r#"{"version":1,"hooks":{"preToolUse":[
732 {"matcher":"Shell","command":"/usr/local/bin/lean-ctx hook rewrite"},
733 {"matcher":"Read|Grep","command":"/usr/local/bin/lean-ctx hook redirect"}
734 ]}}"#,
735 )
736 .unwrap();
737 let old_home = std::env::var("HOME").ok();
738 crate::test_env::set_var("HOME", home);
739 crate::test_env::set_var("LEAN_CTX_MINIMAL", "1");
740
741 let covered = build_instructions_with_client(CrpMode::Off, "cursor");
742
743 if let Some(h) = old_home {
744 crate::test_env::set_var("HOME", h);
745 } else {
746 crate::test_env::remove_var("HOME");
747 }
748 crate::test_env::remove_var("LEAN_CTX_MINIMAL");
749
750 assert!(
751 covered.contains("hooks compress native Shell/Read/Grep"),
752 "hook-covered client must get the hook-aware anchor:\n{covered}"
753 );
754 assert!(
755 !covered.contains("ctx_* tools replace native")
756 && !covered.contains("MANDATORY MAPPING"),
757 "hook-covered client must not carry the replace-native wording:\n{covered}"
758 );
759 }
760
761 #[test]
762 fn non_coding_persona_block_lands_in_instructions() {
763 let _guard = crate::core::data_dir::test_env_lock();
764 crate::test_env::set_var("LEAN_CTX_MINIMAL", "1");
765
766 crate::test_env::set_var("LEAN_CTX_PERSONA", "research");
767 let research = build_instructions_with_client(CrpMode::Off, "");
768
769 crate::test_env::set_var("LEAN_CTX_PERSONA", "coding");
770 let coding = build_instructions_with_client(CrpMode::Off, "");
771
772 crate::test_env::remove_var("LEAN_CTX_PERSONA");
773 crate::test_env::remove_var("LEAN_CTX_MINIMAL");
774
775 assert!(
776 research.contains("PERSONA: research"),
777 "research persona must announce its domain block:\n{research}"
778 );
779 assert!(
780 research.contains("INTENTS: explore, summarize, compare, cite, synthesize"),
781 "research persona must carry its intent vocabulary:\n{research}"
782 );
783 assert!(
784 !coding.contains("PERSONA:"),
785 "the coding default must keep the instructions byte-stable (#498):\n{coding}"
786 );
787 }
788
789 #[test]
790 fn under_cap_keeps_everything() {
791 let base = "tool mapping block";
792 let suffix = "OUTPUT STYLE: dense";
793 let out = assemble_within_cap(base, suffix, INSTRUCTION_CAP_TOKENS);
794 assert!(out.contains(base));
795 assert!(out.contains(suffix));
796 }
797
798 #[test]
799 fn empty_suffix_caps_base_only() {
800 let base = "x\n".repeat(4000);
801 let out = assemble_within_cap(&base, "", INSTRUCTION_CAP_TOKENS);
802 assert!(count_tokens(&out) <= INSTRUCTION_CAP_TOKENS);
803 }
804
805 #[cfg(windows)]
806 #[test]
807 fn shell_hint_stays_within_its_budget() {
808 let hint = build_shell_hint();
809 let tokens = count_tokens(&hint);
810 assert!(
811 tokens <= STATIC_INSTRUCTION_SHELL_HINT_TOKENS,
812 "shell hint = {tokens} tok, budget {STATIC_INSTRUCTION_SHELL_HINT_TOKENS}: {hint}"
813 );
814 }
815
816 #[test]
817 fn minimal_overhead_instructions_stay_within_budget() {
818 const MINIMAL_INSTRUCTION_BUDGET_TOKENS: usize =
819 STATIC_INSTRUCTION_BUDGET_TDD_TOKENS + STATIC_INSTRUCTION_SHELL_HINT_TOKENS;
820 let _iso = crate::core::data_dir::isolated_data_dir();
821 crate::test_env::set_var("LEAN_CTX_MINIMAL", "1");
822 let out = build_instructions(CrpMode::Compact);
823 crate::test_env::remove_var("LEAN_CTX_MINIMAL");
824 let tokens = count_tokens(&out);
825 assert!(
826 tokens <= MINIMAL_INSTRUCTION_BUDGET_TOKENS,
827 "minimal-overhead instructions = {tokens} tok, budget {MINIMAL_INSTRUCTION_BUDGET_TOKENS}\n---\n{out}\n---"
828 );
829 }
830
831 #[test]
832 fn static_skeleton_stays_within_budget() {
833 let _iso = crate::core::data_dir::isolated_data_dir();
834 crate::test_env::set_var("LEAN_CTX_COMPRESSION", "off");
837 for (mode, base_budget) in [
838 (CrpMode::Off, STATIC_INSTRUCTION_BUDGET_TOKENS),
839 (CrpMode::Compact, STATIC_INSTRUCTION_BUDGET_TOKENS),
840 (CrpMode::Tdd, STATIC_INSTRUCTION_BUDGET_TDD_TOKENS),
841 ] {
842 let budget = base_budget + STATIC_INSTRUCTION_SHELL_HINT_TOKENS;
843 let out = build_instructions_for_test(mode);
844 let tokens = count_tokens(&out);
845 assert!(
846 tokens <= budget,
847 "static instructions for {mode:?} = {tokens} tok, budget {budget}\n---\n{out}\n---"
848 );
849 }
850 crate::test_env::remove_var("LEAN_CTX_COMPRESSION");
851 }
852}