1use std::path::Path;
2use std::sync::{Arc, Mutex};
3use std::time::Duration;
4
5use crate::ast_parser::AstParser;
6use crate::budget_tracker::{BudgetTracker, UsageReport};
7use crate::cache_manager::CacheManager;
8use crate::confidence_router::ConfidenceRouter;
9use crate::cost_calculator::{CostCalculator, SessionCostSummary};
10use crate::ctx_format::CtxFormat;
11use crate::error::{Result, SqzError};
12use crate::model_router::ModelRouter;
13use crate::pin_manager::PinManager;
14use crate::pipeline::CompressionPipeline;
15use crate::plugin_api::PluginLoader;
16use crate::preset::{Preset, PresetParser};
17use crate::session_store::{SessionStore, SessionSummary};
18use crate::terse_mode::TerseMode;
19use crate::types::{CompressedContent, PinEntry, Provenance, SessionId};
20use crate::verifier::Verifier;
21
22pub struct SqzEngine {
33 preset: Arc<Mutex<Preset>>,
35 pipeline: Arc<Mutex<CompressionPipeline>>,
36 model_router: Arc<Mutex<ModelRouter>>,
37
38 session_store: SessionStore,
40 cache_manager: CacheManager,
41 budget_tracker: BudgetTracker,
42 cost_calculator: CostCalculator,
43 ast_parser: AstParser,
44 terse_mode: TerseMode,
45 pin_manager: PinManager,
46 confidence_router: ConfidenceRouter,
47 _plugin_loader: PluginLoader,
48}
49
50impl SqzEngine {
51 pub fn new() -> Result<Self> {
56 let preset = Preset::default();
57 let store_path = Self::default_store_path();
58 Self::with_preset_and_store(preset, &store_path)
59 }
60
61 pub fn default_store_path() -> std::path::PathBuf {
70 if let Some(custom) = Self::store_path_from_env(std::env::var("SQZ_DB_PATH").ok()) {
71 return custom;
72 }
73 if let Some(home) = dirs_next::home_dir() {
74 let sqz_dir = home.join(".sqz");
75 if std::fs::create_dir_all(&sqz_dir).is_ok() {
76 #[cfg(unix)]
79 {
80 use std::os::unix::fs::PermissionsExt;
81 let _ = std::fs::set_permissions(
82 &sqz_dir,
83 std::fs::Permissions::from_mode(0o700),
84 );
85 }
86 return sqz_dir.join("sessions.db");
87 }
88 }
89 let dir = std::env::temp_dir();
91 dir.join(format!(
92 "sqz_session_{}_{}.db",
93 std::process::id(),
94 std::time::SystemTime::now()
95 .duration_since(std::time::UNIX_EPOCH)
96 .map(|d| d.as_nanos())
97 .unwrap_or(0)
98 ))
99 }
100
101 fn store_path_from_env(value: Option<String>) -> Option<std::path::PathBuf> {
105 let raw = value?;
106 let trimmed = raw.trim();
107 if trimmed.is_empty() {
108 return None;
109 }
110 let path = std::path::PathBuf::from(trimmed);
111 if let Some(parent) = path.parent() {
112 if !parent.as_os_str().is_empty() && !parent.exists() {
113 if std::fs::create_dir_all(parent).is_ok() {
116 #[cfg(unix)]
117 {
118 use std::os::unix::fs::PermissionsExt;
119 let _ = std::fs::set_permissions(
120 parent,
121 std::fs::Permissions::from_mode(0o700),
122 );
123 }
124 }
125 }
126 }
127 Some(path)
128 }
129
130 pub fn quiet_stderr() -> bool {
138 match std::env::var("SQZ_QUIET") {
139 Ok(v) => !v.is_empty() && v != "0",
140 Err(_) => false,
141 }
142 }
143
144 fn ref_ttl_from_env(value: Option<String>) -> Duration {
151 const DEFAULT: Duration = Duration::from_secs(30 * 60);
152 match value.as_deref().map(str::trim) {
153 Some(raw) if !raw.is_empty() => raw
154 .parse::<u64>()
155 .map(Duration::from_secs)
156 .unwrap_or(DEFAULT),
157 _ => DEFAULT,
158 }
159 }
160
161 pub fn with_preset_and_store(preset: Preset, store_path: &Path) -> Result<Self> {
167 let pipeline = CompressionPipeline::new(&preset);
168 let window_size = preset.budget.default_window_size;
169
170 let session_store = SessionStore::open_or_create(store_path)?;
172 let cache_store = SessionStore::open_or_create(store_path)?;
173 let pin_store = SessionStore::open_or_create(store_path)?;
174
175 Ok(SqzEngine {
176 preset: Arc::new(Mutex::new(preset.clone())),
177 pipeline: Arc::new(Mutex::new(pipeline)),
178 model_router: Arc::new(Mutex::new(ModelRouter::new(&preset))),
179 session_store,
180 cache_manager: CacheManager::with_ref_age_duration(
181 cache_store,
182 512 * 1024 * 1024,
183 Self::ref_ttl_from_env(std::env::var("SQZ_REF_TTL_SECS").ok()),
184 ),
185 budget_tracker: BudgetTracker::new(window_size, &preset),
186 cost_calculator: CostCalculator::with_defaults(),
187 ast_parser: AstParser::new(),
188 terse_mode: TerseMode,
189 pin_manager: PinManager::new(pin_store),
190 confidence_router: ConfidenceRouter::new(),
191 _plugin_loader: PluginLoader::new(Path::new("plugins")),
192 })
193 }
194
195 pub fn compress(&self, input: &str) -> Result<CompressedContent> {
203 let preset = self.preset.lock()
204 .map_err(|_| SqzError::Other("preset lock poisoned".into()))?;
205 let pipeline = self.pipeline.lock()
206 .map_err(|_| SqzError::Other("pipeline lock poisoned".into()))?;
207 let ctx = crate::pipeline::SessionContext {
208 session_id: "engine".to_string(),
209 };
210
211 let mode = self.confidence_router.route(input);
213
214 if mode == crate::confidence_router::CompressionMode::Safe {
216 if std::env::var_os("SQZ_VERBOSE").is_some() {
219 eprintln!("[sqz] safe mode: high-risk content passed through unchanged");
220 }
221 return self.compress_safe(input, &pipeline, &ctx, true);
222 }
223
224 let mut result = pipeline.compress_uncounted(input, &ctx, &preset)?;
226
227 let verify = Verifier::verify(input, &result.data);
229 let fallback = verify.fallback_triggered;
230 result.verify = Some(verify);
231
232 if fallback && result.data != input {
234 if !Self::quiet_stderr() {
235 eprintln!("[sqz] fallback: verifier confidence {:.2} below threshold — re-compressing in safe mode",
236 result.verify.as_ref().map(|v| v.confidence).unwrap_or(0.0));
237 }
238 let safe_result = self.compress_safe(input, &pipeline, &ctx, false)?;
239 return Ok(safe_result);
240 }
241
242 pipeline.count_output(&mut result, &preset);
243 Ok(result)
244 }
245
246 pub fn compress_with_cache(&self, input: &str) -> Result<crate::cache_manager::CacheResult> {
263 if self.never_store(input) {
270 return Ok(crate::cache_manager::CacheResult::Fresh { output: self.compress(input)? });
271 }
272 self.cache_manager.get_or_compress_with(
273 std::path::Path::new(""),
274 input.as_bytes(),
275 &|text| self.compress(text),
276 )
277 }
278
279 pub fn compress_with_cache_lossless(&self, input: &str) -> Result<crate::cache_manager::CacheResult> {
287 let pipeline = self.pipeline.lock()
288 .map_err(|_| SqzError::Other("pipeline lock poisoned".into()))?;
289 if self.never_store(input) {
290 let ctx = crate::pipeline::SessionContext { session_id: "cache".to_string() };
291 let output = pipeline.compress_lossless(input, &ctx, &Preset::default())?;
292 return Ok(crate::cache_manager::CacheResult::Fresh { output });
293 }
294 self.cache_manager.get_or_compress_lossless(
295 std::path::Path::new(""),
296 input.as_bytes(),
297 &pipeline,
298 )
299 }
300
301 pub fn compress_lossless(&self, input: &str) -> Result<CompressedContent> {
303 let pipeline = self.pipeline.lock()
304 .map_err(|_| SqzError::Other("pipeline lock poisoned".into()))?;
305 let ctx = crate::pipeline::SessionContext { session_id: "cache".to_string() };
306 pipeline.compress_lossless(input, &ctx, &Preset::default())
307 }
308
309 pub fn compress_tool_result(&self, text: &str, use_cache: bool) -> Option<ToolResultCompression> {
315 use crate::cache_manager::CacheResult;
316 let tokens_original = chars_over_four(text);
317 if tokens_original < TOOL_RESULT_MIN_SAVING * 2 {
318 return None;
319 }
320 if self.route_compression_mode(text) == crate::confidence_router::CompressionMode::Safe {
321 return None;
322 }
323 let use_cache = use_cache && !self.never_store(text);
324
325 let result = if use_cache {
326 self.compress_with_cache(text).ok()
327 } else {
328 self.compress(text).ok().map(|output| CacheResult::Fresh { output })
329 }?;
330
331 let (out, tokens_compressed, repeat) = match result {
332 CacheResult::Dedup { inline_ref, token_cost } => (inline_ref, token_cost, true),
333 CacheResult::Delta { delta_text, token_cost, .. } => (delta_text, token_cost, true),
334 CacheResult::Fresh { output } => {
335 let mut data = output.data;
337 if use_cache {
338 let hash = crate::cache_manager::CacheManager::sha256_hex(text.as_bytes());
339 data.push_str(&format!(
340 "\n[full output: call sqz_expand with ref \"{}\"]",
341 &hash[..16]
342 ));
343 }
344 let t = chars_over_four(&data);
345 (data, t, false)
346 }
347 };
348 if tokens_original.saturating_sub(tokens_compressed) < TOOL_RESULT_MIN_SAVING {
349 return None;
350 }
351 Some(ToolResultCompression {
352 text: out,
353 tokens_original,
354 tokens_compressed,
355 repeat,
356 })
357 }
358
359 pub fn compress_or_passthrough(&self, input: &str) -> CompressedContent {
367 match self.compress(input) {
368 Ok(result) => result,
369 Err(_) => {
370 let tokens = (input.len() as u32 + 3) / 4;
371 CompressedContent {
372 data: input.to_string(),
373 tokens_compressed: tokens,
374 tokens_original: tokens,
375 stages_applied: vec![],
376 compression_ratio: 1.0,
377 provenance: crate::types::Provenance::default(),
378 verify: None,
379 }
380 }
381 }
382 }
383
384 pub fn compress_with_mode(&self, input: &str, mode: crate::confidence_router::CompressionMode) -> Result<CompressedContent> {
390 let pipeline = self.pipeline.lock()
391 .map_err(|_| SqzError::Other("pipeline lock poisoned".into()))?;
392 let ctx = crate::pipeline::SessionContext {
393 session_id: "engine".to_string(),
394 };
395
396 match mode {
397 crate::confidence_router::CompressionMode::Safe => {
398 let lossless = self.confidence_router.route(input)
399 == crate::confidence_router::CompressionMode::Safe;
400 self.compress_safe(input, &pipeline, &ctx, lossless)
401 }
402 _ => {
403 drop(pipeline); self.compress(input)
406 }
407 }
408 }
409
410 fn compress_safe(
412 &self,
413 input: &str,
414 pipeline: &crate::pipeline::CompressionPipeline,
415 ctx: &crate::pipeline::SessionContext,
416 lossless: bool,
417 ) -> Result<CompressedContent> {
418 use crate::preset::{
419 CompressionConfig, CondenseConfig, CustomTransformsConfig, BudgetConfig,
420 ModelConfig, PresetMeta, TerseModeConfig, TerseLevel, ToolSelectionConfig,
421 };
422
423 let safe_preset = Preset {
424 preset: PresetMeta {
425 name: "safe".to_string(),
426 version: "1.0".to_string(),
427 description: "Safe fallback — minimal compression".to_string(),
428 },
429 compression: CompressionConfig {
430 stages: vec!["condense".to_string()],
431 keep_fields: None,
432 strip_fields: None,
433 condense: Some(CondenseConfig { enabled: true, max_repeated_lines: 3 }),
434 git_diff_fold: None,
435 strip_nulls: None,
436 flatten: None,
437 truncate_strings: None,
438 collapse_arrays: None,
439 custom_transforms: Some(CustomTransformsConfig { enabled: false }),
440 },
441 tool_selection: ToolSelectionConfig {
442 max_tools: 5,
443 similarity_threshold: 0.7,
444 default_tools: vec![],
445 },
446 budget: BudgetConfig {
447 warning_threshold: 0.70,
448 ceiling_threshold: 0.85,
449 default_window_size: 200_000,
450 agents: Default::default(),
451 },
452 terse_mode: TerseModeConfig { enabled: false, level: TerseLevel::Moderate },
453 model: ModelConfig {
454 family: "anthropic".to_string(),
455 primary: String::new(),
456 local: String::new(),
457 complexity_threshold: 0.4,
458 pricing: None,
459 },
460 };
461
462 let mut result = if lossless {
463 pipeline.compress_lossless(input, ctx, &safe_preset)?
464 } else {
465 pipeline.compress(input, ctx, &safe_preset)?
466 };
467 let verify = Verifier::verify(input, &result.data);
468 result.verify = Some(verify);
469 result.provenance = Provenance {
470 label: Some("safe-fallback".to_string()),
471 ..Default::default()
472 };
473 Ok(result)
474 }
475
476 pub fn compress_with_provenance(
478 &self,
479 input: &str,
480 provenance: Provenance,
481 ) -> Result<CompressedContent> {
482 let mut result = self.compress(input)?;
483 result.provenance = provenance;
484 Ok(result)
485 }
486
487 pub fn export_ctx(&self, session_id: &str) -> Result<String> {
489 let session = self.session_store.load_session(session_id.to_string())?;
490 CtxFormat::serialize(&session)
491 }
492
493 pub fn import_ctx(&self, ctx: &str) -> Result<SessionId> {
495 let session = CtxFormat::deserialize(ctx)?;
496 self.session_store.save_session(&session)
497 }
498
499 pub fn pin(&self, session_id: &str, turn_index: usize, reason: &str, tokens: u32) -> Result<PinEntry> {
501 self.pin_manager.pin(session_id, turn_index, reason, tokens)
502 }
503
504 pub fn unpin(&self, session_id: &str, turn_index: usize) -> Result<()> {
506 self.pin_manager.unpin(session_id, turn_index)
507 }
508
509 pub fn search_sessions(&self, query: &str) -> Result<Vec<SessionSummary>> {
511 self.session_store.search(query)
512 }
513
514 pub fn usage_report(&self, agent_id: &str) -> UsageReport {
516 self.budget_tracker.usage_report(agent_id.to_string())
517 }
518
519 pub fn cost_summary(&self, session_id: &str) -> Result<SessionCostSummary> {
521 let session = self.session_store.load_session(session_id.to_string())?;
522 Ok(self.cost_calculator.session_summary(&session))
523 }
524
525 pub fn reload_preset(&mut self, toml: &str) -> Result<()> {
527 let new_preset = PresetParser::parse(toml)?;
528 if let Ok(mut pipeline) = self.pipeline.lock() {
529 pipeline.reload_preset(&new_preset)?;
530 }
531 if let Ok(mut router) = self.model_router.lock() {
532 *router = ModelRouter::new(&new_preset);
533 }
534 if let Ok(mut preset) = self.preset.lock() {
535 *preset = new_preset;
536 }
537 Ok(())
538 }
539
540 pub fn watch_preset_file(&self, path: &Path) -> Result<notify::RecommendedWatcher> {
545 use notify::{Event, EventKind, RecursiveMode, Watcher};
546
547 let preset_arc = Arc::clone(&self.preset);
548 let pipeline_arc = Arc::clone(&self.pipeline);
549 let router_arc = Arc::clone(&self.model_router);
550 let watched_path = path.to_owned();
551
552 let mut watcher = notify::recommended_watcher(move |res: notify::Result<Event>| {
553 if let Ok(event) = res {
554 if matches!(event.kind, EventKind::Modify(_) | EventKind::Create(_)) {
555 match std::fs::read_to_string(&watched_path) {
556 Ok(toml_str) => match PresetParser::parse(&toml_str) {
557 Ok(new_preset) => {
558 if let Ok(mut p) = pipeline_arc.lock() {
559 let _ = p.reload_preset(&new_preset);
560 }
561 if let Ok(mut r) = router_arc.lock() {
562 *r = ModelRouter::new(&new_preset);
563 }
564 if let Ok(mut pr) = preset_arc.lock() {
565 *pr = new_preset;
566 }
567 }
568 Err(e) => eprintln!("[sqz] invalid preset: {e}"),
569 },
570 Err(e) => eprintln!("[sqz] preset read error: {e}"),
571 }
572 }
573 }
574 })
575 .map_err(|e| SqzError::Other(format!("watcher error: {e}")))?;
576
577 watcher
578 .watch(path, RecursiveMode::NonRecursive)
579 .map_err(|e| SqzError::Other(format!("watch error: {e}")))?;
580
581 Ok(watcher)
582 }
583
584 pub fn session_store(&self) -> &SessionStore {
586 &self.session_store
587 }
588
589 pub fn cache_manager(&self) -> &CacheManager {
591 &self.cache_manager
592 }
593
594 pub fn ast_parser(&self) -> &AstParser {
596 &self.ast_parser
597 }
598
599 pub fn terse_mode(&self) -> &TerseMode {
601 &self.terse_mode
602 }
603
604 pub fn reorder_context(
610 &self,
611 sections: &mut Vec<crate::litm_positioner::ContextSection>,
612 strategy: crate::litm_positioner::LitmStrategy,
613 ) {
614 let positioner = crate::litm_positioner::LitmPositioner::new(strategy);
615 positioner.reorder(sections);
616 }
617
618 pub fn route_compression_mode(&self, content: &str) -> crate::confidence_router::CompressionMode {
621 self.confidence_router.route(content)
622 }
623
624 pub fn never_store(&self, content: &str) -> bool {
626 self.confidence_router.never_store(content)
627 }
628
629 pub fn count_tokens(&self, text: &str) -> u32 {
631 let preset = self.preset.lock().unwrap();
632 self.pipeline.lock().unwrap().count_tokens(text, &preset)
633 }
634
635 pub fn clear_poison(&self) {
637 self.preset.clear_poison();
638 self.pipeline.clear_poison();
639 self.model_router.clear_poison();
640 }
641}
642
643pub const TOOL_RESULT_MIN_SAVING: u32 = 16;
646
647#[derive(Debug, Clone, PartialEq, Eq)]
650pub struct ToolResultCompression {
651 pub text: String,
652 pub tokens_original: u32,
653 pub tokens_compressed: u32,
654 pub repeat: bool,
657}
658
659fn chars_over_four(text: &str) -> u32 {
660 ((text.len() as f64) / 4.0).ceil() as u32
661}
662
663#[cfg(test)]
664mod tests {
665 use super::*;
666 use crate::types::{BudgetState, CorrectionLog, ModelFamily, SessionState};
667 use chrono::Utc;
668 use std::path::PathBuf;
669
670 fn make_session(id: &str) -> SessionState {
671 let now = Utc::now();
672 SessionState {
673 id: id.to_string(),
674 project_dir: PathBuf::from("/tmp/test"),
675 conversation: vec![],
676 corrections: CorrectionLog::default(),
677 pins: vec![],
678 learnings: vec![],
679 compressed_summary: "test session".to_string(),
680 budget: BudgetState {
681 window_size: 200_000,
682 consumed: 0,
683 pinned: 0,
684 model_family: ModelFamily::AnthropicClaude,
685 },
686 tool_usage: vec![],
687 created_at: now,
688 updated_at: now,
689 }
690 }
691
692 #[test]
693 fn test_engine_new() {
694 let dir = tempfile::tempdir().unwrap();
695 let engine = SqzEngine::with_preset_and_store(Preset::default(), &dir.path().join("store.db"));
696 assert!(engine.is_ok(), "engine with the default preset should start");
697 }
698
699 #[test]
700 fn test_compress_or_passthrough_returns_result_on_valid_input() {
701 let dir = tempfile::tempdir().unwrap();
702 let engine = SqzEngine::with_preset_and_store(Preset::default(), &dir.path().join("store.db")).unwrap();
703 let result = engine.compress_or_passthrough("hello world");
704 assert_eq!(result.data, "hello world");
705 assert!(result.tokens_original > 0);
706 }
707
708 #[test]
709 fn test_compress_or_passthrough_never_panics_on_empty() {
710 let dir = tempfile::tempdir().unwrap();
711 let engine = SqzEngine::with_preset_and_store(Preset::default(), &dir.path().join("store.db")).unwrap();
712 let result = engine.compress_or_passthrough("");
713 assert_eq!(result.data, "");
714 assert_eq!(result.compression_ratio, 1.0);
715 }
716
717 #[test]
718 fn test_compress_or_passthrough_handles_json() {
719 let dir = tempfile::tempdir().unwrap();
720 let engine = SqzEngine::with_preset_and_store(Preset::default(), &dir.path().join("store.db")).unwrap();
721 let result = engine.compress_or_passthrough(r#"{"key":"value"}"#);
722 assert!(!result.data.is_empty());
724 }
725
726 #[test]
727 fn test_compress_or_passthrough_handles_binary_garbage() {
728 let dir = tempfile::tempdir().unwrap();
729 let engine = SqzEngine::with_preset_and_store(Preset::default(), &dir.path().join("store.db")).unwrap();
730 let garbage = "\x00\x01\x02\x7f invalid control chars \t\n\r";
732 let result = engine.compress_or_passthrough(garbage);
733 assert!(!result.data.is_empty());
734 }
735
736 #[test]
737 fn test_compress_plain_text() {
738 let dir = tempfile::tempdir().unwrap();
739 let engine = SqzEngine::with_preset_and_store(Preset::default(), &dir.path().join("store.db")).unwrap();
740 let result = engine.compress("hello world");
741 assert!(result.is_ok());
742 assert_eq!(result.unwrap().data, "hello world");
743 }
744
745 #[test]
746 fn test_compress_json_applies_toon() {
747 let dir = tempfile::tempdir().unwrap();
748 let engine = SqzEngine::with_preset_and_store(Preset::default(), &dir.path().join("store.db")).unwrap();
749 let result = engine.compress(r#"{"name":"Alice","age":30}"#).unwrap();
750 assert!(result.data.starts_with("TOON:"), "JSON should be TOON-encoded");
751 }
752
753 #[test]
754 fn compress_tool_result_gates_like_the_proxy() {
755 let dir = tempfile::tempdir().unwrap();
756 let engine = SqzEngine::with_preset_and_store(Preset::default(), &dir.path().join("store.db")).unwrap();
757
758 assert_eq!(engine.compress_tool_result("ok, 3 rows updated", true), None, "too small to pay off");
759
760 let rows: String = (0..80)
761 .map(|i| format!("issue row {i} status open priority medium assignee nobody\n"))
762 .collect();
763 let first = engine.compress_tool_result(&rows, true).expect("compresses");
764 assert!(first.tokens_compressed + TOOL_RESULT_MIN_SAVING <= first.tokens_original);
765 assert!(!first.text.starts_with("§ref:") && !first.repeat);
766 let hash = CacheManager::sha256_hex(rows.as_bytes());
768 assert!(first.text.ends_with(&format!("[full output: call sqz_expand with ref \"{}\"]", &hash[..16])), "{}", first.text);
769 let second = engine.compress_tool_result(&rows, true).expect("repeat");
770 assert!(second.text.starts_with("§ref:") && second.repeat, "{}", second.text);
771 let uncached = engine.compress_tool_result(&rows, false).expect("no cache");
772 assert!(!uncached.text.starts_with("§ref:") && !uncached.text.contains("sqz_expand"));
773 }
774
775 #[test]
776 fn clear_poison_after_a_caught_panic() {
777 let dir = tempfile::tempdir().unwrap();
778 let engine = SqzEngine::with_preset_and_store(Preset::default(), &dir.path().join("store.db")).unwrap();
779 let caught = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
780 let _preset = engine.preset.lock().unwrap();
781 let _pipeline = engine.pipeline.lock().unwrap();
782 panic!("a bug while compressing");
783 }));
784 assert!(caught.is_err());
785 assert!(engine.compress("hello world").is_err());
786
787 engine.clear_poison();
788 assert_eq!(engine.compress("hello world").unwrap().data, "hello world");
789 assert!(engine.count_tokens("hello world") > 0);
790 }
791
792 fn high_risk_inputs() -> Vec<(&'static str, String)> {
793 let frames: String = (0..40)
794 .map(|i| format!(" {i:>2}: myapp::scheduler::step_{i}\n at ./src/scheduler/mod_{}.rs:{}:5\n", i % 7, 40 + 7 * i))
795 .collect();
796 let tables: String = ["customers", "orders", "refunds", "payments", "shipments", "invoices"]
797 .iter()
798 .map(|t| format!("CREATE TABLE {t} (\n id BIGSERIAL PRIMARY KEY,\n status TEXT NOT NULL DEFAULT 'pending',\n created_at TIMESTAMPTZ NOT NULL DEFAULT now()\n);\nCREATE INDEX idx_{t}_created_at ON {t} (created_at);\n"))
799 .collect();
800 let pem: String = (0..20).map(|i| CacheManager::sha256_hex(format!("pem {i}").as_bytes()) + "\n").collect();
801 let creds: String = ["stripe", "sendgrid", "github", "datadog", "sentry"]
802 .iter()
803 .map(|s| format!("{s}_api_key: sk_test_{}\n{s}_endpoint: https://{s}.example.com/v1\n", &CacheManager::sha256_hex(s.as_bytes())[..32]))
804 .collect();
805 vec![
806 ("panic", format!("thread 'main' panicked at src/scheduler/queue.rs:217:31:\ncalled `Option::unwrap()` on a `None` value\nstack backtrace:\n{frames}")),
807 ("migration", format!("BEGIN;\n{tables}ALTER TABLE refunds ADD COLUMN reason TEXT;\nCOMMIT;\n")),
808 ("keys", format!("-----BEGIN RSA PRIVATE KEY-----\n{pem}-----END RSA PRIVATE KEY-----\n{creds}")),
809 ]
810 }
811
812 #[test]
813 fn high_risk_content_passes_through_unchanged() {
814 use crate::confidence_router::CompressionMode;
815 let dir = tempfile::tempdir().unwrap();
816 let engine = SqzEngine::with_preset_and_store(Preset::default(), &dir.path().join("store.db")).unwrap();
817 let inputs = high_risk_inputs();
818 for (name, input) in &inputs {
819 assert!(input.len() > 500, "{name}");
820 assert_eq!(engine.route_compression_mode(input), CompressionMode::Safe, "{name}");
821 assert_eq!(&engine.compress(input).unwrap().data, input, "{name}");
822 for mode in [CompressionMode::Safe, CompressionMode::Default, CompressionMode::Aggressive] {
823 assert_eq!(&engine.compress_with_mode(input, mode).unwrap().data, input, "{name} {mode:?}");
824 }
825 }
826 let colored = format!("\x1b[31m{}\x1b[0m", inputs[0].1);
827 assert_eq!(engine.compress(&colored).unwrap().data, inputs[0].1);
828 }
829
830 #[test]
831 fn verifier_fallback_still_compresses() {
832 use crate::confidence_router::CompressionMode;
833 let dir = tempfile::tempdir().unwrap();
834 let engine = SqzEngine::with_preset_and_store(Preset::default(), &dir.path().join("store.db")).unwrap();
835 let input = (0..24)
836 .map(|i| {
837 if i % 2 == 0 {
838 format!("SEG{i} /srv/app/mod_{i}.py llllllllllllllllllllllllllllllllllllllll")
839 } else {
840 format!("SEG{i} the quick brown fox jumps over {i} lazy dogs by silver rivers")
841 }
842 })
843 .collect::<Vec<_>>()
844 .join("\n\n");
845 assert_ne!(engine.route_compression_mode(&input), CompressionMode::Safe);
846 let result = engine.compress(&input).unwrap();
847 assert_eq!(result.provenance.label.as_deref(), Some("safe-fallback"));
848 assert!(result.data.contains("low-information segments omitted"), "{}", result.data);
849 assert_eq!(result.data, engine.compress_with_mode(&input, CompressionMode::Safe).unwrap().data);
850 }
851
852 #[test]
853 fn json_values_survive_every_mode() {
854 use crate::confidence_router::CompressionMode;
855 let dir = tempfile::tempdir().unwrap();
856 let engine = SqzEngine::with_preset_and_store(Preset::default(), &dir.path().join("store.db")).unwrap();
857 let pretty = "[\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 1\n]\n";
858 let expected = serde_json::json!([0, 0, 0, 0, 0, 0, 1]);
859 let mut outputs = vec![engine.compress(pretty).unwrap().data];
860 for mode in [CompressionMode::Safe, CompressionMode::Default, CompressionMode::Aggressive] {
861 outputs.push(engine.compress_with_mode(pretty, mode).unwrap().data);
862 }
863 for data in outputs {
864 assert_eq!(crate::toon::ToonEncoder.decode(&data).unwrap(), expected, "{data}");
865 }
866 }
867
868 #[test]
869 fn never_stored_content_skips_every_cache() {
870 use crate::cache_manager::CacheResult;
871 use crate::confidence_router::CompressionMode;
872 let dir = tempfile::tempdir().unwrap();
873 let engine = SqzEngine::with_preset_and_store(Preset::default(), &dir.path().join("store.db")).unwrap();
874 let mut log: String = (0..120)
875 .map(|i| format!("2026-10-05T12:{:02}:{:02}Z INFO GET /healthz 200 1ms\n", i / 60, i % 60))
876 .collect();
877 log.push_str("2026-10-05T12:30:00Z DEBUG db config host=db.internal user=app password=hunter2\n");
878 assert_ne!(engine.route_compression_mode(&log), CompressionMode::Safe);
879 assert!(engine.never_store(&log));
880 let stored = || engine.cache_manager().check_dedup_with_meta(log.as_bytes()).unwrap().is_some();
881
882 let CacheResult::Fresh { output } = engine.compress_with_cache(&log).unwrap() else { panic!("not fresh") };
883 assert!(output.data.len() < log.len());
884 let CacheResult::Fresh { output } = engine.compress_with_cache_lossless(&log).unwrap() else { panic!("not fresh") };
885 assert_eq!(output.data, log);
886 let result = engine.compress_tool_result(&log, true).expect("compresses");
887 assert!(!result.repeat && !result.text.contains("sqz_expand"), "{}", result.text);
888 assert!(!stored());
889 assert!(matches!(engine.compress_with_cache(&log).unwrap(), CacheResult::Fresh { .. }));
890 }
891
892 #[test]
893 fn test_export_import_ctx_round_trip() {
894 let dir = tempfile::tempdir().unwrap();
895 let store_path = dir.path().join("store.db");
896 let engine = SqzEngine::with_preset_and_store(Preset::default(), &store_path).unwrap();
897
898 let session = make_session("sess-rt");
899 engine.session_store().save_session(&session).unwrap();
900
901 let ctx = engine.export_ctx("sess-rt").unwrap();
902 let imported_id = engine.import_ctx(&ctx).unwrap();
903 assert_eq!(imported_id, "sess-rt");
904 }
905
906 #[test]
907 fn test_search_sessions() {
908 let dir = tempfile::tempdir().unwrap();
909 let store_path = dir.path().join("store.db");
910 let engine = SqzEngine::with_preset_and_store(Preset::default(), &store_path).unwrap();
911
912 let mut session = make_session("sess-search");
913 session.compressed_summary = "authentication refactor".to_string();
914 engine.session_store().save_session(&session).unwrap();
915
916 let results = engine.search_sessions("authentication").unwrap();
917 assert_eq!(results.len(), 1);
918 assert_eq!(results[0].id, "sess-search");
919 }
920
921 #[test]
922 fn test_usage_report_starts_at_zero() {
923 let dir = tempfile::tempdir().unwrap();
924 let engine = SqzEngine::with_preset_and_store(Preset::default(), &dir.path().join("store.db")).unwrap();
925 let report = engine.usage_report("default");
926 assert_eq!(report.consumed, 0);
927 assert_eq!(report.available, report.allocated);
928 }
929
930 #[test]
931 fn test_cost_summary() {
932 let dir = tempfile::tempdir().unwrap();
933 let store_path = dir.path().join("store.db");
934 let engine = SqzEngine::with_preset_and_store(Preset::default(), &store_path).unwrap();
935
936 let session = make_session("sess-cost");
937 engine.session_store().save_session(&session).unwrap();
938
939 let summary = engine.cost_summary("sess-cost").unwrap();
940 assert_eq!(summary.total_tokens, 0);
941 assert!((summary.total_usd - 0.0).abs() < f64::EPSILON);
942 }
943
944 #[test]
945 fn test_reload_preset_updates_state() {
946 let dir = tempfile::tempdir().unwrap();
947 let mut engine = SqzEngine::with_preset_and_store(Preset::default(), &dir.path().join("store.db")).unwrap();
948 let toml = r#"
949[preset]
950name = "reloaded"
951version = "2.0"
952
953[compression]
954stages = []
955
956[tool_selection]
957max_tools = 5
958similarity_threshold = 0.7
959
960[budget]
961warning_threshold = 0.70
962ceiling_threshold = 0.85
963default_window_size = 200000
964
965[terse_mode]
966enabled = false
967level = "moderate"
968
969[model]
970family = "anthropic"
971primary = "claude-sonnet-4-20250514"
972complexity_threshold = 0.4
973"#;
974 assert!(engine.reload_preset(toml).is_ok());
975 let preset = engine.preset.lock().unwrap();
977 assert_eq!(preset.preset.name, "reloaded");
978 }
979
980 #[test]
981 fn test_reload_invalid_preset_returns_error() {
982 let dir = tempfile::tempdir().unwrap();
983 let mut engine = SqzEngine::with_preset_and_store(Preset::default(), &dir.path().join("store.db")).unwrap();
984 let result = engine.reload_preset("not valid toml [[[");
985 assert!(result.is_err(), "invalid TOML should return error");
986 }
987
988 #[test]
989 fn test_export_nonexistent_session_returns_error() {
990 let dir = tempfile::tempdir().unwrap();
991 let engine = SqzEngine::with_preset_and_store(Preset::default(), &dir.path().join("store.db")).unwrap();
992 let result = engine.export_ctx("does-not-exist");
993 assert!(result.is_err());
994 }
995
996 #[test]
997 fn test_import_invalid_ctx_returns_error() {
998 let dir = tempfile::tempdir().unwrap();
999 let engine = SqzEngine::with_preset_and_store(Preset::default(), &dir.path().join("store.db")).unwrap();
1000 let result = engine.import_ctx("not valid json {{{");
1001 assert!(result.is_err());
1002 }
1003
1004 #[test]
1005 fn store_path_env_override_used_when_set() {
1006 let dir = tempfile::tempdir().unwrap();
1007 let target = dir.path().join("nested").join("project.db");
1008 let resolved =
1009 SqzEngine::store_path_from_env(Some(target.to_string_lossy().to_string()));
1010 assert_eq!(resolved, Some(target.clone()));
1011 assert!(target.parent().unwrap().exists());
1013 }
1014
1015 #[test]
1016 fn store_path_env_ignores_empty_values() {
1017 assert_eq!(SqzEngine::store_path_from_env(None), None);
1018 assert_eq!(SqzEngine::store_path_from_env(Some(String::new())), None);
1019 assert_eq!(SqzEngine::store_path_from_env(Some(" ".to_string())), None);
1020 }
1021
1022 #[test]
1023 fn store_path_env_leaves_existing_parent_permissions_alone() {
1024 let dir = tempfile::tempdir().unwrap();
1025 #[cfg(unix)]
1026 {
1027 use std::os::unix::fs::PermissionsExt;
1028 std::fs::set_permissions(dir.path(), std::fs::Permissions::from_mode(0o755))
1029 .unwrap();
1030 }
1031 let target = dir.path().join("sessions.db");
1032 let resolved =
1033 SqzEngine::store_path_from_env(Some(target.to_string_lossy().to_string()));
1034 assert_eq!(resolved, Some(target));
1035 #[cfg(unix)]
1036 {
1037 use std::os::unix::fs::PermissionsExt;
1038 let mode = std::fs::metadata(dir.path()).unwrap().permissions().mode() & 0o777;
1039 assert_eq!(mode, 0o755, "pre-existing parent must not be chmodded");
1040 }
1041 }
1042
1043 #[test]
1044 fn ref_ttl_env_parses_seconds_and_falls_back() {
1045 use std::time::Duration;
1046 assert_eq!(SqzEngine::ref_ttl_from_env(None), Duration::from_secs(1800));
1048 assert_eq!(
1049 SqzEngine::ref_ttl_from_env(Some(String::new())),
1050 Duration::from_secs(1800)
1051 );
1052 assert_eq!(
1053 SqzEngine::ref_ttl_from_env(Some(" ".into())),
1054 Duration::from_secs(1800)
1055 );
1056 assert_eq!(
1057 SqzEngine::ref_ttl_from_env(Some("ten".into())),
1058 Duration::from_secs(1800)
1059 );
1060 assert_eq!(
1061 SqzEngine::ref_ttl_from_env(Some("-5".into())),
1062 Duration::from_secs(1800)
1063 );
1064 assert_eq!(
1066 SqzEngine::ref_ttl_from_env(Some("300".into())),
1067 Duration::from_secs(300)
1068 );
1069 assert_eq!(
1070 SqzEngine::ref_ttl_from_env(Some(" 60 ".into())),
1071 Duration::from_secs(60)
1072 );
1073 assert_eq!(
1074 SqzEngine::ref_ttl_from_env(Some("0".into())),
1075 Duration::ZERO
1076 );
1077 }
1078
1079 #[test]
1080 fn engine_with_custom_store_path_round_trips() {
1081 let dir = tempfile::tempdir().unwrap();
1082 let db = dir.path().join("proj").join("sessions.db");
1083 let resolved = SqzEngine::store_path_from_env(Some(db.to_string_lossy().to_string()))
1084 .unwrap();
1085 let engine = SqzEngine::with_preset_and_store(Preset::default(), &resolved).unwrap();
1086 engine
1087 .session_store()
1088 .log_compression(100, 40, &[], "test")
1089 .unwrap();
1090 assert!(db.exists(), "database file must land at the custom path");
1091 let stats = engine.session_store().compression_stats().unwrap();
1092 assert_eq!(stats.total_compressions, 1);
1093 }
1094}