26382a7ac6
CI / Clippy (push) Failing after 15m13s
CI / Test (ubuntu-latest) (push) Failing after 16m1s
CI / Test (macos-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Build (no embeddings / no ORT) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Cookbook (Node) (push) Has been cancelled
CI / Pi Extension (Node) (push) Has been cancelled
CI / Rust SDK (lean-ctx-client) (push) Has been cancelled
CI / Embed SDK (lean-ctx-sdk) (push) Has been cancelled
CI / Python SDK (leanctx) (push) Has been cancelled
CI / Hermes Plugin (Python) (push) Has been cancelled
CI / SDK Conformance Matrix (push) Has been cancelled
CI / Coverage (push) Has been cancelled
CI / cargo-deny (push) Has been cancelled
CI / Adversarial Safety (push) Has been cancelled
CI / Benchmarks (push) Has been cancelled
CI / Output-Quality Gate (eval A/B) (push) Has been cancelled
CI / Documentation (push) Has been cancelled
CI / CI Green (push) Has been cancelled
JetBrains Plugin / Actionlint (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (rust) (push) Has been cancelled
JetBrains Plugin / Validation (push) Has been cancelled
JetBrains Plugin / Build (push) Has been cancelled
JetBrains Plugin / Test (push) Has been cancelled
Security Check / Security Scan (push) Has been cancelled
867 lines
28 KiB
Rust
867 lines
28 KiB
Rust
//! Scenario tests for the Efficiency + UX Hardening Plan (10 fixes)
|
|
//! and the shell compression error-guard hardening.
|
|
//!
|
|
//! Each test simulates a realistic user interaction pattern.
|
|
#![allow(clippy::needless_raw_string_hashes)]
|
|
|
|
use lean_ctx::core::cache::SessionCache;
|
|
use lean_ctx::core::protocol::CrpMode;
|
|
use std::io::Write;
|
|
|
|
// =============================================================================
|
|
// Fix 1 + 2: ctx_read Fast-Path + mtime guard
|
|
// =============================================================================
|
|
|
|
mod read_cache_hits {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn scenario_repeated_reads_hit_cache() {
|
|
let mut cache = SessionCache::new();
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let file = dir.path().join("main.rs");
|
|
std::fs::write(&file, "fn main() { println!(\"hello\"); }\n").unwrap();
|
|
let path = file.to_str().unwrap();
|
|
|
|
// First read: stores in cache
|
|
let out1 = lean_ctx::tools::ctx_read::handle_with_task_resolved(
|
|
&mut cache,
|
|
path,
|
|
"full",
|
|
CrpMode::Off,
|
|
None,
|
|
);
|
|
assert!(!out1.content.is_empty());
|
|
assert_ne!(out1.resolved_mode, "error");
|
|
|
|
// Second read: should be a cache hit (mtime unchanged → fast path)
|
|
let out2 = lean_ctx::tools::ctx_read::handle_with_task_resolved(
|
|
&mut cache,
|
|
path,
|
|
"full",
|
|
CrpMode::Off,
|
|
None,
|
|
);
|
|
assert!(
|
|
out2.content.contains("unchanged") || out2.content.contains("cached"),
|
|
"Expected cache hit stub, got: {}",
|
|
&out2.content[..out2.content.len().min(200)]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn scenario_modified_file_detects_change() {
|
|
let mut cache = SessionCache::new();
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let file = dir.path().join("app.rs");
|
|
std::fs::write(&file, "fn v1() {}\n").unwrap();
|
|
let path = file.to_str().unwrap();
|
|
|
|
lean_ctx::tools::ctx_read::handle_with_task_resolved(
|
|
&mut cache,
|
|
path,
|
|
"full",
|
|
CrpMode::Off,
|
|
None,
|
|
);
|
|
|
|
// Ensure mtime granularity catches the change
|
|
std::thread::sleep(std::time::Duration::from_millis(1100));
|
|
std::fs::write(&file, "fn v2() { updated(); }\n").unwrap();
|
|
|
|
let out = lean_ctx::tools::ctx_read::handle_with_task_resolved(
|
|
&mut cache,
|
|
path,
|
|
"full",
|
|
CrpMode::Off,
|
|
None,
|
|
);
|
|
// Should detect the change and show new content or delta
|
|
assert!(
|
|
out.content.contains("v2")
|
|
|| out.content.contains("delta")
|
|
|| out.content.contains("diff"),
|
|
"Expected change detection, got: {}",
|
|
&out.content[..out.content.len().min(300)]
|
|
);
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Fix 3: zstd bypass — compressed output cache checked before decompression
|
|
// =============================================================================
|
|
|
|
mod zstd_bypass {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn scenario_map_mode_uses_compressed_cache() {
|
|
let mut cache = SessionCache::new();
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let file = dir.path().join("module.rs");
|
|
std::fs::write(
|
|
&file,
|
|
"use std::io;\npub fn read_all() -> io::Result<String> { Ok(String::new()) }\npub fn write_all(data: &str) -> io::Result<()> { Ok(()) }\n",
|
|
).unwrap();
|
|
let path = file.to_str().unwrap();
|
|
|
|
// First read with full mode to populate cache
|
|
lean_ctx::tools::ctx_read::handle_with_task_resolved(
|
|
&mut cache,
|
|
path,
|
|
"full",
|
|
CrpMode::Off,
|
|
None,
|
|
);
|
|
|
|
// First map read — processes and caches compressed output
|
|
let out1 = lean_ctx::tools::ctx_read::handle_with_task_resolved(
|
|
&mut cache,
|
|
path,
|
|
"map",
|
|
CrpMode::Off,
|
|
None,
|
|
);
|
|
assert!(!out1.content.is_empty());
|
|
|
|
// Second map read — should hit compressed cache (no zstd decompression needed)
|
|
let out2 = lean_ctx::tools::ctx_read::handle_with_task_resolved(
|
|
&mut cache,
|
|
path,
|
|
"map",
|
|
CrpMode::Off,
|
|
None,
|
|
);
|
|
assert!(!out2.content.is_empty());
|
|
assert_eq!(out1.content, out2.content);
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Fix 4: ctx_shell exit code in response
|
|
// =============================================================================
|
|
|
|
mod exit_code {
|
|
#[test]
|
|
fn scenario_exit_code_format() {
|
|
// Test the exit code formatting logic that the registered tool uses
|
|
let code: i32 = 1;
|
|
let exit_suffix = if code != 0 {
|
|
format!("\n[exit:{code}]")
|
|
} else {
|
|
String::new()
|
|
};
|
|
assert_eq!(exit_suffix, "\n[exit:1]");
|
|
|
|
let code: i32 = 0;
|
|
let exit_suffix = if code != 0 {
|
|
format!("\n[exit:{code}]")
|
|
} else {
|
|
String::new()
|
|
};
|
|
assert!(exit_suffix.is_empty());
|
|
|
|
let code: i32 = 127;
|
|
let exit_suffix = if code != 0 {
|
|
format!("\n[exit:{code}]")
|
|
} else {
|
|
String::new()
|
|
};
|
|
assert_eq!(exit_suffix, "\n[exit:127]");
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Fix 5: Error as MCP ErrorData (not success body)
|
|
// =============================================================================
|
|
|
|
mod error_data {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn scenario_nonexistent_file_returns_error_mode() {
|
|
let mut cache = SessionCache::new();
|
|
let out = lean_ctx::tools::ctx_read::handle_with_task_resolved(
|
|
&mut cache,
|
|
"/nonexistent/path/does_not_exist.rs",
|
|
"full",
|
|
CrpMode::Off,
|
|
None,
|
|
);
|
|
assert_eq!(out.resolved_mode, "error");
|
|
assert!(out.content.contains("ERROR"));
|
|
}
|
|
|
|
#[test]
|
|
fn scenario_search_nonexistent_dir_returns_error() {
|
|
let result = lean_ctx::tools::ctx_search::handle(
|
|
"pattern",
|
|
"/nonexistent_dir_xyz",
|
|
None,
|
|
50,
|
|
CrpMode::Off,
|
|
true,
|
|
false,
|
|
false,
|
|
)
|
|
.text;
|
|
assert!(
|
|
result.starts_with("ERROR:"),
|
|
"Expected ERROR prefix, got: {result}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn scenario_tree_nonexistent_dir_returns_error() {
|
|
let (result, _) = lean_ctx::tools::ctx_tree::handle("/nonexistent_xyz", 3, false, true);
|
|
assert!(
|
|
result.starts_with("ERROR:"),
|
|
"Expected ERROR prefix, got: {result}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn scenario_tree_file_input_returns_error() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let file = dir.path().join("file.txt");
|
|
std::fs::write(&file, "data").unwrap();
|
|
|
|
let (result, _) = lean_ctx::tools::ctx_tree::handle(file.to_str().unwrap(), 3, false, true);
|
|
assert!(
|
|
result.starts_with("ERROR:"),
|
|
"Expected ERROR prefix, got: {result}"
|
|
);
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Fix 6: diff mode without cache returns guidance instead of full file
|
|
// =============================================================================
|
|
|
|
mod diff_guard {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn scenario_diff_without_cache_returns_guidance() {
|
|
let mut cache = SessionCache::new();
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let file = dir.path().join("new_file.rs");
|
|
std::fs::write(&file, "fn brand_new() {}\n").unwrap();
|
|
let path = file.to_str().unwrap();
|
|
|
|
let out = lean_ctx::tools::ctx_read::handle_with_task_resolved(
|
|
&mut cache,
|
|
path,
|
|
"diff",
|
|
CrpMode::Off,
|
|
None,
|
|
);
|
|
assert!(
|
|
out.content.contains("no cached version for diff")
|
|
|| out.content.contains("use mode=full first"),
|
|
"Expected guidance message, got: {}",
|
|
&out.content[..out.content.len().min(300)]
|
|
);
|
|
// Must NOT contain the file content
|
|
assert!(
|
|
!out.content.contains("brand_new"),
|
|
"Full file content leaked through diff guard!"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn scenario_diff_after_full_read_works() {
|
|
let mut cache = SessionCache::new();
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let file = dir.path().join("tracked.rs");
|
|
std::fs::write(&file, "fn version_one() {}\n").unwrap();
|
|
let path = file.to_str().unwrap();
|
|
|
|
// Read full first
|
|
lean_ctx::tools::ctx_read::handle_with_task_resolved(
|
|
&mut cache,
|
|
path,
|
|
"full",
|
|
CrpMode::Off,
|
|
None,
|
|
);
|
|
|
|
// Modify file
|
|
std::thread::sleep(std::time::Duration::from_millis(1100));
|
|
std::fs::write(&file, "fn version_two() { changed(); }\n").unwrap();
|
|
|
|
// Now diff should work
|
|
let out = lean_ctx::tools::ctx_read::handle_with_task_resolved(
|
|
&mut cache,
|
|
path,
|
|
"diff",
|
|
CrpMode::Off,
|
|
None,
|
|
);
|
|
assert!(
|
|
out.content.contains("diff") || out.content.contains("version_two"),
|
|
"Expected diff content, got: {}",
|
|
&out.content[..out.content.len().min(300)]
|
|
);
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Fix 7: ctx_search early abort
|
|
// =============================================================================
|
|
|
|
mod search_early_abort {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn scenario_search_respects_max_results() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
for i in 0..20 {
|
|
let file = dir.path().join(format!("file_{i:02}.rs"));
|
|
std::fs::write(&file, format!("fn search_target_{i}() {{}}\n")).unwrap();
|
|
}
|
|
|
|
let result = lean_ctx::tools::ctx_search::handle(
|
|
"search_target",
|
|
dir.path().to_str().unwrap(),
|
|
None,
|
|
5,
|
|
CrpMode::Off,
|
|
false,
|
|
false,
|
|
false,
|
|
)
|
|
.text;
|
|
let match_lines: Vec<&str> = result
|
|
.lines()
|
|
.filter(|l| l.contains("search_target"))
|
|
.collect();
|
|
assert!(
|
|
match_lines.len() <= 5,
|
|
"Expected at most 5 matches, got {}",
|
|
match_lines.len()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn scenario_search_finds_all_when_under_limit() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
for i in 0..3 {
|
|
let file = dir.path().join(format!("src_{i}.rs"));
|
|
std::fs::write(&file, format!("fn unique_pattern_{i}() {{}}\n")).unwrap();
|
|
}
|
|
|
|
let result = lean_ctx::tools::ctx_search::handle(
|
|
"unique_pattern",
|
|
dir.path().to_str().unwrap(),
|
|
None,
|
|
50,
|
|
CrpMode::Off,
|
|
false,
|
|
false,
|
|
false,
|
|
)
|
|
.text;
|
|
assert!(result.contains("3 matches"));
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Fix 8: Ledger debounce
|
|
// =============================================================================
|
|
|
|
mod ledger_debounce {
|
|
use lean_ctx::core::context_ledger::ContextLedger;
|
|
|
|
#[test]
|
|
fn scenario_debounce_skips_rapid_saves() {
|
|
let mut ledger = ContextLedger::new();
|
|
ledger.record("test.rs", "full", 100, 20);
|
|
|
|
// First debounced save — should execute (no prior flush)
|
|
ledger.save_debounced();
|
|
|
|
// Immediate second save — should be skipped (< 3s)
|
|
ledger.record("other.rs", "map", 50, 10);
|
|
ledger.save_debounced(); // No-op due to debounce — verifies no panic
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Fix 9: Cold-read hints only from 2nd read onwards
|
|
// =============================================================================
|
|
|
|
mod cold_read_hints {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn scenario_first_read_has_no_similarity_hints() {
|
|
let mut cache = SessionCache::new();
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let file = dir.path().join("fresh.rs");
|
|
std::fs::write(
|
|
&file,
|
|
"use std::collections::HashMap;\npub fn compute() -> HashMap<String, i32> { HashMap::new() }\n",
|
|
).unwrap();
|
|
let path = file.to_str().unwrap();
|
|
|
|
let out = lean_ctx::tools::ctx_read::handle_with_task_resolved(
|
|
&mut cache,
|
|
path,
|
|
"full",
|
|
CrpMode::Off,
|
|
None,
|
|
);
|
|
assert!(
|
|
!out.content.contains("[similar:") && !out.content.contains("[related:"),
|
|
"First read should not have similarity hints, got: {}",
|
|
&out.content[..out.content.len().min(500)]
|
|
);
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Fix 10: ctx_tree UX — file input / empty directory
|
|
// =============================================================================
|
|
|
|
mod tree_ux {
|
|
#[test]
|
|
fn scenario_file_input_suggests_parent() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let file = dir.path().join("test.txt");
|
|
std::fs::write(&file, "content").unwrap();
|
|
|
|
let (result, _) = lean_ctx::tools::ctx_tree::handle(file.to_str().unwrap(), 3, false, true);
|
|
assert!(result.contains("is a file, not a directory"));
|
|
assert!(result.contains("Use path="));
|
|
}
|
|
|
|
#[test]
|
|
fn scenario_empty_directory_explicit_message() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
|
|
let (result, _) =
|
|
lean_ctx::tools::ctx_tree::handle(dir.path().to_str().unwrap(), 3, false, true);
|
|
assert!(
|
|
result.contains("empty directory"),
|
|
"Expected 'empty directory' message, got: {result}"
|
|
);
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Shell Compression Error Guard — CRITICAL
|
|
// =============================================================================
|
|
|
|
mod compression_error_guard {
|
|
use lean_ctx::shell::compress::compress_if_beneficial_pub;
|
|
|
|
#[test]
|
|
fn scenario_cargo_check_error_fully_preserved() {
|
|
let error_output = r#" Compiling myapp v0.1.0 (/home/user/myapp)
|
|
error[E0308]: mismatched types
|
|
--> src/main.rs:15:20
|
|
|
|
|
15 | let x: i32 = "hello";
|
|
| --- ^^^^^^^ expected `i32`, found `&str`
|
|
| |
|
|
| expected due to this
|
|
|
|
error: aborting due to 1 previous error
|
|
|
|
For more information about this error, try `rustc --explain E0308`.
|
|
error: could not compile `myapp` (bin "myapp") due to 1 previous error
|
|
"#;
|
|
let result = compress_if_beneficial_pub("cargo check", error_output);
|
|
assert!(
|
|
result.contains("src/main.rs:15:20"),
|
|
"Error location was compressed away! Got: {result}"
|
|
);
|
|
assert!(result.contains("E0308"));
|
|
assert!(result.contains("mismatched types"));
|
|
assert!(result.contains("expected `i32`, found `&str`"));
|
|
}
|
|
|
|
#[test]
|
|
fn scenario_cargo_clippy_warnings_fully_preserved() {
|
|
let warning_output = r#" Checking mylib v0.1.0
|
|
warning: unused variable: `x`
|
|
--> src/lib.rs:42:9
|
|
|
|
|
42 | let x = compute_heavy();
|
|
| ^ help: if this is intentional, prefix it with an underscore: `_x`
|
|
|
|
|
= note: `#[warn(unused_variables)]` on by default
|
|
|
|
warning: `mylib` (lib) generated 1 warning
|
|
Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.34s
|
|
"#;
|
|
let result = compress_if_beneficial_pub("cargo clippy -- -D warnings", warning_output);
|
|
assert!(
|
|
result.contains("src/lib.rs:42:9"),
|
|
"Warning location was compressed away! Got: {result}"
|
|
);
|
|
assert!(result.contains("unused variable: `x`"));
|
|
assert!(result.contains("prefix it with an underscore"));
|
|
}
|
|
|
|
#[test]
|
|
fn scenario_cargo_build_multiple_errors_preserved() {
|
|
let error_output = r#" Compiling lean-ctx v3.6.8
|
|
error[E0502]: cannot borrow `*cache` as mutable because it is also borrowed as immutable
|
|
--> src/tools/ctx_read.rs:320:9
|
|
|
|
|
318 | if let Some(existing) = cache.get(path) {
|
|
| -------------- immutable borrow occurs here
|
|
320 | cache.record_cache_hit(path);
|
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
|
|
|
|
error[E0063]: missing field `last_flush` in initializer of `ContextLedger`
|
|
--> src/core/context_ledger.rs:101:9
|
|
|
|
|
101 | Self {
|
|
| ^^^^ missing `last_flush`
|
|
|
|
error: could not compile `lean-ctx` (lib) due to 2 previous errors
|
|
"#;
|
|
let result = compress_if_beneficial_pub("cargo check 2>&1", error_output);
|
|
assert!(
|
|
result.contains("src/tools/ctx_read.rs:320:9"),
|
|
"First error location lost! Got: {result}"
|
|
);
|
|
assert!(
|
|
result.contains("src/core/context_ledger.rs:101:9"),
|
|
"Second error location lost! Got: {result}"
|
|
);
|
|
assert!(result.contains("E0502"));
|
|
assert!(result.contains("E0063"));
|
|
}
|
|
|
|
#[test]
|
|
fn scenario_typescript_tsc_errors_preserved() {
|
|
let tsc_output = r#"src/components/Button.tsx(23,5): error TS2322: Type 'string' is not assignable to type 'number'.
|
|
src/utils/api.ts(45,10): error TS2304: Cannot find name 'fetchData'.
|
|
Found 2 errors in 2 files.
|
|
"#;
|
|
let result = compress_if_beneficial_pub("npx tsc --noEmit", tsc_output);
|
|
assert!(
|
|
result.contains("Button.tsx(23,5)"),
|
|
"TSC error location lost! Got: {result}"
|
|
);
|
|
assert!(result.contains("api.ts(45,10)"));
|
|
assert!(result.contains("TS2322"));
|
|
}
|
|
|
|
#[test]
|
|
fn scenario_go_build_errors_preserved() {
|
|
let go_output = r#"# myapp/internal/server
|
|
./server.go:15:2: undefined: handleRequest
|
|
./server.go:23:15: cannot use str (variable of type string) as int value in argument to process
|
|
"#;
|
|
let result = compress_if_beneficial_pub("go build ./...", go_output);
|
|
assert!(
|
|
result.contains("server.go:15:2"),
|
|
"Go error location lost! Got: {result}"
|
|
);
|
|
assert!(result.contains("undefined: handleRequest"));
|
|
}
|
|
|
|
#[test]
|
|
fn scenario_eslint_errors_preserved() {
|
|
let eslint_output = r#"/home/user/project/src/App.tsx
|
|
15:7 error 'unused' is defined but never used no-unused-vars
|
|
23:1 error Expected indentation of 2 spaces indent
|
|
|
|
/home/user/project/src/utils.ts
|
|
4:10 error 'x' is not defined no-undef
|
|
|
|
✖ 3 problems (3 errors, 0 warnings)
|
|
"#;
|
|
let result = compress_if_beneficial_pub("eslint src/", eslint_output);
|
|
assert!(
|
|
result.contains("15:7 error"),
|
|
"ESLint error compressed away! Got: {result}"
|
|
);
|
|
assert!(result.contains("no-unused-vars"));
|
|
}
|
|
|
|
#[test]
|
|
fn scenario_cargo_test_failures_preserved() {
|
|
let test_output = r#"running 3 tests
|
|
test tests::test_add ... ok
|
|
test tests::test_subtract ... FAILED
|
|
test tests::test_multiply ... ok
|
|
|
|
failures:
|
|
|
|
---- tests::test_subtract stdout ----
|
|
thread 'tests::test_subtract' panicked at src/math.rs:25:5:
|
|
assertion `left == right` failed
|
|
left: 5
|
|
right: 3
|
|
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
|
|
|
|
failures:
|
|
tests::test_subtract
|
|
|
|
test result: FAILED. 2 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
|
|
"#;
|
|
let result = compress_if_beneficial_pub("cargo test", test_output);
|
|
assert!(
|
|
result.contains("src/math.rs:25:5"),
|
|
"Test failure location lost! Got: {result}"
|
|
);
|
|
assert!(result.contains("panicked at"));
|
|
assert!(result.contains("left: 5"));
|
|
}
|
|
|
|
#[test]
|
|
fn scenario_mypy_errors_preserved() {
|
|
let mypy_output = r#"src/main.py:10: error: Incompatible return value type (got "str", expected "int") [return-value]
|
|
src/utils.py:25: error: Name "undefined_var" is not defined [name-defined]
|
|
Found 2 errors in 2 files (checked 5 source files)
|
|
"#;
|
|
let result = compress_if_beneficial_pub("mypy src/", mypy_output);
|
|
assert!(
|
|
result.contains("src/main.py:10"),
|
|
"mypy error location lost! Got: {result}"
|
|
);
|
|
assert!(result.contains("Incompatible return value type"));
|
|
}
|
|
|
|
#[test]
|
|
fn scenario_ruff_errors_preserved() {
|
|
let ruff_output = r#"src/app.py:5:1: F401 [*] `os` imported but unused
|
|
src/utils.py:12:5: E741 Ambiguous variable name: `l`
|
|
Found 2 errors.
|
|
[*] 1 fixable with the `--fix` option.
|
|
"#;
|
|
let result = compress_if_beneficial_pub("ruff check src/", ruff_output);
|
|
assert!(
|
|
result.contains("src/app.py:5:1"),
|
|
"Ruff error location lost! Got: {result}"
|
|
);
|
|
assert!(result.contains("F401"));
|
|
}
|
|
|
|
#[test]
|
|
fn scenario_dotnet_build_errors_preserved() {
|
|
let dotnet_output = r#"Build started...
|
|
Build FAILED.
|
|
|
|
Program.cs(15,13): error CS1002: ; expected [/home/user/MyApp/MyApp.csproj]
|
|
Program.cs(23,5): error CS0103: The name 'undeclared' does not exist in the current context [/home/user/MyApp/MyApp.csproj]
|
|
|
|
2 Error(s)
|
|
"#;
|
|
let result = compress_if_beneficial_pub("dotnet build", dotnet_output);
|
|
assert!(
|
|
result.contains("Program.cs(15,13)"),
|
|
"dotnet error location lost! Got: {result}"
|
|
);
|
|
assert!(result.contains("CS1002"));
|
|
}
|
|
|
|
#[test]
|
|
fn scenario_successful_build_can_still_compress() {
|
|
// SUCCESS output without any error indicators should remain compressible
|
|
let success_output = " Compiling serde v1.0.193\n Compiling tokio v1.35.0\n Compiling myapp v0.1.0\n Finished `dev` profile [unoptimized + debuginfo] target(s) in 45.23s\n";
|
|
let result = compress_if_beneficial_pub("cargo build", success_output);
|
|
// Should not crash, and successful builds are NOT error-guarded
|
|
assert!(!result.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn scenario_make_errors_preserved() {
|
|
let make_output = r#"gcc -Wall -o main main.c utils.c
|
|
main.c:23:5: error: implicit declaration of function 'undefined_func'
|
|
main.c:30:15: error: expected ';' before '}' token
|
|
make: *** [Makefile:12: main] Error 1
|
|
"#;
|
|
let result = compress_if_beneficial_pub("make build", make_output);
|
|
assert!(
|
|
result.contains("main.c:23:5"),
|
|
"Make/gcc error location lost! Got: {result}"
|
|
);
|
|
assert!(result.contains("implicit declaration"));
|
|
}
|
|
|
|
#[test]
|
|
fn scenario_gradle_errors_preserved() {
|
|
let gradle_output = r#"> Task :compileJava FAILED
|
|
/home/user/src/main/java/App.java:15: error: cannot find symbol
|
|
UndefinedClass obj = new UndefinedClass();
|
|
^
|
|
symbol: class UndefinedClass
|
|
location: class App
|
|
|
|
BUILD FAILED in 3s
|
|
"#;
|
|
let result = compress_if_beneficial_pub("./gradlew build", gradle_output);
|
|
assert!(
|
|
result.contains("App.java:15"),
|
|
"Gradle error location lost! Got: {result}"
|
|
);
|
|
assert!(result.contains("cannot find symbol"));
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Integration: Full agent workflow
|
|
// =============================================================================
|
|
|
|
mod integration_workflow {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn scenario_edit_save_reread_cycle() {
|
|
let mut cache = SessionCache::new();
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let file = dir.path().join("workflow.rs");
|
|
std::fs::write(&file, "fn original() {}\n").unwrap();
|
|
let path = file.to_str().unwrap();
|
|
|
|
// 1. Agent reads file for the first time
|
|
let out1 = lean_ctx::tools::ctx_read::handle_with_task_resolved(
|
|
&mut cache,
|
|
path,
|
|
"full",
|
|
CrpMode::Off,
|
|
None,
|
|
);
|
|
assert!(out1.content.contains("original"));
|
|
assert_ne!(out1.resolved_mode, "error");
|
|
|
|
// 2. Agent reads same file again (cache hit, no disk I/O)
|
|
let out2 = lean_ctx::tools::ctx_read::handle_with_task_resolved(
|
|
&mut cache,
|
|
path,
|
|
"full",
|
|
CrpMode::Off,
|
|
None,
|
|
);
|
|
assert!(out2.content.contains("unchanged") || out2.content.contains("cached"));
|
|
|
|
// 3. File is edited externally
|
|
std::thread::sleep(std::time::Duration::from_millis(1100));
|
|
std::fs::write(&file, "fn edited_version() { new_logic(); }\n").unwrap();
|
|
|
|
// 4. Agent reads again — detects change
|
|
let out3 = lean_ctx::tools::ctx_read::handle_with_task_resolved(
|
|
&mut cache,
|
|
path,
|
|
"full",
|
|
CrpMode::Off,
|
|
None,
|
|
);
|
|
assert!(
|
|
out3.content.contains("edited_version")
|
|
|| out3.content.contains("delta")
|
|
|| out3.content.contains("diff"),
|
|
"Should detect file change, got: {}",
|
|
&out3.content[..out3.content.len().min(300)]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn scenario_search_in_project_with_many_files() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let src = dir.path().join("src");
|
|
std::fs::create_dir_all(&src).unwrap();
|
|
|
|
for i in 0..10 {
|
|
let mut f = std::fs::File::create(src.join(format!("mod_{i:02}.rs"))).unwrap();
|
|
writeln!(f, "pub fn handler_{i}() {{}}").unwrap();
|
|
writeln!(f, "pub fn helper_{i}() {{}}").unwrap();
|
|
}
|
|
|
|
let result = lean_ctx::tools::ctx_search::handle(
|
|
"handler_",
|
|
dir.path().to_str().unwrap(),
|
|
Some("*.rs"),
|
|
50,
|
|
CrpMode::Off,
|
|
false,
|
|
false,
|
|
false,
|
|
)
|
|
.text;
|
|
assert!(result.contains("handler_"));
|
|
assert!(
|
|
result.contains("10 matches"),
|
|
"Expected 10 matches, got: {}",
|
|
&result[..result.len().min(200)]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn scenario_tree_of_project_structure() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let src = dir.path().join("src");
|
|
let tests = dir.path().join("tests");
|
|
std::fs::create_dir_all(&src).unwrap();
|
|
std::fs::create_dir_all(&tests).unwrap();
|
|
std::fs::write(src.join("main.rs"), "fn main() {}").unwrap();
|
|
std::fs::write(src.join("lib.rs"), "pub mod utils;").unwrap();
|
|
std::fs::write(tests.join("integration.rs"), "#[test] fn it_works() {}").unwrap();
|
|
|
|
let (result, _) =
|
|
lean_ctx::tools::ctx_tree::handle(dir.path().to_str().unwrap(), 3, false, true);
|
|
assert!(result.contains("src"));
|
|
assert!(result.contains("tests"));
|
|
assert!(!result.starts_with("ERROR:"));
|
|
}
|
|
|
|
#[test]
|
|
fn scenario_multimode_read_workflow() {
|
|
let mut cache = SessionCache::new();
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let file = dir.path().join("multi.rs");
|
|
std::fs::write(
|
|
&file,
|
|
"use std::io;\n\npub struct Config {\n pub name: String,\n pub port: u16,\n}\n\nimpl Config {\n pub fn new() -> Self {\n Self { name: \"app\".into(), port: 8080 }\n }\n}\n",
|
|
).unwrap();
|
|
let path = file.to_str().unwrap();
|
|
|
|
// 1. Read with full mode
|
|
let full = lean_ctx::tools::ctx_read::handle_with_task_resolved(
|
|
&mut cache,
|
|
path,
|
|
"full",
|
|
CrpMode::Off,
|
|
None,
|
|
);
|
|
assert!(full.content.contains("Config"));
|
|
|
|
// 2. Read with signatures mode (should use compressed cache on 2nd call)
|
|
let sig1 = lean_ctx::tools::ctx_read::handle_with_task_resolved(
|
|
&mut cache,
|
|
path,
|
|
"signatures",
|
|
CrpMode::Off,
|
|
None,
|
|
);
|
|
assert!(!sig1.content.is_empty());
|
|
|
|
let sig2 = lean_ctx::tools::ctx_read::handle_with_task_resolved(
|
|
&mut cache,
|
|
path,
|
|
"signatures",
|
|
CrpMode::Off,
|
|
None,
|
|
);
|
|
// Should be identical (compressed cache hit)
|
|
assert_eq!(sig1.content, sig2.content);
|
|
|
|
// 3. Read with map mode
|
|
let map1 = lean_ctx::tools::ctx_read::handle_with_task_resolved(
|
|
&mut cache,
|
|
path,
|
|
"map",
|
|
CrpMode::Off,
|
|
None,
|
|
);
|
|
assert!(!map1.content.is_empty());
|
|
}
|
|
}
|